@perceo/perceo 0.2.0 → 0.3.2

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/index.js CHANGED
@@ -1,14 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ clearStoredAuth,
4
+ getEffectiveAuth,
5
+ getStoredAuth,
6
+ isLoggedIn,
7
+ loginCommand
8
+ } from "./chunk-Y65GRRTT.js";
2
9
 
3
10
  // src/index.ts
4
- import { Command as Command4 } from "commander";
11
+ import { Command as Command6 } from "commander";
5
12
 
6
13
  // src/commands/init.ts
7
14
  import { Command } from "commander";
8
- import chalk from "chalk";
9
- import ora from "ora";
15
+ import chalk3 from "chalk";
16
+ import ora2 from "ora";
10
17
  import fs2 from "fs/promises";
18
+ import path3 from "path";
19
+ import { execSync as execSync2 } from "child_process";
20
+
21
+ // src/projectAccess.ts
11
22
  import path2 from "path";
23
+ import chalk from "chalk";
24
+ import { PerceoDataClient, getSupabaseAnonKey } from "@perceo/supabase";
12
25
 
13
26
  // src/config.ts
14
27
  import fs from "fs/promises";
@@ -20,18 +33,31 @@ async function loadConfig(options = {}) {
20
33
  const projectDir = path.resolve(options.projectDir ?? process.cwd());
21
34
  const envPath = process.env.PERCEO_CONFIG_PATH;
22
35
  const baseConfigPath = envPath ? resolveMaybeRelativePath(envPath, projectDir) : path.join(projectDir, CONFIG_DIR, BASE_CONFIG_FILE);
23
- const baseConfig = await readJsonFile(baseConfigPath);
36
+ let baseConfig = await readJsonFile(baseConfigPath);
24
37
  const isLocalEnv = (process.env.PERCEO_ENV || "").toLowerCase() === "local" || process.env.NODE_ENV === "development";
25
- if (!isLocalEnv) {
26
- return baseConfig;
38
+ if (isLocalEnv) {
39
+ const localConfigPath = path.join(projectDir, CONFIG_DIR, LOCAL_CONFIG_FILE);
40
+ const localExists = await fileExists(localConfigPath);
41
+ if (localExists) {
42
+ const localConfig = await readJsonFile(localConfigPath);
43
+ baseConfig = deepMerge(baseConfig, localConfig);
44
+ }
27
45
  }
28
- const localConfigPath = path.join(projectDir, CONFIG_DIR, LOCAL_CONFIG_FILE);
29
- const localExists = await fileExists(localConfigPath);
30
- if (!localExists) {
31
- return baseConfig;
46
+ if (process.env.PERCEO_TEMPORAL_ENABLED === "true") {
47
+ baseConfig.temporal = {
48
+ enabled: true,
49
+ address: process.env.PERCEO_TEMPORAL_ADDRESS || "localhost:7233",
50
+ namespace: process.env.PERCEO_TEMPORAL_NAMESPACE || "perceo",
51
+ taskQueue: process.env.PERCEO_TEMPORAL_TASK_QUEUE || "observer-engine"
52
+ };
53
+ if (process.env.PERCEO_TEMPORAL_TLS_CERT_PATH) {
54
+ baseConfig.temporal.tls = {
55
+ certPath: process.env.PERCEO_TEMPORAL_TLS_CERT_PATH,
56
+ keyPath: process.env.PERCEO_TEMPORAL_TLS_KEY_PATH || ""
57
+ };
58
+ }
32
59
  }
33
- const localConfig = await readJsonFile(localConfigPath);
34
- return deepMerge(baseConfig, localConfig);
60
+ return baseConfig;
35
61
  }
36
62
  async function readJsonFile(filePath) {
37
63
  const raw = await fs.readFile(filePath, "utf8");
@@ -70,74 +96,894 @@ function isPlainObject(value) {
70
96
  return typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
71
97
  }
72
98
 
99
+ // src/projectAccess.ts
100
+ var ADMIN_ROLES = ["owner", "admin"];
101
+ async function ensureProjectAccess(options = {}) {
102
+ const projectDir = path2.resolve(options.projectDir ?? process.cwd());
103
+ const requireAdmin = options.requireAdmin ?? false;
104
+ const auth = await getEffectiveAuth(projectDir);
105
+ if (!auth?.access_token) {
106
+ console.error(chalk.red("You must log in first. Run ") + chalk.cyan("perceo login"));
107
+ process.exit(1);
108
+ }
109
+ const config = await loadConfig({ projectDir });
110
+ const projectId = config?.project?.id;
111
+ const projectName = config?.project?.name ?? path2.basename(projectDir);
112
+ if (!projectId) {
113
+ console.error(chalk.red("No project linked. Run ") + chalk.cyan("perceo init") + chalk.red(" in this directory first."));
114
+ process.exit(1);
115
+ }
116
+ const supabaseUrl = auth.supabaseUrl;
117
+ const supabaseKey = getSupabaseAnonKey();
118
+ const client = await PerceoDataClient.fromUserSession({
119
+ supabaseUrl,
120
+ supabaseKey,
121
+ accessToken: auth.access_token,
122
+ refreshToken: auth.refresh_token,
123
+ projectId
124
+ });
125
+ const role = await client.getProjectMemberRole(projectId);
126
+ if (!role) {
127
+ console.error(chalk.red("You don't have access to this project."));
128
+ console.error(chalk.gray("Only users added to the project can view or change it. Ask a project owner or admin to add you."));
129
+ process.exit(1);
130
+ }
131
+ if (requireAdmin && !ADMIN_ROLES.includes(role)) {
132
+ console.error(chalk.red("This action requires project owner or admin role."));
133
+ console.error(chalk.gray(`Your role: ${role}. Ask a project owner or admin to perform this action or change your role.`));
134
+ process.exit(1);
135
+ }
136
+ return { client, projectId, projectName, role, config };
137
+ }
138
+ async function checkProjectAccess(client, projectId, options = {}) {
139
+ const role = await client.getProjectMemberRole(projectId);
140
+ if (!role) return null;
141
+ if (options.requireAdmin && !ADMIN_ROLES.includes(role)) return null;
142
+ return role;
143
+ }
144
+
145
+ // src/commands/init.ts
146
+ import { PerceoDataClient as PerceoDataClient2, getSupabaseAnonKey as getSupabaseAnonKey2 } from "@perceo/supabase";
147
+
148
+ // src/github.ts
149
+ import { execSync } from "child_process";
150
+ import chalk2 from "chalk";
151
+ import ora from "ora";
152
+ var GITHUB_CLIENT_ID = process.env.PERCEO_GITHUB_CLIENT_ID || "";
153
+ async function authorizeGitHub() {
154
+ if (!GITHUB_CLIENT_ID) {
155
+ throw new Error("GitHub OAuth client ID not configured. Set PERCEO_GITHUB_CLIENT_ID environment variable.");
156
+ }
157
+ const spinner = ora("Requesting device authorization...").start();
158
+ let deviceResponse;
159
+ try {
160
+ deviceResponse = await fetch("https://github.com/login/device/code", {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ Accept: "application/json"
165
+ },
166
+ body: JSON.stringify({
167
+ client_id: GITHUB_CLIENT_ID,
168
+ scope: "repo"
169
+ })
170
+ });
171
+ } catch (fetchError) {
172
+ spinner.fail("Failed to connect to GitHub");
173
+ throw new Error(`Network error connecting to GitHub: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
174
+ }
175
+ if (!deviceResponse.ok) {
176
+ spinner.fail("Failed to request device code");
177
+ throw new Error(`GitHub API error: ${deviceResponse.statusText}`);
178
+ }
179
+ const deviceData = await deviceResponse.json();
180
+ const { device_code, user_code, verification_uri, expires_in, interval = 5 } = deviceData;
181
+ spinner.succeed("Device code received");
182
+ console.log("\n" + chalk2.bold.yellow("GitHub Authorization Required"));
183
+ console.log(chalk2.gray("\u2500".repeat(50)));
184
+ console.log("\n 1. Visit: " + chalk2.cyan.underline(verification_uri));
185
+ console.log(" 2. Enter code: " + chalk2.bold.green(user_code));
186
+ console.log("\n" + chalk2.gray(" Waiting for you to authorize in your browser..."));
187
+ console.log(chalk2.gray("\u2500".repeat(50)) + "\n");
188
+ const startTime = Date.now();
189
+ const expiresAt = startTime + expires_in * 1e3;
190
+ while (Date.now() < expiresAt) {
191
+ await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
192
+ let tokenResponse;
193
+ try {
194
+ tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ Accept: "application/json"
199
+ },
200
+ body: JSON.stringify({
201
+ client_id: GITHUB_CLIENT_ID,
202
+ device_code,
203
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
204
+ })
205
+ });
206
+ } catch (fetchError) {
207
+ console.log(chalk2.yellow(`
208
+ Warning: Network error polling GitHub, retrying...`));
209
+ continue;
210
+ }
211
+ const tokenData = await tokenResponse.json();
212
+ if (tokenData.access_token) {
213
+ console.log(chalk2.green("\u2713 GitHub authorization successful!\n"));
214
+ return {
215
+ accessToken: tokenData.access_token,
216
+ tokenType: tokenData.token_type || "bearer"
217
+ };
218
+ }
219
+ if (tokenData.error === "authorization_pending") {
220
+ continue;
221
+ }
222
+ if (tokenData.error === "slow_down") {
223
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
224
+ continue;
225
+ }
226
+ if (tokenData.error) {
227
+ throw new Error(`GitHub authorization failed: ${tokenData.error_description || tokenData.error}`);
228
+ }
229
+ }
230
+ throw new Error("GitHub authorization timed out. Please try again.");
231
+ }
232
+ async function createRepositorySecret(token, owner, repo, secretName, secretValue) {
233
+ let keyResponse;
234
+ try {
235
+ keyResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/actions/secrets/public-key`, {
236
+ headers: {
237
+ Authorization: `Bearer ${token}`,
238
+ Accept: "application/vnd.github+json",
239
+ "X-GitHub-Api-Version": "2022-11-28"
240
+ }
241
+ });
242
+ } catch (fetchError) {
243
+ throw new Error(`Network error connecting to GitHub API: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
244
+ }
245
+ if (!keyResponse.ok) {
246
+ const error = await keyResponse.text();
247
+ throw new Error(`Failed to get repository public key: ${error}`);
248
+ }
249
+ const { key: publicKey, key_id: keyId } = await keyResponse.json();
250
+ const encryptedValue = await encryptSecret(secretValue, publicKey);
251
+ let secretResponse;
252
+ try {
253
+ secretResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/actions/secrets/${secretName}`, {
254
+ method: "PUT",
255
+ headers: {
256
+ Authorization: `Bearer ${token}`,
257
+ Accept: "application/vnd.github+json",
258
+ "X-GitHub-Api-Version": "2022-11-28",
259
+ "Content-Type": "application/json"
260
+ },
261
+ body: JSON.stringify({
262
+ encrypted_value: encryptedValue,
263
+ key_id: keyId
264
+ })
265
+ });
266
+ } catch (fetchError) {
267
+ throw new Error(`Network error creating repository secret: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
268
+ }
269
+ if (!secretResponse.ok) {
270
+ const error = await secretResponse.text();
271
+ throw new Error(`Failed to create repository secret: ${error}`);
272
+ }
273
+ }
274
+ async function encryptSecret(secretValue, publicKeyBase64) {
275
+ try {
276
+ const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64");
277
+ const sodium = await import("tweetnacl");
278
+ const { box, randomBytes } = sodium.default;
279
+ const messageBytes = new TextEncoder().encode(secretValue);
280
+ const publicKeyBytes = new Uint8Array(publicKeyBuffer);
281
+ const ephemeralKeyPair = box.keyPair();
282
+ const nonce = randomBytes(box.nonceLength);
283
+ const encrypted = box(messageBytes, nonce, publicKeyBytes, ephemeralKeyPair.secretKey);
284
+ const combined = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
285
+ combined.set(ephemeralKeyPair.publicKey);
286
+ combined.set(nonce, ephemeralKeyPair.publicKey.length);
287
+ combined.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
288
+ return Buffer.from(combined).toString("base64");
289
+ } catch (error) {
290
+ throw new Error(`Failed to encrypt secret. Install tweetnacl: npm install tweetnacl
291
+ Error: ${error instanceof Error ? error.message : String(error)}`);
292
+ }
293
+ }
294
+ function isGitRepository(projectDir) {
295
+ try {
296
+ execSync("git rev-parse --is-inside-work-tree", {
297
+ cwd: projectDir,
298
+ encoding: "utf8",
299
+ stdio: ["pipe", "pipe", "ignore"]
300
+ });
301
+ return true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ function parseGitHubRemoteUrl(url) {
307
+ const trimmed = url.trim();
308
+ let match = trimmed.match(/github\.com[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);
309
+ if (match && match[1] && match[2]) {
310
+ return { owner: match[1], repo: match[2] };
311
+ }
312
+ match = trimmed.match(/^git@([^:]+):([\w-]+)\/([\w.-]+?)(\.git)?$/);
313
+ if (match && match[1] && match[2] && match[3] && /github/i.test(match[1])) {
314
+ return { owner: match[2], repo: match[3] };
315
+ }
316
+ return null;
317
+ }
318
+ function detectGitHubRemote(projectDir) {
319
+ if (!isGitRepository(projectDir)) {
320
+ return null;
321
+ }
322
+ try {
323
+ const origin = execSync("git remote get-url origin", {
324
+ cwd: projectDir,
325
+ encoding: "utf8",
326
+ stdio: ["pipe", "pipe", "ignore"]
327
+ }).trim();
328
+ const parsed = parseGitHubRemoteUrl(origin);
329
+ if (parsed) return parsed;
330
+ } catch {
331
+ }
332
+ try {
333
+ const out = execSync("git remote -v", {
334
+ cwd: projectDir,
335
+ encoding: "utf8",
336
+ stdio: ["pipe", "pipe", "ignore"]
337
+ });
338
+ for (const line of out.split("\n")) {
339
+ const tab = line.indexOf(" ");
340
+ if (tab === -1) continue;
341
+ const url = line.slice(tab + 1).replace(/\s*\((?:fetch|push)\)\s*$/, "").trim();
342
+ const parsed = parseGitHubRemoteUrl(url);
343
+ if (parsed) return parsed;
344
+ }
345
+ } catch {
346
+ }
347
+ return null;
348
+ }
349
+ async function checkRepositoryPermissions(token, owner, repo) {
350
+ try {
351
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
352
+ headers: {
353
+ Authorization: `Bearer ${token}`,
354
+ Accept: "application/vnd.github+json",
355
+ "X-GitHub-Api-Version": "2022-11-28"
356
+ }
357
+ });
358
+ if (!response.ok) {
359
+ console.log(chalk2.yellow(` Warning: Failed to check repository permissions (HTTP ${response.status})`));
360
+ return false;
361
+ }
362
+ const data = await response.json();
363
+ return data.permissions?.admin || data.permissions?.push || false;
364
+ } catch (fetchError) {
365
+ console.log(chalk2.yellow(` Warning: Network error checking repository permissions: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`));
366
+ return false;
367
+ }
368
+ }
369
+
73
370
  // src/commands/init.ts
74
- import { ObserverEngine } from "@perceo/observer-engine";
371
+ import { createInterface } from "readline";
372
+ import os from "os";
75
373
  var CONFIG_DIR2 = ".perceo";
76
374
  var CONFIG_FILE = "config.json";
77
- var initCommand = new Command("init").description("Initialize Perceo in your project").option("-d, --dir <directory>", "Project directory", process.cwd()).action(async (options) => {
78
- const projectDir = path2.resolve(options.dir || process.cwd());
79
- const spinner = ora(`Initializing Perceo in ${chalk.cyan(projectDir)}...`).start();
375
+ var SUPPORTED_FRAMEWORKS = ["nextjs", "react", "remix"];
376
+ function formatDbError(err) {
377
+ if (err instanceof Error) return err.message;
378
+ const o = err;
379
+ if (o && typeof o.message === "string") {
380
+ const parts = [o.message];
381
+ if (typeof o.code === "string") parts.push(`(code: ${o.code})`);
382
+ if (typeof o.details === "string") parts.push(o.details);
383
+ return parts.join(" ");
384
+ }
385
+ return String(err);
386
+ }
387
+ var initCommand = new Command("init").description("Initialize Perceo in your project and discover flows").option("-d, --dir <directory>", "Project directory", process.cwd()).option("--skip-github", "Skip GitHub Actions setup", false).option("-y, --yes", "Skip confirmation prompt (e.g. for CI)", false).option("-b, --branch <branch>", "Main branch name (default: auto-detect)").option("--configure-personas", "Configure custom user personas instead of auto-generating them", false).action(async (options) => {
388
+ const projectDir = path3.resolve(options.dir || process.cwd());
389
+ const loggedIn = await isLoggedIn(projectDir);
390
+ if (!loggedIn) {
391
+ console.error(chalk3.red("You must log in first. Run ") + chalk3.cyan("perceo login") + chalk3.red(", then run ") + chalk3.cyan("perceo init") + chalk3.red(" again."));
392
+ process.exit(1);
393
+ }
394
+ if (!options.yes) {
395
+ const isGit = isGitRepository(projectDir);
396
+ const remote = detectGitHubRemote(projectDir);
397
+ const isHomeDir = projectDir === os.homedir();
398
+ console.log(chalk3.bold("Initialize Perceo in this directory?"));
399
+ console.log(chalk3.gray(" Directory: ") + chalk3.cyan(projectDir));
400
+ if (remote) {
401
+ console.log(chalk3.gray(" Repository: ") + chalk3.cyan(`${remote.owner}/${remote.repo}`));
402
+ } else if (isGit) {
403
+ console.log(chalk3.gray(" Repository: ") + chalk3.yellow("Git repository (no GitHub remote)"));
404
+ } else {
405
+ console.log(chalk3.gray(" Repository: ") + chalk3.yellow("Not a git repository"));
406
+ }
407
+ if (isHomeDir) {
408
+ console.log(chalk3.yellow(" Warning: This is your home directory. Prefer running from a project directory."));
409
+ }
410
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
411
+ const answer = await new Promise((resolve) => {
412
+ rl.question(chalk3.bold("Continue? [y/N]: "), (ans) => {
413
+ rl.close();
414
+ resolve((ans || "n").trim().toLowerCase());
415
+ });
416
+ });
417
+ if (answer !== "y" && answer !== "yes") {
418
+ console.log(chalk3.gray("Init cancelled."));
419
+ process.exit(0);
420
+ }
421
+ }
422
+ const spinner = ora2(`Initializing Perceo in ${chalk3.cyan(projectDir)}...`).start();
80
423
  try {
81
424
  const pkg = await readPackageJson(projectDir);
82
- const projectName = pkg?.name || path2.basename(projectDir);
425
+ const projectName = pkg?.name || path3.basename(projectDir);
83
426
  const framework = await detectFramework(projectDir, pkg);
84
- const perceoDir = path2.join(projectDir, CONFIG_DIR2);
85
- const perceoConfigPath = path2.join(perceoDir, CONFIG_FILE);
427
+ let branch = options.branch;
428
+ if (!branch) {
429
+ branch = detectDefaultBranch(projectDir);
430
+ console.log(chalk3.gray(` Auto-detected default branch: ${branch}`));
431
+ } else {
432
+ console.log(chalk3.gray(` Using specified branch: ${branch}`));
433
+ }
434
+ if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
435
+ spinner.fail("Unsupported project type");
436
+ const detected = framework === "unknown" ? "No React/Next.js/Remix project detected" : `Detected: ${framework}`;
437
+ console.error(chalk3.red(detected + "."));
438
+ console.error(chalk3.yellow("Perceo currently supports React, Next.js, and Remix projects. Support for more frameworks is coming soon."));
439
+ process.exit(1);
440
+ }
441
+ const perceoDir = path3.join(projectDir, CONFIG_DIR2);
442
+ const perceoConfigPath = path3.join(perceoDir, CONFIG_FILE);
86
443
  await fs2.mkdir(perceoDir, { recursive: true });
444
+ spinner.text = "Bootstrapping project via Perceo Observer...";
445
+ const storedAuth = await getEffectiveAuth(projectDir);
446
+ if (!storedAuth) {
447
+ spinner.fail("Authentication required");
448
+ throw new Error("Please run 'perceo login' first");
449
+ }
450
+ const supabaseUrl = storedAuth.supabaseUrl;
451
+ const supabaseKey = getSupabaseAnonKey2();
452
+ let tempClient;
453
+ try {
454
+ console.log(chalk3.gray(`
455
+ Connecting to Perceo Cloud: ${supabaseUrl}`));
456
+ tempClient = await PerceoDataClient2.fromUserSession({
457
+ supabaseUrl: storedAuth.supabaseUrl,
458
+ supabaseKey,
459
+ accessToken: storedAuth.access_token,
460
+ refreshToken: storedAuth.refresh_token
461
+ });
462
+ } catch (authError) {
463
+ spinner.fail("Failed to connect to Perceo Cloud");
464
+ console.error(chalk3.red("\nAuthentication error details:"));
465
+ console.error(chalk3.gray(` Supabase URL: ${supabaseUrl}`));
466
+ console.error(chalk3.gray(` Error: ${authError instanceof Error ? authError.message : String(authError)}`));
467
+ if (authError instanceof Error && authError.stack) {
468
+ console.error(chalk3.gray(` Stack: ${authError.stack}`));
469
+ }
470
+ throw new Error(`Failed to authenticate with Perceo Cloud. Try running 'perceo logout' followed by 'perceo login'.`);
471
+ }
472
+ let tempProject;
473
+ const remote = detectGitHubRemote(projectDir);
474
+ const gitRemoteUrl = remote ? `https://github.com/${remote.owner}/${remote.repo}` : null;
475
+ try {
476
+ spinner.text = "Looking up project...";
477
+ tempProject = await tempClient.getProjectByName(projectName);
478
+ } catch (dbError) {
479
+ spinner.fail("Failed to query project");
480
+ console.error(chalk3.red("\nDatabase error details:"));
481
+ console.error(chalk3.gray(` ${formatDbError(dbError)}`));
482
+ throw new Error("Failed to query project from database");
483
+ }
484
+ if (!tempProject) {
485
+ try {
486
+ spinner.text = "Creating project...";
487
+ tempProject = await tempClient.createProject({
488
+ name: projectName,
489
+ framework,
490
+ config: { source: "cli-init" },
491
+ git_remote_url: gitRemoteUrl
492
+ });
493
+ } catch (createError) {
494
+ spinner.fail("Failed to create project");
495
+ console.error(chalk3.red("\nDatabase error details:"));
496
+ console.error(chalk3.gray(` ${formatDbError(createError)}`));
497
+ throw new Error("Failed to create project in database");
498
+ }
499
+ } else {
500
+ const role = await checkProjectAccess(tempClient, tempProject.id);
501
+ if (!role) {
502
+ spinner.fail("Access denied");
503
+ console.error(chalk3.red(`
504
+ You don't have access to the project "${tempProject.name}".`));
505
+ console.error(chalk3.gray("Only users added to the project can run init. Ask a project owner or admin to add you."));
506
+ process.exit(1);
507
+ }
508
+ }
509
+ spinner.text = "Generating workflow authorization key...";
510
+ let workflowApiKey;
511
+ try {
512
+ const {
513
+ data: { user }
514
+ } = await tempClient.getSupabaseClient().auth.getUser();
515
+ const userId = user?.id;
516
+ try {
517
+ const existingKeys = await tempClient.getApiKeys(tempProject.id);
518
+ const existingWorkflowKey = existingKeys.find((k) => k.name === "temporal-workflow-auth");
519
+ if (existingWorkflowKey) {
520
+ console.log(chalk3.gray("\n \u2713 Found existing workflow key, deleting it"));
521
+ await tempClient.deleteApiKey(existingWorkflowKey.id);
522
+ }
523
+ } catch (cleanupError) {
524
+ console.log(chalk3.gray(` Note: Could not check for existing keys: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`));
525
+ }
526
+ const { key } = await tempClient.createApiKey(tempProject.id, {
527
+ name: "temporal-workflow-auth",
528
+ scopes: ["workflows:start"],
529
+ createdBy: userId
530
+ });
531
+ workflowApiKey = key;
532
+ console.log(chalk3.gray("\n \u2713 Workflow authorization key generated"));
533
+ console.log(chalk3.gray(` Key prefix: ${key.substring(0, 12)}...`));
534
+ } catch (keyError) {
535
+ spinner.fail("Database key generation failed");
536
+ console.error(chalk3.red("\n \u2717 CRITICAL: Cannot generate workflow authorization key in database"));
537
+ if (keyError && typeof keyError === "object") {
538
+ const err = keyError;
539
+ console.error(chalk3.gray(` Error code: ${err.code || "N/A"}`));
540
+ console.error(chalk3.gray(` Error message: ${err.message || "Unknown error"}`));
541
+ if (err.details) {
542
+ console.error(chalk3.gray(` Details: ${err.details}`));
543
+ }
544
+ if (err.hint) {
545
+ console.error(chalk3.gray(` Hint: ${err.hint}`));
546
+ }
547
+ } else if (keyError instanceof Error) {
548
+ console.error(chalk3.gray(` Error: ${keyError.message}`));
549
+ if (keyError.stack) {
550
+ console.error(chalk3.gray(` Stack: ${keyError.stack}`));
551
+ }
552
+ } else {
553
+ console.error(chalk3.gray(` Error: ${String(keyError)}`));
554
+ }
555
+ const crypto = await import("crypto");
556
+ const keyBytes = crypto.randomBytes(32);
557
+ workflowApiKey = `prc_${keyBytes.toString("base64url")}`;
558
+ console.log(chalk3.yellow("\n \u26A0 Using temporary local key as fallback"));
559
+ console.log(chalk3.gray(` Key prefix: ${workflowApiKey.substring(0, 12)}...`));
560
+ console.log(chalk3.red(" \u2717 WARNING: Temporal workflows will NOT work with this key"));
561
+ console.log(chalk3.red(" \u2717 You must fix the database issue before workflows can run"));
562
+ throw new Error("Failed to generate workflow authorization key in database");
563
+ }
564
+ const workerApiUrl = process.env.PERCEO_WORKER_API_URL || "https://perceo-temporal-worker-331577200018.us-west1.run.app/";
565
+ const workerApiKey = process.env.PERCEO_WORKER_API_KEY;
566
+ const headers = {
567
+ "Content-Type": "application/json"
568
+ };
569
+ if (workerApiKey) {
570
+ headers["x-api-key"] = workerApiKey;
571
+ }
572
+ spinner.stop();
573
+ console.log(chalk3.gray("\n \u2139\uFE0F LLM API key will be fetched from secure storage during bootstrap"));
574
+ console.log(chalk3.gray(" (configured by admin in Supabase project_secrets table)"));
575
+ let userConfiguredPersonas = [];
576
+ let useCustomPersonas = false;
577
+ if (options.configurePersonas) {
578
+ spinner.stop();
579
+ console.log(chalk3.bold("\n\u{1F3AD} Configure User Personas"));
580
+ console.log(chalk3.gray("Define custom user personas for your application, or let Perceo auto-generate them from your codebase."));
581
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
582
+ const configurePersonasAnswer = await new Promise((resolve) => {
583
+ rl.question(chalk3.cyan("Do you want to configure custom personas? [y/N]: "), (ans) => {
584
+ resolve((ans || "n").trim().toLowerCase());
585
+ });
586
+ });
587
+ if (configurePersonasAnswer === "y" || configurePersonasAnswer === "yes") {
588
+ useCustomPersonas = true;
589
+ console.log(chalk3.green("\n\u2713 Configuring custom personas"));
590
+ console.log(chalk3.gray("Enter persona details (press Enter with empty name to finish):"));
591
+ let personaIndex = 1;
592
+ while (true) {
593
+ console.log(chalk3.bold(`
594
+ Persona ${personaIndex}:`));
595
+ const name = await new Promise((resolve) => {
596
+ rl.question(chalk3.cyan(" Name: "), resolve);
597
+ });
598
+ if (!name.trim()) {
599
+ break;
600
+ }
601
+ const description = await new Promise((resolve) => {
602
+ rl.question(chalk3.cyan(" Description: "), resolve);
603
+ });
604
+ const behaviors = await new Promise((resolve) => {
605
+ rl.question(chalk3.cyan(" Key behaviors (comma-separated): "), resolve);
606
+ });
607
+ const behaviorList = behaviors.split(",").map((b) => b.trim()).filter((b) => b.length > 0);
608
+ const behaviorObj = {};
609
+ behaviorList.forEach((behavior, idx) => {
610
+ behaviorObj[`behavior_${idx + 1}`] = behavior;
611
+ });
612
+ userConfiguredPersonas.push({
613
+ name: name.trim(),
614
+ description: description.trim() || null,
615
+ behaviors: behaviorObj
616
+ });
617
+ console.log(chalk3.green(` \u2713 Added persona: ${name}`));
618
+ personaIndex++;
619
+ }
620
+ if (userConfiguredPersonas.length === 0) {
621
+ console.log(chalk3.yellow(" No personas configured, will use auto-generation"));
622
+ useCustomPersonas = false;
623
+ } else {
624
+ console.log(chalk3.green(`
625
+ \u2713 Configured ${userConfiguredPersonas.length} custom personas`));
626
+ try {
627
+ console.log(chalk3.gray(" Saving personas to database..."));
628
+ await tempClient.createUserConfiguredPersonas(userConfiguredPersonas, tempProject.id);
629
+ console.log(chalk3.green(" \u2713 Personas saved successfully"));
630
+ } catch (personaError) {
631
+ console.error(chalk3.red(" \u2717 Failed to save personas:"), personaError);
632
+ throw new Error("Failed to save custom personas to database");
633
+ }
634
+ }
635
+ }
636
+ rl.close();
637
+ spinner.start("Starting bootstrap workflow...");
638
+ } else {
639
+ spinner.start("Starting bootstrap workflow...");
640
+ }
641
+ let bootstrapResponse;
642
+ try {
643
+ console.log(chalk3.gray(`
644
+ Connecting to worker API: ${workerApiUrl}`));
645
+ console.log(chalk3.gray(` Project ID: ${tempProject.id}`));
646
+ console.log(chalk3.gray(` Git Remote URL: ${gitRemoteUrl || "Not detected"}`));
647
+ console.log(chalk3.gray(` Workflow API Key: ${workflowApiKey.substring(0, 12)}...`));
648
+ if (!gitRemoteUrl) {
649
+ spinner.fail("Cannot start bootstrap workflow");
650
+ throw new Error("Git remote URL not detected. Please ensure this is a Git repository with a GitHub remote configured.");
651
+ }
652
+ bootstrapResponse = await fetch(`${workerApiUrl}/api/workflows/bootstrap`, {
653
+ method: "POST",
654
+ headers,
655
+ body: JSON.stringify({
656
+ projectId: tempProject.id,
657
+ gitRemoteUrl,
658
+ projectName,
659
+ framework,
660
+ branch,
661
+ workflowApiKey,
662
+ useCustomPersonas
663
+ })
664
+ });
665
+ } catch (fetchError) {
666
+ spinner.fail("Failed to connect to worker API");
667
+ console.error(chalk3.red("\nNetwork error details:"));
668
+ console.error(chalk3.gray(` URL: ${workerApiUrl}/api/workflows/bootstrap`));
669
+ console.error(chalk3.gray(` Error: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`));
670
+ if (fetchError instanceof Error && fetchError.cause) {
671
+ console.error(chalk3.gray(` Cause: ${JSON.stringify(fetchError.cause, null, 2)}`));
672
+ }
673
+ throw new Error(`Failed to connect to worker API at ${workerApiUrl}. Check that PERCEO_WORKER_API_URL is correct and the service is running.`);
674
+ }
675
+ if (!bootstrapResponse.ok) {
676
+ const errorData = await bootstrapResponse.json().catch(() => ({}));
677
+ spinner.fail("Failed to start bootstrap workflow");
678
+ console.error(chalk3.red(` HTTP ${bootstrapResponse.status}: ${bootstrapResponse.statusText}`));
679
+ throw new Error(`Bootstrap start failed: ${errorData.error || bootstrapResponse.statusText}`);
680
+ }
681
+ const { workflowId } = await bootstrapResponse.json();
682
+ console.log(chalk3.gray(`
683
+ Workflow ID: ${workflowId}`));
684
+ spinner.text = "Initializing workflow...";
685
+ let temporalResult;
686
+ for (; ; ) {
687
+ let queryResponse;
688
+ try {
689
+ queryResponse = await fetch(`${workerApiUrl}/api/workflows/${workflowId}`, {
690
+ headers
691
+ });
692
+ } catch (fetchError) {
693
+ console.log(chalk3.yellow("\n Warning: Failed to query workflow progress (network error), retrying..."));
694
+ console.log(chalk3.gray(` Error: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`));
695
+ await sleep(2e3);
696
+ continue;
697
+ }
698
+ if (!queryResponse.ok) {
699
+ console.log(chalk3.yellow(`
700
+ Warning: Failed to query workflow progress (HTTP ${queryResponse.status}), retrying...`));
701
+ await sleep(2e3);
702
+ continue;
703
+ }
704
+ const queryResult = await queryResponse.json();
705
+ if (queryResult.progress?.message) {
706
+ spinner.prefixText = chalk3.gray(`[${queryResult.progress.stage}]`);
707
+ spinner.text = `${queryResult.progress.message} (${queryResult.progress.percentage}%)`;
708
+ }
709
+ if (queryResult.completed) {
710
+ if (queryResult.error) {
711
+ spinner.fail("Bootstrap workflow failed");
712
+ throw new Error(queryResult.error);
713
+ }
714
+ if (!queryResult.result) {
715
+ spinner.fail("Bootstrap workflow completed but no result returned");
716
+ throw new Error("No result from workflow");
717
+ }
718
+ temporalResult = queryResult.result;
719
+ break;
720
+ }
721
+ if (queryResult.progress?.stage === "error") {
722
+ spinner.fail("Bootstrap workflow failed");
723
+ throw new Error(queryResult.progress.error || "Unknown workflow error");
724
+ }
725
+ await sleep(1e3);
726
+ }
727
+ spinner.succeed("Bootstrap complete!");
728
+ console.log(chalk3.green("\n\u2713 Bootstrap successful:"));
729
+ console.log(chalk3.gray(` Personas: ${temporalResult.personasExtracted}`));
730
+ console.log(chalk3.gray(` Flows: ${temporalResult.flowsExtracted}`));
731
+ console.log(chalk3.gray(` Steps: ${temporalResult.stepsExtracted}`));
732
+ console.log(chalk3.gray(` Commits: ${temporalResult.totalCommitsProcessed}`));
733
+ console.log();
734
+ spinner.start("Finishing initialization...");
735
+ spinner.text = "Connecting to Perceo Cloud...";
736
+ let client;
737
+ if (storedAuth) {
738
+ try {
739
+ client = await PerceoDataClient2.fromUserSession({
740
+ supabaseUrl: storedAuth.supabaseUrl,
741
+ supabaseKey,
742
+ accessToken: storedAuth.access_token,
743
+ refreshToken: storedAuth.refresh_token
744
+ });
745
+ } catch (authError) {
746
+ spinner.warn("Failed to attach user session, falling back to anonymous access");
747
+ console.log(chalk3.gray(` ${authError instanceof Error ? authError.message : typeof authError === "string" ? authError : "Unknown error while restoring session"}`));
748
+ spinner.start("Connecting to Perceo Cloud...");
749
+ client = new PerceoDataClient2({ supabaseUrl, supabaseKey });
750
+ }
751
+ } else {
752
+ client = new PerceoDataClient2({ supabaseUrl, supabaseKey });
753
+ }
754
+ let project;
755
+ try {
756
+ spinner.text = "Syncing project...";
757
+ project = await client.getProjectByName(projectName);
758
+ } catch (dbError) {
759
+ spinner.fail("Failed to query project");
760
+ console.error(chalk3.red("\nDatabase error details:"));
761
+ console.error(chalk3.gray(` ${formatDbError(dbError)}`));
762
+ throw new Error("Failed to query project from database");
763
+ }
764
+ if (!project) {
765
+ try {
766
+ spinner.text = "Creating project...";
767
+ project = await client.createProject({
768
+ name: projectName,
769
+ framework,
770
+ config: { source: "cli-init" },
771
+ git_remote_url: gitRemoteUrl
772
+ });
773
+ } catch (createError) {
774
+ spinner.fail("Failed to create project");
775
+ console.error(chalk3.red("\nDatabase error details:"));
776
+ console.error(chalk3.gray(` ${formatDbError(createError)}`));
777
+ throw new Error("Failed to create project in database");
778
+ }
779
+ } else if (gitRemoteUrl && project.git_remote_url !== gitRemoteUrl) {
780
+ try {
781
+ await client.updateProject(project.id, { git_remote_url: gitRemoteUrl });
782
+ } catch (updateError) {
783
+ console.log(chalk3.yellow(` Warning: Failed to update git remote URL: ${updateError instanceof Error ? updateError.message : String(updateError)}`));
784
+ }
785
+ }
786
+ const projectId = project.id;
787
+ const flowsDiscovered = temporalResult.flowsExtracted;
788
+ const flowsNew = temporalResult.flowsExtracted;
789
+ let apiKey = null;
790
+ let workflowCreated = false;
791
+ let githubAutoConfigured = false;
792
+ if (projectId && !options.skipGithub) {
793
+ spinner.text = "Generating CI API key...";
794
+ const scopes = ["ci:analyze", "ci:test", "flows:read", "insights:read", "events:publish"];
795
+ try {
796
+ const {
797
+ data: { user }
798
+ } = await client.getSupabaseClient().auth.getUser();
799
+ const userId = user?.id;
800
+ try {
801
+ const existingKeys = await client.getApiKeys(projectId);
802
+ const existingGhKey = existingKeys.find((k) => k.name === "github-actions");
803
+ if (existingGhKey) {
804
+ console.log(chalk3.gray("\n \u2713 Found existing GitHub Actions key, deleting it"));
805
+ await client.deleteApiKey(existingGhKey.id);
806
+ }
807
+ } catch (cleanupError) {
808
+ console.log(chalk3.gray(` Note: Could not check for existing keys: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`));
809
+ }
810
+ const { key } = await client.createApiKey(projectId, {
811
+ name: "github-actions",
812
+ scopes,
813
+ createdBy: userId
814
+ });
815
+ apiKey = key;
816
+ const remote2 = detectGitHubRemote(projectDir);
817
+ if (remote2) {
818
+ spinner.stop();
819
+ console.log(chalk3.cyan(`
820
+ \u{1F4E6} Detected GitHub repository: ${remote2.owner}/${remote2.repo}`));
821
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
822
+ const answer = await new Promise((resolve) => {
823
+ rl.question(chalk3.bold("Auto-configure GitHub Actions? (Y/n): "), (ans) => {
824
+ rl.close();
825
+ resolve((ans || "y").toLowerCase());
826
+ });
827
+ });
828
+ if (answer === "y" || answer === "yes" || answer === "") {
829
+ try {
830
+ spinner.start("Authorizing with GitHub...");
831
+ const ghAuth = await authorizeGitHub();
832
+ spinner.text = "Checking repository permissions...";
833
+ const hasPermission = await checkRepositoryPermissions(ghAuth.accessToken, remote2.owner, remote2.repo);
834
+ if (!hasPermission) {
835
+ spinner.warn("Insufficient permissions to write to repository");
836
+ console.log(chalk3.yellow(" You need admin or push access to configure secrets automatically."));
837
+ } else {
838
+ spinner.text = "Creating PERCEO_API_KEY secret...";
839
+ if (!apiKey) {
840
+ throw new Error("API key not found after creation");
841
+ }
842
+ await createRepositorySecret(ghAuth.accessToken, remote2.owner, remote2.repo, "PERCEO_API_KEY", apiKey);
843
+ githubAutoConfigured = true;
844
+ spinner.text = "Creating GitHub Actions workflow...";
845
+ }
846
+ } catch (error) {
847
+ spinner.warn("GitHub authorization failed");
848
+ console.log(chalk3.yellow(` ${error instanceof Error ? error.message : "Unknown error"}`));
849
+ console.log(chalk3.gray(" Continuing with manual setup instructions..."));
850
+ }
851
+ }
852
+ if (!githubAutoConfigured) {
853
+ spinner.start("Creating GitHub Actions workflow...");
854
+ }
855
+ } else {
856
+ spinner.text = "Creating GitHub Actions workflow...";
857
+ }
858
+ const workflowDir = path3.join(projectDir, ".github", "workflows");
859
+ const workflowPath = path3.join(workflowDir, "perceo.yml");
860
+ await fs2.mkdir(workflowDir, { recursive: true });
861
+ if (!await fileExists2(workflowPath)) {
862
+ const workflowContent = generateGitHubWorkflow(branch);
863
+ await fs2.writeFile(workflowPath, workflowContent, "utf8");
864
+ workflowCreated = true;
865
+ }
866
+ } catch (error) {
867
+ console.log(chalk3.yellow("\n \u26A0 Database key generation failed, using local fallback"));
868
+ if (error && typeof error === "object") {
869
+ const err = error;
870
+ console.error(chalk3.gray(` Error code: ${err.code || "N/A"}`));
871
+ console.error(chalk3.gray(` Error message: ${err.message || "Unknown error"}`));
872
+ if (err.details) {
873
+ console.error(chalk3.gray(` Details: ${err.details}`));
874
+ }
875
+ if (err.hint) {
876
+ console.error(chalk3.gray(` Hint: ${err.hint}`));
877
+ }
878
+ } else if (error instanceof Error) {
879
+ console.error(chalk3.gray(` Error: ${error.message}`));
880
+ } else {
881
+ console.error(chalk3.gray(` Error: ${String(error)}`));
882
+ }
883
+ const crypto = await import("crypto");
884
+ const keyBytes = crypto.randomBytes(32);
885
+ apiKey = `prc_${keyBytes.toString("base64url")}`;
886
+ console.log(chalk3.gray(" \u2713 Local CI API key generated"));
887
+ console.log(chalk3.gray(` Key prefix: ${apiKey.substring(0, 12)}...`));
888
+ console.log(chalk3.yellow(" \u26A0 This key is not stored in the database and is local-only"));
889
+ try {
890
+ const workflowDir = path3.join(projectDir, ".github", "workflows");
891
+ const workflowPath = path3.join(workflowDir, "perceo.yml");
892
+ await fs2.mkdir(workflowDir, { recursive: true });
893
+ if (!await fileExists2(workflowPath)) {
894
+ const workflowContent = generateGitHubWorkflow(branch);
895
+ await fs2.writeFile(workflowPath, workflowContent, "utf8");
896
+ workflowCreated = true;
897
+ }
898
+ } catch (workflowError) {
899
+ console.log(chalk3.yellow(" \u26A0 Could not create workflow file"));
900
+ console.log(chalk3.gray(` ${workflowError instanceof Error ? workflowError.message : "Unknown error"}`));
901
+ }
902
+ }
903
+ }
87
904
  if (await fileExists2(perceoConfigPath)) {
88
905
  spinner.stop();
89
- console.log(chalk.yellow(`
906
+ console.log(chalk3.yellow(`
90
907
  .perceo/${CONFIG_FILE} already exists. Skipping config generation.`));
91
908
  } else {
92
- const config = createDefaultConfig(projectName, framework);
909
+ const config = createDefaultConfig(projectName, framework, projectId, branch);
93
910
  await fs2.writeFile(perceoConfigPath, JSON.stringify(config, null, 2) + "\n", "utf8");
94
911
  }
95
- const readmePath = path2.join(perceoDir, "README.md");
912
+ const readmePath = path3.join(perceoDir, "README.md");
96
913
  if (!await fileExists2(readmePath)) {
97
914
  await fs2.writeFile(readmePath, createPerceoReadme(projectName), "utf8");
98
915
  }
99
- let bootstrapSummary = null;
100
- try {
101
- spinner.text = "Initializing flows and personas with Perceo Observer Engine...";
102
- const config = await loadConfig({ projectDir });
103
- const observerConfig = {
104
- observer: config.observer,
105
- flowGraph: config.flowGraph,
106
- eventBus: config.eventBus
107
- };
108
- const engine = new ObserverEngine(observerConfig);
109
- const result = await engine.bootstrapProject({
110
- projectDir,
111
- projectName,
112
- framework
113
- });
114
- const warnings = result.warnings && result.warnings.length > 0 ? ` (${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""})` : "";
115
- bootstrapSummary = `Initialized ${result.flowsInitialized} flow(s) and ${result.personasInitialized} persona(s)${warnings}.`;
116
- } catch (error) {
117
- const message = error instanceof Error ? error.message : "Unknown error";
118
- console.log(chalk.yellow(`
119
- \u26A0\uFE0F Skipped automatic Observer bootstrap. You can configure managed services later. Details: ${message}`));
120
- }
121
916
  spinner.succeed("Perceo initialized successfully!");
122
- console.log("\n" + chalk.bold("Project: ") + projectName);
123
- console.log(chalk.bold("Detected framework: ") + framework);
124
- console.log(chalk.bold("Config: ") + path2.relative(projectDir, perceoConfigPath));
125
- if (bootstrapSummary) {
126
- console.log(chalk.bold("Observer bootstrap: ") + bootstrapSummary);
127
- }
128
- console.log("\n" + chalk.bold("Next steps:"));
129
- console.log(" 1. Review and customize " + chalk.cyan(`.perceo/${CONFIG_FILE}`) + " for your project.");
130
- console.log(" 2. Set up local managed services (Neo4j, Supabase) as described in " + chalk.cyan(".perceo/README.md") + ".");
131
- console.log(" 3. Start the watcher with: " + chalk.cyan("perceo watch --dev --analyze"));
132
- console.log("\n" + chalk.gray("For full architecture and managed services guides, see docs/cli_architecture.md."));
917
+ console.log("\n" + chalk3.bold("Project: ") + projectName);
918
+ console.log(chalk3.bold("Framework: ") + framework);
919
+ console.log(chalk3.bold("Branch: ") + branch);
920
+ console.log(chalk3.bold("Config: ") + path3.relative(projectDir, perceoConfigPath));
921
+ console.log(chalk3.bold("Project ID: ") + projectId);
922
+ console.log(chalk3.bold("Flows discovered: ") + `${flowsDiscovered} (${flowsNew} new)`);
923
+ if (githubAutoConfigured && workflowCreated) {
924
+ console.log("\n" + chalk3.bold.green("\u2713 GitHub Actions configured automatically!"));
925
+ console.log(chalk3.gray("\u2500".repeat(50)));
926
+ console.log("\n Workflow: " + chalk3.cyan(".github/workflows/perceo.yml"));
927
+ console.log(" Secret: " + chalk3.green("PERCEO_API_KEY") + " " + chalk3.gray("(already added)"));
928
+ console.log("\n" + chalk3.gray(" CI will run on pull requests automatically."));
929
+ console.log(chalk3.gray("\u2500".repeat(50)));
930
+ } else if (apiKey && workflowCreated) {
931
+ console.log("\n" + chalk3.bold.yellow("GitHub Actions Setup (Manual):"));
932
+ console.log(chalk3.gray("\u2500".repeat(50)));
933
+ console.log("\n Workflow created at: " + chalk3.cyan(".github/workflows/perceo.yml"));
934
+ console.log("\n " + chalk3.bold("Add this secret to your repository:"));
935
+ console.log(" " + chalk3.gray("Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret"));
936
+ console.log("\n Name: " + chalk3.yellow("PERCEO_API_KEY"));
937
+ console.log(" Value: " + chalk3.green(apiKey));
938
+ console.log("\n" + chalk3.gray(" \u26A0\uFE0F This key is shown only once. Store it securely."));
939
+ console.log(chalk3.gray(" \u26A0\uFE0F Manage keys with: perceo keys list"));
940
+ console.log(chalk3.gray("\u2500".repeat(50)));
941
+ } else if (apiKey && !workflowCreated) {
942
+ console.log("\n" + chalk3.bold.green("CI API Key Generated:"));
943
+ console.log(chalk3.gray("\u2500".repeat(50)));
944
+ console.log("\n " + chalk3.bold("Add this secret to your CI:"));
945
+ console.log("\n Name: " + chalk3.yellow("PERCEO_API_KEY"));
946
+ console.log(" Value: " + chalk3.green(apiKey));
947
+ console.log("\n Workflow already exists at .github/workflows/perceo.yml");
948
+ console.log(chalk3.gray("\u2500".repeat(50)));
949
+ } else if (!options.skipGithub && projectId) {
950
+ console.log("\n" + chalk3.yellow("\u26A0\uFE0F GitHub Actions setup skipped."));
951
+ }
952
+ console.log("\n" + chalk3.bold("Next steps:"));
953
+ if (githubAutoConfigured) {
954
+ console.log(" 1. Review flows: " + chalk3.cyan("perceo flows list"));
955
+ console.log(" 2. Commit and push: " + chalk3.cyan("git add . && git commit -m 'Add Perceo' && git push"));
956
+ console.log(" 3. Open a PR to see Perceo analyze your changes!");
957
+ } else if (apiKey) {
958
+ console.log(" 1. " + chalk3.bold("Add the PERCEO_API_KEY secret to GitHub") + " (see above)");
959
+ console.log(" 2. Review flows: " + chalk3.cyan("perceo flows list"));
960
+ console.log(" 3. Commit and push to trigger CI: " + chalk3.cyan("git add . && git commit -m 'Add Perceo'"));
961
+ } else {
962
+ console.log(" 1. Review flows: " + chalk3.cyan("perceo flows list"));
963
+ console.log(" 2. Analyze PR changes: " + chalk3.cyan(`perceo analyze --base ${branch}`));
964
+ }
965
+ console.log("\n" + chalk3.gray("Run `perceo logout` to sign out if needed."));
133
966
  } catch (error) {
134
967
  spinner.fail("Failed to initialize Perceo");
135
- console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
968
+ if (error instanceof Error) {
969
+ console.error(chalk3.red(error.message));
970
+ if (process.env.PERCEO_DEBUG === "1" || process.env.NODE_ENV === "development") {
971
+ if (error.stack) {
972
+ console.error(chalk3.gray(error.stack));
973
+ }
974
+ }
975
+ } else {
976
+ try {
977
+ console.error(chalk3.red(`Unexpected error: ${JSON.stringify(error, null, 2)}`));
978
+ } catch {
979
+ console.error(chalk3.red(`Unexpected error: ${String(error)}`));
980
+ }
981
+ }
136
982
  process.exit(1);
137
983
  }
138
984
  });
139
985
  async function readPackageJson(projectDir) {
140
- const pkgPath = path2.join(projectDir, "package.json");
986
+ const pkgPath = path3.join(projectDir, "package.json");
141
987
  if (!await fileExists2(pkgPath)) return null;
142
988
  try {
143
989
  const raw = await fs2.readFile(pkgPath, "utf8");
@@ -147,10 +993,10 @@ async function readPackageJson(projectDir) {
147
993
  }
148
994
  }
149
995
  async function detectFramework(projectDir, pkg) {
150
- const nextConfig = await fileExists2(path2.join(projectDir, "next.config.js"));
151
- const nextConfigTs = await fileExists2(path2.join(projectDir, "next.config.ts"));
996
+ const nextConfig = await fileExists2(path3.join(projectDir, "next.config.js"));
997
+ const nextConfigTs = await fileExists2(path3.join(projectDir, "next.config.ts"));
152
998
  if (nextConfig || nextConfigTs) return "nextjs";
153
- const remixConfig = await fileExists2(path2.join(projectDir, "remix.config.js"));
999
+ const remixConfig = await fileExists2(path3.join(projectDir, "remix.config.js"));
154
1000
  if (remixConfig) return "remix";
155
1001
  const deps = {
156
1002
  ...pkg?.dependencies || {},
@@ -171,74 +1017,57 @@ async function fileExists2(p) {
171
1017
  return false;
172
1018
  }
173
1019
  }
174
- function createDefaultConfig(projectName, framework) {
1020
+ function detectDefaultBranch(projectDir) {
1021
+ try {
1022
+ const result = execSync2("git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'", {
1023
+ cwd: projectDir,
1024
+ encoding: "utf-8"
1025
+ }).trim();
1026
+ if (result) return result;
1027
+ } catch {
1028
+ }
1029
+ try {
1030
+ const currentBranch = execSync2("git branch --show-current", {
1031
+ cwd: projectDir,
1032
+ encoding: "utf-8"
1033
+ }).trim();
1034
+ if (currentBranch) return currentBranch;
1035
+ } catch {
1036
+ }
1037
+ try {
1038
+ execSync2("git rev-parse --verify main", {
1039
+ cwd: projectDir,
1040
+ encoding: "utf-8",
1041
+ stdio: "pipe"
1042
+ });
1043
+ return "main";
1044
+ } catch {
1045
+ }
1046
+ try {
1047
+ execSync2("git rev-parse --verify master", {
1048
+ cwd: projectDir,
1049
+ encoding: "utf-8",
1050
+ stdio: "pipe"
1051
+ });
1052
+ return "master";
1053
+ } catch {
1054
+ }
1055
+ return "main";
1056
+ }
1057
+ function createDefaultConfig(projectName, framework, projectId, branch) {
175
1058
  return {
176
1059
  version: "1.0",
177
1060
  project: {
1061
+ id: projectId,
178
1062
  name: projectName,
179
- framework
1063
+ framework,
1064
+ branch
180
1065
  },
181
1066
  observer: {
182
1067
  watch: {
183
1068
  paths: inferDefaultWatchPaths(framework),
184
1069
  ignore: ["node_modules/", ".next/", "dist/", "build/"],
185
- debounceMs: 500,
186
- autoTest: true
187
- },
188
- ci: {
189
- strategy: "affected-flows",
190
- parallelism: 5
191
- },
192
- analysis: {
193
- useLLM: true,
194
- llmThreshold: 0.7
195
- }
196
- },
197
- analyzer: {
198
- insights: {
199
- enabled: true,
200
- updateInterval: 3600,
201
- minSeverity: "medium"
202
- },
203
- predictions: {
204
- enabled: true,
205
- model: "ml",
206
- confidenceThreshold: 0.6
207
- },
208
- coverage: {
209
- minCoverageScore: 0.7,
210
- alertOnGaps: true
211
- }
212
- },
213
- analytics: {
214
- provider: "ga4",
215
- credentials: "${ANALYTICS_CREDENTIALS}",
216
- syncInterval: 300,
217
- correlation: {
218
- algorithm: "smith-waterman",
219
- minSimilarity: 0.7
220
- },
221
- revenueTracking: {
222
- enabled: true,
223
- avgOrderValueSource: "analytics"
224
- }
225
- },
226
- flowGraph: {
227
- endpoint: "bolt://localhost:7687",
228
- database: "Perceo"
229
- },
230
- eventBus: {
231
- type: "in-memory",
232
- redisUrl: "redis://localhost:6379"
233
- },
234
- notifications: {
235
- slack: {
236
- enabled: false,
237
- webhook: ""
238
- },
239
- email: {
240
- enabled: false,
241
- recipients: []
1070
+ debounceMs: 500
242
1071
  }
243
1072
  }
244
1073
  };
@@ -259,132 +1088,719 @@ function inferDefaultWatchPaths(framework) {
259
1088
  return ["src/"];
260
1089
  }
261
1090
  }
1091
+ function sleep(ms) {
1092
+ return new Promise((resolve) => setTimeout(resolve, ms));
1093
+ }
262
1094
  function createPerceoReadme(projectName) {
263
1095
  return `# Perceo configuration for ${projectName}
264
1096
 
265
- This folder was generated by \`perceo init\` and contains configuration for the Perceo CLI.
1097
+ This folder was generated by \`perceo init\`.
266
1098
 
267
1099
  ## Files
268
1100
 
269
- - \`.perceo/config.json\` \u2014 main configuration file for the Perceo CLI.
1101
+ - \`.perceo/config.json\` \u2014 project configuration (project id, name, branch). Access to this project is controlled in Perceo Cloud; only users added as members can view or change project data.
270
1102
 
271
- ## What this config does
1103
+ ## Commands
272
1104
 
273
- - Tells the Perceo backend which project and framework you are using.
274
- - Configures how the Observer, Analyzer, and Analytics features should behave for this project.
275
- - Stores connection settings that the Perceo backend and packages use to talk to managed services.
1105
+ - \`perceo analyze --base <branch>\` \u2014 analyze PR changes and find affected flows
1106
+ - \`perceo flows list\` \u2014 list all discovered flows
276
1107
 
277
- The actual connection logic and authentication to Perceo-managed services (including any databases, event buses, or analytics backends) is handled by Perceo\u2019s backend packages and platform \u2014 not by this CLI config.
1108
+ ## Environment Variables
278
1109
 
279
- In most cases you should only need to:
280
-
281
- 1. Review \`.perceo/config.json\` and adjust paths (for example, \`src/\` vs \`app/\`).
282
- 2. Commit \`.perceo/config.json\` to your repository (if you want it shared with your team).
283
- 3. Run:
1110
+ Set these to enable Supabase sync:
284
1111
 
285
1112
  \`\`\`bash
286
- perceo watch --dev --analyze
1113
+ PERCEO_SUPABASE_URL=https://your-project.supabase.co
1114
+ PERCEO_SUPABASE_ANON_KEY=your-anon-key
287
1115
  \`\`\`
1116
+ `;
1117
+ }
1118
+ function generateGitHubWorkflow(branch) {
1119
+ const branches = Array.from(/* @__PURE__ */ new Set([branch, "main", "master"])).join(", ");
1120
+ return `# Perceo CI - Automated regression impact analysis
1121
+ # Generated by perceo init
1122
+ # Docs: https://perceo.dev/docs/ci
1123
+
1124
+ name: Perceo CI
1125
+
1126
+ on:
1127
+ pull_request:
1128
+ branches: [${branches}]
1129
+ push:
1130
+ branches: [${branches}]
288
1131
 
289
- to start Perceo in your local development workflow.
1132
+ permissions:
1133
+ contents: read
1134
+ pull-requests: write # For PR comments
1135
+
1136
+ jobs:
1137
+ analyze:
1138
+ name: Analyze Changes
1139
+ runs-on: ubuntu-latest
1140
+
1141
+ steps:
1142
+ - name: Checkout
1143
+ uses: actions/checkout@v4
1144
+ with:
1145
+ fetch-depth: 0 # Full history for accurate diff
1146
+
1147
+ - name: Setup Node
1148
+ uses: actions/setup-node@v4
1149
+ with:
1150
+ node-version: '20'
1151
+
1152
+ - name: Install Perceo CLI
1153
+ run: npm install -g @perceo/perceo
1154
+
1155
+ - name: Analyze PR Changes
1156
+ if: github.event_name == 'pull_request'
1157
+ env:
1158
+ PERCEO_API_KEY: \${{ secrets.PERCEO_API_KEY }}
1159
+ run: |
1160
+ perceo ci analyze \\
1161
+ --base \${{ github.event.pull_request.base.sha }} \\
1162
+ --head \${{ github.sha }} \\
1163
+ --pr \${{ github.event.pull_request.number }}
1164
+
1165
+ - name: Analyze Push Changes
1166
+ if: github.event_name == 'push'
1167
+ env:
1168
+ PERCEO_API_KEY: \${{ secrets.PERCEO_API_KEY }}
1169
+ run: |
1170
+ perceo ci analyze \\
1171
+ --base \${{ github.event.before }} \\
1172
+ --head \${{ github.sha }}
290
1173
  `;
291
1174
  }
292
1175
 
293
- // src/commands/watch.ts
1176
+ // src/commands/logout.ts
294
1177
  import { Command as Command2 } from "commander";
295
- import chalk2 from "chalk";
296
- import ora2 from "ora";
297
- import { ObserverEngine as ObserverEngine2 } from "@perceo/observer-engine";
298
- var watchCommand = new Command2("watch").description("Watch for code changes and run tests").option("--dev", "Run against local development server").option("--port <port>", "Development server port", "3000").action(async (options) => {
299
- const spinner = ora2("Starting Perceo observer...").start();
1178
+ import chalk4 from "chalk";
1179
+ import path4 from "path";
1180
+ var logoutCommand = new Command2("logout").description("Log out from Perceo (remove stored auth for the given scope)").option("-s, --scope <scope>", "Which login to remove: 'project' or 'global'", "global").option("-d, --dir <directory>", "Project directory (for project scope)", process.cwd()).action(async (options) => {
1181
+ const scope = options.scope?.toLowerCase() === "project" ? "project" : "global";
1182
+ const projectDir = path4.resolve(options.dir || process.cwd());
1183
+ const existing = await getStoredAuth(scope, scope === "project" ? projectDir : void 0);
1184
+ if (!existing?.access_token) {
1185
+ console.log(chalk4.yellow(`Not logged in (${scope} scope). Nothing to do.`));
1186
+ return;
1187
+ }
1188
+ await clearStoredAuth(scope, scope === "project" ? projectDir : void 0);
1189
+ console.log(chalk4.green(`Logged out successfully (${scope} scope).`));
1190
+ });
1191
+
1192
+ // src/commands/del.ts
1193
+ import { Command as Command3 } from "commander";
1194
+ import chalk5 from "chalk";
1195
+ import fs3 from "fs/promises";
1196
+ import path5 from "path";
1197
+ import { createInterface as createInterface2 } from "readline";
1198
+ var CONFIG_DIR3 = ".perceo";
1199
+ var CONFIG_FILE2 = "config.json";
1200
+ var GITHUB_WORKFLOW_PATH = ".github/workflows/perceo.yml";
1201
+ async function fileExists3(p) {
300
1202
  try {
301
- const config = await loadConfig();
302
- const observerConfig = {
303
- observer: config.observer,
304
- flowGraph: config.flowGraph,
305
- eventBus: config.eventBus
1203
+ await fs3.access(p);
1204
+ return true;
1205
+ } catch {
1206
+ return false;
1207
+ }
1208
+ }
1209
+ async function readProjectFromConfig(projectDir) {
1210
+ const configPath = path5.join(projectDir, CONFIG_DIR3, CONFIG_FILE2);
1211
+ if (!await fileExists3(configPath)) return { id: null, name: null };
1212
+ try {
1213
+ const raw = await fs3.readFile(configPath, "utf8");
1214
+ const config = JSON.parse(raw);
1215
+ const project = config?.project;
1216
+ return {
1217
+ id: project?.id ?? null,
1218
+ name: project?.name ?? null
1219
+ };
1220
+ } catch {
1221
+ return { id: null, name: null };
1222
+ }
1223
+ }
1224
+ async function fetchProjectData(client, projectId) {
1225
+ try {
1226
+ const project = await client.getProject(projectId);
1227
+ if (!project) return null;
1228
+ const [flows, personas, apiKeys, recentTestRuns, insights] = await Promise.all([
1229
+ client.getFlows(projectId).catch(() => []),
1230
+ client.getPersonas(projectId).catch(() => []),
1231
+ client.getApiKeys(projectId).catch(() => []),
1232
+ client.getRecentTestRuns(projectId, 1e3).catch(() => []),
1233
+ client.getInsights(projectId).catch(() => [])
1234
+ ]);
1235
+ return {
1236
+ id: project.id,
1237
+ name: project.name,
1238
+ flows: flows.map((f) => ({ id: f.id, name: f.name })),
1239
+ personas: personas.map((p) => ({ id: p.id, name: p.name })),
1240
+ apiKeys: apiKeys.map((k) => ({ id: k.id, name: k.name, key_prefix: k.key_prefix })),
1241
+ testRunsCount: recentTestRuns.length,
1242
+ insightsCount: insights.length
306
1243
  };
307
- const engine = new ObserverEngine2(observerConfig);
308
- spinner.succeed("Observer started");
309
- console.log(chalk2.blue("\n\u{1F440} Watching for changes..."));
310
- console.log(chalk2.gray("Press Ctrl+C to stop\n"));
311
- const envLabel = (process.env.PERCEO_ENV || "").toLowerCase() === "local" ? chalk2.yellow("local (using .perceo/config.local.json overrides when present)") : chalk2.green("default");
312
- console.log(chalk2.bold("Environment: "), envLabel);
313
- if (config?.flowGraph?.endpoint) {
314
- console.log(chalk2.bold("Flow graph endpoint: "), config.flowGraph.endpoint);
315
- }
316
- if (config?.eventBus?.type) {
317
- console.log(chalk2.bold("Event bus: "), config.eventBus.type);
318
- }
319
- if (options.dev) {
320
- console.log(chalk2.green(`\u2713 Connected to localhost:${options.port}`));
321
- }
322
- process.stdin.resume();
323
1244
  } catch (error) {
324
- spinner.fail("Failed to start observer");
325
- console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
326
- process.exit(1);
1245
+ console.warn(chalk5.yellow(`Warning: Could not fetch complete project data: ${error instanceof Error ? error.message : String(error)}`));
1246
+ return null;
1247
+ }
1248
+ }
1249
+ async function requireReauth(projectDir) {
1250
+ console.log(chalk5.yellow.bold("\n\u{1F512} Re-authentication Required"));
1251
+ console.log(chalk5.gray("For security, you must re-authenticate before deleting project data."));
1252
+ console.log(chalk5.gray("Your current session will be cleared and you'll need to log in again.\n"));
1253
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1254
+ const answer = await new Promise((resolve) => {
1255
+ rl.question(chalk5.bold("Continue with re-authentication? [y/N]: "), (ans) => {
1256
+ rl.close();
1257
+ resolve((ans || "n").trim().toLowerCase());
1258
+ });
1259
+ });
1260
+ if (answer !== "y" && answer !== "yes") {
1261
+ console.log(chalk5.gray("Cancelled."));
1262
+ return false;
1263
+ }
1264
+ try {
1265
+ await clearStoredAuth("project", projectDir);
1266
+ await clearStoredAuth("global");
1267
+ console.log(chalk5.gray("Existing authentication cleared."));
1268
+ } catch (error) {
1269
+ console.warn(chalk5.yellow(`Warning: Could not clear existing auth: ${error instanceof Error ? error.message : String(error)}`));
1270
+ }
1271
+ console.log(chalk5.cyan("\nPlease log in again:"));
1272
+ try {
1273
+ const { loginCommand: loginCommand2 } = await import("./login-47R62VE3.js");
1274
+ await loginCommand2.parseAsync(["login", "--scope", "global"], { from: "user" });
1275
+ return true;
1276
+ } catch (error) {
1277
+ console.error(chalk5.red(`Re-authentication failed: ${error instanceof Error ? error.message : String(error)}`));
1278
+ return false;
1279
+ }
1280
+ }
1281
+ var delCommand = new Command3("del").description("Remove Perceo from this project - PERMANENTLY deletes ALL project data including flows, personas, test runs, and API keys").alias("rm").option("-d, --dir <directory>", "Project directory", process.cwd()).option("-y, --yes", "Skip confirmation prompt (NOT recommended)", false).option("--skip-server", "Only remove local files; do not delete the project on the server", false).option("--keep-github", "Do not remove .github/workflows/perceo.yml", false).option("--skip-reauth", "Skip re-authentication requirement (DANGEROUS - for development only)", false).action(async (options) => {
1282
+ const projectDir = path5.resolve(options.dir || process.cwd());
1283
+ const perceoDir = path5.join(projectDir, CONFIG_DIR3);
1284
+ const workflowPath = path5.join(projectDir, GITHUB_WORKFLOW_PATH);
1285
+ const hasPerceoDir = await fileExists3(perceoDir);
1286
+ const hasWorkflow = await fileExists3(workflowPath);
1287
+ const { id: projectId, name: projectName } = await readProjectFromConfig(projectDir);
1288
+ const willDeleteServer = !options["skip-server"] && projectId;
1289
+ if (!hasPerceoDir && !hasWorkflow && !willDeleteServer) {
1290
+ console.log(chalk5.yellow("No Perceo setup found in this directory. Nothing to remove."));
1291
+ return;
1292
+ }
1293
+ if (willDeleteServer && !options["skip-reauth"]) {
1294
+ const reauthSuccess = await requireReauth(projectDir);
1295
+ if (!reauthSuccess) {
1296
+ console.log(chalk5.red("Re-authentication failed. Deletion cancelled for security."));
1297
+ process.exit(1);
1298
+ }
1299
+ }
1300
+ let projectData = null;
1301
+ let deleteClient = null;
1302
+ let resolvedProjectId = projectId;
1303
+ if (willDeleteServer && (projectId || projectName)) {
1304
+ try {
1305
+ if (projectId) {
1306
+ const access = await ensureProjectAccess({ projectDir, requireAdmin: true });
1307
+ deleteClient = access.client;
1308
+ resolvedProjectId = access.projectId;
1309
+ projectData = await fetchProjectData(deleteClient, access.projectId);
1310
+ } else {
1311
+ const access = await ensureProjectAccess({ projectDir, requireAdmin: false });
1312
+ const project = await access.client.getProjectByName(projectName);
1313
+ if (!project) {
1314
+ console.error(chalk5.red("Project not found or you don't have access."));
1315
+ process.exit(1);
1316
+ }
1317
+ const role = await checkProjectAccess(access.client, project.id, { requireAdmin: true });
1318
+ if (!role) {
1319
+ console.error(chalk5.red("Deleting the project requires owner or admin role."));
1320
+ process.exit(1);
1321
+ }
1322
+ deleteClient = access.client;
1323
+ resolvedProjectId = project.id;
1324
+ projectData = await fetchProjectData(deleteClient, project.id);
1325
+ }
1326
+ } catch (err) {
1327
+ console.warn(chalk5.yellow(`Warning: Could not fetch project data for confirmation: ${err instanceof Error ? err.message : String(err)}`));
1328
+ }
1329
+ }
1330
+ if (!options.yes) {
1331
+ console.log(chalk5.red.bold("\n\u26A0\uFE0F DESTRUCTIVE OPERATION - PERMANENT DATA DELETION"));
1332
+ console.log(chalk5.red("This will permanently delete ALL Perceo data for this project."));
1333
+ console.log(chalk5.red("This action CANNOT be undone.\n"));
1334
+ console.log(chalk5.bold("\u{1F4C1} Local files to be removed:"));
1335
+ console.log(chalk5.gray(" Directory: ") + chalk5.cyan(projectDir));
1336
+ if (hasPerceoDir) console.log(chalk5.gray(" \u2022 ") + chalk5.cyan(".perceo/ (config, auth, etc.)"));
1337
+ if (hasWorkflow && !options["keep-github"]) console.log(chalk5.gray(" \u2022 ") + chalk5.cyan(GITHUB_WORKFLOW_PATH));
1338
+ if (willDeleteServer) {
1339
+ console.log(chalk5.bold("\n\u{1F5C4}\uFE0F Server data to be PERMANENTLY DELETED:"));
1340
+ if (projectData) {
1341
+ console.log(chalk5.gray(" Project: ") + chalk5.cyan(`${projectData.name} (${projectData.id})`));
1342
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red(`${projectData.flows.length} flows`) + chalk5.gray(" (including all steps)"));
1343
+ if (projectData.flows.length > 0) {
1344
+ projectData.flows.slice(0, 5).forEach((flow) => {
1345
+ console.log(chalk5.gray(" - ") + chalk5.cyan(flow.name));
1346
+ });
1347
+ if (projectData.flows.length > 5) {
1348
+ console.log(chalk5.gray(` ... and ${projectData.flows.length - 5} more`));
1349
+ }
1350
+ }
1351
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red(`${projectData.personas.length} personas`));
1352
+ if (projectData.personas.length > 0) {
1353
+ projectData.personas.slice(0, 3).forEach((persona) => {
1354
+ console.log(chalk5.gray(" - ") + chalk5.cyan(persona.name));
1355
+ });
1356
+ if (projectData.personas.length > 3) {
1357
+ console.log(chalk5.gray(` ... and ${projectData.personas.length - 3} more`));
1358
+ }
1359
+ }
1360
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red(`${projectData.apiKeys.length} API keys`));
1361
+ if (projectData.apiKeys.length > 0) {
1362
+ projectData.apiKeys.forEach((key) => {
1363
+ console.log(chalk5.gray(" - ") + chalk5.cyan(`${key.name} (${key.key_prefix}...)`));
1364
+ });
1365
+ }
1366
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red(`${projectData.testRunsCount} test runs`));
1367
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red(`${projectData.insightsCount} insights`));
1368
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red("All project secrets"));
1369
+ } else {
1370
+ console.log(chalk5.gray(" Project: ") + chalk5.cyan(projectId ?? projectName ?? "Unknown"));
1371
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red("All flows, personas, and related data"));
1372
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red("All API keys and project secrets"));
1373
+ console.log(chalk5.gray(" \u2022 ") + chalk5.red("All test runs and insights"));
1374
+ }
1375
+ }
1376
+ console.log(chalk5.red.bold("\n\u{1F480} This deletion is PERMANENT and IRREVERSIBLE."));
1377
+ console.log(chalk5.gray("Type the project name to confirm deletion:"));
1378
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1379
+ const expectedName = projectData?.name ?? projectName ?? "CONFIRM";
1380
+ const answer = await new Promise((resolve) => {
1381
+ rl.question(chalk5.bold(`Enter "${expectedName}" to confirm: `), (ans) => {
1382
+ rl.close();
1383
+ resolve((ans || "").trim());
1384
+ });
1385
+ });
1386
+ if (answer !== expectedName) {
1387
+ console.log(chalk5.gray("Project name does not match. Deletion cancelled."));
1388
+ process.exit(0);
1389
+ }
1390
+ const finalAnswer = await new Promise((resolve) => {
1391
+ const rl2 = createInterface2({ input: process.stdin, output: process.stdout });
1392
+ rl2.question(chalk5.red.bold("Are you absolutely sure? This cannot be undone. [type 'DELETE']: "), (ans) => {
1393
+ rl2.close();
1394
+ resolve((ans || "").trim());
1395
+ });
1396
+ });
1397
+ if (finalAnswer !== "DELETE") {
1398
+ console.log(chalk5.gray("Final confirmation failed. Deletion cancelled."));
1399
+ process.exit(0);
1400
+ }
1401
+ }
1402
+ console.log(chalk5.yellow("\n\u{1F5D1}\uFE0F Starting deletion process..."));
1403
+ const deletionSummary = {
1404
+ localFiles: [],
1405
+ serverData: {
1406
+ project: null,
1407
+ flows: 0,
1408
+ personas: 0,
1409
+ apiKeys: 0,
1410
+ testRuns: 0,
1411
+ insights: 0,
1412
+ secrets: 0
1413
+ }
1414
+ };
1415
+ if (willDeleteServer && deleteClient && resolvedProjectId) {
1416
+ try {
1417
+ if (projectData) {
1418
+ deletionSummary.serverData = {
1419
+ project: { id: projectData.id, name: projectData.name },
1420
+ flows: projectData.flows.length,
1421
+ personas: projectData.personas.length,
1422
+ apiKeys: projectData.apiKeys.length,
1423
+ testRuns: projectData.testRunsCount,
1424
+ insights: projectData.insightsCount,
1425
+ secrets: 0
1426
+ // We can't easily count secrets, but they'll be deleted by cascade
1427
+ };
1428
+ }
1429
+ await deleteClient.deleteProject(resolvedProjectId);
1430
+ console.log(chalk5.green("\u2705 Deleted project and all related data on server"));
1431
+ } catch (err) {
1432
+ console.error(chalk5.red("\u274C Failed to delete project on server: " + (err instanceof Error ? err.message : String(err))));
1433
+ process.exit(1);
1434
+ }
1435
+ }
1436
+ if (hasWorkflow && !options["keep-github"]) {
1437
+ await fs3.rm(workflowPath, { force: true });
1438
+ deletionSummary.localFiles.push(GITHUB_WORKFLOW_PATH);
1439
+ console.log(chalk5.green("\u2705 Removed ") + chalk5.cyan(GITHUB_WORKFLOW_PATH));
1440
+ }
1441
+ if (hasPerceoDir) {
1442
+ await fs3.rm(perceoDir, { recursive: true, force: true });
1443
+ deletionSummary.localFiles.push(".perceo/");
1444
+ console.log(chalk5.green("\u2705 Removed ") + chalk5.cyan(".perceo/"));
1445
+ }
1446
+ console.log(chalk5.bold.green("\n\u{1F389} Perceo successfully removed from this project"));
1447
+ console.log(chalk5.bold("\u{1F4CA} Deletion Summary:"));
1448
+ if (deletionSummary.localFiles.length > 0) {
1449
+ console.log(chalk5.gray(" Local files removed:"));
1450
+ deletionSummary.localFiles.forEach((file) => {
1451
+ console.log(chalk5.gray(" \u2022 ") + chalk5.cyan(file));
1452
+ });
327
1453
  }
1454
+ if (deletionSummary.serverData.project) {
1455
+ console.log(chalk5.gray(" Server data deleted:"));
1456
+ console.log(chalk5.gray(" \u2022 Project: ") + chalk5.cyan(deletionSummary.serverData.project.name));
1457
+ console.log(chalk5.gray(" \u2022 Flows: ") + chalk5.red(deletionSummary.serverData.flows.toString()));
1458
+ console.log(chalk5.gray(" \u2022 Personas: ") + chalk5.red(deletionSummary.serverData.personas.toString()));
1459
+ console.log(chalk5.gray(" \u2022 API Keys: ") + chalk5.red(deletionSummary.serverData.apiKeys.toString()));
1460
+ console.log(chalk5.gray(" \u2022 Test Runs: ") + chalk5.red(deletionSummary.serverData.testRuns.toString()));
1461
+ console.log(chalk5.gray(" \u2022 Insights: ") + chalk5.red(deletionSummary.serverData.insights.toString()));
1462
+ console.log(chalk5.gray(" \u2022 Project secrets and all related data"));
1463
+ }
1464
+ console.log(chalk5.gray("\nTo set up Perceo again, run: ") + chalk5.cyan("perceo init"));
328
1465
  });
329
1466
 
330
- // src/commands/ci.ts
331
- import { Command as Command3 } from "commander";
332
- import chalk3 from "chalk";
1467
+ // src/commands/analyze.ts
1468
+ import { Command as Command4 } from "commander";
1469
+ import chalk6 from "chalk";
333
1470
  import ora3 from "ora";
334
- import { ObserverEngine as ObserverEngine3 } from "@perceo/observer-engine";
335
- var ci = new Command3("ci").description("CI / PR analysis and testing");
336
- ci.command("analyze").description("Analyze changes between two Git refs and report affected flows").requiredOption("--base <sha>", "Base Git ref / SHA").requiredOption("--head <sha>", "Head Git ref / SHA").option("--project-dir <dir>", "Project directory (defaults to process.cwd())").option("--json", "Print machine-readable JSON output", false).action(async (options) => {
337
- const projectRoot = options.projectDir ? options.projectDir : process.cwd();
338
- const spinner = ora3("Analyzing changes with Perceo Observer Engine...").start();
1471
+ import path6 from "path";
1472
+ import { computeChangeAnalysis } from "@perceo/observer-engine";
1473
+ var analyzeCommand = new Command4("analyze").description("Analyze git diff to find affected flows").requiredOption("--base <sha>", "Base Git ref (e.g., main, origin/main, commit SHA)").option("--head <sha>", "Head Git ref (default: HEAD)", "HEAD").option("--project-dir <dir>", "Project directory", process.cwd()).option("--json", "Output JSON for CI integration", false).action(async (options) => {
1474
+ const projectRoot = path6.resolve(options.projectDir ?? process.cwd());
1475
+ const spinner = ora3("Analyzing changes...").start();
339
1476
  try {
340
- const rawConfig = await loadConfig({ projectDir: projectRoot });
341
- const observerConfig = {
342
- observer: rawConfig.observer,
343
- flowGraph: rawConfig.flowGraph,
344
- eventBus: rawConfig.eventBus
345
- };
346
- const engine = new ObserverEngine3(observerConfig);
347
- const report = await engine.analyzeChanges({
348
- baseSha: options.base,
349
- headSha: options.head,
350
- projectRoot
1477
+ const { client, projectId, projectName } = await ensureProjectAccess({ projectDir: projectRoot });
1478
+ spinner.text = "Computing git diff...";
1479
+ const baseSha = options.base;
1480
+ const headSha = options.head ?? "HEAD";
1481
+ const analysis = await computeChangeAnalysis(projectRoot, baseSha, headSha);
1482
+ if (analysis.files.length === 0) {
1483
+ spinner.succeed("No changes found");
1484
+ if (options.json) {
1485
+ console.log(JSON.stringify({ flows: [], changes: [], riskLevel: "low", riskScore: 0 }, null, 2));
1486
+ } else {
1487
+ console.log(chalk6.green("\n\u2713 No files changed between the commits"));
1488
+ }
1489
+ return;
1490
+ }
1491
+ spinner.text = "Matching against flows...";
1492
+ const flows = await client.getFlows(projectId);
1493
+ if (flows.length === 0) {
1494
+ spinner.warn("No flows found");
1495
+ console.log(chalk6.yellow("\nNo flows have been discovered yet. Run `perceo init` to discover flows."));
1496
+ return;
1497
+ }
1498
+ const affectedFlows = matchAffectedFlows(flows, analysis.files);
1499
+ spinner.text = "Storing analysis...";
1500
+ const codeChange = await client.createCodeChange({
1501
+ project_id: projectId,
1502
+ base_sha: baseSha,
1503
+ head_sha: headSha,
1504
+ files: analysis.files.map((f) => ({
1505
+ path: f.path,
1506
+ status: f.status
1507
+ }))
1508
+ });
1509
+ const riskScore = calculateOverallRisk(affectedFlows);
1510
+ const riskLevel = getRiskLevel(riskScore);
1511
+ await client.updateCodeChangeAnalysis(codeChange.id, {
1512
+ risk_level: riskLevel,
1513
+ risk_score: riskScore,
1514
+ affected_flow_ids: affectedFlows.map((f) => f.flow.id)
351
1515
  });
1516
+ if (affectedFlows.length > 0) {
1517
+ await client.markFlowsAffected(
1518
+ affectedFlows.map((f) => f.flow.id),
1519
+ codeChange.id,
1520
+ riskScore * 0.2
1521
+ );
1522
+ }
352
1523
  spinner.succeed("Analysis complete");
1524
+ const result = {
1525
+ projectId,
1526
+ projectName,
1527
+ baseSha,
1528
+ headSha,
1529
+ flows: affectedFlows.map((f) => ({
1530
+ id: f.flow.id,
1531
+ name: f.flow.name,
1532
+ priority: f.flow.priority,
1533
+ riskScore: f.riskScore,
1534
+ confidence: f.confidence,
1535
+ matchedFiles: f.matchedFiles
1536
+ })),
1537
+ changes: analysis.files,
1538
+ riskLevel,
1539
+ riskScore,
1540
+ createdAt: Date.now()
1541
+ };
353
1542
  if (options.json) {
354
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
355
- return;
1543
+ console.log(JSON.stringify(result, null, 2));
1544
+ } else {
1545
+ printResults(result);
356
1546
  }
357
- console.log();
358
- console.log(chalk3.bold("Change: ") + `${report.baseSha}...${report.headSha}`);
359
- console.log(chalk3.bold("Flows affected: ") + report.flows.length);
360
- if (report.flows.length > 0) {
361
- console.log();
362
- for (const flow of report.flows) {
363
- const risk = flow.riskScore.toFixed(2);
364
- const confidence = (flow.confidence * 100).toFixed(0) + "%";
365
- const priority = flow.priority ? ` [${flow.priority}]` : "";
366
- console.log(`- ${chalk3.cyan(flow.name)}${priority} (risk=${risk}, confidence=${confidence})`);
1547
+ } catch (error) {
1548
+ spinner.fail("Analysis failed");
1549
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1550
+ process.exit(1);
1551
+ }
1552
+ });
1553
+ function matchAffectedFlows(flows, changedFiles) {
1554
+ const affected = [];
1555
+ for (const flow of flows) {
1556
+ const matchedFiles = [];
1557
+ let maxConfidence = 0;
1558
+ for (const file of changedFiles) {
1559
+ const confidence = calculateMatchConfidence(flow, file.path);
1560
+ if (confidence > 0.3) {
1561
+ matchedFiles.push(file.path);
1562
+ maxConfidence = Math.max(maxConfidence, confidence);
367
1563
  }
368
1564
  }
369
- if (report.changes.length > 0) {
370
- console.log();
371
- console.log(chalk3.bold("Files changed:"));
372
- for (const file of report.changes) {
373
- console.log(`- [${file.status}] ${file.path}`);
1565
+ if (matchedFiles.length > 0) {
1566
+ affected.push({
1567
+ flow,
1568
+ riskScore: calculateFlowRisk(flow, matchedFiles, changedFiles),
1569
+ confidence: maxConfidence,
1570
+ matchedFiles
1571
+ });
1572
+ }
1573
+ }
1574
+ return affected.sort((a, b) => b.riskScore - a.riskScore);
1575
+ }
1576
+ function calculateMatchConfidence(flow, filePath) {
1577
+ const normalizedPath = filePath.toLowerCase();
1578
+ const flowName = flow.name.toLowerCase();
1579
+ if (flow.entry_point) {
1580
+ const entryNormalized = flow.entry_point.toLowerCase().replace(/\//g, "");
1581
+ if (normalizedPath.includes(entryNormalized)) {
1582
+ return 0.9;
1583
+ }
1584
+ }
1585
+ const keywords = flowName.split(/[\s-_]+/).filter((k) => k.length > 2);
1586
+ for (const keyword of keywords) {
1587
+ if (normalizedPath.includes(keyword)) {
1588
+ return 0.7;
1589
+ }
1590
+ }
1591
+ const graphData = flow.graph_data;
1592
+ if (graphData) {
1593
+ const allRefs = [...graphData.components ?? [], ...graphData.pages ?? []];
1594
+ for (const ref of allRefs) {
1595
+ if (normalizedPath.includes(ref.toLowerCase())) {
1596
+ return 0.8;
374
1597
  }
375
1598
  }
1599
+ }
1600
+ return 0;
1601
+ }
1602
+ function calculateFlowRisk(flow, matchedFiles, allChanges) {
1603
+ const priorityWeight = {
1604
+ critical: 1,
1605
+ high: 0.75,
1606
+ medium: 0.5,
1607
+ low: 0.25
1608
+ };
1609
+ const baseRisk = priorityWeight[flow.priority] ?? 0.5;
1610
+ const fileRatio = matchedFiles.length / Math.max(allChanges.length, 1);
1611
+ return Math.min(1, baseRisk + fileRatio * 0.3);
1612
+ }
1613
+ function calculateOverallRisk(affected) {
1614
+ if (affected.length === 0) return 0;
1615
+ let totalWeight = 0;
1616
+ let weightedRisk = 0;
1617
+ for (const a of affected) {
1618
+ const weight = a.flow.priority === "critical" ? 2 : a.flow.priority === "high" ? 1.5 : 1;
1619
+ totalWeight += weight;
1620
+ weightedRisk += a.riskScore * weight;
1621
+ }
1622
+ return Math.min(1, weightedRisk / totalWeight);
1623
+ }
1624
+ function getRiskLevel(score) {
1625
+ if (score >= 0.8) return "critical";
1626
+ if (score >= 0.6) return "high";
1627
+ if (score >= 0.3) return "medium";
1628
+ return "low";
1629
+ }
1630
+ function printResults(result) {
1631
+ console.log();
1632
+ console.log(chalk6.bold("Project: ") + result.projectName);
1633
+ console.log(chalk6.bold("Diff: ") + `${result.baseSha}...${result.headSha}`);
1634
+ console.log();
1635
+ const riskColor = result.riskLevel === "critical" ? chalk6.red : result.riskLevel === "high" ? chalk6.yellow : result.riskLevel === "medium" ? chalk6.blue : chalk6.green;
1636
+ console.log(chalk6.bold("Overall Risk: ") + riskColor(`${result.riskLevel.toUpperCase()} (${(result.riskScore * 100).toFixed(0)}%)`));
1637
+ console.log();
1638
+ if (result.flows.length === 0) {
1639
+ console.log(chalk6.green("\u2713 No flows affected by these changes"));
1640
+ } else {
1641
+ console.log(chalk6.bold(`${result.flows.length} flow(s) affected:`));
1642
+ console.log();
1643
+ for (const flow of result.flows) {
1644
+ const riskColor2 = flow.riskScore > 0.7 ? chalk6.red : flow.riskScore > 0.4 ? chalk6.yellow : chalk6.green;
1645
+ const priorityColor = flow.priority === "critical" ? chalk6.red : flow.priority === "high" ? chalk6.yellow : chalk6.gray;
1646
+ const risk = (flow.riskScore * 100).toFixed(0);
1647
+ const confidence = (flow.confidence * 100).toFixed(0);
1648
+ console.log(` ${chalk6.cyan(flow.name)} ${priorityColor(`[${flow.priority}]`)}`);
1649
+ console.log(` Risk: ${riskColor2(`${risk}%`)} Confidence: ${confidence}%`);
1650
+ console.log(` Matched: ${chalk6.gray(flow.matchedFiles.slice(0, 3).join(", "))}${flow.matchedFiles.length > 3 ? ` (+${flow.matchedFiles.length - 3})` : ""}`);
1651
+ }
1652
+ }
1653
+ console.log();
1654
+ console.log(chalk6.bold(`${result.changes.length} file(s) changed:`));
1655
+ const maxShow = 10;
1656
+ for (const file of result.changes.slice(0, maxShow)) {
1657
+ const icon = file.status === "added" ? chalk6.green("+") : file.status === "deleted" ? chalk6.red("-") : chalk6.yellow("~");
1658
+ console.log(` ${icon} ${file.path}`);
1659
+ }
1660
+ if (result.changes.length > maxShow) {
1661
+ console.log(chalk6.gray(` ... and ${result.changes.length - maxShow} more`));
1662
+ }
1663
+ console.log();
1664
+ if (result.riskLevel === "critical" || result.riskLevel === "high") {
1665
+ console.log(chalk6.red(`\u26A0\uFE0F High-risk changes detected. Recommend running tests for affected flows.`));
1666
+ } else if (result.flows.length > 0) {
1667
+ console.log(chalk6.yellow(`\u2139\uFE0F ${result.flows.length} flow(s) may be affected. Consider testing them.`));
1668
+ } else {
1669
+ console.log(chalk6.green(`\u2713 Low risk - no critical flows affected.`));
1670
+ }
1671
+ }
1672
+
1673
+ // src/commands/keys.ts
1674
+ import { Command as Command5 } from "commander";
1675
+ import chalk7 from "chalk";
1676
+ import ora4 from "ora";
1677
+ var keysCommand = new Command5("keys").description("Manage project API keys for CI/CD authentication");
1678
+ keysCommand.command("list").description("List all API keys for the current project").option("-a, --all", "Include revoked and expired keys", false).action(async (options) => {
1679
+ const spinner = ora4("Loading API keys...").start();
1680
+ try {
1681
+ const { client, projectId } = await ensureProjectAccess();
1682
+ const keys = options.all ? await client.getApiKeys(projectId) : await client.getActiveApiKeys(projectId);
1683
+ spinner.stop();
1684
+ if (keys.length === 0) {
1685
+ console.log(chalk7.yellow("No API keys found."));
1686
+ console.log(chalk7.gray("Create one with: perceo keys create --name <name>"));
1687
+ return;
1688
+ }
1689
+ console.log(chalk7.bold("\nAPI Keys:\n"));
1690
+ console.log(chalk7.gray("\u2500".repeat(80)));
1691
+ for (const key of keys) {
1692
+ const status = getKeyStatus(key);
1693
+ const statusColor = status === "active" ? chalk7.green : status === "expired" ? chalk7.yellow : chalk7.red;
1694
+ console.log(` ${chalk7.bold(key.name)}`);
1695
+ console.log(` Prefix: ${chalk7.cyan(key.key_prefix)}...`);
1696
+ console.log(` Status: ${statusColor(status)}`);
1697
+ console.log(` Scopes: ${chalk7.gray(key.scopes.join(", "))}`);
1698
+ console.log(` Created: ${chalk7.gray(formatDate(key.created_at))}`);
1699
+ if (key.last_used_at) {
1700
+ console.log(` Last used: ${chalk7.gray(formatDate(key.last_used_at))}`);
1701
+ }
1702
+ if (key.expires_at) {
1703
+ console.log(` Expires: ${chalk7.gray(formatDate(key.expires_at))}`);
1704
+ }
1705
+ console.log(chalk7.gray("\u2500".repeat(80)));
1706
+ }
1707
+ console.log(chalk7.gray(`
1708
+ Total: ${keys.length} key(s)`));
376
1709
  } catch (error) {
377
- spinner.fail("Analysis failed");
378
- console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
1710
+ spinner.fail("Failed to load API keys");
1711
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1712
+ process.exit(1);
1713
+ }
1714
+ });
1715
+ keysCommand.command("create").description("Create a new API key").requiredOption("-n, --name <name>", "Name for the API key (e.g., 'github-actions', 'jenkins')").option("-s, --scopes <scopes>", "Comma-separated list of scopes", "ci:analyze,ci:test,flows:read,insights:read,events:publish").option("-e, --expires <days>", "Number of days until expiration (default: never)").action(async (options) => {
1716
+ const spinner = ora4("Creating API key...").start();
1717
+ try {
1718
+ const { client, projectId } = await ensureProjectAccess({ requireAdmin: true });
1719
+ const scopes = options.scopes.split(",").map((s) => s.trim());
1720
+ const expiresAt = options.expires ? new Date(Date.now() + parseInt(options.expires, 10) * 24 * 60 * 60 * 1e3) : void 0;
1721
+ const { key, keyRecord } = await client.createApiKey(projectId, {
1722
+ name: options.name,
1723
+ scopes,
1724
+ expiresAt
1725
+ });
1726
+ spinner.succeed("API key created successfully!");
1727
+ console.log("\n" + chalk7.bold.green("New API Key:"));
1728
+ console.log(chalk7.gray("\u2500".repeat(60)));
1729
+ console.log(` Name: ${chalk7.bold(keyRecord.name)}`);
1730
+ console.log(` Prefix: ${chalk7.cyan(keyRecord.key_prefix)}...`);
1731
+ console.log(` Scopes: ${chalk7.gray(scopes.join(", "))}`);
1732
+ if (expiresAt) {
1733
+ console.log(` Expires: ${chalk7.gray(formatDate(expiresAt.toISOString()))}`);
1734
+ }
1735
+ console.log(chalk7.gray("\u2500".repeat(60)));
1736
+ console.log("\n" + chalk7.bold.yellow(" API Key (copy this now - shown only once):"));
1737
+ console.log("\n " + chalk7.green(key));
1738
+ console.log("\n" + chalk7.gray("\u2500".repeat(60)));
1739
+ console.log(chalk7.gray("\n Add this as a secret in your CI environment:"));
1740
+ console.log(chalk7.gray(" - GitHub: Settings \u2192 Secrets \u2192 PERCEO_API_KEY"));
1741
+ console.log(chalk7.gray(" - GitLab: Settings \u2192 CI/CD \u2192 Variables"));
1742
+ console.log(chalk7.gray(" - Other: Set PERCEO_API_KEY environment variable\n"));
1743
+ } catch (error) {
1744
+ spinner.fail("Failed to create API key");
1745
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1746
+ process.exit(1);
1747
+ }
1748
+ });
1749
+ keysCommand.command("revoke").description("Revoke an API key").argument("<prefix>", "Key prefix to revoke (e.g., prc_abc12345)").option("-r, --reason <reason>", "Reason for revocation").action(async (prefix, options) => {
1750
+ const spinner = ora4("Revoking API key...").start();
1751
+ try {
1752
+ const { client, projectId } = await ensureProjectAccess({ requireAdmin: true });
1753
+ const keys = await client.getApiKeys(projectId);
1754
+ const keyToRevoke = keys.find((k) => k.key_prefix === prefix || k.key_prefix.startsWith(prefix));
1755
+ if (!keyToRevoke) {
1756
+ spinner.fail(`No key found with prefix: ${prefix}`);
1757
+ console.log(chalk7.gray("Run 'perceo keys list' to see available keys."));
1758
+ process.exit(1);
1759
+ }
1760
+ if (keyToRevoke.revoked_at) {
1761
+ spinner.fail(`Key ${keyToRevoke.name} is already revoked.`);
1762
+ process.exit(1);
1763
+ }
1764
+ await client.revokeApiKey(keyToRevoke.id, { reason: options.reason });
1765
+ spinner.succeed(`API key "${keyToRevoke.name}" revoked successfully!`);
1766
+ console.log(chalk7.gray("\nThe key can no longer be used for authentication."));
1767
+ } catch (error) {
1768
+ spinner.fail("Failed to revoke API key");
1769
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
379
1770
  process.exit(1);
380
1771
  }
381
1772
  });
382
- var ciCommand = ci;
1773
+ function getKeyStatus(key) {
1774
+ if (key.revoked_at) return "revoked";
1775
+ if (key.expires_at && new Date(key.expires_at) < /* @__PURE__ */ new Date()) return "expired";
1776
+ return "active";
1777
+ }
1778
+ function formatDate(dateStr) {
1779
+ const date = new Date(dateStr);
1780
+ return date.toLocaleDateString("en-US", {
1781
+ year: "numeric",
1782
+ month: "short",
1783
+ day: "numeric",
1784
+ hour: "2-digit",
1785
+ minute: "2-digit"
1786
+ });
1787
+ }
383
1788
 
384
1789
  // src/index.ts
385
- var program = new Command4();
1790
+ var originalEmit = process.emit.bind(process);
1791
+ process.emit = function(event, ...args) {
1792
+ const warning = args[0];
1793
+ if (event === "warning" && warning && typeof warning === "object" && "name" in warning && warning.name === "DeprecationWarning" && "message" in warning && typeof warning.message === "string" && (warning.message.includes("url.parse()") || warning.code === "DEP0169")) {
1794
+ return true;
1795
+ }
1796
+ return originalEmit.apply(process, [event, ...args]);
1797
+ };
1798
+ var program = new Command6();
386
1799
  program.name("perceo").description("Intelligent regression testing through multi-agent simulation").version("0.1.0");
1800
+ program.addCommand(loginCommand);
1801
+ program.addCommand(logoutCommand);
387
1802
  program.addCommand(initCommand);
388
- program.addCommand(watchCommand);
389
- program.addCommand(ciCommand);
1803
+ program.addCommand(delCommand);
1804
+ program.addCommand(analyzeCommand);
1805
+ program.addCommand(keysCommand);
390
1806
  program.parse();