screencraft 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.
Files changed (40) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.env.example +3 -0
  3. package/MCP_README.md +200 -0
  4. package/README.md +148 -0
  5. package/bin/screencraft.js +61 -0
  6. package/package.json +31 -0
  7. package/src/auth/keystore.js +148 -0
  8. package/src/commands/init.js +119 -0
  9. package/src/commands/launch.js +405 -0
  10. package/src/detectors/detectBrand.js +1222 -0
  11. package/src/detectors/simulator.js +317 -0
  12. package/src/generators/analyzeStyleReference.js +471 -0
  13. package/src/generators/compositePSD.js +682 -0
  14. package/src/generators/copy.js +147 -0
  15. package/src/mcp/index.js +394 -0
  16. package/src/pipeline/aeSwap.js +369 -0
  17. package/src/pipeline/download.js +32 -0
  18. package/src/pipeline/queue.js +101 -0
  19. package/src/server/index.js +627 -0
  20. package/src/server/public/app.js +738 -0
  21. package/src/server/public/index.html +255 -0
  22. package/src/server/public/style.css +751 -0
  23. package/src/server/session.js +36 -0
  24. package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
  25. package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
  26. package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
  27. package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
  28. package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
  29. package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
  30. package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
  31. package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
  32. package/templates/ae_swap.jsx +284 -0
  33. package/templates/layouts/minimal.psd +0 -0
  34. package/templates/screencraft.config.example.js +165 -0
  35. package/test/output/layout_test.png +0 -0
  36. package/test/output/style_profile.json +64 -0
  37. package/test/reference.png +0 -0
  38. package/test/test_brand.js +69 -0
  39. package/test/test_psd.js +83 -0
  40. package/test/test_style_analysis.js +114 -0
@@ -0,0 +1,30 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node -e:*)",
5
+ "Bash(node:*)",
6
+ "Bash(npm install:*)",
7
+ "Bash(xcrun simctl:*)",
8
+ "Bash(ls:*)",
9
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -help)",
10
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-output/source/app-launch.aep' -comp \"Screenshot_scene_1\" -RStemplate \"Best Settings\" -output '/tmp/test-ae-output/video/preview.mov' -v ERRORS_AND_PROGRESS -continueOnMissingFootage)",
11
+ "Bash(open:*)",
12
+ "Bash(find:*)",
13
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-wild/source/app-launch.aep' -s '/tmp/test-ae-wild/source/ae_swap.jsx' -close SAVE_CHANGES -v ERRORS_AND_PROGRESS)",
14
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-wild/source/app-launch.aep' -comp \"MASTER_app_launch\" -s '/tmp/test-ae-wild/source/ae_swap.jsx' -e 1 -close SAVE_CHANGES -v ERRORS_AND_PROGRESS)",
15
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-wild/source/app-launch.aep' -comp \"MASTER_app_launch\" -s '/tmp/test-ae-wild/source/ae_swap.jsx' -output '/tmp/test-ae-wild/video/preview.mp4' -RStemplate \"Best Settings\" -v ERRORS_AND_PROGRESS -continueOnMissingFootage)",
16
+ "Bash(osascript:*)",
17
+ "Bash(kill:*)",
18
+ "Bash(pkill:*)",
19
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-wild/source/app-launch.aep' -comp \"MASTER_app_launch\" -output '/tmp/test-ae-wild/video/preview.mp4' -RStemplate \"Best Settings\" -v ERRORS_AND_PROGRESS -continueOnMissingFootage)",
20
+ "Bash('/Applications/Adobe After Effects 2026/aerender' -project '/tmp/test-ae-final/source/app-launch.aep' -comp \"MASTER_app_launch\" -output '/tmp/test-ae-final/video/preview.mp4' -RStemplate \"Best Settings\" -v ERRORS_AND_PROGRESS -continueOnMissingFootage)",
21
+ "Bash(ffmpeg:*)",
22
+ "Bash(for:*)",
23
+ "Bash(do)",
24
+ "Bash(base=\"$f%.mp4\")",
25
+ "Bash(echo:*)",
26
+ "Bash(done)",
27
+ "Bash(sips -g hasAlpha:*)"
28
+ ]
29
+ }
30
+ }
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ ANTHROPIC_API_KEY=sk-ant-your-key-here
2
+ SCREENCRAFT_API=https://api.screencraft.dev
3
+ NODE_ENV=development
package/MCP_README.md ADDED
@@ -0,0 +1,200 @@
1
+ # ScreenCraft MCP Server
2
+
3
+ ScreenCraft plugs directly into **Claude Code**, **Claude Desktop**, and **Claude Cowork** as an MCP (Model Context Protocol) server. This means your AI assistant can generate your entire App Store launch kit — screenshots, headlines, device mockups, PSDs, and preview video — without you leaving your coding session.
4
+
5
+ The key insight: **Claude already understands your app**. It's read your codebase, knows your features, and understands your users. ScreenCraft lets Claude turn that understanding into App Store assets, with zero additional API costs.
6
+
7
+ ---
8
+
9
+ ## What It Does
10
+
11
+ When ScreenCraft is connected as an MCP server, your AI assistant gets these tools:
12
+
13
+ | Tool | What It Does |
14
+ |------|-------------|
15
+ | `detect_brand` | Scans your project for brand colors, app icon, fonts, logo, and framework |
16
+ | `set_brand_overrides` | Lets Claude fix or refine colors/name based on what it knows from your code |
17
+ | `suggest_screenshots` | Claude recommends which screens to capture based on your app's routes and features |
18
+ | `set_headlines` | Claude writes App Store headline copy — the white text + accent word for each screen |
19
+ | `list_templates` | Shows available PSD layout templates |
20
+ | `set_template` | Picks a template for screenshot compositing |
21
+ | `open_ui` | Launches the visual web UI at localhost:3141, pre-filled with everything Claude set up |
22
+ | `generate_launch_kit` | Runs the full pipeline headlessly — no UI needed |
23
+ | `get_session_status` | Checks what's been configured and what's still needed |
24
+
25
+ ---
26
+
27
+ ## Setup (One Time)
28
+
29
+ ### Option 1: Claude Code CLI
30
+
31
+ ```bash
32
+ claude mcp add --transport stdio screencraft -- npx screencraft --mcp
33
+ ```
34
+
35
+ ### Option 2: Project config file
36
+
37
+ Add a `.mcp.json` file to your project root:
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "screencraft": {
43
+ "type": "stdio",
44
+ "command": "npx",
45
+ "args": ["screencraft", "--mcp"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ This gets committed to git — anyone on your team who uses Claude Code gets ScreenCraft tools automatically.
52
+
53
+ ### Option 3: Claude Desktop
54
+
55
+ Add to your Claude Desktop config (`claude_desktop_config.json`):
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "screencraft": {
61
+ "command": "npx",
62
+ "args": ["screencraft", "--mcp"]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## How It Works
71
+
72
+ ### The Flow
73
+
74
+ 1. You're coding with Claude (Code, Desktop, or Cowork)
75
+ 2. You say: **"Generate App Store screenshots for my app"**
76
+ 3. Claude reads your codebase — it already knows your app's features, colors, and purpose
77
+ 4. Claude calls ScreenCraft tools:
78
+ - `detect_brand` → finds your colors, icon, font from the project
79
+ - `set_headlines` → writes headline copy for each screen (no API call — Claude IS the AI)
80
+ - `suggest_screenshots` → recommends which screens to capture
81
+ - `open_ui` → opens the web UI with everything pre-filled
82
+ 5. You review in the browser, capture screenshots from the Simulator, and click Generate
83
+ 6. Done — composited App Store PNGs, layered PSDs, AE project, and preview video
84
+
85
+ ### Why This Is Different
86
+
87
+ Traditional approach: Screenshot tool asks you to type headlines, pick colors, configure everything manually. Or it calls an AI API (costing tokens) to analyze your app from scratch.
88
+
89
+ ScreenCraft + MCP approach: Claude already has full context on your app from your coding session. It generates the headlines, suggests the right screens, and knows your brand — then pushes all of that into ScreenCraft. **Zero extra AI cost. Zero manual config.**
90
+
91
+ ---
92
+
93
+ ## Example Prompts
94
+
95
+ Once MCP is connected, try these in Claude Code or Claude Desktop:
96
+
97
+ ### Full launch kit
98
+ > Generate my App Store launch kit. Detect my brand, write headlines for 6 key screens, and open the ScreenCraft UI so I can capture screenshots.
99
+
100
+ ### Just headlines
101
+ > Write App Store headline copy for my app. I need a short white text phrase and an accent word for each of these screens: home, settings, onboarding, profile, analytics, pricing.
102
+
103
+ ### Brand check
104
+ > Run ScreenCraft brand detection on my project and tell me what colors, icon, and font it found. Fix anything that looks wrong.
105
+
106
+ ### Headless generation
107
+ > I already have screenshots in my screenshots/ folder. Detect my brand, write headlines, and generate the launch kit without opening the UI.
108
+
109
+ ---
110
+
111
+ ## What Gets Generated
112
+
113
+ | Output | Free Tier | Licensed |
114
+ |--------|-----------|----------|
115
+ | Composited App Store PNGs (1290x2796) | Yes | Yes |
116
+ | Raw simulator screenshots | Yes | Yes |
117
+ | Brand assets (icon, logo, font) | Yes | Yes |
118
+ | Layered PSD files | — | Yes |
119
+ | After Effects project (.aep) | — | Yes |
120
+ | App Store preview video (.mp4) | — | Yes |
121
+
122
+ Output lands in `your-project/launch-kit/`.
123
+
124
+ ---
125
+
126
+ ## Prerequisites
127
+
128
+ - **Node.js 18+**
129
+ - **ScreenCraft installed**: `npm install -g screencraft`
130
+ - **For screenshot capture**: Xcode with your app running in the iOS Simulator (build with Cmd+R before generating)
131
+ - **For video rendering** (optional): Adobe After Effects 2024-2026 with scripting enabled
132
+
133
+ ---
134
+
135
+ ## Architecture
136
+
137
+ ```
138
+ Your AI Session (Claude Code / Desktop / Cowork)
139
+
140
+ │ MCP tools (stdio)
141
+
142
+ ScreenCraft MCP Server (src/mcp/index.js)
143
+
144
+ │ Shared session state
145
+
146
+ ScreenCraft Web UI (localhost:3141)
147
+
148
+ │ Calls existing pipeline
149
+
150
+ ┌─────────────────────────────────┐
151
+ │ Brand Detection │
152
+ │ Screenshot Capture (Simulator) │
153
+ │ PSD Compositing (sharp/ag-psd)│
154
+ │ AE Project Prep + Render │
155
+ └─────────────────────────────────┘
156
+ ```
157
+
158
+ The MCP server and web UI share the same session. MCP tools populate brand data, headlines, and preferences. The web UI reads that state and presents it visually. The user reviews, captures screenshots, and triggers generation.
159
+
160
+ ---
161
+
162
+ ## Registering as an Official MCP Server
163
+
164
+ ScreenCraft can be published to the [Official MCP Registry](https://registry.modelcontextprotocol.io/) so users can discover it alongside 19,000+ other MCP servers.
165
+
166
+ Steps:
167
+ 1. Publish the npm package (`npm publish`)
168
+ 2. Create a `server.json` with registry metadata
169
+ 3. Authenticate namespace via GitHub or DNS verification for screencraft.dev
170
+ 4. Submit via the MCP publisher CLI
171
+
172
+ Once registered, ScreenCraft appears in the registry and downstream aggregators like mcp.so and mcpservers.org.
173
+
174
+ ---
175
+
176
+ ## Troubleshooting
177
+
178
+ **"Tools not showing up in Claude Code"**
179
+ - Run `/mcp` in Claude Code to check server status
180
+ - Make sure `npx screencraft --mcp` works standalone (should produce no output — it's waiting for stdio messages)
181
+
182
+ **"Brand detection found wrong colors"**
183
+ - Use `set_brand_overrides` to fix them, or edit in the web UI
184
+ - Add a `screencraft.config.js` to your project root for permanent overrides
185
+
186
+ **"Screenshots fail to capture"**
187
+ - Build and run your app in the Simulator (Xcode Cmd+R) before capturing
188
+ - ScreenCraft detects the running simulator — it won't boot a blank one
189
+
190
+ **GNotificationCenterDelegate warning in terminal**
191
+ - Harmless. Caused by canvas and sharp both bundling libgio. Does not affect functionality.
192
+
193
+ ---
194
+
195
+ ## Links
196
+
197
+ - [ScreenCraft Website](https://screencraft.dev)
198
+ - [Get a License Key](https://screencraft.dev/activate)
199
+ - [MCP Protocol Docs](https://modelcontextprotocol.io)
200
+ - [MCP Registry](https://registry.modelcontextprotocol.io)
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # ScreenCraft CLI
2
+
3
+ > App Store launch kit from one terminal command.
4
+
5
+ ```bash
6
+ npx screencraft launch
7
+ ```
8
+
9
+ Generates App Store screenshots, layered PSDs, and a preview video — automatically branded from your codebase.
10
+
11
+ ---
12
+
13
+ ## What it does
14
+
15
+ 1. **Detects** your framework (Expo, React Native, Flutter, Swift, Android)
16
+ 2. **Reads** brand colors + app name from your theme files — no config needed
17
+ 3. **Captures** screenshots from the iOS/Android simulator (or uses `./screenshots/` folder)
18
+ 4. **Suggests** App Store headline text via Claude AI — you approve each one
19
+ 5. **Composites** screenshots into device mockups with your headline text baked in
20
+ 6. **Renders** a 30s vertical preview video using your branded After Effects template
21
+ 7. **Outputs** a complete `/launch-kit/` folder in your project root
22
+
23
+ ---
24
+
25
+ ## Output
26
+
27
+ ```
28
+ your-app/
29
+ launch-kit/
30
+ screenshots/
31
+ screen_01.png ← App Store-ready PNG, 1290×2796px
32
+ screen_02.png
33
+ screen_03.png
34
+ screen_04.png
35
+ screen_05.png
36
+ video/
37
+ preview.mp4 ← 30s vertical preview (watermarked until activated)
38
+ source/
39
+ app-launch.aep ← Editable After Effects project
40
+ screen_01.psd ← Layered PSD (Pro tier)
41
+ ...
42
+ README.md ← App Store submission checklist
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Tiers
48
+
49
+ | | Free | Launch ($49) | Pro ($99) |
50
+ |---|---|---|---|
51
+ | App Store PNGs | ✓ | ✓ | ✓ |
52
+ | Preview video (clean) | watermarked | ✓ | ✓ |
53
+ | After Effects file | — | ✓ | ✓ |
54
+ | Layered PSDs | — | — | ✓ |
55
+ | Social cut (1:1) | — | — | ✓ |
56
+ | App Store copy | — | — | ✓ |
57
+
58
+ ---
59
+
60
+ ## Quick Start
61
+
62
+ ### Option A — Automated (with simulator)
63
+
64
+ Add routes to `screencraft.config.js` in your project root:
65
+
66
+ ```js
67
+ module.exports = {
68
+ app: {
69
+ name: "YourApp",
70
+ tagline: "Your tagline here.",
71
+ cta: "Download Free",
72
+ },
73
+ screenshots: [
74
+ { route: "/home", label: "Everything at a glance" },
75
+ { route: "/features", label: "Built for how you work" },
76
+ { route: "/profile", label: "Your space, your way" },
77
+ { route: "/onboard", label: "Get started in seconds" },
78
+ { route: "/settings", label: "Simple and powerful" },
79
+ ],
80
+ };
81
+ ```
82
+
83
+ Then run:
84
+ ```bash
85
+ npx screencraft launch
86
+ ```
87
+
88
+ ### Option B — Manual screenshots (fastest for testing)
89
+
90
+ 1. Take screenshots on your phone or simulator
91
+ 2. Place them in `./screenshots/` in your project root
92
+ 3. Run `npx screencraft launch`
93
+
94
+ The CLI will pick them up automatically.
95
+
96
+ ---
97
+
98
+ ## Commands
99
+
100
+ ```bash
101
+ npx screencraft launch # Full pipeline
102
+ npx screencraft launch --screenshots-only # Free tier (no video)
103
+ npx screencraft activate MF-XXXX-XXXX # Store license key
104
+ npx screencraft status # Check credits
105
+ npx screencraft init # Generate config file
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Testing
111
+
112
+ ```bash
113
+ # Test brand detection on your app project
114
+ node test/test_brand.js /path/to/your/app
115
+
116
+ # Test PSD compositor (add a screenshot first)
117
+ cp /path/to/screenshot.png test/sample_screenshot.png
118
+ node test/test_psd.js
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Environment
124
+
125
+ ```bash
126
+ ANTHROPIC_API_KEY=sk-ant-... # For AI headline suggestions
127
+ SCREENCRAFT_API=https://... # Override API endpoint
128
+ NODE_ENV=development # Dev mode (accepts MF-* test keys)
129
+ SCREENCRAFT_DEV=1 # Also enables dev mode
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Template Files (for motion designer)
135
+
136
+ Place these in `templates/`:
137
+
138
+ | File | Description |
139
+ |------|-------------|
140
+ | `templates/device_frame.png` | Transparent phone mockup overlay, 1130×1900px |
141
+ | `templates/screen_mask.png` | White shape of screen area (for clipping) |
142
+ | `templates/app-launch.aep` | After Effects template with swap layer naming |
143
+
144
+ See `TEMPLATE_SPEC.md` for the full AE build specification.
145
+
146
+ ---
147
+
148
+ screencraft.dev
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/screencraft.js
4
+ * CLI entry point. Routes commands.
5
+ *
6
+ * Usage:
7
+ * npx screencraft launch — full launch kit pipeline
8
+ * npx screencraft launch --screenshots-only — free tier, no video
9
+ * npx screencraft activate <key> — store license key
10
+ * npx screencraft status — show credits remaining
11
+ * npx screencraft serve — open web UI at localhost:3141
12
+ * npx screencraft init — generate screencraft.config.js
13
+ * npx screencraft --mcp — start MCP server (stdio)
14
+ */
15
+
16
+ const path = require('path');
17
+
18
+ const [,, command, ...args] = process.argv;
19
+
20
+ // ── MCP mode — must run before banner (stdout is protocol) ───────
21
+ if (command === '--mcp' || process.argv.includes('--mcp')) {
22
+ require('../src/mcp').startMCP();
23
+ } else {
24
+ // Normal CLI mode
25
+ const chalk = require('chalk');
26
+
27
+ // ── Banner ──────────────────────────────────────────────────────
28
+ console.log('');
29
+ console.log(chalk.hex('#A78BFA')('◆ ScreenCraft') + chalk.dim(' v0.1.0'));
30
+ console.log(chalk.dim('─────────────────────────────────────'));
31
+ console.log('');
32
+
33
+ // ── Route ───────────────────────────────────────────────────────
34
+ switch (command) {
35
+ case 'launch':
36
+ require('../src/commands/launch')(args);
37
+ break;
38
+ case 'activate':
39
+ require('../src/auth/keystore').activate(args[0]);
40
+ break;
41
+ case 'status':
42
+ require('../src/auth/keystore').status();
43
+ break;
44
+ case 'serve':
45
+ require('../src/server').startServer();
46
+ break;
47
+ case 'init':
48
+ require('../src/commands/init')();
49
+ break;
50
+ default:
51
+ console.log(chalk.dim('Commands:'));
52
+ console.log(' ' + chalk.white('launch') + chalk.dim(' — generate launch kit'));
53
+ console.log(' ' + chalk.white('launch --screenshots-only') + chalk.dim(' — free tier'));
54
+ console.log(' ' + chalk.white('activate <key>') + chalk.dim(' — activate license'));
55
+ console.log(' ' + chalk.white('status') + chalk.dim(' — check credits'));
56
+ console.log(' ' + chalk.white('serve') + chalk.dim(' — open web UI'));
57
+ console.log(' ' + chalk.white('init') + chalk.dim(' — create config file'));
58
+ console.log(' ' + chalk.white('--mcp') + chalk.dim(' — start MCP server'));
59
+ console.log('');
60
+ }
61
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "screencraft",
3
+ "version": "0.1.0",
4
+ "description": "App launch kit generator — screenshots, PSDs, and preview video from one CLI command",
5
+ "main": "src/commands/launch.js",
6
+ "bin": {
7
+ "screencraft": "./bin/screencraft.js"
8
+ },
9
+ "scripts": {
10
+ "test:brand": "node test/test_brand.js",
11
+ "test:psd": "node test/test_psd.js",
12
+ "test:pipeline": "node test/test_pipeline.js",
13
+ "launch": "node bin/screencraft.js launch"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.27.1",
17
+ "ag-psd": "^30.1.0",
18
+ "archiver": "^6.0.1",
19
+ "canvas": "^3.2.1",
20
+ "chalk": "^4.1.2",
21
+ "dotenv": "^16.0.3",
22
+ "express": "^5.2.1",
23
+ "inquirer": "^8.2.6",
24
+ "node-fetch": "^2.6.9",
25
+ "ora": "^5.4.1",
26
+ "sharp": "^0.33.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ }
31
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * src/auth/keystore.js
3
+ * --------------------
4
+ * Manages license key storage in ~/.screencraft/config.json
5
+ * and validates keys against the ScreenCraft API (screencraft.dev).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const chalk = require('chalk');
12
+
13
+ const CONFIG_DIR = path.join(os.homedir(), '.screencraft');
14
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
15
+ const API_BASE = process.env.SCREENCRAFT_API || 'https://screencraft.dev/api';
16
+
17
+ // ── Read / Write ──────────────────────────────────────────────────
18
+
19
+ function readConfig() {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function writeConfig(data) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
30
+ }
31
+
32
+ function getStoredKey() {
33
+ return readConfig().licenseKey || null;
34
+ }
35
+
36
+ // ── Activate ──────────────────────────────────────────────────────
37
+
38
+ async function activate(key) {
39
+ if (!key) {
40
+ console.log(chalk.hex('#C9A84C')(' Usage: screencraft activate <license-key>'));
41
+ console.log(chalk.dim(' Get a key at screencraft.dev'));
42
+ return;
43
+ }
44
+
45
+ console.log(chalk.dim(' Validating key...'));
46
+ const result = await validateKey(key);
47
+
48
+ if (result) {
49
+ const config = readConfig();
50
+ config.licenseKey = key;
51
+ config.tier = result.tier;
52
+ config.activatedAt = new Date().toISOString();
53
+ writeConfig(config);
54
+
55
+ console.log(chalk.hex('#6BC46A')(` ✓ Activated — ${result.tier} tier`));
56
+ console.log(chalk.dim(` ${result.generationsRemaining} generation${result.generationsRemaining !== 1 ? 's' : ''} remaining`));
57
+ console.log(chalk.dim(' Key saved to ~/.screencraft/config.json'));
58
+ } else {
59
+ console.log(chalk.hex('#C9A84C')(' ✗ Invalid or expired key'));
60
+ console.log(chalk.dim(' Visit screencraft.dev to get a valid key'));
61
+ }
62
+ }
63
+
64
+ // ── Status ────────────────────────────────────────────────────────
65
+
66
+ async function status() {
67
+ const config = readConfig();
68
+
69
+ if (!config.licenseKey) {
70
+ console.log(chalk.dim(' No license key found.'));
71
+ console.log(chalk.dim(' Run: screencraft activate <key>'));
72
+ return;
73
+ }
74
+
75
+ console.log(chalk.dim(' Key: ') + chalk.white(maskKey(config.licenseKey)));
76
+ console.log(chalk.dim(' Tier: ') + chalk.white(config.tier || 'unknown'));
77
+ console.log(chalk.dim(' Activated: ') + chalk.white(config.activatedAt?.split('T')[0] || 'unknown'));
78
+
79
+ // Check remaining generations
80
+ const result = await validateKey(config.licenseKey);
81
+ if (result) {
82
+ console.log(chalk.dim(' Remaining: ') + chalk.white(`${result.generationsRemaining} generation${result.generationsRemaining !== 1 ? 's' : ''}`));
83
+ } else {
84
+ console.log(chalk.dim(' Status: ') + chalk.hex('#C9A84C')('expired or all generations used'));
85
+ }
86
+ }
87
+
88
+ // ── Validate key against API ──────────────────────────────────────
89
+
90
+ /**
91
+ * Returns { tier, generationsRemaining } or null if invalid.
92
+ */
93
+ async function validateKey(key) {
94
+ if (!key) return null;
95
+
96
+ // Dev mode: accept SC- prefixed keys for testing
97
+ const isDev = process.env.NODE_ENV === 'development' || process.env.SCREENCRAFT_DEV;
98
+ if (isDev && key.startsWith('SC-')) {
99
+ const tier = key.includes('PRO') ? 'pro' : 'launch';
100
+ return { tier, generationsRemaining: tier === 'pro' ? 10 : 1 };
101
+ }
102
+
103
+ try {
104
+ const res = await fetch(`${API_BASE}/keys/validate`, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({ key }),
108
+ signal: AbortSignal.timeout(5000),
109
+ });
110
+ if (!res.ok) return null;
111
+ const data = await res.json();
112
+ return data.valid ? { tier: data.tier, generationsRemaining: data.generationsRemaining } : null;
113
+ } catch {
114
+ if (isDev) {
115
+ console.log(chalk.dim(' (dev mode: API unreachable, key accepted)'));
116
+ return { tier: 'launch', generationsRemaining: 1 };
117
+ }
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Consume one generation after a successful render.
124
+ */
125
+ async function useGeneration(key) {
126
+ if (!key) return false;
127
+ try {
128
+ const res = await fetch(`${API_BASE}/keys/use`, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ key }),
132
+ signal: AbortSignal.timeout(5000),
133
+ });
134
+ const data = await res.json();
135
+ return data.ok;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ // ── Helpers ───────────────────────────────────────────────────────
142
+
143
+ function maskKey(key) {
144
+ if (!key || key.length < 8) return '****';
145
+ return key.slice(0, 6) + '****' + key.slice(-4);
146
+ }
147
+
148
+ module.exports = { activate, status, getStoredKey, validateKey, useGeneration };