@perceo/perceo 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +390 -0
- package/package.json +4 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import fs2 from "fs/promises";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
import fs from "fs/promises";
|
|
15
|
+
import path from "path";
|
|
16
|
+
var CONFIG_DIR = ".perceo";
|
|
17
|
+
var BASE_CONFIG_FILE = "config.json";
|
|
18
|
+
var LOCAL_CONFIG_FILE = "config.local.json";
|
|
19
|
+
async function loadConfig(options = {}) {
|
|
20
|
+
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
21
|
+
const envPath = process.env.PERCEO_CONFIG_PATH;
|
|
22
|
+
const baseConfigPath = envPath ? resolveMaybeRelativePath(envPath, projectDir) : path.join(projectDir, CONFIG_DIR, BASE_CONFIG_FILE);
|
|
23
|
+
const baseConfig = await readJsonFile(baseConfigPath);
|
|
24
|
+
const isLocalEnv = (process.env.PERCEO_ENV || "").toLowerCase() === "local" || process.env.NODE_ENV === "development";
|
|
25
|
+
if (!isLocalEnv) {
|
|
26
|
+
return baseConfig;
|
|
27
|
+
}
|
|
28
|
+
const localConfigPath = path.join(projectDir, CONFIG_DIR, LOCAL_CONFIG_FILE);
|
|
29
|
+
const localExists = await fileExists(localConfigPath);
|
|
30
|
+
if (!localExists) {
|
|
31
|
+
return baseConfig;
|
|
32
|
+
}
|
|
33
|
+
const localConfig = await readJsonFile(localConfigPath);
|
|
34
|
+
return deepMerge(baseConfig, localConfig);
|
|
35
|
+
}
|
|
36
|
+
async function readJsonFile(filePath) {
|
|
37
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
async function fileExists(p) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(p);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveMaybeRelativePath(p, projectDir) {
|
|
49
|
+
if (path.isAbsolute(p)) return p;
|
|
50
|
+
return path.join(projectDir, p);
|
|
51
|
+
}
|
|
52
|
+
function deepMerge(target, source) {
|
|
53
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
56
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
57
|
+
const result = { ...target };
|
|
58
|
+
for (const key of Object.keys(source)) {
|
|
59
|
+
if (key in target) {
|
|
60
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
61
|
+
} else {
|
|
62
|
+
result[key] = source[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
return source;
|
|
68
|
+
}
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/commands/init.ts
|
|
74
|
+
import { ObserverEngine } from "@perceo/observer-engine";
|
|
75
|
+
var CONFIG_DIR2 = ".perceo";
|
|
76
|
+
var CONFIG_FILE = "config.json";
|
|
77
|
+
var initCommand = new Command("init").description("Initialize Perceo in your project").option("-d, --dir <directory>", "Project directory", process.cwd()).action(async (options) => {
|
|
78
|
+
const projectDir = path2.resolve(options.dir || process.cwd());
|
|
79
|
+
const spinner = ora(`Initializing Perceo in ${chalk.cyan(projectDir)}...`).start();
|
|
80
|
+
try {
|
|
81
|
+
const pkg = await readPackageJson(projectDir);
|
|
82
|
+
const projectName = pkg?.name || path2.basename(projectDir);
|
|
83
|
+
const framework = await detectFramework(projectDir, pkg);
|
|
84
|
+
const perceoDir = path2.join(projectDir, CONFIG_DIR2);
|
|
85
|
+
const perceoConfigPath = path2.join(perceoDir, CONFIG_FILE);
|
|
86
|
+
await fs2.mkdir(perceoDir, { recursive: true });
|
|
87
|
+
if (await fileExists2(perceoConfigPath)) {
|
|
88
|
+
spinner.stop();
|
|
89
|
+
console.log(chalk.yellow(`
|
|
90
|
+
.perceo/${CONFIG_FILE} already exists. Skipping config generation.`));
|
|
91
|
+
} else {
|
|
92
|
+
const config = createDefaultConfig(projectName, framework);
|
|
93
|
+
await fs2.writeFile(perceoConfigPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
94
|
+
}
|
|
95
|
+
const readmePath = path2.join(perceoDir, "README.md");
|
|
96
|
+
if (!await fileExists2(readmePath)) {
|
|
97
|
+
await fs2.writeFile(readmePath, createPerceoReadme(projectName), "utf8");
|
|
98
|
+
}
|
|
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
|
+
spinner.succeed("Perceo initialized successfully!");
|
|
122
|
+
console.log("\n" + chalk.bold("Project: ") + projectName);
|
|
123
|
+
console.log(chalk.bold("Detected framework: ") + framework);
|
|
124
|
+
console.log(chalk.bold("Config: ") + path2.relative(projectDir, perceoConfigPath));
|
|
125
|
+
if (bootstrapSummary) {
|
|
126
|
+
console.log(chalk.bold("Observer bootstrap: ") + bootstrapSummary);
|
|
127
|
+
}
|
|
128
|
+
console.log("\n" + chalk.bold("Next steps:"));
|
|
129
|
+
console.log(" 1. Review and customize " + chalk.cyan(`.perceo/${CONFIG_FILE}`) + " for your project.");
|
|
130
|
+
console.log(" 2. Set up local managed services (Neo4j, Supabase) as described in " + chalk.cyan(".perceo/README.md") + ".");
|
|
131
|
+
console.log(" 3. Start the watcher with: " + chalk.cyan("perceo watch --dev --analyze"));
|
|
132
|
+
console.log("\n" + chalk.gray("For full architecture and managed services guides, see docs/cli_architecture.md."));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
spinner.fail("Failed to initialize Perceo");
|
|
135
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
async function readPackageJson(projectDir) {
|
|
140
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
141
|
+
if (!await fileExists2(pkgPath)) return null;
|
|
142
|
+
try {
|
|
143
|
+
const raw = await fs2.readFile(pkgPath, "utf8");
|
|
144
|
+
return JSON.parse(raw);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function detectFramework(projectDir, pkg) {
|
|
150
|
+
const nextConfig = await fileExists2(path2.join(projectDir, "next.config.js"));
|
|
151
|
+
const nextConfigTs = await fileExists2(path2.join(projectDir, "next.config.ts"));
|
|
152
|
+
if (nextConfig || nextConfigTs) return "nextjs";
|
|
153
|
+
const remixConfig = await fileExists2(path2.join(projectDir, "remix.config.js"));
|
|
154
|
+
if (remixConfig) return "remix";
|
|
155
|
+
const deps = {
|
|
156
|
+
...pkg?.dependencies || {},
|
|
157
|
+
...pkg?.devDependencies || {}
|
|
158
|
+
};
|
|
159
|
+
if (deps["next"]) return "nextjs";
|
|
160
|
+
if (deps["react"] || deps["react-dom"]) return "react";
|
|
161
|
+
if (deps["@remix-run/react"]) return "remix";
|
|
162
|
+
if (deps["@angular/core"]) return "angular";
|
|
163
|
+
if (deps["vue"]) return "vue";
|
|
164
|
+
return "unknown";
|
|
165
|
+
}
|
|
166
|
+
async function fileExists2(p) {
|
|
167
|
+
try {
|
|
168
|
+
await fs2.access(p);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function createDefaultConfig(projectName, framework) {
|
|
175
|
+
return {
|
|
176
|
+
version: "1.0",
|
|
177
|
+
project: {
|
|
178
|
+
name: projectName,
|
|
179
|
+
framework
|
|
180
|
+
},
|
|
181
|
+
observer: {
|
|
182
|
+
watch: {
|
|
183
|
+
paths: inferDefaultWatchPaths(framework),
|
|
184
|
+
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: []
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function inferDefaultWatchPaths(framework) {
|
|
247
|
+
switch (framework) {
|
|
248
|
+
case "nextjs":
|
|
249
|
+
return ["app/", "src/"];
|
|
250
|
+
case "react":
|
|
251
|
+
return ["src/"];
|
|
252
|
+
case "remix":
|
|
253
|
+
return ["app/"];
|
|
254
|
+
case "angular":
|
|
255
|
+
return ["src/"];
|
|
256
|
+
case "vue":
|
|
257
|
+
return ["src/"];
|
|
258
|
+
default:
|
|
259
|
+
return ["src/"];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function createPerceoReadme(projectName) {
|
|
263
|
+
return `# Perceo configuration for ${projectName}
|
|
264
|
+
|
|
265
|
+
This folder was generated by \`perceo init\` and contains configuration for the Perceo CLI.
|
|
266
|
+
|
|
267
|
+
## Files
|
|
268
|
+
|
|
269
|
+
- \`.perceo/config.json\` \u2014 main configuration file for the Perceo CLI.
|
|
270
|
+
|
|
271
|
+
## What this config does
|
|
272
|
+
|
|
273
|
+
- Tells the Perceo backend which project and framework you are using.
|
|
274
|
+
- Configures how the Observer, Analyzer, and Analytics features should behave for this project.
|
|
275
|
+
- Stores connection settings that the Perceo backend and packages use to talk to managed services.
|
|
276
|
+
|
|
277
|
+
The actual connection logic and authentication to Perceo-managed services (including any databases, event buses, or analytics backends) is handled by Perceo\u2019s backend packages and platform \u2014 not by this CLI config.
|
|
278
|
+
|
|
279
|
+
In most cases you should only need to:
|
|
280
|
+
|
|
281
|
+
1. Review \`.perceo/config.json\` and adjust paths (for example, \`src/\` vs \`app/\`).
|
|
282
|
+
2. Commit \`.perceo/config.json\` to your repository (if you want it shared with your team).
|
|
283
|
+
3. Run:
|
|
284
|
+
|
|
285
|
+
\`\`\`bash
|
|
286
|
+
perceo watch --dev --analyze
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
to start Perceo in your local development workflow.
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/commands/watch.ts
|
|
294
|
+
import { Command as Command2 } from "commander";
|
|
295
|
+
import chalk2 from "chalk";
|
|
296
|
+
import ora2 from "ora";
|
|
297
|
+
import { ObserverEngine as ObserverEngine2 } from "@perceo/observer-engine";
|
|
298
|
+
var watchCommand = new Command2("watch").description("Watch for code changes and run tests").option("--dev", "Run against local development server").option("--port <port>", "Development server port", "3000").action(async (options) => {
|
|
299
|
+
const spinner = ora2("Starting Perceo observer...").start();
|
|
300
|
+
try {
|
|
301
|
+
const config = await loadConfig();
|
|
302
|
+
const observerConfig = {
|
|
303
|
+
observer: config.observer,
|
|
304
|
+
flowGraph: config.flowGraph,
|
|
305
|
+
eventBus: config.eventBus
|
|
306
|
+
};
|
|
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
|
+
} catch (error) {
|
|
324
|
+
spinner.fail("Failed to start observer");
|
|
325
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// src/commands/ci.ts
|
|
331
|
+
import { Command as Command3 } from "commander";
|
|
332
|
+
import chalk3 from "chalk";
|
|
333
|
+
import ora3 from "ora";
|
|
334
|
+
import { ObserverEngine as ObserverEngine3 } from "@perceo/observer-engine";
|
|
335
|
+
var ci = new Command3("ci").description("CI / PR analysis and testing");
|
|
336
|
+
ci.command("analyze").description("Analyze changes between two Git refs and report affected flows").requiredOption("--base <sha>", "Base Git ref / SHA").requiredOption("--head <sha>", "Head Git ref / SHA").option("--project-dir <dir>", "Project directory (defaults to process.cwd())").option("--json", "Print machine-readable JSON output", false).action(async (options) => {
|
|
337
|
+
const projectRoot = options.projectDir ? options.projectDir : process.cwd();
|
|
338
|
+
const spinner = ora3("Analyzing changes with Perceo Observer Engine...").start();
|
|
339
|
+
try {
|
|
340
|
+
const rawConfig = await loadConfig({ projectDir: projectRoot });
|
|
341
|
+
const observerConfig = {
|
|
342
|
+
observer: rawConfig.observer,
|
|
343
|
+
flowGraph: rawConfig.flowGraph,
|
|
344
|
+
eventBus: rawConfig.eventBus
|
|
345
|
+
};
|
|
346
|
+
const engine = new ObserverEngine3(observerConfig);
|
|
347
|
+
const report = await engine.analyzeChanges({
|
|
348
|
+
baseSha: options.base,
|
|
349
|
+
headSha: options.head,
|
|
350
|
+
projectRoot
|
|
351
|
+
});
|
|
352
|
+
spinner.succeed("Analysis complete");
|
|
353
|
+
if (options.json) {
|
|
354
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(chalk3.bold("Change: ") + `${report.baseSha}...${report.headSha}`);
|
|
359
|
+
console.log(chalk3.bold("Flows affected: ") + report.flows.length);
|
|
360
|
+
if (report.flows.length > 0) {
|
|
361
|
+
console.log();
|
|
362
|
+
for (const flow of report.flows) {
|
|
363
|
+
const risk = flow.riskScore.toFixed(2);
|
|
364
|
+
const confidence = (flow.confidence * 100).toFixed(0) + "%";
|
|
365
|
+
const priority = flow.priority ? ` [${flow.priority}]` : "";
|
|
366
|
+
console.log(`- ${chalk3.cyan(flow.name)}${priority} (risk=${risk}, confidence=${confidence})`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (report.changes.length > 0) {
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(chalk3.bold("Files changed:"));
|
|
372
|
+
for (const file of report.changes) {
|
|
373
|
+
console.log(`- [${file.status}] ${file.path}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
spinner.fail("Analysis failed");
|
|
378
|
+
console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
var ciCommand = ci;
|
|
383
|
+
|
|
384
|
+
// src/index.ts
|
|
385
|
+
var program = new Command4();
|
|
386
|
+
program.name("perceo").description("Intelligent regression testing through multi-agent simulation").version("0.1.0");
|
|
387
|
+
program.addCommand(initCommand);
|
|
388
|
+
program.addCommand(watchCommand);
|
|
389
|
+
program.addCommand(ciCommand);
|
|
390
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perceo/perceo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Intelligent regression testing through multi-agent simulation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
|
-
"ora": "^8.0.1"
|
|
36
|
+
"ora": "^8.0.1",
|
|
37
|
+
"@perceo/observer-engine": "1.0.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@changesets/cli": "^2.29.8",
|
|
40
41
|
"@types/node": "^20.11.0",
|
|
41
42
|
"tsup": "^8.0.1",
|
|
42
43
|
"typescript": "^5.3.3",
|
|
43
|
-
"@repo/typescript-config": "0.
|
|
44
|
+
"@repo/typescript-config": "0.2.0"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|
|
46
47
|
"node": ">=18"
|