@prsense/cli 0.1.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.
Files changed (66) hide show
  1. package/.env +3 -0
  2. package/LICENSE +201 -0
  3. package/README.md +1 -0
  4. package/dist/commands/config.d.ts +3 -0
  5. package/dist/commands/config.d.ts.map +1 -0
  6. package/dist/commands/config.js +41 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.d.ts +4 -0
  9. package/dist/commands/daemon.d.ts.map +1 -0
  10. package/dist/commands/daemon.js +120 -0
  11. package/dist/commands/daemon.js.map +1 -0
  12. package/dist/commands/doctor.d.ts +3 -0
  13. package/dist/commands/doctor.d.ts.map +1 -0
  14. package/dist/commands/doctor.js +74 -0
  15. package/dist/commands/doctor.js.map +1 -0
  16. package/dist/commands/index.d.ts +3 -0
  17. package/dist/commands/index.d.ts.map +1 -0
  18. package/dist/commands/index.js +112 -0
  19. package/dist/commands/index.js.map +1 -0
  20. package/dist/commands/review.d.ts +3 -0
  21. package/dist/commands/review.d.ts.map +1 -0
  22. package/dist/commands/review.js +115 -0
  23. package/dist/commands/review.js.map +1 -0
  24. package/dist/commands/setup.d.ts +3 -0
  25. package/dist/commands/setup.d.ts.map +1 -0
  26. package/dist/commands/setup.js +102 -0
  27. package/dist/commands/setup.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +4 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/program.d.ts +3 -0
  33. package/dist/program.d.ts.map +1 -0
  34. package/dist/program.js +17 -0
  35. package/dist/program.js.map +1 -0
  36. package/dist/shared/types.d.ts +11 -0
  37. package/dist/shared/types.d.ts.map +1 -0
  38. package/dist/shared/types.js +2 -0
  39. package/dist/shared/types.js.map +1 -0
  40. package/dist/ui/eventToTask.d.ts +7 -0
  41. package/dist/ui/eventToTask.d.ts.map +1 -0
  42. package/dist/ui/eventToTask.js +167 -0
  43. package/dist/ui/eventToTask.js.map +1 -0
  44. package/dist/ui/spinnerRenderer.d.ts +3 -0
  45. package/dist/ui/spinnerRenderer.d.ts.map +1 -0
  46. package/dist/ui/spinnerRenderer.js +43 -0
  47. package/dist/ui/spinnerRenderer.js.map +1 -0
  48. package/dist/ui/tasks.d.ts +12 -0
  49. package/dist/ui/tasks.d.ts.map +1 -0
  50. package/dist/ui/tasks.js +3 -0
  51. package/dist/ui/tasks.js.map +1 -0
  52. package/package.json +53 -0
  53. package/prsense-cli-0.1.0.tgz +0 -0
  54. package/src/commands/daemon.ts +146 -0
  55. package/src/commands/doctor.ts +83 -0
  56. package/src/commands/index.ts +139 -0
  57. package/src/commands/review.ts +153 -0
  58. package/src/commands/setup.ts +124 -0
  59. package/src/index.ts +4 -0
  60. package/src/program.ts +18 -0
  61. package/src/shared/types.ts +11 -0
  62. package/src/ui/eventToTask.ts +192 -0
  63. package/src/ui/spinnerRenderer.ts +55 -0
  64. package/src/ui/tasks.ts +15 -0
  65. package/tsconfig.json +22 -0
  66. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,146 @@
1
+ // apps/cli/src/commands/daemon/index.ts
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
4
+ import { spawn } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import http from "node:http";
9
+
10
+ const STATE_DIR = path.join(os.homedir(), ".local", "state", "prsense");
11
+ const PID_FILE = path.join(STATE_DIR, "daemon.pid");
12
+ const DAEMON_PORT = 3000;
13
+
14
+ function ensureStateDir() {
15
+ fs.mkdirSync(STATE_DIR, { recursive: true });
16
+ }
17
+
18
+ function isProcessRunning(pid: number): boolean {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function checkHealth(): Promise<boolean> {
28
+ return new Promise((resolve) => {
29
+ const req = http.get(
30
+ {
31
+ hostname: "127.0.0.1",
32
+ port: DAEMON_PORT,
33
+ path: "/health",
34
+ timeout: 1000,
35
+ },
36
+ (res) => {
37
+ resolve(res.statusCode === 200);
38
+ },
39
+ );
40
+ req.on("error", () => resolve(false));
41
+ req.on("timeout", () => {
42
+ req.destroy();
43
+ resolve(false);
44
+ });
45
+ });
46
+ }
47
+
48
+ const daemonCommand = new Command("daemon").description(
49
+ "Manage the PRsense daemon",
50
+ );
51
+
52
+ daemonCommand
53
+ .command("start")
54
+ .option("--foreground", "Run in foreground")
55
+ .action(async (opts) => {
56
+ ensureStateDir();
57
+
58
+ if (fs.existsSync(PID_FILE)) {
59
+ const pid = Number(fs.readFileSync(PID_FILE, "utf8"));
60
+
61
+ if (isProcessRunning(pid)) {
62
+ console.log("Daemon already running.");
63
+ return;
64
+ }
65
+
66
+ fs.unlinkSync(PID_FILE);
67
+ }
68
+
69
+ const require = createRequire(import.meta.url);
70
+ const daemonBin = require.resolve("@prsense/daemon/dist/index.js");
71
+
72
+ const logFile = path.join(STATE_DIR, "daemon.log");
73
+
74
+ const child = spawn(process.execPath, [daemonBin], {
75
+ detached: !opts.foreground,
76
+ cwd: process.cwd(),
77
+ stdio: opts.foreground
78
+ ? "inherit"
79
+ : ["ignore", fs.openSync(logFile, "a"), fs.openSync(logFile, "a")],
80
+ });
81
+
82
+ if (!opts.foreground) {
83
+ child.unref();
84
+ fs.writeFileSync(PID_FILE, String(child.pid));
85
+
86
+ // wait for daemon health
87
+ for (let i = 0; i < 15; i++) {
88
+ await new Promise((r) => setTimeout(r, 200));
89
+
90
+ if (await checkHealth()) {
91
+ console.log("Daemon started.");
92
+ return;
93
+ }
94
+
95
+ if (!isProcessRunning(child.pid!)) {
96
+ console.error("Daemon failed to start. See logs:");
97
+ console.error(logFile);
98
+ fs.unlinkSync(PID_FILE);
99
+ return;
100
+ }
101
+ }
102
+
103
+ console.error("Daemon did not become healthy.");
104
+ console.error("Check logs:", logFile);
105
+ }
106
+ });
107
+ daemonCommand.command("stop").action(() => {
108
+ if (!fs.existsSync(PID_FILE)) {
109
+ console.log("Daemon not running.");
110
+ return;
111
+ }
112
+
113
+ const pid = Number(fs.readFileSync(PID_FILE, "utf8"));
114
+
115
+ try {
116
+ process.kill(pid, "SIGTERM");
117
+ fs.unlinkSync(PID_FILE);
118
+ console.log("Daemon stopped.");
119
+ } catch {
120
+ console.log("Failed to stop daemon.");
121
+ }
122
+ });
123
+
124
+ daemonCommand.command("status").action(async () => {
125
+ if (!fs.existsSync(PID_FILE)) {
126
+ console.log("Daemon not running.");
127
+ return;
128
+ }
129
+
130
+ const pid = Number(fs.readFileSync(PID_FILE, "utf8"));
131
+
132
+ if (!isProcessRunning(pid)) {
133
+ console.log("Daemon not running (stale PID).");
134
+ return;
135
+ }
136
+
137
+ const healthy = await checkHealth();
138
+
139
+ if (healthy) {
140
+ console.log("Daemon is running and healthy.");
141
+ } else {
142
+ console.log("Daemon process running but health check failed.");
143
+ }
144
+ });
145
+
146
+ export default daemonCommand;
@@ -0,0 +1,83 @@
1
+ // apps/cli/src/commands/doctor.ts
2
+
3
+ import { Command } from "commander";
4
+ import { runDoctorWorkflow } from "@prsense/workflows";
5
+ import { stdoutDoctorReporter, stdoutConfigReporter } from "@prsense/reporters";
6
+ import { createPinoLogger, logEvent } from "@prsense/logging";
7
+ import { createEventBus, CoreEvents } from "@prsense/core";
8
+ import { resolveEnvironment } from "@prsense/config";
9
+
10
+ import { createSpinnerRenderer } from "../ui/spinnerRenderer.js";
11
+ import { eventToCliTask } from "../ui/eventToTask.js";
12
+
13
+ const logLevel = (process.env.PRSENSE_LOG_LEVEL as any) ?? "warn";
14
+
15
+ const logger = createPinoLogger({
16
+ level: logLevel,
17
+ pretty: true,
18
+ });
19
+
20
+ const renderer = createSpinnerRenderer(process.stdout);
21
+
22
+ const eventBus = createEventBus((event) => {
23
+ // structured logs -> stderr
24
+ logEvent(logger, event);
25
+ // ui ->stdout
26
+ const mapped = eventToCliTask(event);
27
+ if (!mapped) return;
28
+ if (mapped.kind === "start") {
29
+ renderer.start(mapped.task);
30
+ } else if (mapped.kind === "update") {
31
+ renderer.update(mapped.task);
32
+ } else {
33
+ renderer.finish(mapped.task);
34
+ }
35
+ });
36
+
37
+ export const doctorCommand = new Command("doctor")
38
+ .description("Diagnose PRsense installation and environment")
39
+ .action(async () => {
40
+ eventBus.emit(CoreEvents.RunStarted, {
41
+ mode: "cli",
42
+ command: "doctor",
43
+ });
44
+
45
+ try {
46
+ const env = resolveEnvironment("cli", {
47
+ root: ".",
48
+ provider: "filesystem",
49
+ });
50
+ if (env.issues.length > 0) {
51
+ eventBus.emit(CoreEvents.RunFailed, {
52
+ reason: "invalid-config",
53
+ });
54
+
55
+ await stdoutConfigReporter.report({
56
+ issues: env.issues,
57
+ });
58
+ process.exit(1);
59
+ }
60
+ const result = await runDoctorWorkflow({ config: env.config, eventBus });
61
+
62
+ await stdoutDoctorReporter.report(result);
63
+
64
+ const hasFailures = result.checks.some((c) => c.status === "fail");
65
+
66
+ eventBus.emit(CoreEvents.RunFinished, {
67
+ outcome: hasFailures ? "failure" : "success",
68
+ });
69
+
70
+ process.exit(hasFailures ? 1 : 0);
71
+ } catch (err) {
72
+ eventBus.emit(CoreEvents.RunFailed, {
73
+ error: String(err),
74
+ });
75
+
76
+ renderer.finish({
77
+ id: "run",
78
+ label: "PRSense failed",
79
+ state: "failed",
80
+ });
81
+ return;
82
+ }
83
+ });
@@ -0,0 +1,139 @@
1
+ import { Command } from "commander";
2
+ import path from "node:path";
3
+
4
+ import { runIndexWorkflow, listIndexedRepositories } from "@prsense/workflows";
5
+ import { createPinoLogger, logEvent, LogLevel } from "@prsense/logging";
6
+ import { createEventBus, CoreEvents, PRSENSE_VERSION } from "@prsense/core";
7
+ import { resolveEnvironment } from "@prsense/config";
8
+
9
+ import { createSpinnerRenderer } from "../ui/spinnerRenderer.js";
10
+ import { eventToCliTask } from "../ui/eventToTask.js";
11
+ import {
12
+ stdoutConfigReporter,
13
+ stdoutIndexedReposReporter,
14
+ } from "@prsense/reporters";
15
+
16
+ export const indexCommand = new Command("index")
17
+ .argument("[target]", "Path or GitHub/GitLab URL", ".")
18
+ .option("--force", "Rebuild the index from scratch")
19
+ .option("--dry-run", "Show what would be indexed without writing")
20
+ .option("--stats", "Print indexing statistics after completion")
21
+ .option("--chunk-size <n>", "Override chunk size (characters)")
22
+ .option("--list", "List indexed repositories")
23
+ .action(async (target, options) => {
24
+ try {
25
+ /* ------------------------------------------------- */
26
+ /* Load Config */
27
+ /* ------------------------------------------------- */
28
+
29
+ const logger = createPinoLogger({
30
+ level: (process.env.PRSENSE_LOG_LEVEL ?? "warn") as LogLevel,
31
+ pretty: true,
32
+ });
33
+
34
+ const renderer = createSpinnerRenderer(process.stdout);
35
+
36
+ const eventBus = createEventBus((event) => {
37
+ logEvent(logger, event);
38
+
39
+ const mapped = eventToCliTask(event);
40
+ if (!mapped) return;
41
+
42
+ if (mapped.kind === "start") {
43
+ renderer.start(mapped.task);
44
+ } else if (mapped.kind === "update") {
45
+ renderer.update(mapped.task);
46
+ } else {
47
+ renderer.finish(mapped.task);
48
+ }
49
+ });
50
+
51
+ eventBus.emit(CoreEvents.RunStarted, {
52
+ mode: "cli",
53
+ command: "index",
54
+ });
55
+
56
+ /* ------------------------------------------------- */
57
+ /* Determine Repository Provider */
58
+ /* ------------------------------------------------- */
59
+
60
+ const isGithub = /github\.com/.test(target);
61
+ const isGitlab = /gitlab\.com/.test(target);
62
+
63
+ const repoProvider = isGithub
64
+ ? "github"
65
+ : isGitlab
66
+ ? "gitlab"
67
+ : "filesystem";
68
+
69
+ const repoRoot =
70
+ repoProvider === "filesystem" ? path.resolve(target) : target;
71
+
72
+ const env = resolveEnvironment("cli", {
73
+ root: repoRoot,
74
+ provider: repoProvider,
75
+ });
76
+ const resolved = env.config;
77
+
78
+ /* ------------------------------------------------- */
79
+ /* CLI Overrides (Before Resolve) */
80
+ /* ------------------------------------------------- */
81
+
82
+ if (options.list) {
83
+ const repos = await listIndexedRepositories(resolved);
84
+
85
+ await stdoutIndexedReposReporter.report(repos);
86
+ eventBus.emit(CoreEvents.RunFinished);
87
+
88
+ return;
89
+ }
90
+
91
+ if (env.issues.some((i) => i.level === "error")) {
92
+ eventBus.emit(CoreEvents.RunFailed, {
93
+ reason: "invalid-config",
94
+ });
95
+
96
+ await stdoutConfigReporter.report({ issues: env.issues });
97
+ process.exit(1);
98
+ }
99
+
100
+ /* ------------------------------------------------- */
101
+ /* Run Workflow */
102
+ /* ------------------------------------------------- */
103
+
104
+ const result = await runIndexWorkflow({
105
+ config: resolved,
106
+ credentials: env.credentials,
107
+ target,
108
+ force: Boolean(options.force),
109
+ dryRun: Boolean(options.dryRun),
110
+ eventBus,
111
+ version: PRSENSE_VERSION,
112
+ });
113
+
114
+ eventBus.emit(CoreEvents.RunFinished, {
115
+ outcome: result.outcome,
116
+ });
117
+
118
+ /* ------------------------------------------------- */
119
+ /* Optional Stats */
120
+ /* ------------------------------------------------- */
121
+
122
+ if (options.stats) {
123
+ const { chunksIndexed, commitSha, upToDate } = result.payload;
124
+
125
+ if (upToDate) {
126
+ console.log(`Index is up to date (commit ${commitSha ?? "unknown"})`);
127
+ } else {
128
+ console.log(
129
+ `Indexed ${chunksIndexed} chunks (commit ${commitSha ?? "unknown"})`,
130
+ );
131
+ }
132
+ }
133
+
134
+ process.exit(result.outcome === "failure" ? 1 : 0);
135
+ } catch (err) {
136
+ console.error(err instanceof Error ? err.message : String(err));
137
+ process.exit(1);
138
+ }
139
+ });
@@ -0,0 +1,153 @@
1
+ // apps/cli/src/commands/review.ts
2
+ import { Command } from "commander";
3
+ import { runReviewWorkflow } from "@prsense/workflows";
4
+ import { createPinoLogger, logEvent, LogLevel } from "@prsense/logging";
5
+ import { createEventBus, CoreEvents } from "@prsense/core";
6
+ import { resolveEnvironment } from "@prsense/config";
7
+
8
+ import { createSpinnerRenderer } from "../ui/spinnerRenderer.js";
9
+ import { eventToCliTask } from "../ui/eventToTask.js";
10
+ import { stdoutConfigReporter } from "@prsense/reporters";
11
+ import path from "node:path";
12
+ import {
13
+ LocalGitDiffProvider,
14
+ GitHubPrDiffProvider,
15
+ GitLabMrDiffProvider,
16
+ } from "@prsense/context";
17
+
18
+ export const reviewCommand = new Command("review")
19
+ .argument("[target]", "Path to repository", ".")
20
+ .option("--base-branch <branch>", "Base branch to diff against")
21
+ .action(async (target, options) => {
22
+ const logger = createPinoLogger({
23
+ level: (process.env.PRSENSE_LOG_LEVEL ?? "warn") as LogLevel,
24
+ pretty: true,
25
+ });
26
+
27
+ const renderer = createSpinnerRenderer(process.stdout);
28
+
29
+ const eventBus = createEventBus((event) => {
30
+ logEvent(logger, event);
31
+
32
+ const mapped = eventToCliTask(event);
33
+ if (!mapped) return;
34
+
35
+ if (mapped.kind === "start") {
36
+ renderer.start(mapped.task);
37
+ } else if (mapped.kind === "update") {
38
+ renderer.update(mapped.task);
39
+ } else {
40
+ renderer.finish(mapped.task);
41
+ }
42
+ });
43
+
44
+ eventBus.emit(CoreEvents.RunStarted, {
45
+ mode: "cli",
46
+ command: "review",
47
+ });
48
+
49
+ try {
50
+ // -------------------------------------------------
51
+ // Load Config
52
+ // -------------------------------------------------
53
+
54
+ const repoRoot = path.resolve(target);
55
+
56
+ const githubPrMatch = target.match(
57
+ /github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/,
58
+ );
59
+ const gitlabMrMatch = target.match(
60
+ /gitlab\.com\/([^\/]+)\/([^\/]+)\/-\/merge_requests\/(\d+)/,
61
+ );
62
+
63
+ const repoProvider = githubPrMatch
64
+ ? "github"
65
+ : gitlabMrMatch
66
+ ? "gitlab"
67
+ : "filesystem";
68
+
69
+ const env = resolveEnvironment("cli", {
70
+ root: repoRoot,
71
+ provider: repoProvider,
72
+ });
73
+
74
+ if (env.issues.some((i) => i.level === "error")) {
75
+ eventBus.emit(CoreEvents.RunFailed, {
76
+ reason: "invalid-config",
77
+ });
78
+
79
+ await stdoutConfigReporter.report({ issues: env.issues });
80
+ process.exit(1);
81
+ }
82
+
83
+ // -------------------------------------------------
84
+ // Create Diff Provider
85
+ // -------------------------------------------------
86
+
87
+ let diffProvider;
88
+
89
+ if (githubPrMatch) {
90
+ const [, owner, repo, prNumber] = githubPrMatch;
91
+
92
+ diffProvider = new GitHubPrDiffProvider(
93
+ owner,
94
+ repo.replace(".git", ""),
95
+ prNumber,
96
+ );
97
+ } else if (gitlabMrMatch) {
98
+ const [, group, project, mrNumber] = gitlabMrMatch;
99
+
100
+ diffProvider = new GitLabMrDiffProvider(
101
+ group,
102
+ project.replace(".git", ""),
103
+ mrNumber,
104
+ );
105
+ } else {
106
+ diffProvider = new LocalGitDiffProvider(repoRoot, options.baseBranch);
107
+ }
108
+
109
+ // -------------------------------------------------
110
+ // Run Review Workflow
111
+ // -------------------------------------------------
112
+
113
+ const result = await runReviewWorkflow({
114
+ config: env.config,
115
+ credentials: env.credentials,
116
+ diffProvider,
117
+ eventBus,
118
+ });
119
+
120
+ eventBus.emit(CoreEvents.RunFinished, {
121
+ outcome: result.outcome,
122
+ });
123
+
124
+ if (result.outcome === "failure") {
125
+ process.exit(1);
126
+ }
127
+
128
+ // -------------------------------------------------
129
+ // Print Signals
130
+ // -------------------------------------------------
131
+
132
+ for (const signal of result.payload.signals) {
133
+ console.log(`\n[${signal.severity.toUpperCase()}] ${signal.file}`);
134
+ console.log(signal.message);
135
+
136
+ if (signal.rationale) {
137
+ console.log(` ↳ ${signal.rationale}`);
138
+ }
139
+
140
+ if (signal.suggestedFix) {
141
+ console.log(` 💡 ${signal.suggestedFix}`);
142
+ }
143
+ }
144
+
145
+ process.exit(0);
146
+ } catch (err) {
147
+ eventBus.emit(CoreEvents.RunFailed, {
148
+ error: String(err),
149
+ });
150
+
151
+ process.exit(1);
152
+ }
153
+ });
@@ -0,0 +1,124 @@
1
+ import { Command } from "commander";
2
+ import { createPinoLogger, logEvent } from "@prsense/logging";
3
+ import { createEventBus, CoreEvents } from "@prsense/core";
4
+
5
+ import { createSpinnerRenderer } from "../ui/spinnerRenderer.js";
6
+ import { eventToCliTask } from "../ui/eventToTask.js";
7
+ import { stdoutConfigReporter } from "@prsense/reporters";
8
+
9
+ import { runSetupWorkflow } from "@prsense/workflows";
10
+ import type { Capability } from "@prsense/preflight";
11
+ import { resolveEnvironment } from "@prsense/config";
12
+
13
+ import {
14
+ postgresCapability,
15
+ pgVectorCapability,
16
+ dockerCapability,
17
+ schemaCapability,
18
+ } from "@prsense/preflight";
19
+
20
+ export const setupCommand = new Command("setup")
21
+ .description("Validate and optionally provision required infrastructure")
22
+ .action(async () => {
23
+ const logLevel = (process.env.PRSENSE_LOG_LEVEL as any) ?? "warn";
24
+
25
+ const logger = createPinoLogger({
26
+ level: logLevel,
27
+ pretty: true,
28
+ });
29
+
30
+ const renderer = createSpinnerRenderer(process.stdout);
31
+
32
+ const eventBus = createEventBus((event) => {
33
+ logEvent(logger, event);
34
+
35
+ const mapped = eventToCliTask(event);
36
+ if (!mapped) return;
37
+
38
+ if (mapped.kind === "start") {
39
+ renderer.start(mapped.task);
40
+ } else if (mapped.kind === "update") {
41
+ renderer.update(mapped.task);
42
+ } else {
43
+ renderer.finish(mapped.task);
44
+ }
45
+ });
46
+
47
+ eventBus.emit(CoreEvents.RunStarted, {
48
+ mode: "cli",
49
+ command: "setup",
50
+ });
51
+
52
+ try {
53
+ // -------------------------------------------------
54
+ // Load Config
55
+ // -------------------------------------------------
56
+
57
+ const cwd = process.cwd();
58
+ const env = resolveEnvironment("cli", {
59
+ root: cwd,
60
+ provider: "filesystem",
61
+ });
62
+
63
+ if (env.issues.length > 0) {
64
+ eventBus.emit(CoreEvents.RunFailed, {
65
+ reason: "invalid-config",
66
+ });
67
+
68
+ await stdoutConfigReporter.report({ issues: env.issues });
69
+ process.exit(1);
70
+ }
71
+
72
+ // -------------------------------------------------
73
+ // Capabilities
74
+ // -------------------------------------------------
75
+
76
+ const capabilities: Capability[] = [
77
+ dockerCapability,
78
+ postgresCapability,
79
+ pgVectorCapability,
80
+ schemaCapability,
81
+ ];
82
+
83
+ const result = await runSetupWorkflow({
84
+ capabilities,
85
+ ctx: {
86
+ config: env.config,
87
+ env: process.env,
88
+ cwd,
89
+ },
90
+ eventBus,
91
+ });
92
+
93
+ eventBus.emit(CoreEvents.RunFinished, {
94
+ outcome: result.outcome,
95
+ });
96
+
97
+ // -------------------------------------------------
98
+ // Human Summary
99
+ // -------------------------------------------------
100
+
101
+ console.log("\nSetup Summary:");
102
+
103
+ for (const step of result.steps) {
104
+ if (step.outcome === "applied") {
105
+ console.log(`✔ ${step.id} applied`);
106
+ } else if (step.outcome === "skipped") {
107
+ console.log(`• ${step.id} ready`);
108
+ } else {
109
+ console.log(`✖ ${step.id} failed`);
110
+ if (step.error) {
111
+ console.log(` ↳ ${step.error}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ process.exit(result.outcome === "failure" ? 1 : 0);
117
+ } catch (err) {
118
+ eventBus.emit(CoreEvents.RunFailed, {
119
+ error: String(err),
120
+ });
121
+
122
+ process.exit(1);
123
+ }
124
+ });
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "./program.js";
3
+
4
+ program.parse(process.argv);
package/src/program.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Command } from "commander";
2
+ import { reviewCommand } from "./commands/review.js";
3
+ import { indexCommand } from "./commands/index.js";
4
+ import { doctorCommand } from "./commands/doctor.js";
5
+ import { setupCommand } from "./commands/setup.js";
6
+ import daemonCommand from "./commands/daemon.js";
7
+ import { PRSENSE_VERSION } from "@prsense/core";
8
+
9
+ export const program = new Command()
10
+ .name("prsense")
11
+ .description("PRsense – signal-based pull request reviews")
12
+ .version(PRSENSE_VERSION);
13
+
14
+ program.addCommand(reviewCommand);
15
+ program.addCommand(indexCommand);
16
+ program.addCommand(doctorCommand);
17
+ program.addCommand(setupCommand);
18
+ program.addCommand(daemonCommand);
@@ -0,0 +1,11 @@
1
+ import { ReviewSignal } from "@prsense/core";
2
+
3
+ export type CliReviewResult = {
4
+ signals: ReviewSignal[];
5
+ summary: {
6
+ total: number;
7
+ high: number;
8
+ medium: number;
9
+ low: number;
10
+ };
11
+ };