@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.
- package/dist/__tests__/capture.test.js +22 -0
- package/dist/__tests__/capture.test.js.map +1 -1
- package/dist/__tests__/experiment.test.d.ts +6 -0
- package/dist/__tests__/experiment.test.d.ts.map +1 -0
- package/dist/__tests__/experiment.test.js +69 -0
- package/dist/__tests__/experiment.test.js.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts +2 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.js +46 -0
- package/dist/__tests__/onboarding-path-b.test.js.map +1 -0
- package/dist/__tests__/onboarding.test.d.ts +1 -1
- package/dist/__tests__/onboarding.test.js +304 -156
- package/dist/__tests__/onboarding.test.js.map +1 -1
- package/dist/__tests__/setup.test.js +22 -74
- package/dist/__tests__/setup.test.js.map +1 -1
- package/dist/commands/connect-integration.test.d.ts +7 -0
- package/dist/commands/connect-integration.test.d.ts.map +1 -0
- package/dist/commands/connect-integration.test.js +148 -0
- package/dist/commands/connect-integration.test.js.map +1 -0
- package/dist/commands/connect.d.ts +17 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +280 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/connect.test.d.ts +6 -0
- package/dist/commands/connect.test.d.ts.map +1 -0
- package/dist/commands/connect.test.js +230 -0
- package/dist/commands/connect.test.js.map +1 -0
- package/dist/commands/handshake.d.ts +2 -0
- package/dist/commands/handshake.d.ts.map +1 -1
- package/dist/commands/handshake.js +20 -5
- package/dist/commands/handshake.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +57 -81
- package/dist/commands/setup.js.map +1 -1
- package/dist/generators/context-md.d.ts +1 -1
- package/dist/generators/context-md.d.ts.map +1 -1
- package/dist/generators/context-md.js +12 -1
- package/dist/generators/context-md.js.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +25 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/conversation-engine.d.ts +45 -0
- package/dist/lib/conversation-engine.d.ts.map +1 -0
- package/dist/lib/conversation-engine.js +112 -0
- package/dist/lib/conversation-engine.js.map +1 -0
- package/dist/lib/conversation-phases.d.ts +59 -0
- package/dist/lib/conversation-phases.d.ts.map +1 -0
- package/dist/lib/conversation-phases.js +11 -0
- package/dist/lib/conversation-phases.js.map +1 -0
- package/dist/lib/conversation-signals.d.ts +30 -0
- package/dist/lib/conversation-signals.d.ts.map +1 -0
- package/dist/lib/conversation-signals.js +64 -0
- package/dist/lib/conversation-signals.js.map +1 -0
- package/dist/lib/experiment.d.ts +18 -0
- package/dist/lib/experiment.d.ts.map +1 -0
- package/dist/lib/experiment.js +28 -0
- package/dist/lib/experiment.js.map +1 -0
- package/dist/lib/onboarding-path-b.d.ts +10 -0
- package/dist/lib/onboarding-path-b.d.ts.map +1 -0
- package/dist/lib/onboarding-path-b.js +214 -0
- package/dist/lib/onboarding-path-b.js.map +1 -0
- package/dist/lib/onboarding-phases.d.ts +9 -0
- package/dist/lib/onboarding-phases.d.ts.map +1 -0
- package/dist/lib/onboarding-phases.js +120 -0
- package/dist/lib/onboarding-phases.js.map +1 -0
- package/dist/lib/onboarding-shared.d.ts +81 -0
- package/dist/lib/onboarding-shared.d.ts.map +1 -0
- package/dist/lib/onboarding-shared.js +190 -0
- package/dist/lib/onboarding-shared.js.map +1 -0
- package/dist/lib/onboarding-topics.d.ts +27 -0
- package/dist/lib/onboarding-topics.d.ts.map +1 -0
- package/dist/lib/onboarding-topics.js +57 -0
- package/dist/lib/onboarding-topics.js.map +1 -0
- package/dist/lib/onboarding.d.ts +3 -5
- package/dist/lib/onboarding.d.ts.map +1 -1
- package/dist/lib/onboarding.js +224 -247
- package/dist/lib/onboarding.js.map +1 -1
- package/dist/lib/profiles.d.ts +5 -0
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +7 -0
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/telemetry.d.ts +5 -5
- package/dist/lib/telemetry.d.ts.map +1 -1
- package/dist/lib/telemetry.js +34 -16
- package/dist/lib/telemetry.js.map +1 -1
- package/dist/lib/wizard-surfaces.d.ts +47 -0
- package/dist/lib/wizard-surfaces.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.js +176 -0
- package/dist/lib/wizard-surfaces.js.map +1 -0
- package/dist/lib/wizard-surfaces.test.d.ts +2 -0
- package/dist/lib/wizard-surfaces.test.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.test.js +127 -0
- package/dist/lib/wizard-surfaces.test.js.map +1 -0
- package/dist/lib/wizard-trust.d.ts +31 -0
- package/dist/lib/wizard-trust.d.ts.map +1 -0
- package/dist/lib/wizard-trust.js +66 -0
- package/dist/lib/wizard-trust.js.map +1 -0
- package/dist/lib/wizard-trust.test.d.ts +2 -0
- package/dist/lib/wizard-trust.test.d.ts.map +1 -0
- package/dist/lib/wizard-trust.test.js +32 -0
- package/dist/lib/wizard-trust.test.js.map +1 -0
- 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 @@
|
|
|
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;
|
|
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;
|