@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/chunk-Y65GRRTT.js +197 -0
- package/dist/index.js +1621 -205
- package/dist/login-47R62VE3.js +6 -0
- package/package.json +7 -3
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
|
|
11
|
+
import { Command as Command6 } from "commander";
|
|
5
12
|
|
|
6
13
|
// src/commands/init.ts
|
|
7
14
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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
|
-
|
|
36
|
+
let baseConfig = await readJsonFile(baseConfigPath);
|
|
24
37
|
const isLocalEnv = (process.env.PERCEO_ENV || "").toLowerCase() === "local" || process.env.NODE_ENV === "development";
|
|
25
|
-
if (
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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 ||
|
|
425
|
+
const projectName = pkg?.name || path3.basename(projectDir);
|
|
83
426
|
const framework = await detectFramework(projectDir, pkg);
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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 =
|
|
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" +
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
151
|
-
const nextConfigTs = await fileExists2(
|
|
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(
|
|
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
|
|
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
|
|
1097
|
+
This folder was generated by \`perceo init\`.
|
|
266
1098
|
|
|
267
1099
|
## Files
|
|
268
1100
|
|
|
269
|
-
- \`.perceo/config.json\` \u2014
|
|
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
|
-
##
|
|
1103
|
+
## Commands
|
|
272
1104
|
|
|
273
|
-
-
|
|
274
|
-
-
|
|
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
|
-
|
|
1108
|
+
## Environment Variables
|
|
278
1109
|
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1176
|
+
// src/commands/logout.ts
|
|
294
1177
|
import { Command as Command2 } from "commander";
|
|
295
|
-
import
|
|
296
|
-
import
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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/
|
|
331
|
-
import { Command as
|
|
332
|
-
import
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const projectRoot =
|
|
338
|
-
const spinner = ora3("Analyzing changes
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
1543
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1544
|
+
} else {
|
|
1545
|
+
printResults(result);
|
|
356
1546
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
console.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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 (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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("
|
|
378
|
-
console.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
|
-
|
|
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
|
|
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(
|
|
389
|
-
program.addCommand(
|
|
1803
|
+
program.addCommand(delCommand);
|
|
1804
|
+
program.addCommand(analyzeCommand);
|
|
1805
|
+
program.addCommand(keysCommand);
|
|
390
1806
|
program.parse();
|