@productbrain/cli 0.1.0-beta.35 → 0.1.0-beta.37

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 (105) hide show
  1. package/dist/__tests__/capture.test.js +22 -0
  2. package/dist/__tests__/capture.test.js.map +1 -1
  3. package/dist/__tests__/experiment.test.d.ts +6 -0
  4. package/dist/__tests__/experiment.test.d.ts.map +1 -0
  5. package/dist/__tests__/experiment.test.js +69 -0
  6. package/dist/__tests__/experiment.test.js.map +1 -0
  7. package/dist/__tests__/onboarding-path-b.test.d.ts +2 -0
  8. package/dist/__tests__/onboarding-path-b.test.d.ts.map +1 -0
  9. package/dist/__tests__/onboarding-path-b.test.js +46 -0
  10. package/dist/__tests__/onboarding-path-b.test.js.map +1 -0
  11. package/dist/__tests__/onboarding.test.d.ts +1 -1
  12. package/dist/__tests__/onboarding.test.js +304 -156
  13. package/dist/__tests__/onboarding.test.js.map +1 -1
  14. package/dist/__tests__/setup.test.js +22 -74
  15. package/dist/__tests__/setup.test.js.map +1 -1
  16. package/dist/commands/connect-integration.test.d.ts +7 -0
  17. package/dist/commands/connect-integration.test.d.ts.map +1 -0
  18. package/dist/commands/connect-integration.test.js +148 -0
  19. package/dist/commands/connect-integration.test.js.map +1 -0
  20. package/dist/commands/connect.d.ts +17 -0
  21. package/dist/commands/connect.d.ts.map +1 -0
  22. package/dist/commands/connect.js +280 -0
  23. package/dist/commands/connect.js.map +1 -0
  24. package/dist/commands/connect.test.d.ts +6 -0
  25. package/dist/commands/connect.test.d.ts.map +1 -0
  26. package/dist/commands/connect.test.js +230 -0
  27. package/dist/commands/connect.test.js.map +1 -0
  28. package/dist/commands/handshake.d.ts +2 -0
  29. package/dist/commands/handshake.d.ts.map +1 -1
  30. package/dist/commands/handshake.js +20 -5
  31. package/dist/commands/handshake.js.map +1 -1
  32. package/dist/commands/setup.d.ts.map +1 -1
  33. package/dist/commands/setup.js +57 -81
  34. package/dist/commands/setup.js.map +1 -1
  35. package/dist/generators/context-md.d.ts +1 -1
  36. package/dist/generators/context-md.d.ts.map +1 -1
  37. package/dist/generators/context-md.js +12 -1
  38. package/dist/generators/context-md.js.map +1 -1
  39. package/dist/index.js +13 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/lib/config.d.ts +1 -0
  42. package/dist/lib/config.d.ts.map +1 -1
  43. package/dist/lib/config.js +25 -1
  44. package/dist/lib/config.js.map +1 -1
  45. package/dist/lib/conversation-engine.d.ts +45 -0
  46. package/dist/lib/conversation-engine.d.ts.map +1 -0
  47. package/dist/lib/conversation-engine.js +112 -0
  48. package/dist/lib/conversation-engine.js.map +1 -0
  49. package/dist/lib/conversation-phases.d.ts +59 -0
  50. package/dist/lib/conversation-phases.d.ts.map +1 -0
  51. package/dist/lib/conversation-phases.js +11 -0
  52. package/dist/lib/conversation-phases.js.map +1 -0
  53. package/dist/lib/conversation-signals.d.ts +30 -0
  54. package/dist/lib/conversation-signals.d.ts.map +1 -0
  55. package/dist/lib/conversation-signals.js +64 -0
  56. package/dist/lib/conversation-signals.js.map +1 -0
  57. package/dist/lib/experiment.d.ts +18 -0
  58. package/dist/lib/experiment.d.ts.map +1 -0
  59. package/dist/lib/experiment.js +28 -0
  60. package/dist/lib/experiment.js.map +1 -0
  61. package/dist/lib/onboarding-path-b.d.ts +10 -0
  62. package/dist/lib/onboarding-path-b.d.ts.map +1 -0
  63. package/dist/lib/onboarding-path-b.js +214 -0
  64. package/dist/lib/onboarding-path-b.js.map +1 -0
  65. package/dist/lib/onboarding-phases.d.ts +9 -0
  66. package/dist/lib/onboarding-phases.d.ts.map +1 -0
  67. package/dist/lib/onboarding-phases.js +120 -0
  68. package/dist/lib/onboarding-phases.js.map +1 -0
  69. package/dist/lib/onboarding-shared.d.ts +81 -0
  70. package/dist/lib/onboarding-shared.d.ts.map +1 -0
  71. package/dist/lib/onboarding-shared.js +190 -0
  72. package/dist/lib/onboarding-shared.js.map +1 -0
  73. package/dist/lib/onboarding-topics.d.ts +27 -0
  74. package/dist/lib/onboarding-topics.d.ts.map +1 -0
  75. package/dist/lib/onboarding-topics.js +57 -0
  76. package/dist/lib/onboarding-topics.js.map +1 -0
  77. package/dist/lib/onboarding.d.ts +3 -5
  78. package/dist/lib/onboarding.d.ts.map +1 -1
  79. package/dist/lib/onboarding.js +224 -247
  80. package/dist/lib/onboarding.js.map +1 -1
  81. package/dist/lib/profiles.d.ts +5 -0
  82. package/dist/lib/profiles.d.ts.map +1 -1
  83. package/dist/lib/profiles.js +7 -0
  84. package/dist/lib/profiles.js.map +1 -1
  85. package/dist/lib/telemetry.d.ts +5 -5
  86. package/dist/lib/telemetry.d.ts.map +1 -1
  87. package/dist/lib/telemetry.js +34 -16
  88. package/dist/lib/telemetry.js.map +1 -1
  89. package/dist/lib/wizard-surfaces.d.ts +47 -0
  90. package/dist/lib/wizard-surfaces.d.ts.map +1 -0
  91. package/dist/lib/wizard-surfaces.js +176 -0
  92. package/dist/lib/wizard-surfaces.js.map +1 -0
  93. package/dist/lib/wizard-surfaces.test.d.ts +2 -0
  94. package/dist/lib/wizard-surfaces.test.d.ts.map +1 -0
  95. package/dist/lib/wizard-surfaces.test.js +127 -0
  96. package/dist/lib/wizard-surfaces.test.js.map +1 -0
  97. package/dist/lib/wizard-trust.d.ts +31 -0
  98. package/dist/lib/wizard-trust.d.ts.map +1 -0
  99. package/dist/lib/wizard-trust.js +66 -0
  100. package/dist/lib/wizard-trust.js.map +1 -0
  101. package/dist/lib/wizard-trust.test.d.ts +2 -0
  102. package/dist/lib/wizard-trust.test.d.ts.map +1 -0
  103. package/dist/lib/wizard-trust.test.js +32 -0
  104. package/dist/lib/wizard-trust.test.js.map +1 -0
  105. package/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * pb connect — redeem a short-lived connect token to save an API key locally.
3
+ *
4
+ * Part of FEAT-958 Path C: Onboarding flow.
5
+ * Token is obtained from an onboarding link (e.g., email, QR code).
6
+ * S1 endpoint: POST /auth/redeem-connect-token
7
+ * S2 (this): CLI command to parse token, redeem, and save key locally.
8
+ * S3: Wizard flow (surface detection + trust level).
9
+ * S4: Final handshake (pb handshake).
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
12
+ import { resolve } from 'path';
13
+ import { resolveAppUrl } from '../lib/constants.js';
14
+ import { CLIError, ErrorCode } from '../lib/errors.js';
15
+ import { createProfile, listProfiles, PROFILES_DIR } from '../lib/profiles.js';
16
+ import { withSpinner } from '../lib/spinner.js';
17
+ import * as clack from '@clack/prompts';
18
+ import { runHandshake } from './handshake.js';
19
+ const APP_URL = resolveAppUrl();
20
+ const TOKEN_PREFIX = 'pb_ct_';
21
+ /**
22
+ * Validate token format — must start with pb_ct_ and be >20 chars.
23
+ * @returns error message if invalid, undefined if valid.
24
+ */
25
+ function validateTokenFormat(token) {
26
+ if (!token.startsWith(TOKEN_PREFIX)) {
27
+ return `Invalid token format. Token must start with '${TOKEN_PREFIX}'.`;
28
+ }
29
+ if (token.length < 30) {
30
+ return `Token is too short. Got ${token.length} chars, expected >30.`;
31
+ }
32
+ return undefined;
33
+ }
34
+ /**
35
+ * POST /auth/redeem-connect-token with the token.
36
+ * Returns the API key + workspace info, or throws CLIError on failure.
37
+ */
38
+ async function redeemToken(token, siteUrl) {
39
+ return withSpinner('Redeeming token', async () => {
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => controller.abort(), 5000);
42
+ try {
43
+ const res = await fetch(`${siteUrl}/auth/redeem-connect-token`, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: JSON.stringify({ token }),
49
+ signal: controller.signal,
50
+ });
51
+ clearTimeout(timer);
52
+ if (!res.ok) {
53
+ let errorCode = 'unknown_error';
54
+ let errorMessage = `Server returned ${res.status}`;
55
+ try {
56
+ const errorData = (await res.json());
57
+ if (errorData.error?.code) {
58
+ errorCode = errorData.error.code;
59
+ errorMessage = errorData.error.message ?? errorMessage;
60
+ }
61
+ }
62
+ catch {
63
+ // Failed to parse error JSON — use default message
64
+ }
65
+ // Map error codes to user-friendly messages
66
+ if (errorCode === 'invalid_token' || res.status === 400) {
67
+ throw new CLIError('Token is invalid, expired, or already used. Get a new token from your onboarding link.', {
68
+ code: ErrorCode.AUTH_INVALID,
69
+ category: 'auth',
70
+ guidance: `Visit ${APP_URL} to get a new onboarding link, or contact support.`,
71
+ });
72
+ }
73
+ if (errorCode === 'workspace_not_found' || res.status === 404) {
74
+ throw new CLIError('Workspace not found. The token may be from a deleted workspace.', {
75
+ code: ErrorCode.AUTH_DENIED,
76
+ category: 'auth',
77
+ guidance: 'Contact support for help.',
78
+ });
79
+ }
80
+ throw new CLIError(`Token redemption failed: ${errorMessage}`, {
81
+ code: ErrorCode.AUTH_INVALID,
82
+ category: 'auth',
83
+ guidance: `Visit ${APP_URL} to get a new onboarding link.`,
84
+ });
85
+ }
86
+ const json = (await res.json());
87
+ // Check for error response (has error field)
88
+ if ('error' in json && json.error) {
89
+ const errorCode = json.error.code || 'unknown_error';
90
+ const errorMessage = json.error.message || 'Token redemption failed';
91
+ throw new CLIError(errorCode === 'invalid_token' ? 'Token is invalid, expired, or already used.' : errorMessage, {
92
+ code: ErrorCode.AUTH_INVALID,
93
+ category: 'auth',
94
+ guidance: `Visit ${APP_URL} to get a new onboarding link.`,
95
+ });
96
+ }
97
+ return json;
98
+ }
99
+ catch (err) {
100
+ clearTimeout(timer);
101
+ // Re-throw CLIErrors (validation failures)
102
+ if (err instanceof CLIError)
103
+ throw err;
104
+ // Network/timeout error
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ if (msg === 'fetch failed' || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
107
+ throw new CLIError('Could not reach ProductBrain. Check your internet connection.', {
108
+ code: ErrorCode.NETWORK_UNREACHABLE,
109
+ category: 'network',
110
+ guidance: 'Check your connection, then try again: pb connect <token>',
111
+ });
112
+ }
113
+ // Unknown error
114
+ throw new CLIError(`Network error: ${msg}`, {
115
+ code: ErrorCode.NETWORK_UNREACHABLE,
116
+ category: 'network',
117
+ guidance: 'Check your internet connection and try again.',
118
+ });
119
+ }
120
+ });
121
+ }
122
+ /**
123
+ * Slugify workspace name for profile naming.
124
+ * Lowercase, replace spaces/special chars with hyphens, max 32 chars.
125
+ */
126
+ function slugify(name) {
127
+ return name
128
+ .toLowerCase()
129
+ .replace(/[^a-z0-9]+/g, '-')
130
+ .replace(/^-|-$/g, '')
131
+ .slice(0, 32) || 'default';
132
+ }
133
+ /**
134
+ * Save API key as a named profile. Creates or updates the profile.
135
+ * Does NOT activate the profile globally — project config (.productbrain/config.json)
136
+ * handles per-project resolution. User can switch manually with `pb use <profile>`.
137
+ * Sets process.env for the current process only (so handshake works).
138
+ */
139
+ function saveKeyAsProfile(apiKey, siteUrl, profileName) {
140
+ const existing = listProfiles();
141
+ if (existing.includes(profileName)) {
142
+ // Overwrite existing profile with fresh credentials (re-connect scenario)
143
+ const lines = [
144
+ `# Product Brain CLI profile: ${profileName}`,
145
+ `PRODUCTBRAIN_API_KEY=${apiKey}`,
146
+ ...(siteUrl ? [`CONVEX_SITE_URL=${siteUrl}`] : []),
147
+ '',
148
+ ];
149
+ mkdirSync(PROFILES_DIR, { recursive: true });
150
+ writeFileSync(resolve(PROFILES_DIR, `${profileName}.env`), lines.join('\n'), { mode: 0o600 });
151
+ }
152
+ else {
153
+ createProfile(profileName, apiKey, siteUrl);
154
+ }
155
+ // Don't call useProfile() — that hijacks the global active profile.
156
+ // Project config pins the workspace per-project instead.
157
+ process.env.PRODUCTBRAIN_API_KEY = apiKey;
158
+ process.env.CONVEX_SITE_URL = siteUrl;
159
+ }
160
+ /**
161
+ * Write .productbrain/config.json to pin this project to the workspace.
162
+ * Stores profile name + siteUrl. Does NOT include the API key (that stays in profiles, never committed).
163
+ */
164
+ function saveProjectConfig(siteUrl, profileName) {
165
+ const configDir = resolve(process.cwd(), '.productbrain');
166
+ const configPath = resolve(configDir, 'config.json');
167
+ // Read existing config to preserve other fields
168
+ let existing = {};
169
+ if (existsSync(configPath)) {
170
+ try {
171
+ existing = JSON.parse(readFileSync(configPath, 'utf8'));
172
+ }
173
+ catch { /* ignore parse errors */ }
174
+ }
175
+ // Write profile + siteUrl (no API key — that stays in profiles, never committed)
176
+ const config = { ...existing, siteUrl, profile: profileName };
177
+ mkdirSync(configDir, { recursive: true });
178
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
179
+ }
180
+ /**
181
+ * Main entrypoint for `pb connect <token>`.
182
+ * Validates token format, redeems it via /auth/redeem-connect-token,
183
+ * saves the key, and runs the wizard (S3).
184
+ */
185
+ export async function runConnect(token) {
186
+ // 1. Validate token format
187
+ const formatError = validateTokenFormat(token);
188
+ if (formatError) {
189
+ throw new CLIError(formatError, {
190
+ code: ErrorCode.AUTH_INVALID,
191
+ category: 'auth',
192
+ guidance: `Usage: pb connect <token>\nToken must start with '${TOKEN_PREFIX}' and be at least 30 characters long.`,
193
+ });
194
+ }
195
+ // 2. Determine site URL (env var or default)
196
+ const siteUrl = (process.env.CONVEX_SITE_URL || process.env.PUBLIC_CONVEX_URL || 'https://trustworthy-kangaroo-277.convex.site').replace(/\/$/, '');
197
+ // 3. Redeem token
198
+ const result = await redeemToken(token, siteUrl);
199
+ // 4. Save API key as named profile + pin project config
200
+ const profileName = slugify(result.workspaceName);
201
+ saveKeyAsProfile(result.apiKey, siteUrl, profileName);
202
+ saveProjectConfig(siteUrl, profileName);
203
+ // 5. Print success
204
+ process.stdout.write('\n');
205
+ console.log(`✓ Connected to "${result.workspaceName}"`);
206
+ process.stdout.write('\n');
207
+ // 6. Run handshake — write ALL target files (no filtering)
208
+ console.log('Setting up your product context...\n');
209
+ try {
210
+ await runHandshake({ quiet: true });
211
+ console.log('✓ Context files synced to your AI tools:\n');
212
+ console.log(' .cursor/rules/ → Cursor');
213
+ console.log(' CLAUDE.md → Claude Code');
214
+ console.log(' .github/ → GitHub Copilot');
215
+ console.log(' AGENTS.md → Codex / CI');
216
+ console.log('');
217
+ console.log(' These update when you run `pb session start`.');
218
+ }
219
+ catch (err) {
220
+ // Handshake failure is non-fatal during connect
221
+ console.log('⚠ Context sync skipped (run `pb handshake` manually later)');
222
+ if (err instanceof Error)
223
+ console.log(` ${err.message}`);
224
+ }
225
+ process.stdout.write('\n');
226
+ // Screen 1 → Continue gate
227
+ const cont1 = await clack.select({
228
+ message: '',
229
+ options: [{ value: 'continue', label: 'Continue →' }],
230
+ });
231
+ if (clack.isCancel(cont1))
232
+ process.exit(130);
233
+ // 7. Profile & multi-workspace info
234
+ console.log(` Your API key is saved as profile "${profileName}".`);
235
+ console.log(' To connect another workspace, run `pb connect` with that token.');
236
+ console.log(' Switch between workspaces with `pb use <profile>`.\n');
237
+ console.log(' Context files were synced to this project.');
238
+ console.log(' For other repos, run `pb handshake` to set them up too.');
239
+ process.stdout.write('\n');
240
+ // Screen 2 → Continue gate
241
+ const cont2 = await clack.select({
242
+ message: '',
243
+ options: [{ value: 'continue', label: 'Continue →' }],
244
+ });
245
+ if (clack.isCancel(cont2))
246
+ process.exit(130);
247
+ // 8. Interactive "What's next?" menu
248
+ const next = await clack.select({
249
+ message: "What's next?",
250
+ options: [
251
+ { value: 'start', label: 'Start coding', hint: 'Just one step' },
252
+ { value: 'connect', label: 'Connect another workspace' },
253
+ { value: 'docs', label: 'Learn more', hint: 'productbrain.io/docs' },
254
+ { value: 'community', label: 'Join the community', hint: 'community.productbrain.io' },
255
+ ],
256
+ });
257
+ if (clack.isCancel(next))
258
+ process.exit(130);
259
+ process.stdout.write('\n');
260
+ switch (next) {
261
+ case 'start':
262
+ console.log(' Next time you open your AI tool in this project, run:\n');
263
+ console.log(' pb session start\n');
264
+ console.log(' That refreshes your product context and starts a tracked session.');
265
+ console.log(' Then just code normally — your AI already has your context.');
266
+ break;
267
+ case 'connect':
268
+ console.log(' Run `pb connect <token>` with the token from your other workspace.');
269
+ console.log(' Each workspace gets its own profile. Switch with `pb use <profile>`.');
270
+ break;
271
+ case 'docs':
272
+ console.log(' → https://productbrain.io/docs');
273
+ break;
274
+ case 'community':
275
+ console.log(' → https://community.productbrain.io');
276
+ break;
277
+ }
278
+ process.stdout.write('\n');
279
+ }
280
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tests for pb connect command (FEAT-958 S2).
3
+ * Covers token parsing, redemption, error handling, and key saving.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=connect.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.test.d.ts","sourceRoot":"","sources":["../../src/commands/connect.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Tests for pb connect command (FEAT-958 S2).
3
+ * Covers token parsing, redemption, error handling, and key saving.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { runConnect } from './connect.js';
7
+ import { CLIError, ErrorCode } from '../lib/errors.js';
8
+ // Mock withSpinner to bypass spinner logic in tests
9
+ vi.mock('../lib/spinner.js', () => ({
10
+ withSpinner: vi.fn(async (message, fn) => {
11
+ // withSpinner just calls the function immediately in tests
12
+ return fn();
13
+ }),
14
+ }));
15
+ // Mock fs operations
16
+ vi.mock('fs', () => ({
17
+ mkdirSync: vi.fn(),
18
+ writeFileSync: vi.fn(),
19
+ existsSync: vi.fn(() => false),
20
+ readFileSync: vi.fn(),
21
+ }));
22
+ // Mock @clack/prompts (connect now uses interactive prompts)
23
+ vi.mock('@clack/prompts', () => ({
24
+ select: vi.fn(async () => 'continue'),
25
+ isCancel: vi.fn(() => false),
26
+ }));
27
+ // Mock profiles (connect saves key as named profile)
28
+ vi.mock('../lib/profiles.js', () => ({
29
+ createProfile: vi.fn(),
30
+ listProfiles: vi.fn(() => []),
31
+ PROFILES_DIR: '/tmp/profiles',
32
+ }));
33
+ // Mock handshake (connect runs handshake after token redemption)
34
+ vi.mock('./handshake.js', () => ({
35
+ runHandshake: vi.fn(async () => { }),
36
+ }));
37
+ // Import mocked modules after mocking
38
+ import * as fs from 'fs';
39
+ // Mock fetch globally
40
+ const mockFetch = vi.fn();
41
+ global.fetch = mockFetch;
42
+ describe('pb connect', () => {
43
+ beforeEach(() => {
44
+ mockFetch.mockClear();
45
+ vi.clearAllMocks();
46
+ process.env.PRODUCTBRAIN_API_KEY = undefined;
47
+ process.env.CONVEX_SITE_URL = undefined;
48
+ });
49
+ afterEach(() => {
50
+ vi.clearAllMocks();
51
+ });
52
+ describe('token validation', () => {
53
+ it('rejects token without pb_ct_ prefix', async () => {
54
+ await expect(runConnect('invalid_token')).rejects.toThrow(CLIError);
55
+ // Should not make fetch call
56
+ expect(mockFetch).not.toHaveBeenCalled();
57
+ });
58
+ it('rejects token that is too short', async () => {
59
+ await expect(runConnect('pb_ct_short')).rejects.toThrow(CLIError);
60
+ expect(mockFetch).not.toHaveBeenCalled();
61
+ });
62
+ it('accepts valid token format', async () => {
63
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
64
+ mockFetch.mockResolvedValueOnce({
65
+ ok: true,
66
+ json: async () => ({
67
+ ok: true,
68
+ apiKey: 'pb_sk_test',
69
+ workspaceId: 'ws123',
70
+ workspaceName: 'My Workspace',
71
+ }),
72
+ });
73
+ await runConnect(validToken);
74
+ expect(mockFetch).toHaveBeenCalledOnce();
75
+ });
76
+ });
77
+ describe('token redemption', () => {
78
+ it('successfully redeems valid token', async () => {
79
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
80
+ const mockResponse = {
81
+ ok: true,
82
+ apiKey: 'pb_sk_redeemed123',
83
+ workspaceId: 'ws456',
84
+ workspaceName: 'Dev Workspace',
85
+ };
86
+ mockFetch.mockResolvedValueOnce({
87
+ ok: true,
88
+ json: async () => mockResponse,
89
+ });
90
+ await runConnect(validToken);
91
+ // Verify POST to /auth/redeem-connect-token
92
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/auth/redeem-connect-token'), expect.objectContaining({
93
+ method: 'POST',
94
+ headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
95
+ body: JSON.stringify({ token: validToken }),
96
+ }));
97
+ // Verify profile was created and project config was written
98
+ const { createProfile } = await import('../lib/profiles.js');
99
+ expect(createProfile).toHaveBeenCalledWith('dev-workspace', 'pb_sk_redeemed123', expect.any(String));
100
+ expect(fs.writeFileSync).toHaveBeenCalled();
101
+ });
102
+ it('handles invalid_token error (400)', async () => {
103
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
104
+ mockFetch.mockResolvedValueOnce({
105
+ ok: false,
106
+ status: 400,
107
+ json: async () => ({
108
+ ok: false,
109
+ error: {
110
+ code: 'invalid_token',
111
+ message: 'Token is invalid or expired',
112
+ },
113
+ }),
114
+ });
115
+ const err = await runConnect(validToken).catch((e) => e);
116
+ expect(err).toBeInstanceOf(CLIError);
117
+ expect(err.code).toBe(ErrorCode.AUTH_INVALID);
118
+ });
119
+ it('handles workspace_not_found error (404)', async () => {
120
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
121
+ mockFetch.mockResolvedValueOnce({
122
+ ok: false,
123
+ status: 404,
124
+ json: async () => ({
125
+ ok: false,
126
+ error: {
127
+ code: 'workspace_not_found',
128
+ message: 'Workspace does not exist',
129
+ },
130
+ }),
131
+ });
132
+ const err = await runConnect(validToken).catch((e) => e);
133
+ expect(err).toBeInstanceOf(CLIError);
134
+ expect(err.code).toBe(ErrorCode.AUTH_DENIED);
135
+ });
136
+ it('handles network error (fetch failed)', async () => {
137
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
138
+ mockFetch.mockRejectedValueOnce(new Error('fetch failed'));
139
+ await expect(runConnect(validToken)).rejects.toThrow(CLIError);
140
+ const err = await runConnect(validToken).catch((e) => e);
141
+ expect(err.code).toBe(ErrorCode.NETWORK_UNREACHABLE);
142
+ });
143
+ it('handles timeout', async () => {
144
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
145
+ mockFetch.mockImplementationOnce(() => new Promise((_, reject) => {
146
+ setTimeout(() => reject(new Error('AbortError')), 10);
147
+ }));
148
+ await expect(runConnect(validToken)).rejects.toThrow(CLIError);
149
+ });
150
+ });
151
+ describe('key saving', () => {
152
+ it('saves API key as named profile', async () => {
153
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
154
+ mockFetch.mockResolvedValueOnce({
155
+ ok: true,
156
+ json: async () => ({
157
+ ok: true,
158
+ apiKey: 'pb_sk_final',
159
+ workspaceId: 'ws789',
160
+ workspaceName: 'Final Workspace',
161
+ }),
162
+ });
163
+ await runConnect(validToken);
164
+ // Profile created with slugified name and API key
165
+ const { createProfile } = await import('../lib/profiles.js');
166
+ expect(createProfile).toHaveBeenCalledWith('final-workspace', 'pb_sk_final', expect.any(String));
167
+ });
168
+ it('sets process.env for current session', async () => {
169
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
170
+ mockFetch.mockResolvedValueOnce({
171
+ ok: true,
172
+ json: async () => ({
173
+ ok: true,
174
+ apiKey: 'pb_sk_env_test',
175
+ workspaceId: 'ws999',
176
+ workspaceName: 'Env Test',
177
+ }),
178
+ });
179
+ await runConnect(validToken);
180
+ expect(process.env.PRODUCTBRAIN_API_KEY).toBe('pb_sk_env_test');
181
+ expect(process.env.CONVEX_SITE_URL).toBeTruthy();
182
+ });
183
+ });
184
+ describe('error messages', () => {
185
+ it('provides guidance for invalid token format', async () => {
186
+ try {
187
+ await runConnect('bad_token');
188
+ expect.fail('should have thrown');
189
+ }
190
+ catch (err) {
191
+ if (err instanceof CLIError) {
192
+ expect(err.guidance).toContain('pb_ct_');
193
+ }
194
+ }
195
+ });
196
+ it('provides guidance for invalid_token response', async () => {
197
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
198
+ mockFetch.mockResolvedValueOnce({
199
+ ok: false,
200
+ status: 400,
201
+ json: async () => ({
202
+ ok: false,
203
+ error: {
204
+ code: 'invalid_token',
205
+ message: 'Token is invalid',
206
+ },
207
+ }),
208
+ });
209
+ try {
210
+ await runConnect(validToken);
211
+ expect.fail('should have thrown');
212
+ }
213
+ catch (err) {
214
+ if (err instanceof CLIError) {
215
+ expect(err.message).toContain('invalid');
216
+ expect(err.guidance).toContain('onboarding');
217
+ }
218
+ }
219
+ });
220
+ it('provides guidance for network errors', async () => {
221
+ const validToken = 'pb_ct_' + 'a'.repeat(40);
222
+ mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
223
+ const err = await runConnect(validToken).catch((e) => e);
224
+ expect(err).toBeInstanceOf(CLIError);
225
+ expect(err.category).toBe('network');
226
+ expect(err.guidance).toMatch(/connection|internet/i);
227
+ });
228
+ });
229
+ });
230
+ //# sourceMappingURL=connect.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.test.js","sourceRoot":"","sources":["../../src/commands/connect.test.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,8 @@ interface HandshakeOptions {
14
14
  quiet?: boolean;
15
15
  /** When true, fetch governance entries from the Chain and merge generated rules. */
16
16
  generate?: boolean;
17
+ /** Target surfaces to write adapter files for. Empty = write all. */
18
+ surfaces?: string[];
17
19
  }
18
20
  /**
19
21
  * Normalize volatile handshake-only timestamps before comparing generated files.
@@ -1 +1 @@
1
- {"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../../src/commands/handshake.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsMH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiGxG;AACD,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,sCAAsC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAO9E;AAsBD,wBAAsB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6WhF"}
1
+ {"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../../src/commands/handshake.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsMH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiGxG;AACD,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,sCAAsC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAO9E;AAsBD,wBAAsB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgYhF"}
@@ -468,7 +468,7 @@ export async function runHandshake(options = {}) {
468
468
  skills: copilotSkills.length > 0 ? copilotSkills : undefined,
469
469
  rules: copilotRules.length > 0 ? copilotRules : undefined,
470
470
  };
471
- const contextContent = orientView ? generateContextMd(orientView, repo, timestamp) : null;
471
+ const contextContent = orientView ? generateContextMd(orientView, repo, timestamp, workspaceProfile?.stage) : null;
472
472
  const briefingContent = generateBriefingMd(matchedEntries, repo, uniqueQueries, timestamp);
473
473
  const agentsContent = generateAgentsMd(timestamp, {
474
474
  workspaceContext: agentsWorkspaceContext,
@@ -480,13 +480,17 @@ export async function runHandshake(options = {}) {
480
480
  // 7. Write files
481
481
  const filesWritten = [];
482
482
  const filesSkipped = [];
483
+ // Surface filtering: skip adapter writes for targets not in the allowed set
484
+ const allowedTargets = options.surfaces && options.surfaces.length > 0
485
+ ? new Set(options.surfaces)
486
+ : null; // null = write all
483
487
  const writes = [
484
488
  ...(contextContent ? [{ path: join(cwd, '.productbrain', 'context.md'), relative: '.productbrain/context.md', content: contextContent, dirs: join(cwd, '.productbrain'), isAdapter: false }] : []),
485
489
  { path: join(cwd, '.productbrain', 'briefing.md'), relative: '.productbrain/briefing.md', content: briefingContent, isAdapter: false },
486
- { path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true },
487
- { path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true },
488
- { path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true },
489
- { path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true },
490
+ { path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true, target: 'codex' },
491
+ { path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true, target: 'claude' },
492
+ { path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true, target: 'cursor' },
493
+ { path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true, target: 'copilot' },
490
494
  ];
491
495
  // Add Cursor skill copies (filtered by target)
492
496
  const cursorProfile = resolveSurfaceProfile('cursor');
@@ -500,6 +504,7 @@ export async function runHandshake(options = {}) {
500
504
  content: generateCursorSkill(skill, cursorProfile),
501
505
  dirs: skillDir,
502
506
  isAdapter: true,
507
+ target: 'cursor',
503
508
  });
504
509
  }
505
510
  // Add Codex skill copies (projected markdown + index)
@@ -512,6 +517,7 @@ export async function runHandshake(options = {}) {
512
517
  content: generateCodexSkill(skill, codexProfile),
513
518
  dirs: join(cwd, '.codex', 'skills'),
514
519
  isAdapter: true,
520
+ target: 'codex',
515
521
  });
516
522
  }
517
523
  writes.push({
@@ -520,6 +526,7 @@ export async function runHandshake(options = {}) {
520
526
  content: generateCodexSkillIndex(codexSkills),
521
527
  dirs: join(cwd, '.codex', 'skills'),
522
528
  isAdapter: true,
529
+ target: 'codex',
523
530
  });
524
531
  // Validate Codex-projected skills for dead references
525
532
  const codexWarnings = validateCodexSkills(codexSkills);
@@ -533,6 +540,7 @@ export async function runHandshake(options = {}) {
533
540
  content: generateCursorRule(rule, cursorProfile),
534
541
  dirs: join(cwd, '.cursor', 'rules'),
535
542
  isAdapter: true,
543
+ target: 'cursor',
536
544
  });
537
545
  }
538
546
  // Add Claude Code rule copies (filtered by target)
@@ -546,6 +554,7 @@ export async function runHandshake(options = {}) {
546
554
  content: generateClaudeRule(rule, claudeProfile),
547
555
  dirs: join(cwd, '.claude', 'rules'),
548
556
  isAdapter: true,
557
+ target: 'claude',
549
558
  });
550
559
  }
551
560
  // Add Claude Code skill router (filtered by target)
@@ -558,9 +567,15 @@ export async function runHandshake(options = {}) {
558
567
  content: skillRouterContent,
559
568
  dirs: join(cwd, '.claude', 'rules'),
560
569
  isAdapter: true,
570
+ target: 'claude',
561
571
  });
562
572
  }
563
573
  for (const w of writes) {
574
+ // Surface filtering: skip adapter writes for targets not in the allowed set
575
+ if (allowedTargets && w.target && !allowedTargets.has(w.target)) {
576
+ filesSkipped.push({ path: w.relative, reason: `filtered (surface: ${w.target})` });
577
+ continue;
578
+ }
564
579
  if (w.isAdapter && !shouldWriteAdapter(w.path, force)) {
565
580
  filesSkipped.push({ path: w.relative, reason: 'exists without auto-generated marker (use --force to overwrite)' });
566
581
  continue;