@sanurb/ringi 0.3.1 → 0.3.3
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/cli.mjs +534 -1598
- package/dist/cli.mjs.map +1 -1
- package/dist/mcp.mjs +4 -4
- package/dist/runtime.mjs +2437 -2437
- package/dist/runtime.mjs.map +1 -1
- package/package.json +2 -1
- package/server/nitro.json +1 -1
- package/server/public/assets/{_reviewId-DdOpDx4U.js → _reviewId-DszBZzPS.js} +1 -1
- package/server/public/assets/{main-FvxVz-kD.js → main-BQVB_Z0Z.js} +2 -2
- package/server/public/assets/{new-DOyplRwM.js → new-BpF6zgAP.js} +1 -1
- package/server/public/assets/{reviews-CfbuF6ib.js → reviews-Cqy8KE2-.js} +1 -1
- package/server/public/assets/reviews-YuW-Er6I.js +1 -0
- package/server/public/assets/{routes-DNxq1Fba.js → routes-Bp_N1q52.js} +1 -1
- package/server/public/assets/{routes-Dp0ODZ55.js → routes-CdmXLllM.js} +1 -1
- package/server/server/_chunks/ssr-renderer.mjs +2 -2
- package/server/server/_libs/effect+[...].mjs +396 -193
- package/server/server/{_reviewId-AWnOGz5k.mjs → _reviewId-BBo0j3l1.mjs} +2 -2
- package/server/server/{_reviewId-p9mhYVwa.mjs → _reviewId-CBLnTeaJ.mjs} +2 -2
- package/server/server/{_reviewId-DAhmekJ2.mjs → _reviewId-Ce1qqku3.mjs} +3 -3
- package/server/server/_ssr/action-bar-C68xGnWW.mjs +1 -1
- package/server/server/_ssr/{api-handler-CstW2n82.mjs → api-handler-yxu7Cbl5.mjs} +35 -3
- package/server/server/_ssr/client-runtime-BoPuAEoA.mjs +1 -1
- package/server/server/_ssr/domain-rpc-3Ds9DPr0.mjs +1 -1
- package/server/server/_ssr/file-tree-CQ5w2GHh.mjs +1 -1
- package/server/server/_ssr/load-scoped-diff-NL2XAcdz.mjs +2 -2
- package/server/server/_ssr/new-BKl_G2Ks.mjs +1 -1
- package/server/server/_ssr/new-DCz5eHkb.mjs +1 -1
- package/server/server/_ssr/reviews-C7_NIhY8.mjs +1 -1
- package/server/server/_ssr/{router-DLxN8FOm.mjs → router-5hEjszhz.mjs} +4 -4
- package/server/server/_ssr/routes-lz0AN75A.mjs +1 -1
- package/server/server/_ssr/runtime-D9IbnMlF.mjs +1 -1
- package/server/server/_ssr/ssr.mjs +7 -7
- package/server/server/_ssr/todo-m_uUvxca.mjs +1 -1
- package/server/server/{_tanstack-start-manifest_v-CnL10NRH.mjs → _tanstack-start-manifest_v--jyOFxuR.mjs} +9 -9
- package/server/server/index.mjs +905 -905
- package/server/public/assets/reviews-CJvVXRLH.js +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { c as getDiffSummary, h as serializeHunks, i as TodoService, l as parseDiff, m as ReviewFileRepo, n as GhService, o as ReviewService, p as ReviewRepo, r as ExportService, s as GitService, t as CoreLive, u as CommentService } from "./runtime.mjs";
|
|
3
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
|
|
3
6
|
import { exec, execFileSync, fork } from "node:child_process";
|
|
4
7
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { resolve } from "node:path";
|
|
6
|
-
import * as Schema from "effect/Schema";
|
|
7
|
-
import * as Result from "effect/Result";
|
|
8
8
|
import { writeFile } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
import * as Effect from "effect/Effect";
|
|
9
|
+
import { resolve } from "node:path";
|
|
11
10
|
import * as Layer from "effect/Layer";
|
|
11
|
+
import { ServiceMap } from "effect";
|
|
12
12
|
import * as Option from "effect/Option";
|
|
13
|
-
import * as
|
|
13
|
+
import * as Schema from "effect/Schema";
|
|
14
14
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
15
15
|
//#region ../../packages/core/src/services/pr-preflight.ts
|
|
16
16
|
var PreflightFailure = class extends Schema.TaggedErrorClass()("PreflightFailure", {
|
|
@@ -238,13 +238,6 @@ var InvalidPrUrl = class extends Schema.TaggedErrorClass()("InvalidPrUrl", {
|
|
|
238
238
|
url: Schema.String
|
|
239
239
|
}) {};
|
|
240
240
|
/**
|
|
241
|
-
* Quick heuristic: does this string look like a PR URL?
|
|
242
|
-
*
|
|
243
|
-
* Used by the CLI parser to distinguish `review <url>` from `review <verb>`.
|
|
244
|
-
* No ambiguity: no review verb starts with `http`.
|
|
245
|
-
*/
|
|
246
|
-
const looksLikePrUrl = (s) => /^https?:\/\/[^/]+\/[^/]+\/[^/]+\/pull\/\d+/.test(s);
|
|
247
|
-
/**
|
|
248
241
|
* Parses a GitHub PR URL into structured components.
|
|
249
242
|
*
|
|
250
243
|
* Supports:
|
|
@@ -291,14 +284,13 @@ const parsePrUrl = Effect.fn("parsePrUrl")(function* (raw) {
|
|
|
291
284
|
};
|
|
292
285
|
});
|
|
293
286
|
//#endregion
|
|
294
|
-
//#region src/cli/
|
|
295
|
-
var CliConfig = class extends ServiceMap.Service()("@ringi/CliConfig") {};
|
|
287
|
+
//#region src/cli/cli-errors.ts
|
|
296
288
|
/**
|
|
297
|
-
*
|
|
289
|
+
* Typed CLI errors.
|
|
290
|
+
*
|
|
291
|
+
* All CLI errors use `Schema.TaggedErrorClass` so they flow through Effect's
|
|
292
|
+
* typed error channels.
|
|
298
293
|
*/
|
|
299
|
-
const CliConfigLive = (config) => Layer.succeed(CliConfig, CliConfig.of(config));
|
|
300
|
-
//#endregion
|
|
301
|
-
//#region src/cli/contracts.ts
|
|
302
294
|
const ExitCode = {
|
|
303
295
|
AuthFailure: 5,
|
|
304
296
|
ResourceNotFound: 3,
|
|
@@ -307,19 +299,6 @@ const ExitCode = {
|
|
|
307
299
|
Success: 0,
|
|
308
300
|
UsageError: 2
|
|
309
301
|
};
|
|
310
|
-
const success = (command, result, nextActions = []) => ({
|
|
311
|
-
command,
|
|
312
|
-
next_actions: nextActions,
|
|
313
|
-
ok: true,
|
|
314
|
-
result
|
|
315
|
-
});
|
|
316
|
-
const failure = (command, error, fix, nextActions = []) => ({
|
|
317
|
-
command,
|
|
318
|
-
error,
|
|
319
|
-
fix,
|
|
320
|
-
next_actions: nextActions,
|
|
321
|
-
ok: false
|
|
322
|
-
});
|
|
323
302
|
/**
|
|
324
303
|
* Carries an exit code and optional operator-facing details so callers can
|
|
325
304
|
* present a short message without losing the underlying reason.
|
|
@@ -330,96 +309,99 @@ var CliFailure = class extends Schema.TaggedErrorClass()("CliFailure", {
|
|
|
330
309
|
message: Schema.String
|
|
331
310
|
}) {};
|
|
332
311
|
//#endregion
|
|
333
|
-
//#region src/cli/
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"ID",
|
|
350
|
-
"STATUS",
|
|
351
|
-
"SOURCE",
|
|
352
|
-
"FILES",
|
|
353
|
-
"CREATED"
|
|
354
|
-
], reviews.map((review) => [
|
|
355
|
-
review.id,
|
|
356
|
-
review.status,
|
|
357
|
-
review.sourceType,
|
|
358
|
-
String(review.fileCount),
|
|
359
|
-
review.createdAt
|
|
360
|
-
]));
|
|
361
|
-
};
|
|
362
|
-
const renderReviewShow = (input) => {
|
|
363
|
-
const { comments, review, todos } = input;
|
|
364
|
-
const lines = [
|
|
365
|
-
`Review ${review.id}`,
|
|
366
|
-
`Status: ${review.status}`,
|
|
367
|
-
`Source: ${review.sourceType}${review.sourceRef ? ` (${review.sourceRef})` : ""}`,
|
|
368
|
-
`Created: ${review.createdAt}`,
|
|
369
|
-
`Files: ${review.summary.totalFiles}`,
|
|
370
|
-
`Diff: +${review.summary.totalAdditions} / -${review.summary.totalDeletions}`
|
|
371
|
-
];
|
|
372
|
-
if (review.files.length > 0) {
|
|
373
|
-
lines.push("", "Files:");
|
|
374
|
-
for (const file of review.files) lines.push(`- ${file.status.toUpperCase()} ${file.filePath} (+${file.additions} -${file.deletions})`);
|
|
375
|
-
}
|
|
376
|
-
if (comments && comments.length > 0) {
|
|
377
|
-
lines.push("", "Comments:");
|
|
378
|
-
for (const comment of comments) {
|
|
379
|
-
const location = `${comment.filePath}:${comment.lineNumber ?? "-"}`;
|
|
380
|
-
const state = comment.resolved ? "resolved" : "open";
|
|
381
|
-
lines.push(`- [${state}] ${location} ${comment.content}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (todos && todos.length > 0) {
|
|
385
|
-
lines.push("", "Todos:");
|
|
386
|
-
for (const todo of todos) {
|
|
387
|
-
const marker = todo.completed ? "x" : " ";
|
|
388
|
-
lines.push(`- [${marker}] (${todo.position + 1}) ${todo.content}`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return lines.join("\n");
|
|
312
|
+
//#region src/cli/config.ts
|
|
313
|
+
var CliConfig = class extends ServiceMap.Service()("@ringi/CliConfig") {};
|
|
314
|
+
/**
|
|
315
|
+
* Wraps a concrete {@link CliConfigShape} in a layer for the Effect runtime.
|
|
316
|
+
*/
|
|
317
|
+
const CliConfigLive = (config) => Layer.succeed(CliConfig, CliConfig.of(config));
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/cli/output.ts
|
|
320
|
+
const successEnvelope = (command, result, nextActions = []) => ({
|
|
321
|
+
command,
|
|
322
|
+
next_actions: nextActions,
|
|
323
|
+
ok: true,
|
|
324
|
+
result
|
|
325
|
+
});
|
|
326
|
+
const writeJson = (payload) => {
|
|
327
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
392
328
|
};
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
return todos.map((todo) => `- [${todo.completed ? "x" : " "}] (${todo.position + 1}) ${todo.content}`).join("\n");
|
|
329
|
+
const writeHuman = (text) => {
|
|
330
|
+
if (text && text.length > 0) process.stdout.write(`${text}\n`);
|
|
396
331
|
};
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/cli/commands.ts
|
|
334
|
+
/**
|
|
335
|
+
* Ringi CLI command definitions using `effect/unstable/cli`.
|
|
336
|
+
*
|
|
337
|
+
* Each command is a typed `Command.make(name, config, handler)` with its flags
|
|
338
|
+
* and arguments declared via `Flag` and `Argument`. The framework handles
|
|
339
|
+
* parsing, help generation, shell completions, and version display.
|
|
340
|
+
*
|
|
341
|
+
* Command handlers produce `CommandOutput<T>` and emit output via the
|
|
342
|
+
* `--json` global flag setting.
|
|
343
|
+
*/
|
|
344
|
+
const JsonSetting = GlobalFlag.setting("json")({ flag: Flag.boolean("json").pipe(Flag.withDescription("Emit structured JSON envelope to stdout"), Flag.withDefault(false)) });
|
|
345
|
+
const QuietSetting = GlobalFlag.setting("quiet")({ flag: Flag.boolean("quiet").pipe(Flag.withDescription("Suppress human-readable success output"), Flag.withDefault(false)) });
|
|
346
|
+
const RepoSetting = GlobalFlag.setting("repo")({ flag: Flag.string("repo").pipe(Flag.withDescription("Use a specific Git repository root"), Flag.optional) });
|
|
347
|
+
const DbPathSetting = GlobalFlag.setting("db-path")({ flag: Flag.string("db-path").pipe(Flag.withDescription("Override the SQLite database path"), Flag.optional) });
|
|
348
|
+
const emitOutput = (commandLabel, output) => Effect.gen(function* () {
|
|
349
|
+
const jsonMode = yield* JsonSetting;
|
|
350
|
+
const quietMode = yield* QuietSetting;
|
|
351
|
+
if (jsonMode) writeJson(successEnvelope(commandLabel, output.data, output.nextActions ?? []));
|
|
352
|
+
else if (!quietMode) writeHuman(output.human);
|
|
353
|
+
});
|
|
354
|
+
const resolveRepositoryRoot = (repoOverride) => {
|
|
355
|
+
const cwd = Option.isSome(repoOverride) ? resolve(repoOverride.value) : process.cwd();
|
|
356
|
+
try {
|
|
357
|
+
return Effect.succeed(execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
358
|
+
cwd,
|
|
359
|
+
encoding: "utf8"
|
|
360
|
+
}).trim());
|
|
361
|
+
} catch {
|
|
362
|
+
return Effect.fail(new CliFailure({
|
|
363
|
+
exitCode: ExitCode.StateUnavailable,
|
|
364
|
+
message: Option.isSome(repoOverride) ? `Path ${cwd} is not a Git repository. Use --repo <path> with a valid repository root.` : `Could not resolve a Git repository from ${cwd}. Use --repo <path> with a valid repository root.`
|
|
365
|
+
}));
|
|
415
366
|
}
|
|
416
|
-
return lines.join("\n");
|
|
417
367
|
};
|
|
368
|
+
const resolveDbPath = (repoRoot, dbPathOverride) => Option.isSome(dbPathOverride) ? resolve(dbPathOverride.value) : resolve(repoRoot, ".ringi/reviews.db");
|
|
369
|
+
const makeCliConfigLayer = Effect.gen(function* () {
|
|
370
|
+
const repoOpt = yield* RepoSetting;
|
|
371
|
+
const dbPathOpt = yield* DbPathSetting;
|
|
372
|
+
const repoRoot = yield* resolveRepositoryRoot(repoOpt);
|
|
373
|
+
const dbPath = resolveDbPath(repoRoot, dbPathOpt);
|
|
374
|
+
const config = {
|
|
375
|
+
color: true,
|
|
376
|
+
cwd: process.cwd(),
|
|
377
|
+
dbPath,
|
|
378
|
+
outputMode: "human",
|
|
379
|
+
quiet: false,
|
|
380
|
+
repoRoot,
|
|
381
|
+
verbose: false
|
|
382
|
+
};
|
|
383
|
+
return Layer.mergeAll(CliConfigLive(config), ConfigProvider.layer(ConfigProvider.fromUnknown({
|
|
384
|
+
DB_PATH: dbPath,
|
|
385
|
+
REPOSITORY_PATH: repoRoot
|
|
386
|
+
})));
|
|
387
|
+
});
|
|
388
|
+
const ensureDatabaseExists = Effect.gen(function* () {
|
|
389
|
+
const cliConfig = yield* CliConfig;
|
|
390
|
+
if (!existsSync(cliConfig.dbPath)) yield* new CliFailure({
|
|
391
|
+
exitCode: ExitCode.StateUnavailable,
|
|
392
|
+
message: `Local state is missing at ${cliConfig.dbPath}. Run 'ringi data migrate' or start 'ringi serve' once to initialize local state.`
|
|
393
|
+
});
|
|
394
|
+
});
|
|
418
395
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
396
|
+
* Provides CoreLive + CliConfig to a command's handler.
|
|
397
|
+
* Constructs the layer from the global flags (--repo, --db-path).
|
|
421
398
|
*/
|
|
422
|
-
const
|
|
399
|
+
const provideCoreLayer = (self) => Command.provide(self, () => Layer.unwrap(makeCliConfigLayer.pipe(Effect.map((configLayer) => Layer.mergeAll(CoreLive, configLayer).pipe(Layer.provideMerge(configLayer))))));
|
|
400
|
+
/**
|
|
401
|
+
* Provides only GitService + CliConfig (lighter for git-only commands).
|
|
402
|
+
*/
|
|
403
|
+
const provideGitLayer = (self) => Command.provide(self, () => Layer.unwrap(makeCliConfigLayer.pipe(Effect.map((configLayer) => Layer.mergeAll(GitService.Default, configLayer).pipe(Layer.provideMerge(configLayer))))));
|
|
404
|
+
const resolveReviewSelector = Effect.fn("CLI.resolveReviewSelector")(function* (selector) {
|
|
423
405
|
if (selector !== "last") return selector;
|
|
424
406
|
const cliConfig = yield* CliConfig;
|
|
425
407
|
const [review] = (yield* (yield* ReviewService).list({
|
|
@@ -433,29 +415,47 @@ const resolveReviewSelector = Effect.fn("CLI.resolveReviewSelector")(function* r
|
|
|
433
415
|
});
|
|
434
416
|
return review.id;
|
|
435
417
|
});
|
|
436
|
-
/**
|
|
437
|
-
* Mutating CLI commands stay server-backed so they share the same write path as
|
|
438
|
-
* the other clients instead of growing a second local-only behavior surface.
|
|
439
|
-
*/
|
|
440
418
|
const requireServerMode = (label) => Effect.fail(new CliFailure({
|
|
441
419
|
details: "Start 'ringi serve' and retry the command.",
|
|
442
420
|
exitCode: ExitCode.StateUnavailable,
|
|
443
421
|
message: `${label} requires a running local Ringi server. Standalone local writes are intentionally unsupported.`
|
|
444
422
|
}));
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
423
|
+
const formatTable = (headers, rows) => {
|
|
424
|
+
const widths = headers.map((header, index) => {
|
|
425
|
+
const cellWidths = rows.map((row) => row[index]?.length ?? 0);
|
|
426
|
+
return Math.max(header.length, ...cellWidths);
|
|
427
|
+
});
|
|
428
|
+
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths.at(index) ?? 0)).join(" ").trimEnd();
|
|
429
|
+
return [
|
|
430
|
+
renderRow(headers),
|
|
431
|
+
renderRow(widths.map((width) => "-".repeat(width))),
|
|
432
|
+
...rows.map(renderRow)
|
|
433
|
+
].join("\n");
|
|
449
434
|
};
|
|
450
|
-
const
|
|
435
|
+
const reviewList = Command.make("list", {
|
|
436
|
+
status: Flag.choice("status", [
|
|
437
|
+
"in_progress",
|
|
438
|
+
"approved",
|
|
439
|
+
"changes_requested"
|
|
440
|
+
]).pipe(Flag.withDescription("Filter by review status"), Flag.optional),
|
|
441
|
+
source: Flag.choice("source", [
|
|
442
|
+
"staged",
|
|
443
|
+
"branch",
|
|
444
|
+
"commits",
|
|
445
|
+
"pull_request"
|
|
446
|
+
]).pipe(Flag.withDescription("Filter by source type"), Flag.optional),
|
|
447
|
+
limit: Flag.integer("limit").pipe(Flag.withDefault(20), Flag.withDescription("Number of results per page")),
|
|
448
|
+
page: Flag.integer("page").pipe(Flag.withDefault(1), Flag.withDescription("Page number"))
|
|
449
|
+
}, (config) => Effect.gen(function* () {
|
|
450
|
+
yield* ensureDatabaseExists;
|
|
451
451
|
const reviewService = yield* ReviewService;
|
|
452
452
|
const cliConfig = yield* CliConfig;
|
|
453
453
|
const result = yield* reviewService.list({
|
|
454
|
-
page:
|
|
455
|
-
pageSize:
|
|
454
|
+
page: config.page,
|
|
455
|
+
pageSize: config.limit,
|
|
456
456
|
repositoryPath: cliConfig.repoRoot,
|
|
457
|
-
sourceType:
|
|
458
|
-
status:
|
|
457
|
+
sourceType: Option.getOrUndefined(config.source),
|
|
458
|
+
status: Option.getOrUndefined(config.status)
|
|
459
459
|
});
|
|
460
460
|
const nextActions = [];
|
|
461
461
|
for (const review of result.reviews.slice(0, 3)) nextActions.push({
|
|
@@ -463,87 +463,106 @@ const runReviewList = Effect.fn("CLI.reviewList")(function* runReviewList(comman
|
|
|
463
463
|
description: `Inspect review ${review.id} (${review.status})`
|
|
464
464
|
});
|
|
465
465
|
if (result.reviews.length > 0) nextActions.push({
|
|
466
|
-
command: "ringi review show <id>
|
|
467
|
-
description: "Show full review details"
|
|
468
|
-
params: { id: {
|
|
469
|
-
description: "Review ID or 'last'",
|
|
470
|
-
required: true
|
|
471
|
-
} }
|
|
466
|
+
command: "ringi review show <id> --comments --todos",
|
|
467
|
+
description: "Show full review details"
|
|
472
468
|
});
|
|
473
469
|
nextActions.push({
|
|
474
|
-
command: "ringi review create
|
|
475
|
-
description: "Create a new review session"
|
|
476
|
-
params: { source: {
|
|
477
|
-
default: "staged",
|
|
478
|
-
enum: [
|
|
479
|
-
"staged",
|
|
480
|
-
"branch",
|
|
481
|
-
"commits"
|
|
482
|
-
]
|
|
483
|
-
} }
|
|
470
|
+
command: "ringi review create --source <source>",
|
|
471
|
+
description: "Create a new review session"
|
|
484
472
|
});
|
|
485
|
-
|
|
473
|
+
yield* emitOutput("ringi review list", {
|
|
486
474
|
data: result,
|
|
487
|
-
human:
|
|
475
|
+
human: result.reviews.length === 0 ? "No reviews found." : formatTable([
|
|
476
|
+
"ID",
|
|
477
|
+
"STATUS",
|
|
478
|
+
"SOURCE",
|
|
479
|
+
"FILES",
|
|
480
|
+
"CREATED"
|
|
481
|
+
], result.reviews.map((r) => [
|
|
482
|
+
r.id,
|
|
483
|
+
r.status,
|
|
484
|
+
r.sourceType,
|
|
485
|
+
String(r.fileCount),
|
|
486
|
+
r.createdAt
|
|
487
|
+
])),
|
|
488
488
|
nextActions
|
|
489
|
-
};
|
|
490
|
-
});
|
|
491
|
-
const
|
|
489
|
+
});
|
|
490
|
+
})).pipe(Command.withDescription("List review sessions"));
|
|
491
|
+
const reviewShow = Command.make("show", {
|
|
492
|
+
id: Argument.string("id"),
|
|
493
|
+
comments: Flag.boolean("comments").pipe(Flag.withDefault(false), Flag.withDescription("Include comments")),
|
|
494
|
+
todos: Flag.boolean("todos").pipe(Flag.withDefault(false), Flag.withDescription("Include todos"))
|
|
495
|
+
}, (config) => Effect.gen(function* () {
|
|
496
|
+
yield* ensureDatabaseExists;
|
|
492
497
|
const reviewService = yield* ReviewService;
|
|
493
498
|
const todoService = yield* TodoService;
|
|
494
499
|
const commentService = yield* CommentService;
|
|
495
|
-
const reviewId = yield* resolveReviewSelector(
|
|
500
|
+
const reviewId = yield* resolveReviewSelector(config.id);
|
|
496
501
|
const review = yield* reviewService.getById(reviewId);
|
|
502
|
+
const comments = config.comments ? yield* commentService.getByReview(reviewId) : void 0;
|
|
503
|
+
const todos = config.todos ? (yield* todoService.list({ reviewId })).data : void 0;
|
|
497
504
|
const data = {
|
|
498
|
-
comments
|
|
505
|
+
comments,
|
|
499
506
|
review,
|
|
500
|
-
todos
|
|
507
|
+
todos
|
|
501
508
|
};
|
|
502
|
-
const
|
|
503
|
-
{
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
{
|
|
508
|
-
|
|
509
|
-
description: "Show with comments and todos"
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
command: "ringi todo list [--review <review-id>] [--status <status>]",
|
|
513
|
-
description: "List todos for this review",
|
|
514
|
-
params: {
|
|
515
|
-
"review-id": { value: reviewId },
|
|
516
|
-
status: {
|
|
517
|
-
default: "pending",
|
|
518
|
-
enum: [
|
|
519
|
-
"pending",
|
|
520
|
-
"done",
|
|
521
|
-
"all"
|
|
522
|
-
]
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
command: "ringi review list",
|
|
528
|
-
description: "Back to review list"
|
|
529
|
-
}
|
|
509
|
+
const lines = [
|
|
510
|
+
`Review ${review.id}`,
|
|
511
|
+
`Status: ${review.status}`,
|
|
512
|
+
`Source: ${review.sourceType}${review.sourceRef ? ` (${review.sourceRef})` : ""}`,
|
|
513
|
+
`Created: ${review.createdAt}`,
|
|
514
|
+
`Files: ${review.summary.totalFiles}`,
|
|
515
|
+
`Diff: +${review.summary.totalAdditions} / -${review.summary.totalDeletions}`
|
|
530
516
|
];
|
|
531
|
-
|
|
517
|
+
if (review.files.length > 0) {
|
|
518
|
+
lines.push("", "Files:");
|
|
519
|
+
for (const file of review.files) lines.push(`- ${file.status.toUpperCase()} ${file.filePath} (+${file.additions} -${file.deletions})`);
|
|
520
|
+
}
|
|
521
|
+
if (comments && comments.length > 0) {
|
|
522
|
+
lines.push("", "Comments:");
|
|
523
|
+
for (const comment of comments) {
|
|
524
|
+
const location = `${comment.filePath}:${comment.lineNumber ?? "-"}`;
|
|
525
|
+
const state = comment.resolved ? "resolved" : "open";
|
|
526
|
+
lines.push(`- [${state}] ${location} ${comment.content}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (todos && todos.length > 0) {
|
|
530
|
+
lines.push("", "Todos:");
|
|
531
|
+
for (const todo of todos) {
|
|
532
|
+
const marker = todo.completed ? "x" : " ";
|
|
533
|
+
lines.push(`- [${marker}] (${todo.position + 1}) ${todo.content}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
yield* emitOutput("ringi review show", {
|
|
532
537
|
data,
|
|
533
|
-
human:
|
|
534
|
-
nextActions
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
538
|
+
human: lines.join("\n"),
|
|
539
|
+
nextActions: [
|
|
540
|
+
{
|
|
541
|
+
command: `ringi review export ${reviewId}`,
|
|
542
|
+
description: "Export this review as markdown"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
command: `ringi review show ${reviewId} --comments --todos`,
|
|
546
|
+
description: "Show with comments and todos"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
command: "ringi review list",
|
|
550
|
+
description: "Back to review list"
|
|
551
|
+
}
|
|
552
|
+
]
|
|
541
553
|
});
|
|
554
|
+
})).pipe(Command.withDescription("Show review details"));
|
|
555
|
+
const reviewExport = Command.make("export", {
|
|
556
|
+
id: Argument.string("id"),
|
|
557
|
+
output: Flag.string("output").pipe(Flag.withDescription("Output file path"), Flag.optional),
|
|
558
|
+
stdout: Flag.boolean("stdout").pipe(Flag.withDefault(false), Flag.withDescription("Print to stdout instead of a file"))
|
|
559
|
+
}, (config) => Effect.gen(function* () {
|
|
560
|
+
yield* ensureDatabaseExists;
|
|
542
561
|
const exportService = yield* ExportService;
|
|
543
562
|
const cliConfig = yield* CliConfig;
|
|
544
|
-
const reviewId = yield* resolveReviewSelector(
|
|
563
|
+
const reviewId = yield* resolveReviewSelector(config.id);
|
|
545
564
|
const markdown = yield* exportService.exportReview(reviewId);
|
|
546
|
-
const outputPath =
|
|
565
|
+
const outputPath = Option.isSome(config.output) ? resolve(cliConfig.cwd, config.output.value) : void 0;
|
|
547
566
|
if (outputPath) yield* Effect.tryPromise({
|
|
548
567
|
catch: (error) => new CliFailure({
|
|
549
568
|
exitCode: ExitCode.RuntimeFailure,
|
|
@@ -551,107 +570,49 @@ const runReviewExport = Effect.fn("CLI.reviewExport")(function* runReviewExport(
|
|
|
551
570
|
}),
|
|
552
571
|
try: () => writeFile(outputPath, markdown, "utf8")
|
|
553
572
|
});
|
|
554
|
-
const shouldPrintMarkdown =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
command: `ringi review show ${reviewId}`,
|
|
562
|
-
description: "View the exported review"
|
|
563
|
-
}, {
|
|
564
|
-
command: "ringi review list",
|
|
565
|
-
description: "Back to review list"
|
|
566
|
-
}];
|
|
567
|
-
return {
|
|
568
|
-
data,
|
|
573
|
+
const shouldPrintMarkdown = config.stdout || !outputPath;
|
|
574
|
+
yield* emitOutput("ringi review export", {
|
|
575
|
+
data: {
|
|
576
|
+
markdown,
|
|
577
|
+
outputPath: outputPath ?? null,
|
|
578
|
+
reviewId
|
|
579
|
+
},
|
|
569
580
|
human: shouldPrintMarkdown ? markdown : `Exported review ${reviewId} to ${outputPath}.`,
|
|
570
|
-
nextActions
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const data = {
|
|
578
|
-
branches: yield* gitService.getBranches,
|
|
579
|
-
commits: (yield* gitService.getCommits({
|
|
580
|
-
limit: 10,
|
|
581
|
-
offset: 0
|
|
582
|
-
})).commits,
|
|
583
|
-
repo,
|
|
584
|
-
stagedFiles
|
|
585
|
-
};
|
|
586
|
-
return {
|
|
587
|
-
data,
|
|
588
|
-
human: renderSourceList(data),
|
|
589
|
-
nextActions: [
|
|
590
|
-
{
|
|
591
|
-
command: "ringi source diff <source> [--stat]",
|
|
592
|
-
description: "View diff for a source",
|
|
593
|
-
params: { source: { enum: [
|
|
594
|
-
"staged",
|
|
595
|
-
"branch",
|
|
596
|
-
"commits"
|
|
597
|
-
] } }
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
command: "ringi review create [--source <source>]",
|
|
601
|
-
description: "Create a review from a source",
|
|
602
|
-
params: { source: {
|
|
603
|
-
default: "staged",
|
|
604
|
-
enum: [
|
|
605
|
-
"staged",
|
|
606
|
-
"branch",
|
|
607
|
-
"commits"
|
|
608
|
-
]
|
|
609
|
-
} }
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
command: "ringi review list",
|
|
613
|
-
description: "List existing reviews"
|
|
614
|
-
}
|
|
615
|
-
]
|
|
616
|
-
};
|
|
617
|
-
});
|
|
618
|
-
const runSourceDiff = Effect.fn("CLI.sourceDiff")(function* runSourceDiff(command) {
|
|
619
|
-
const gitService = yield* GitService;
|
|
620
|
-
const strategy = diffSourceStrategies[command.source];
|
|
621
|
-
if (!strategy) return yield* new CliFailure({
|
|
622
|
-
exitCode: ExitCode.UsageError,
|
|
623
|
-
message: "Unsupported review source."
|
|
624
|
-
});
|
|
625
|
-
const diffText = yield* strategy(gitService, command);
|
|
626
|
-
if (!diffText.trim()) yield* new CliFailure({
|
|
627
|
-
exitCode: ExitCode.RuntimeFailure,
|
|
628
|
-
message: "No diff available for the requested source."
|
|
581
|
+
nextActions: [{
|
|
582
|
+
command: `ringi review show ${reviewId}`,
|
|
583
|
+
description: "View the exported review"
|
|
584
|
+
}, {
|
|
585
|
+
command: "ringi review list",
|
|
586
|
+
description: "Back to review list"
|
|
587
|
+
}]
|
|
629
588
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
589
|
+
})).pipe(Command.withDescription("Export review as markdown"));
|
|
590
|
+
const reviewCreate = Command.make("create", {
|
|
591
|
+
source: Flag.choice("source", [
|
|
592
|
+
"staged",
|
|
593
|
+
"branch",
|
|
594
|
+
"commits",
|
|
595
|
+
"pull_request"
|
|
596
|
+
]).pipe(Flag.withDefault("staged"), Flag.withDescription("Review source type")),
|
|
597
|
+
branch: Flag.string("branch").pipe(Flag.withDescription("Branch name for branch source"), Flag.optional),
|
|
598
|
+
commits: Flag.string("commits").pipe(Flag.withDescription("Commit range for commits source"), Flag.optional),
|
|
599
|
+
title: Flag.string("title").pipe(Flag.withDescription("Review title"), Flag.optional)
|
|
600
|
+
}, (_config) => requireServerMode("ringi review create")).pipe(Command.withDescription("Create a review session"));
|
|
601
|
+
const reviewResolve = Command.make("resolve", {
|
|
602
|
+
id: Argument.string("id"),
|
|
603
|
+
allComments: Flag.boolean("all-comments").pipe(Flag.withDefault(true), Flag.withDescription("Resolve all comments")),
|
|
604
|
+
yes: Flag.boolean("yes").pipe(Flag.withDefault(false), Flag.withDescription("Skip confirmation prompt"))
|
|
605
|
+
}, (_config) => requireServerMode("ringi review resolve")).pipe(Command.withDescription("Resolve a review session"));
|
|
606
|
+
const reviewStatus = Command.make("status", {
|
|
607
|
+
reviewId: Flag.string("review").pipe(Flag.withDescription("Review ID or 'last'"), Flag.optional),
|
|
608
|
+
source: Flag.choice("source", [
|
|
609
|
+
"staged",
|
|
610
|
+
"branch",
|
|
611
|
+
"commits",
|
|
612
|
+
"pull_request"
|
|
613
|
+
]).pipe(Flag.withDescription("Filter by source type"), Flag.optional)
|
|
614
|
+
}, (config) => Effect.gen(function* () {
|
|
615
|
+
yield* ensureDatabaseExists;
|
|
655
616
|
const reviewService = yield* ReviewService;
|
|
656
617
|
const todoService = yield* TodoService;
|
|
657
618
|
const commentService = yield* CommentService;
|
|
@@ -660,12 +621,12 @@ const runReviewStatus = Effect.fn("CLI.reviewStatus")(function* runReviewStatus(
|
|
|
660
621
|
const repo = yield* gitService.getRepositoryInfo;
|
|
661
622
|
const stagedFiles = yield* gitService.getStagedFiles;
|
|
662
623
|
let reviewId;
|
|
663
|
-
if (
|
|
624
|
+
if (Option.isSome(config.reviewId)) reviewId = yield* resolveReviewSelector(config.reviewId.value);
|
|
664
625
|
const reviews = yield* reviewService.list({
|
|
665
626
|
page: 1,
|
|
666
627
|
pageSize: 1,
|
|
667
628
|
repositoryPath: cliConfig.repoRoot,
|
|
668
|
-
sourceType:
|
|
629
|
+
sourceType: Option.getOrUndefined(config.source)
|
|
669
630
|
});
|
|
670
631
|
const latestReview = reviewId ? yield* reviewService.getById(reviewId) : reviews.reviews[0];
|
|
671
632
|
let commentStats;
|
|
@@ -709,64 +670,32 @@ const runReviewStatus = Effect.fn("CLI.reviewStatus")(function* runReviewStatus(
|
|
|
709
670
|
description: "Export the latest review"
|
|
710
671
|
});
|
|
711
672
|
nextActions.push({
|
|
712
|
-
command: "ringi review create
|
|
713
|
-
description: "Create a new review session"
|
|
714
|
-
params: { source: {
|
|
715
|
-
default: "staged",
|
|
716
|
-
enum: [
|
|
717
|
-
"staged",
|
|
718
|
-
"branch",
|
|
719
|
-
"commits"
|
|
720
|
-
]
|
|
721
|
-
} }
|
|
673
|
+
command: "ringi review create --source <source>",
|
|
674
|
+
description: "Create a new review session"
|
|
722
675
|
});
|
|
723
|
-
|
|
676
|
+
yield* emitOutput("ringi review status", {
|
|
724
677
|
data,
|
|
725
678
|
human: lines.join("\n"),
|
|
726
679
|
nextActions
|
|
727
|
-
};
|
|
728
|
-
});
|
|
729
|
-
const runTodoList = Effect.fn("CLI.todoList")(function* runTodoList(command) {
|
|
730
|
-
const result = yield* (yield* TodoService).list({
|
|
731
|
-
completed: command.status === "all" ? void 0 : command.status === "done",
|
|
732
|
-
limit: command.limit,
|
|
733
|
-
offset: command.offset,
|
|
734
|
-
reviewId: command.reviewId
|
|
735
|
-
});
|
|
736
|
-
const nextActions = [];
|
|
737
|
-
if (command.reviewId) nextActions.push({
|
|
738
|
-
command: `ringi review show ${command.reviewId}`,
|
|
739
|
-
description: "View the associated review"
|
|
740
680
|
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
command: "ringi review list",
|
|
750
|
-
description: "List reviews"
|
|
751
|
-
});
|
|
752
|
-
return {
|
|
753
|
-
data: result,
|
|
754
|
-
human: renderTodoList(result.data),
|
|
755
|
-
nextActions
|
|
756
|
-
};
|
|
757
|
-
});
|
|
758
|
-
const runReviewPr = Effect.fn("CLI.reviewPr")(function* runReviewPr(command) {
|
|
759
|
-
const target = yield* parsePrUrl(command.prUrl).pipe(Effect.mapError((e) => new CliFailure({
|
|
681
|
+
})).pipe(Command.withDescription("Show repository and review status"));
|
|
682
|
+
const reviewPr = Command.make("pr", {
|
|
683
|
+
prUrl: Argument.string("pr-url"),
|
|
684
|
+
port: Flag.integer("port").pipe(Flag.withDefault(3e3), Flag.withDescription("Local server port")),
|
|
685
|
+
noOpen: Flag.boolean("no-open").pipe(Flag.withDefault(false), Flag.withDescription("Skip opening the browser")),
|
|
686
|
+
forceRefresh: Flag.boolean("force-refresh").pipe(Flag.withDefault(false), Flag.withDescription("Re-fetch PR data with latest changes"))
|
|
687
|
+
}, (config) => Effect.gen(function* () {
|
|
688
|
+
const target = yield* parsePrUrl(config.prUrl).pipe(Effect.mapError((e) => new CliFailure({
|
|
760
689
|
exitCode: ExitCode.UsageError,
|
|
761
690
|
message: e.message
|
|
762
691
|
})));
|
|
763
692
|
const preflight = yield* runPreflight(target).pipe(Effect.mapError((e) => new CliFailure({
|
|
764
|
-
exitCode: e.exitCode,
|
|
693
|
+
exitCode: e.exitCode ?? ExitCode.RuntimeFailure,
|
|
765
694
|
message: e.message
|
|
766
695
|
})));
|
|
767
696
|
if (preflight.affinityWarning) yield* Effect.logWarning(preflight.affinityWarning);
|
|
768
697
|
let session;
|
|
769
|
-
if (
|
|
698
|
+
if (config.forceRefresh) {
|
|
770
699
|
const reviewService = yield* ReviewService;
|
|
771
700
|
const sourceRef = prSourceRef(target);
|
|
772
701
|
const cliConfig = yield* CliConfig;
|
|
@@ -795,17 +724,9 @@ const runReviewPr = Effect.fn("CLI.reviewPr")(function* runReviewPr(command) {
|
|
|
795
724
|
message: e.message
|
|
796
725
|
})));
|
|
797
726
|
if (session.staleWarning) yield* Effect.logWarning(session.staleWarning);
|
|
798
|
-
const serverUrl = `http://localhost:${
|
|
727
|
+
const serverUrl = `http://localhost:${config.port}`;
|
|
799
728
|
const reviewUrl = `${serverUrl}/review/${session.reviewId}`;
|
|
800
|
-
const
|
|
801
|
-
isResumed: session.isResumed,
|
|
802
|
-
isStale: session.isStale,
|
|
803
|
-
prNumber: target.prNumber,
|
|
804
|
-
prUrl: target.url,
|
|
805
|
-
reviewId: session.reviewId,
|
|
806
|
-
reviewUrl
|
|
807
|
-
};
|
|
808
|
-
const statusLabel = session.isResumed ? command.forceRefresh ? "(refreshed)" : "(resumed)" : "(new)";
|
|
729
|
+
const statusLabel = session.isResumed ? config.forceRefresh ? "(refreshed)" : "(resumed)" : "(new)";
|
|
809
730
|
const humanLines = [
|
|
810
731
|
`PR #${target.prNumber}: ${preflight.metadata.title}`,
|
|
811
732
|
`Review: ${session.reviewId} ${statusLabel}`,
|
|
@@ -816,8 +737,14 @@ const runReviewPr = Effect.fn("CLI.reviewPr")(function* runReviewPr(command) {
|
|
|
816
737
|
`Server: ${serverUrl}`,
|
|
817
738
|
`Review: ${reviewUrl}`
|
|
818
739
|
];
|
|
819
|
-
|
|
820
|
-
|
|
740
|
+
const data = {
|
|
741
|
+
isResumed: session.isResumed,
|
|
742
|
+
isStale: session.isStale,
|
|
743
|
+
prNumber: target.prNumber,
|
|
744
|
+
prUrl: target.url,
|
|
745
|
+
reviewId: session.reviewId,
|
|
746
|
+
reviewUrl
|
|
747
|
+
};
|
|
821
748
|
const nextActions = [{
|
|
822
749
|
command: `ringi review show ${session.reviewId} --comments --todos`,
|
|
823
750
|
description: "Inspect review details"
|
|
@@ -825,1181 +752,186 @@ const runReviewPr = Effect.fn("CLI.reviewPr")(function* runReviewPr(command) {
|
|
|
825
752
|
command: `ringi review export ${session.reviewId}`,
|
|
826
753
|
description: "Export review as markdown"
|
|
827
754
|
}];
|
|
828
|
-
|
|
829
|
-
command: `ringi review ${command.prUrl} --force-refresh`,
|
|
830
|
-
description: "Re-fetch PR data with latest changes"
|
|
831
|
-
});
|
|
832
|
-
return {
|
|
755
|
+
yield* emitOutput("ringi review pr", {
|
|
833
756
|
data,
|
|
834
757
|
human: humanLines.join("\n"),
|
|
835
758
|
nextActions
|
|
836
|
-
};
|
|
837
|
-
});
|
|
838
|
-
/**
|
|
839
|
-
* Data-driven command registry. Each command kind maps to its handler.
|
|
840
|
-
* Adding a new command means adding one entry — no switch duplication.
|
|
841
|
-
*/
|
|
842
|
-
const COMMAND_HANDLERS = {
|
|
843
|
-
"data-migrate": () => requireServerMode("ringi data migrate"),
|
|
844
|
-
"data-reset": () => requireServerMode("ringi data reset"),
|
|
845
|
-
doctor: () => Effect.succeed({
|
|
846
|
-
data: {
|
|
847
|
-
checks: [],
|
|
848
|
-
ok: true
|
|
849
|
-
},
|
|
850
|
-
human: "ringi doctor: not yet implemented.",
|
|
851
|
-
nextActions: []
|
|
852
|
-
}),
|
|
853
|
-
events: () => requireServerMode("ringi events"),
|
|
854
|
-
mcp: () => Effect.fail(new CliFailure({
|
|
855
|
-
exitCode: ExitCode.UsageError,
|
|
856
|
-
message: "ringi mcp is a runtime command. Use it directly, not through the command dispatcher."
|
|
857
|
-
})),
|
|
858
|
-
"review-create": () => requireServerMode("ringi review create"),
|
|
859
|
-
"review-export": (c) => runReviewExport(c),
|
|
860
|
-
"review-list": (c) => runReviewList(c),
|
|
861
|
-
"review-pr": (c) => runReviewPr(c),
|
|
862
|
-
"review-resolve": () => requireServerMode("ringi review resolve"),
|
|
863
|
-
"review-show": (c) => runReviewShow(c),
|
|
864
|
-
"review-status": (c) => runReviewStatus(c),
|
|
865
|
-
serve: () => Effect.fail(new CliFailure({
|
|
866
|
-
exitCode: ExitCode.UsageError,
|
|
867
|
-
message: "ringi serve is a runtime command. Use it directly, not through the command dispatcher."
|
|
868
|
-
})),
|
|
869
|
-
"source-diff": (c) => runSourceDiff(c),
|
|
870
|
-
"source-list": () => runSourceList(),
|
|
871
|
-
"todo-add": () => requireServerMode("ringi todo add"),
|
|
872
|
-
"todo-clear": () => requireServerMode("ringi todo clear"),
|
|
873
|
-
"todo-done": () => requireServerMode("ringi todo done"),
|
|
874
|
-
"todo-list": (c) => runTodoList(c),
|
|
875
|
-
"todo-move": () => requireServerMode("ringi todo move"),
|
|
876
|
-
"todo-remove": () => requireServerMode("ringi todo remove"),
|
|
877
|
-
"todo-undone": () => requireServerMode("ringi todo undone")
|
|
878
|
-
};
|
|
879
|
-
/** Human-readable command label for the JSON envelope `command` field. */
|
|
880
|
-
const COMMAND_LABELS = {
|
|
881
|
-
"data-migrate": "ringi data migrate",
|
|
882
|
-
"data-reset": "ringi data reset",
|
|
883
|
-
doctor: "ringi doctor",
|
|
884
|
-
events: "ringi events",
|
|
885
|
-
mcp: "ringi mcp",
|
|
886
|
-
"review-create": "ringi review create",
|
|
887
|
-
"review-export": "ringi review export",
|
|
888
|
-
"review-list": "ringi review list",
|
|
889
|
-
"review-pr": "ringi review <pr-url>",
|
|
890
|
-
"review-resolve": "ringi review resolve",
|
|
891
|
-
"review-show": "ringi review show",
|
|
892
|
-
"review-status": "ringi review status",
|
|
893
|
-
serve: "ringi serve",
|
|
894
|
-
"source-diff": "ringi source diff",
|
|
895
|
-
"source-list": "ringi source list",
|
|
896
|
-
"todo-add": "ringi todo add",
|
|
897
|
-
"todo-clear": "ringi todo clear",
|
|
898
|
-
"todo-done": "ringi todo done",
|
|
899
|
-
"todo-list": "ringi todo list",
|
|
900
|
-
"todo-move": "ringi todo move",
|
|
901
|
-
"todo-remove": "ringi todo remove",
|
|
902
|
-
"todo-undone": "ringi todo undone"
|
|
903
|
-
};
|
|
904
|
-
const commandLabel = (command) => COMMAND_LABELS[command.kind] ?? `ringi ${command.kind}`;
|
|
905
|
-
const runCommand = (command) => {
|
|
906
|
-
const handler = COMMAND_HANDLERS[command.kind];
|
|
907
|
-
if (!handler) return Effect.fail(new CliFailure({
|
|
908
|
-
exitCode: ExitCode.UsageError,
|
|
909
|
-
message: `No executable handler exists for ${command.kind}.`
|
|
910
|
-
}));
|
|
911
|
-
return handler(command);
|
|
912
|
-
};
|
|
913
|
-
//#endregion
|
|
914
|
-
//#region src/cli/parser.ts
|
|
915
|
-
const REVIEW_SOURCES = new Set([
|
|
916
|
-
"branch",
|
|
917
|
-
"commits",
|
|
918
|
-
"pull_request",
|
|
919
|
-
"staged"
|
|
920
|
-
]);
|
|
921
|
-
const REVIEW_STATUSES = new Set([
|
|
922
|
-
"approved",
|
|
923
|
-
"changes_requested",
|
|
924
|
-
"in_progress"
|
|
925
|
-
]);
|
|
926
|
-
const TODO_STATUSES = new Set([
|
|
927
|
-
"all",
|
|
928
|
-
"done",
|
|
929
|
-
"pending"
|
|
930
|
-
]);
|
|
931
|
-
const usageError = (message) => new CliFailure({
|
|
932
|
-
exitCode: ExitCode.UsageError,
|
|
933
|
-
message
|
|
934
|
-
});
|
|
935
|
-
/**
|
|
936
|
-
* Consumes the next token as a flag value, advancing the cursor by 2.
|
|
937
|
-
* Rejects another flag in the value slot so typos fail fast.
|
|
938
|
-
*/
|
|
939
|
-
const requireValue = (state, flag) => {
|
|
940
|
-
const value = state.tokens[state.index + 1];
|
|
941
|
-
if (!value || value.startsWith("-")) return Option.some(usageError(`Missing value for ${flag}.`));
|
|
942
|
-
state.index += 2;
|
|
943
|
-
return Option.none();
|
|
944
|
-
};
|
|
945
|
-
/** Peek at the value that {@link requireValue} would consume. */
|
|
946
|
-
const peekValue = (state) => state.tokens[state.index + 1] ?? "";
|
|
947
|
-
const decodePositiveInt = (raw, flag) => {
|
|
948
|
-
const value = Number.parseInt(raw, 10);
|
|
949
|
-
if (!Number.isInteger(value) || value < 0) return Result.fail(usageError(`${flag} must be a non-negative integer.`));
|
|
950
|
-
return Result.succeed(value);
|
|
951
|
-
};
|
|
952
|
-
const decodeEnum = (raw, valid, label) => {
|
|
953
|
-
if (!valid.has(raw)) return Result.fail(usageError(`Invalid ${label}: ${raw}.`));
|
|
954
|
-
return Result.succeed(raw);
|
|
955
|
-
};
|
|
956
|
-
/** Boolean flag: sets a key to `true`, advances cursor by 1. */
|
|
957
|
-
const boolFlag = (key) => (state, acc) => {
|
|
958
|
-
acc[key] = true;
|
|
959
|
-
state.index += 1;
|
|
960
|
-
return Option.none();
|
|
961
|
-
};
|
|
962
|
-
/** String flag: consumes next token, assigns to key. */
|
|
963
|
-
const stringFlag = (key) => (state, acc) => {
|
|
964
|
-
const flag = state.tokens[state.index] ?? "";
|
|
965
|
-
const value = peekValue(state);
|
|
966
|
-
const error = requireValue(state, flag);
|
|
967
|
-
if (Option.isSome(error)) return error;
|
|
968
|
-
acc[key] = value;
|
|
969
|
-
return Option.none();
|
|
970
|
-
};
|
|
971
|
-
/** Positive integer flag with an optional minimum (exclusive). */
|
|
972
|
-
const positiveIntFlag = (key, opts) => (state, acc) => {
|
|
973
|
-
const flag = state.tokens[state.index] ?? "";
|
|
974
|
-
const raw = peekValue(state);
|
|
975
|
-
const error = requireValue(state, flag);
|
|
976
|
-
if (Option.isSome(error)) return error;
|
|
977
|
-
const decoded = decodePositiveInt(raw, flag);
|
|
978
|
-
if (Result.isFailure(decoded)) return Option.some(decoded.failure);
|
|
979
|
-
if (opts?.min !== void 0 && decoded.success <= opts.min) return Option.some(usageError(`${flag} must be greater than ${opts.min}.`));
|
|
980
|
-
acc[key] = decoded.success;
|
|
981
|
-
return Option.none();
|
|
982
|
-
};
|
|
983
|
-
/** Enum flag: consumes next token, validates membership, assigns to key. */
|
|
984
|
-
const enumFlag = (key, valid, label) => (state, acc) => {
|
|
985
|
-
const flag = state.tokens[state.index] ?? "";
|
|
986
|
-
const raw = peekValue(state);
|
|
987
|
-
const error = requireValue(state, flag);
|
|
988
|
-
if (Option.isSome(error)) return error;
|
|
989
|
-
const decoded = decodeEnum(raw, valid, label);
|
|
990
|
-
if (Result.isFailure(decoded)) return Option.some(decoded.failure);
|
|
991
|
-
acc[key] = decoded.success;
|
|
992
|
-
return Option.none();
|
|
993
|
-
};
|
|
994
|
-
const createDefaultOptions = () => ({
|
|
995
|
-
color: true,
|
|
996
|
-
dbPath: void 0,
|
|
997
|
-
help: false,
|
|
998
|
-
json: false,
|
|
999
|
-
quiet: false,
|
|
1000
|
-
repo: void 0,
|
|
1001
|
-
verbose: false,
|
|
1002
|
-
version: false
|
|
1003
|
-
});
|
|
1004
|
-
/**
|
|
1005
|
-
* Global flags are accepted before or after subcommands because wrappers often
|
|
1006
|
-
* prepend them without preserving the CLI's preferred ordering.
|
|
1007
|
-
*/
|
|
1008
|
-
const GLOBAL_FLAG_HANDLERS = {
|
|
1009
|
-
"--db-path": stringFlag("dbPath"),
|
|
1010
|
-
"--help": boolFlag("help"),
|
|
1011
|
-
"--json": boolFlag("json"),
|
|
1012
|
-
"--no-color": (state, acc) => {
|
|
1013
|
-
acc.color = false;
|
|
1014
|
-
state.index += 1;
|
|
1015
|
-
return Option.none();
|
|
1016
|
-
},
|
|
1017
|
-
"--quiet": boolFlag("quiet"),
|
|
1018
|
-
"--repo": stringFlag("repo"),
|
|
1019
|
-
"--verbose": boolFlag("verbose"),
|
|
1020
|
-
"--version": boolFlag("version")
|
|
1021
|
-
};
|
|
1022
|
-
const maybeParseGlobalFlag = (state) => {
|
|
1023
|
-
const token = state.tokens[state.index];
|
|
1024
|
-
if (!token) return false;
|
|
1025
|
-
const handler = GLOBAL_FLAG_HANDLERS[token];
|
|
1026
|
-
if (!handler) return false;
|
|
1027
|
-
handler(state, state.options);
|
|
1028
|
-
return true;
|
|
1029
|
-
};
|
|
1030
|
-
/**
|
|
1031
|
-
* Consumes all remaining tokens in {@link state} by dispatching to the matching
|
|
1032
|
-
* handler in {@link handlers}. Global flags are tried first. Unknown flags
|
|
1033
|
-
* produce a usage error naming the {@link commandLabel}.
|
|
1034
|
-
*/
|
|
1035
|
-
const runFlagLoop = (state, acc, handlers, commandLabel) => {
|
|
1036
|
-
while (state.index < state.tokens.length) {
|
|
1037
|
-
if (maybeParseGlobalFlag(state)) continue;
|
|
1038
|
-
const token = state.tokens[state.index] ?? "";
|
|
1039
|
-
const handler = handlers[token];
|
|
1040
|
-
if (!handler) return Option.some(usageError(`Unknown flag for ${commandLabel}: ${token}.`));
|
|
1041
|
-
const error = handler(state, acc);
|
|
1042
|
-
if (Option.isSome(error)) return error;
|
|
1043
|
-
}
|
|
1044
|
-
return Option.none();
|
|
1045
|
-
};
|
|
1046
|
-
const REVIEW_LIST_FLAGS = {
|
|
1047
|
-
"--limit": positiveIntFlag("limit", { min: 0 }),
|
|
1048
|
-
"--page": positiveIntFlag("page", { min: 0 }),
|
|
1049
|
-
"--source": enumFlag("source", REVIEW_SOURCES, "review source"),
|
|
1050
|
-
"--status": enumFlag("status", REVIEW_STATUSES, "review status")
|
|
1051
|
-
};
|
|
1052
|
-
const parseReviewList = (state) => {
|
|
1053
|
-
const acc = {
|
|
1054
|
-
limit: 20,
|
|
1055
|
-
page: 1,
|
|
1056
|
-
source: void 0,
|
|
1057
|
-
status: void 0
|
|
1058
|
-
};
|
|
1059
|
-
const error = runFlagLoop(state, acc, REVIEW_LIST_FLAGS, "review list");
|
|
1060
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1061
|
-
return Result.succeed({
|
|
1062
|
-
kind: "review-list",
|
|
1063
|
-
...acc
|
|
1064
|
-
});
|
|
1065
|
-
};
|
|
1066
|
-
const REVIEW_SHOW_FLAGS = {
|
|
1067
|
-
"--comments": boolFlag("comments"),
|
|
1068
|
-
"--todos": boolFlag("todos")
|
|
1069
|
-
};
|
|
1070
|
-
const parseReviewShow = (state) => {
|
|
1071
|
-
const id = state.tokens[state.index];
|
|
1072
|
-
if (!id) return Result.fail(usageError("review show requires <id|last>."));
|
|
1073
|
-
state.index += 1;
|
|
1074
|
-
const acc = {
|
|
1075
|
-
comments: false,
|
|
1076
|
-
todos: false
|
|
1077
|
-
};
|
|
1078
|
-
const error = runFlagLoop(state, acc, REVIEW_SHOW_FLAGS, "review show");
|
|
1079
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1080
|
-
return Result.succeed({
|
|
1081
|
-
id,
|
|
1082
|
-
kind: "review-show",
|
|
1083
|
-
...acc
|
|
1084
|
-
});
|
|
1085
|
-
};
|
|
1086
|
-
const REVIEW_EXPORT_FLAGS = {
|
|
1087
|
-
"--no-resolved": boolFlag("noResolved"),
|
|
1088
|
-
"--no-snippets": boolFlag("noSnippets"),
|
|
1089
|
-
"--output": stringFlag("outputPath"),
|
|
1090
|
-
"--stdout": boolFlag("stdout")
|
|
1091
|
-
};
|
|
1092
|
-
const parseReviewExport = (state) => {
|
|
1093
|
-
const id = state.tokens[state.index];
|
|
1094
|
-
if (!id) return Result.fail(usageError("review export requires <id|last>."));
|
|
1095
|
-
state.index += 1;
|
|
1096
|
-
const acc = {
|
|
1097
|
-
noResolved: false,
|
|
1098
|
-
noSnippets: false,
|
|
1099
|
-
outputPath: void 0,
|
|
1100
|
-
stdout: false
|
|
1101
|
-
};
|
|
1102
|
-
const error = runFlagLoop(state, acc, REVIEW_EXPORT_FLAGS, "review export");
|
|
1103
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1104
|
-
return Result.succeed({
|
|
1105
|
-
id,
|
|
1106
|
-
kind: "review-export",
|
|
1107
|
-
...acc
|
|
1108
|
-
});
|
|
1109
|
-
};
|
|
1110
|
-
const REVIEW_CREATE_FLAGS = {
|
|
1111
|
-
"--branch": stringFlag("branch"),
|
|
1112
|
-
"--commits": stringFlag("commits"),
|
|
1113
|
-
"--source": enumFlag("source", REVIEW_SOURCES, "review source"),
|
|
1114
|
-
"--title": stringFlag("title")
|
|
1115
|
-
};
|
|
1116
|
-
const validateReviewCreate = (acc) => {
|
|
1117
|
-
if (acc.source === "branch" && !acc.branch) return Option.some(usageError("review create --source branch requires --branch."));
|
|
1118
|
-
if (acc.source === "commits" && !acc.commits) return Option.some(usageError("review create --source commits requires --commits."));
|
|
1119
|
-
if (acc.source === "staged" && (acc.branch || acc.commits)) return Option.some(usageError("review create --source staged does not accept --branch or --commits."));
|
|
1120
|
-
return Option.none();
|
|
1121
|
-
};
|
|
1122
|
-
const parseReviewCreate = (state) => {
|
|
1123
|
-
const acc = {
|
|
1124
|
-
branch: void 0,
|
|
1125
|
-
commits: void 0,
|
|
1126
|
-
source: "staged",
|
|
1127
|
-
title: void 0
|
|
1128
|
-
};
|
|
1129
|
-
const error = runFlagLoop(state, acc, REVIEW_CREATE_FLAGS, "review create");
|
|
1130
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1131
|
-
const validationError = validateReviewCreate(acc);
|
|
1132
|
-
if (Option.isSome(validationError)) return Result.fail(validationError.value);
|
|
1133
|
-
return Result.succeed({
|
|
1134
|
-
kind: "review-create",
|
|
1135
|
-
...acc
|
|
1136
|
-
});
|
|
1137
|
-
};
|
|
1138
|
-
const SOURCE_DIFF_FLAGS = {
|
|
1139
|
-
"--branch": stringFlag("branch"),
|
|
1140
|
-
"--commits": stringFlag("commits"),
|
|
1141
|
-
"--stat": boolFlag("stat")
|
|
1142
|
-
};
|
|
1143
|
-
const validateSourceDiff = (source, acc) => {
|
|
1144
|
-
if (source === "branch" && !acc.branch) return Option.some(usageError("source diff branch requires --branch."));
|
|
1145
|
-
if (source === "commits" && !acc.commits) return Option.some(usageError("source diff commits requires --commits."));
|
|
1146
|
-
return Option.none();
|
|
1147
|
-
};
|
|
1148
|
-
const parseSourceDiff = (state) => {
|
|
1149
|
-
const source = state.tokens[state.index];
|
|
1150
|
-
if (!source || !REVIEW_SOURCES.has(source)) return Result.fail(usageError("source diff requires <staged|branch|commits>."));
|
|
1151
|
-
state.index += 1;
|
|
1152
|
-
const acc = {
|
|
1153
|
-
branch: void 0,
|
|
1154
|
-
commits: void 0,
|
|
1155
|
-
stat: false
|
|
1156
|
-
};
|
|
1157
|
-
const error = runFlagLoop(state, acc, SOURCE_DIFF_FLAGS, "source diff");
|
|
1158
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1159
|
-
const validationError = validateSourceDiff(source, acc);
|
|
1160
|
-
if (Option.isSome(validationError)) return Result.fail(validationError.value);
|
|
1161
|
-
return Result.succeed({
|
|
1162
|
-
kind: "source-diff",
|
|
1163
|
-
source,
|
|
1164
|
-
...acc
|
|
1165
|
-
});
|
|
1166
|
-
};
|
|
1167
|
-
const TODO_LIST_FLAGS = {
|
|
1168
|
-
"--limit": positiveIntFlag("limit"),
|
|
1169
|
-
"--offset": positiveIntFlag("offset"),
|
|
1170
|
-
"--review": stringFlag("reviewId"),
|
|
1171
|
-
"--status": enumFlag("status", TODO_STATUSES, "todo status")
|
|
1172
|
-
};
|
|
1173
|
-
const parseTodoList = (state) => {
|
|
1174
|
-
const acc = {
|
|
1175
|
-
limit: void 0,
|
|
1176
|
-
offset: 0,
|
|
1177
|
-
reviewId: void 0,
|
|
1178
|
-
status: "pending"
|
|
1179
|
-
};
|
|
1180
|
-
const error = runFlagLoop(state, acc, TODO_LIST_FLAGS, "todo list");
|
|
1181
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1182
|
-
return Result.succeed({
|
|
1183
|
-
kind: "todo-list",
|
|
1184
|
-
...acc
|
|
1185
759
|
});
|
|
1186
|
-
};
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const acc = { position: void 0 };
|
|
1209
|
-
const error = runFlagLoop(state, acc, TODO_MOVE_FLAGS, "todo move");
|
|
1210
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1211
|
-
if (acc.position === void 0) return Result.fail(usageError("todo move requires --position."));
|
|
1212
|
-
return Result.succeed({
|
|
1213
|
-
id,
|
|
1214
|
-
kind: "todo-move",
|
|
1215
|
-
position: acc.position
|
|
1216
|
-
});
|
|
1217
|
-
};
|
|
1218
|
-
const TODO_REMOVE_FLAGS = { "--yes": boolFlag("yes") };
|
|
1219
|
-
const parseTodoRemove = (state) => {
|
|
1220
|
-
const id = state.tokens[state.index];
|
|
1221
|
-
if (!id) return Result.fail(usageError("todo remove requires <id>."));
|
|
1222
|
-
state.index += 1;
|
|
1223
|
-
const acc = { yes: false };
|
|
1224
|
-
const error = runFlagLoop(state, acc, TODO_REMOVE_FLAGS, "todo remove");
|
|
1225
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1226
|
-
return Result.succeed({
|
|
1227
|
-
id,
|
|
1228
|
-
kind: "todo-remove",
|
|
1229
|
-
...acc
|
|
1230
|
-
});
|
|
1231
|
-
};
|
|
1232
|
-
const TODO_CLEAR_FLAGS = {
|
|
1233
|
-
"--all": boolFlag("all"),
|
|
1234
|
-
"--done-only": boolFlag("doneOnly"),
|
|
1235
|
-
"--review": stringFlag("reviewId"),
|
|
1236
|
-
"--yes": boolFlag("yes")
|
|
1237
|
-
};
|
|
1238
|
-
const parseTodoClear = (state) => {
|
|
1239
|
-
const acc = {
|
|
1240
|
-
all: false,
|
|
1241
|
-
doneOnly: true,
|
|
1242
|
-
reviewId: void 0,
|
|
1243
|
-
yes: false
|
|
1244
|
-
};
|
|
1245
|
-
const error = runFlagLoop(state, acc, TODO_CLEAR_FLAGS, "todo clear");
|
|
1246
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1247
|
-
return Result.succeed({
|
|
1248
|
-
kind: "todo-clear",
|
|
1249
|
-
...acc
|
|
1250
|
-
});
|
|
1251
|
-
};
|
|
1252
|
-
const REVIEW_STATUS_FLAGS = {
|
|
1253
|
-
"--review": stringFlag("reviewId"),
|
|
1254
|
-
"--source": enumFlag("source", REVIEW_SOURCES, "review source")
|
|
1255
|
-
};
|
|
1256
|
-
const parseReviewStatus = (state) => {
|
|
1257
|
-
const acc = {
|
|
1258
|
-
reviewId: void 0,
|
|
1259
|
-
source: void 0
|
|
1260
|
-
};
|
|
1261
|
-
const error = runFlagLoop(state, acc, REVIEW_STATUS_FLAGS, "review status");
|
|
1262
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1263
|
-
return Result.succeed({
|
|
1264
|
-
kind: "review-status",
|
|
1265
|
-
...acc
|
|
1266
|
-
});
|
|
1267
|
-
};
|
|
1268
|
-
const REVIEW_RESOLVE_FLAGS = {
|
|
1269
|
-
"--all-comments": boolFlag("allComments"),
|
|
1270
|
-
"--yes": boolFlag("yes")
|
|
1271
|
-
};
|
|
1272
|
-
const parseReviewResolve = (state) => {
|
|
1273
|
-
const id = state.tokens[state.index];
|
|
1274
|
-
if (!id) return Result.fail(usageError("review resolve requires <id|last>."));
|
|
1275
|
-
state.index += 1;
|
|
1276
|
-
const acc = {
|
|
1277
|
-
allComments: true,
|
|
1278
|
-
yes: false
|
|
1279
|
-
};
|
|
1280
|
-
const error = runFlagLoop(state, acc, REVIEW_RESOLVE_FLAGS, "review resolve");
|
|
1281
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1282
|
-
return Result.succeed({
|
|
1283
|
-
id,
|
|
1284
|
-
kind: "review-resolve",
|
|
1285
|
-
...acc
|
|
1286
|
-
});
|
|
1287
|
-
};
|
|
1288
|
-
const TODO_ADD_FLAGS = {
|
|
1289
|
-
"--position": positiveIntFlag("position"),
|
|
1290
|
-
"--review": stringFlag("reviewId"),
|
|
1291
|
-
"--text": stringFlag("text")
|
|
1292
|
-
};
|
|
1293
|
-
const parseTodoAdd = (state) => {
|
|
1294
|
-
const acc = {
|
|
1295
|
-
position: void 0,
|
|
1296
|
-
reviewId: void 0,
|
|
1297
|
-
text: ""
|
|
1298
|
-
};
|
|
1299
|
-
const error = runFlagLoop(state, acc, TODO_ADD_FLAGS, "todo add");
|
|
1300
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1301
|
-
if (!acc.text.trim()) return Result.fail(usageError("todo add requires --text."));
|
|
1302
|
-
return Result.succeed({
|
|
1303
|
-
kind: "todo-add",
|
|
1304
|
-
...acc
|
|
1305
|
-
});
|
|
1306
|
-
};
|
|
1307
|
-
const SERVE_FLAGS = {
|
|
1308
|
-
"--auth": boolFlag("auth"),
|
|
1309
|
-
"--cert": stringFlag("cert"),
|
|
1310
|
-
"--host": stringFlag("host"),
|
|
1311
|
-
"--https": boolFlag("https"),
|
|
1312
|
-
"--key": stringFlag("key"),
|
|
1313
|
-
"--no-open": boolFlag("noOpen"),
|
|
1314
|
-
"--password": stringFlag("password"),
|
|
1315
|
-
"--port": positiveIntFlag("port", { min: 0 }),
|
|
1316
|
-
"--username": stringFlag("username")
|
|
1317
|
-
};
|
|
1318
|
-
const parseServe = (state) => {
|
|
1319
|
-
const acc = {
|
|
1320
|
-
auth: false,
|
|
1321
|
-
cert: void 0,
|
|
1322
|
-
host: "127.0.0.1",
|
|
1323
|
-
https: false,
|
|
1324
|
-
key: void 0,
|
|
1325
|
-
noOpen: false,
|
|
1326
|
-
password: void 0,
|
|
1327
|
-
port: 3e3,
|
|
1328
|
-
username: void 0
|
|
1329
|
-
};
|
|
1330
|
-
const error = runFlagLoop(state, acc, SERVE_FLAGS, "serve");
|
|
1331
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1332
|
-
return Result.succeed({
|
|
1333
|
-
kind: "serve",
|
|
1334
|
-
...acc
|
|
1335
|
-
});
|
|
1336
|
-
};
|
|
1337
|
-
const MCP_LOG_LEVELS = new Set([
|
|
1338
|
-
"debug",
|
|
1339
|
-
"error",
|
|
1340
|
-
"info",
|
|
1341
|
-
"silent"
|
|
1342
|
-
]);
|
|
1343
|
-
const MCP_FLAGS = {
|
|
1344
|
-
"--log-level": enumFlag("logLevel", MCP_LOG_LEVELS, "log level"),
|
|
1345
|
-
"--readonly": boolFlag("readonly")
|
|
1346
|
-
};
|
|
1347
|
-
const parseMcp = (state) => {
|
|
1348
|
-
const acc = {
|
|
1349
|
-
logLevel: "error",
|
|
1350
|
-
readonly: false
|
|
1351
|
-
};
|
|
1352
|
-
const error = runFlagLoop(state, acc, MCP_FLAGS, "mcp");
|
|
1353
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1354
|
-
return Result.succeed({
|
|
1355
|
-
kind: "mcp",
|
|
1356
|
-
...acc
|
|
1357
|
-
});
|
|
1358
|
-
};
|
|
1359
|
-
const EVENT_TYPES = new Set([
|
|
1360
|
-
"comments",
|
|
1361
|
-
"files",
|
|
1362
|
-
"reviews",
|
|
1363
|
-
"todos"
|
|
1364
|
-
]);
|
|
1365
|
-
const EVENTS_FLAGS = {
|
|
1366
|
-
"--since": positiveIntFlag("since"),
|
|
1367
|
-
"--type": enumFlag("type", EVENT_TYPES, "event type")
|
|
1368
|
-
};
|
|
1369
|
-
const parseEvents = (state) => {
|
|
1370
|
-
const acc = {
|
|
1371
|
-
since: void 0,
|
|
1372
|
-
type: void 0
|
|
1373
|
-
};
|
|
1374
|
-
const error = runFlagLoop(state, acc, EVENTS_FLAGS, "events");
|
|
1375
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1376
|
-
return Result.succeed({
|
|
1377
|
-
kind: "events",
|
|
1378
|
-
...acc
|
|
1379
|
-
});
|
|
1380
|
-
};
|
|
1381
|
-
const DATA_RESET_FLAGS = {
|
|
1382
|
-
"--keep-exports": boolFlag("keepExports"),
|
|
1383
|
-
"--yes": boolFlag("yes")
|
|
1384
|
-
};
|
|
1385
|
-
const parseDataReset = (state) => {
|
|
1386
|
-
const acc = {
|
|
1387
|
-
keepExports: false,
|
|
1388
|
-
yes: false
|
|
1389
|
-
};
|
|
1390
|
-
const error = runFlagLoop(state, acc, DATA_RESET_FLAGS, "data reset");
|
|
1391
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1392
|
-
return Result.succeed({
|
|
1393
|
-
kind: "data-reset",
|
|
1394
|
-
...acc
|
|
1395
|
-
});
|
|
1396
|
-
};
|
|
1397
|
-
const ensureNoExtraArgs = (state, label) => {
|
|
1398
|
-
if (state.index < state.tokens.length) return Option.some(usageError(`Unexpected argument for ${label}: ${state.tokens[state.index]}.`));
|
|
1399
|
-
return Option.none();
|
|
1400
|
-
};
|
|
1401
|
-
/** Review verb parsers keyed by verb name. */
|
|
1402
|
-
const REVIEW_VERB_PARSERS = {
|
|
1403
|
-
create: parseReviewCreate,
|
|
1404
|
-
export: parseReviewExport,
|
|
1405
|
-
list: parseReviewList,
|
|
1406
|
-
resolve: parseReviewResolve,
|
|
1407
|
-
show: parseReviewShow,
|
|
1408
|
-
status: parseReviewStatus
|
|
1409
|
-
};
|
|
1410
|
-
/** Todo verb parsers keyed by verb name. */
|
|
1411
|
-
const TODO_VERB_PARSERS = {
|
|
1412
|
-
add: parseTodoAdd,
|
|
1413
|
-
clear: parseTodoClear,
|
|
1414
|
-
done: parseTodoDone,
|
|
1415
|
-
list: parseTodoList,
|
|
1416
|
-
move: parseTodoMove,
|
|
1417
|
-
remove: parseTodoRemove,
|
|
1418
|
-
undone: parseTodoUndone
|
|
1419
|
-
};
|
|
1420
|
-
const REVIEW_PR_FLAGS = {
|
|
1421
|
-
"--force-refresh": boolFlag("forceRefresh"),
|
|
1422
|
-
"--no-open": boolFlag("noOpen"),
|
|
1423
|
-
"--port": positiveIntFlag("port", { min: 0 })
|
|
1424
|
-
};
|
|
1425
|
-
const parseReviewPr = (state, prUrl) => {
|
|
1426
|
-
const acc = {
|
|
1427
|
-
forceRefresh: false,
|
|
1428
|
-
noOpen: false,
|
|
1429
|
-
port: 3e3
|
|
760
|
+
})).pipe(Command.withDescription("Create or resume a PR review"));
|
|
761
|
+
const review = Command.make("review").pipe(Command.withDescription("Review management commands"), Command.withSubcommands([
|
|
762
|
+
reviewList,
|
|
763
|
+
reviewShow,
|
|
764
|
+
reviewExport,
|
|
765
|
+
reviewCreate,
|
|
766
|
+
reviewResolve,
|
|
767
|
+
reviewStatus,
|
|
768
|
+
reviewPr
|
|
769
|
+
]));
|
|
770
|
+
const sourceList = Command.make("list", {}, () => Effect.gen(function* () {
|
|
771
|
+
const gitService = yield* GitService;
|
|
772
|
+
const repo = yield* gitService.getRepositoryInfo;
|
|
773
|
+
const stagedFiles = yield* gitService.getStagedFiles;
|
|
774
|
+
const data = {
|
|
775
|
+
branches: yield* gitService.getBranches,
|
|
776
|
+
commits: (yield* gitService.getCommits({
|
|
777
|
+
limit: 10,
|
|
778
|
+
offset: 0
|
|
779
|
+
})).commits,
|
|
780
|
+
repo,
|
|
781
|
+
stagedFiles
|
|
1430
782
|
};
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
/** Data verb parsers keyed by verb name. */
|
|
1441
|
-
const DATA_VERB_PARSERS = {
|
|
1442
|
-
migrate: (state) => {
|
|
1443
|
-
const error = ensureNoExtraArgs(state, "data migrate");
|
|
1444
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1445
|
-
return Result.succeed({ kind: "data-migrate" });
|
|
1446
|
-
},
|
|
1447
|
-
reset: parseDataReset
|
|
1448
|
-
};
|
|
1449
|
-
/** Subcommand family parsers keyed by family name. */
|
|
1450
|
-
const FAMILY_PARSERS = {
|
|
1451
|
-
data: (state) => {
|
|
1452
|
-
const verb = state.tokens[state.index];
|
|
1453
|
-
if (!verb) return Result.succeed({
|
|
1454
|
-
kind: "help",
|
|
1455
|
-
topic: ["data"]
|
|
1456
|
-
});
|
|
1457
|
-
state.index += 1;
|
|
1458
|
-
const parser = DATA_VERB_PARSERS[verb];
|
|
1459
|
-
if (!parser) return Result.fail(usageError(`Unknown data command: ${verb}.`));
|
|
1460
|
-
return parser(state);
|
|
1461
|
-
},
|
|
1462
|
-
doctor: (state) => {
|
|
1463
|
-
const error = ensureNoExtraArgs(state, "doctor");
|
|
1464
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1465
|
-
return Result.succeed({ kind: "doctor" });
|
|
1466
|
-
},
|
|
1467
|
-
events: parseEvents,
|
|
1468
|
-
export: parseReviewExport,
|
|
1469
|
-
mcp: parseMcp,
|
|
1470
|
-
review: (state) => {
|
|
1471
|
-
const verb = state.tokens[state.index];
|
|
1472
|
-
if (!verb) return Result.succeed({
|
|
1473
|
-
kind: "help",
|
|
1474
|
-
topic: ["review"]
|
|
1475
|
-
});
|
|
1476
|
-
if (looksLikePrUrl(verb)) {
|
|
1477
|
-
state.index += 1;
|
|
1478
|
-
return parseReviewPr(state, verb);
|
|
1479
|
-
}
|
|
1480
|
-
state.index += 1;
|
|
1481
|
-
const parser = REVIEW_VERB_PARSERS[verb];
|
|
1482
|
-
if (!parser) return Result.fail(usageError(`Unknown review command: ${verb}.`));
|
|
1483
|
-
return parser(state);
|
|
1484
|
-
},
|
|
1485
|
-
serve: parseServe,
|
|
1486
|
-
source: (state) => {
|
|
1487
|
-
const verb = state.tokens[state.index];
|
|
1488
|
-
if (!verb) return Result.succeed({
|
|
1489
|
-
kind: "help",
|
|
1490
|
-
topic: ["source"]
|
|
1491
|
-
});
|
|
1492
|
-
state.index += 1;
|
|
1493
|
-
if (verb === "list") {
|
|
1494
|
-
const error = ensureNoExtraArgs(state, "source list");
|
|
1495
|
-
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1496
|
-
return Result.succeed({ kind: "source-list" });
|
|
1497
|
-
}
|
|
1498
|
-
if (verb === "diff") return parseSourceDiff(state);
|
|
1499
|
-
return Result.fail(usageError(`Unknown source command: ${verb}.`));
|
|
1500
|
-
},
|
|
1501
|
-
todo: (state) => {
|
|
1502
|
-
const verb = state.tokens[state.index];
|
|
1503
|
-
if (!verb) return Result.succeed({
|
|
1504
|
-
kind: "help",
|
|
1505
|
-
topic: ["todo"]
|
|
1506
|
-
});
|
|
1507
|
-
state.index += 1;
|
|
1508
|
-
const parser = TODO_VERB_PARSERS[verb];
|
|
1509
|
-
if (!parser) return Result.fail(usageError(`Unknown todo command: ${verb}.`));
|
|
1510
|
-
return parser(state);
|
|
783
|
+
const lines = [
|
|
784
|
+
`Repository: ${data.repo.name}`,
|
|
785
|
+
`Path: ${data.repo.path}`,
|
|
786
|
+
`Current branch: ${data.repo.branch}`,
|
|
787
|
+
`Staged files: ${data.stagedFiles.length}`
|
|
788
|
+
];
|
|
789
|
+
if (data.stagedFiles.length > 0) {
|
|
790
|
+
lines.push("", "Staged:");
|
|
791
|
+
for (const file of data.stagedFiles) lines.push(`- ${file.status} ${file.path}`);
|
|
1511
792
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
* into a command family so `--help` and `--version` behave consistently.
|
|
1516
|
-
*/
|
|
1517
|
-
const parseWithState = (state) => {
|
|
1518
|
-
while (state.index < state.tokens.length && maybeParseGlobalFlag(state));
|
|
1519
|
-
if (state.options.version) return Result.succeed({ kind: "version" });
|
|
1520
|
-
if (state.index >= state.tokens.length) return Result.succeed({
|
|
1521
|
-
kind: "help",
|
|
1522
|
-
topic: []
|
|
1523
|
-
});
|
|
1524
|
-
const first = state.tokens[state.index];
|
|
1525
|
-
if (!first) return Result.succeed({
|
|
1526
|
-
kind: "help",
|
|
1527
|
-
topic: []
|
|
1528
|
-
});
|
|
1529
|
-
if (first === "help") {
|
|
1530
|
-
const topic = state.tokens.slice(state.index + 1);
|
|
1531
|
-
state.index = state.tokens.length;
|
|
1532
|
-
return Result.succeed({
|
|
1533
|
-
kind: "help",
|
|
1534
|
-
topic
|
|
1535
|
-
});
|
|
793
|
+
if (data.branches.length > 0) {
|
|
794
|
+
lines.push("", "Branches:");
|
|
795
|
+
for (const branch of data.branches.slice(0, 10)) lines.push(`- ${branch.current ? "*" : " "} ${branch.name}`);
|
|
1536
796
|
}
|
|
1537
|
-
if (
|
|
1538
|
-
|
|
1539
|
-
|
|
797
|
+
if (data.commits.length > 0) {
|
|
798
|
+
lines.push("", "Recent commits:");
|
|
799
|
+
for (const commit of data.commits.slice(0, 5)) lines.push(`- ${commit.hash.slice(0, 8)} ${commit.message} (${commit.author})`);
|
|
800
|
+
}
|
|
801
|
+
yield* emitOutput("ringi source list", {
|
|
802
|
+
data,
|
|
803
|
+
human: lines.join("\n"),
|
|
804
|
+
nextActions: [
|
|
805
|
+
{
|
|
806
|
+
command: "ringi source diff <source> --stat",
|
|
807
|
+
description: "View diff for a source"
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
command: "ringi review create --source <source>",
|
|
811
|
+
description: "Create a review from a source"
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
command: "ringi review list",
|
|
815
|
+
description: "List existing reviews"
|
|
816
|
+
}
|
|
817
|
+
]
|
|
1540
818
|
});
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
819
|
+
})).pipe(Command.withDescription("List repository sources"));
|
|
820
|
+
const diffStrategies = {
|
|
821
|
+
branch: (git, branch) => git.getBranchDiff(branch ?? ""),
|
|
822
|
+
commits: (git, _branch, commits) => git.getCommitDiff((commits ?? "").split(",").map((i) => i.trim()).filter(Boolean)),
|
|
823
|
+
staged: (git) => git.getStagedDiff
|
|
1545
824
|
};
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
825
|
+
const sourceDiff = Command.make("diff", {
|
|
826
|
+
source: Argument.choice("source", [
|
|
827
|
+
"staged",
|
|
828
|
+
"branch",
|
|
829
|
+
"commits"
|
|
830
|
+
]),
|
|
831
|
+
branch: Flag.string("branch").pipe(Flag.withDescription("Branch name"), Flag.optional),
|
|
832
|
+
commits: Flag.string("commits").pipe(Flag.withDescription("Commit range"), Flag.optional),
|
|
833
|
+
stat: Flag.boolean("stat").pipe(Flag.withDefault(false), Flag.withDescription("Show stats only"))
|
|
834
|
+
}, (config) => Effect.gen(function* () {
|
|
835
|
+
const gitService = yield* GitService;
|
|
836
|
+
const strategy = diffStrategies[config.source];
|
|
837
|
+
if (!strategy) yield* new CliFailure({
|
|
838
|
+
exitCode: ExitCode.UsageError,
|
|
839
|
+
message: "Unsupported review source."
|
|
1552
840
|
});
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
841
|
+
const diffText = yield* strategy(gitService, Option.getOrUndefined(config.branch), Option.getOrUndefined(config.commits));
|
|
842
|
+
if (!diffText.trim()) yield* new CliFailure({
|
|
843
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
844
|
+
message: "No diff available for the requested source."
|
|
1557
845
|
});
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
try {
|
|
1564
|
-
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1565
|
-
cwd,
|
|
1566
|
-
encoding: "utf8"
|
|
1567
|
-
}).trim();
|
|
1568
|
-
} catch {
|
|
1569
|
-
return new CliFailure({
|
|
1570
|
-
exitCode: ExitCode.StateUnavailable,
|
|
1571
|
-
message: repoOverride ? `Path ${cwd} is not a Git repository. Use --repo <path> with a valid repository root.` : `Could not resolve a Git repository from ${cwd}. Use --repo <path> with a valid repository root.`
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
};
|
|
1575
|
-
const resolveDbPath = (repoRoot, dbPathOverride) => dbPathOverride ? resolve(dbPathOverride) : resolve(repoRoot, ".ringi/reviews.db");
|
|
1576
|
-
const commandNeedsRepository = (command) => command.kind !== "help" && command.kind !== "version" && command.kind !== "mcp" && command.kind !== "serve";
|
|
1577
|
-
/**
|
|
1578
|
-
* Commands that need the database to already exist. `review-pr` is NOT here
|
|
1579
|
-
* because it auto-initializes `.ringi/` (like `serve` does).
|
|
1580
|
-
*/
|
|
1581
|
-
const commandNeedsDatabase = (command) => command.kind === "review-list" || command.kind === "review-show" || command.kind === "review-export" || command.kind === "review-status" || command.kind === "todo-list" || command.kind === "doctor";
|
|
1582
|
-
const commandUsesCoreRuntime = (command) => command.kind === "review-list" || command.kind === "review-show" || command.kind === "review-export" || command.kind === "review-pr" || command.kind === "review-status" || command.kind === "todo-list" || command.kind === "review-create" || command.kind === "todo-add" || command.kind === "doctor";
|
|
1583
|
-
const resolveCliConfig = (args) => {
|
|
1584
|
-
const repoRootResult = resolveRepositoryRoot(args.repo);
|
|
1585
|
-
if (repoRootResult instanceof CliFailure) return repoRootResult;
|
|
1586
|
-
return {
|
|
1587
|
-
color: args.color,
|
|
1588
|
-
cwd: process.cwd(),
|
|
1589
|
-
dbPath: resolveDbPath(repoRootResult, args.dbPath),
|
|
1590
|
-
outputMode: "human",
|
|
1591
|
-
quiet: args.quiet,
|
|
1592
|
-
repoRoot: repoRootResult,
|
|
1593
|
-
verbose: args.verbose
|
|
846
|
+
const files = parseDiff(diffText);
|
|
847
|
+
const data = {
|
|
848
|
+
diff: diffText,
|
|
849
|
+
source: config.source,
|
|
850
|
+
summary: getDiffSummary(files)
|
|
1594
851
|
};
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
852
|
+
yield* emitOutput("ringi source diff", {
|
|
853
|
+
data,
|
|
854
|
+
human: config.stat ? [
|
|
855
|
+
`Source: ${config.source}`,
|
|
856
|
+
`Files: ${data.summary.totalFiles}`,
|
|
857
|
+
`Additions: ${data.summary.totalAdditions}`,
|
|
858
|
+
`Deletions: ${data.summary.totalDeletions}`
|
|
859
|
+
].join("\n") : diffText,
|
|
860
|
+
nextActions: [{
|
|
861
|
+
command: `ringi review create --source ${config.source}`,
|
|
862
|
+
description: `Create a review from this ${config.source} diff`
|
|
863
|
+
}, {
|
|
864
|
+
command: "ringi source list",
|
|
865
|
+
description: "List repository sources"
|
|
866
|
+
}]
|
|
1600
867
|
});
|
|
1601
|
-
};
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
};
|
|
1620
|
-
};
|
|
1621
|
-
//#endregion
|
|
1622
|
-
//#region src/cli/main.ts
|
|
1623
|
-
const CLI_VERSION = "0.3.1";
|
|
1624
|
-
const COMMAND_TREE = {
|
|
1625
|
-
commands: [
|
|
1626
|
-
{
|
|
1627
|
-
description: "List review sessions",
|
|
1628
|
-
name: "review list",
|
|
1629
|
-
usage: "ringi review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]"
|
|
1630
|
-
},
|
|
1631
|
-
{
|
|
1632
|
-
description: "Show review details",
|
|
1633
|
-
name: "review show",
|
|
1634
|
-
usage: "ringi review show <id|last> [--comments] [--todos]"
|
|
1635
|
-
},
|
|
1636
|
-
{
|
|
1637
|
-
description: "Create a review session",
|
|
1638
|
-
name: "review create",
|
|
1639
|
-
usage: "ringi review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]"
|
|
1640
|
-
},
|
|
1641
|
-
{
|
|
1642
|
-
description: "Export review as markdown",
|
|
1643
|
-
name: "review export",
|
|
1644
|
-
usage: "ringi review export <id|last> [--output <path>] [--stdout]"
|
|
1645
|
-
},
|
|
1646
|
-
{
|
|
1647
|
-
description: "Resolve a review session",
|
|
1648
|
-
name: "review resolve",
|
|
1649
|
-
usage: "ringi review resolve <id|last> [--all-comments] [--yes]"
|
|
1650
|
-
},
|
|
1651
|
-
{
|
|
1652
|
-
description: "Show repository and review status",
|
|
1653
|
-
name: "review status",
|
|
1654
|
-
usage: "ringi review status [--review <id|last>] [--source <type>]"
|
|
1655
|
-
},
|
|
1656
|
-
{
|
|
1657
|
-
description: "List repository sources",
|
|
1658
|
-
name: "source list",
|
|
1659
|
-
usage: "ringi source list"
|
|
1660
|
-
},
|
|
1661
|
-
{
|
|
1662
|
-
description: "Show diff for a source",
|
|
1663
|
-
name: "source diff",
|
|
1664
|
-
usage: "ringi source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]"
|
|
1665
|
-
},
|
|
1666
|
-
{
|
|
1667
|
-
description: "List todo items",
|
|
1668
|
-
name: "todo list",
|
|
1669
|
-
usage: "ringi todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]"
|
|
1670
|
-
},
|
|
1671
|
-
{
|
|
1672
|
-
description: "Add a todo item",
|
|
1673
|
-
name: "todo add",
|
|
1674
|
-
usage: "ringi todo add --text <text> [--review <id>] [--position <n>]"
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
description: "Mark a todo as done",
|
|
1678
|
-
name: "todo done",
|
|
1679
|
-
usage: "ringi todo done <id>"
|
|
1680
|
-
},
|
|
1681
|
-
{
|
|
1682
|
-
description: "Reopen a completed todo",
|
|
1683
|
-
name: "todo undone",
|
|
1684
|
-
usage: "ringi todo undone <id>"
|
|
1685
|
-
},
|
|
1686
|
-
{
|
|
1687
|
-
description: "Move a todo to a position",
|
|
1688
|
-
name: "todo move",
|
|
1689
|
-
usage: "ringi todo move <id> --position <n>"
|
|
1690
|
-
},
|
|
1691
|
-
{
|
|
1692
|
-
description: "Remove a todo",
|
|
1693
|
-
name: "todo remove",
|
|
1694
|
-
usage: "ringi todo remove <id> [--yes]"
|
|
1695
|
-
},
|
|
1696
|
-
{
|
|
1697
|
-
description: "Clear completed todos",
|
|
1698
|
-
name: "todo clear",
|
|
1699
|
-
usage: "ringi todo clear [--review <id>] [--done-only] [--all] [--yes]"
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
description: "Start the local Ringi server",
|
|
1703
|
-
name: "serve",
|
|
1704
|
-
usage: "ringi serve [--host <host>] [--port <port>] [--https] [--auth] [--no-open]"
|
|
1705
|
-
},
|
|
1706
|
-
{
|
|
1707
|
-
description: "Start the MCP stdio server",
|
|
1708
|
-
name: "mcp",
|
|
1709
|
-
usage: "ringi mcp [--readonly] [--log-level <level>]"
|
|
1710
|
-
},
|
|
1711
|
-
{
|
|
1712
|
-
description: "Run local diagnostics",
|
|
1713
|
-
name: "doctor",
|
|
1714
|
-
usage: "ringi doctor"
|
|
1715
|
-
},
|
|
1716
|
-
{
|
|
1717
|
-
description: "Tail server events",
|
|
1718
|
-
name: "events",
|
|
1719
|
-
usage: "ringi events [--type <reviews|comments|todos|files>]"
|
|
1720
|
-
},
|
|
1721
|
-
{
|
|
1722
|
-
description: "Run database migrations",
|
|
1723
|
-
name: "data migrate",
|
|
1724
|
-
usage: "ringi data migrate"
|
|
1725
|
-
},
|
|
1726
|
-
{
|
|
1727
|
-
description: "Reset local data",
|
|
1728
|
-
name: "data reset",
|
|
1729
|
-
usage: "ringi data reset [--yes] [--keep-exports]"
|
|
1730
|
-
}
|
|
1731
|
-
],
|
|
1732
|
-
description: "ringi — local-first code review CLI",
|
|
1733
|
-
version: CLI_VERSION
|
|
1734
|
-
};
|
|
1735
|
-
const ROOT_NEXT_ACTIONS = [
|
|
1736
|
-
{
|
|
1737
|
-
command: "ringi review list [--status <status>] [--source <type>]",
|
|
1738
|
-
description: "List review sessions",
|
|
1739
|
-
params: {
|
|
1740
|
-
source: { enum: [
|
|
1741
|
-
"staged",
|
|
1742
|
-
"branch",
|
|
1743
|
-
"commits"
|
|
1744
|
-
] },
|
|
1745
|
-
status: { enum: [
|
|
1746
|
-
"in_progress",
|
|
1747
|
-
"approved",
|
|
1748
|
-
"changes_requested"
|
|
1749
|
-
] }
|
|
1750
|
-
}
|
|
1751
|
-
},
|
|
1752
|
-
{
|
|
1753
|
-
command: "ringi source list",
|
|
1754
|
-
description: "List repository sources"
|
|
1755
|
-
},
|
|
1756
|
-
{
|
|
1757
|
-
command: "ringi review create [--source <source>]",
|
|
1758
|
-
description: "Create a new review session",
|
|
1759
|
-
params: { source: {
|
|
1760
|
-
default: "staged",
|
|
1761
|
-
enum: [
|
|
1762
|
-
"staged",
|
|
1763
|
-
"branch",
|
|
1764
|
-
"commits"
|
|
1765
|
-
]
|
|
1766
|
-
} }
|
|
1767
|
-
},
|
|
1768
|
-
{
|
|
1769
|
-
command: "ringi todo list [--status <status>]",
|
|
1770
|
-
description: "List todos",
|
|
1771
|
-
params: { status: {
|
|
1772
|
-
default: "pending",
|
|
1773
|
-
enum: [
|
|
1774
|
-
"pending",
|
|
1775
|
-
"done",
|
|
1776
|
-
"all"
|
|
1777
|
-
]
|
|
1778
|
-
} }
|
|
1779
|
-
},
|
|
1780
|
-
{
|
|
1781
|
-
command: "ringi review status",
|
|
1782
|
-
description: "Show repository and review status"
|
|
1783
|
-
}
|
|
1784
|
-
];
|
|
1785
|
-
const ROOT_HELP = `ringi — local-first review CLI
|
|
1786
|
-
|
|
1787
|
-
Usage:
|
|
1788
|
-
ringi [global options] <command>
|
|
1789
|
-
|
|
1790
|
-
Global options:
|
|
1791
|
-
--json Emit structured JSON envelope to stdout
|
|
1792
|
-
--repo <path> Use a specific Git repository root
|
|
1793
|
-
--db-path <path> Override the SQLite database path
|
|
1794
|
-
--quiet Suppress human-readable success output
|
|
1795
|
-
--verbose Include stack traces on failures
|
|
1796
|
-
--no-color Disable ANSI color output
|
|
1797
|
-
--help Show help
|
|
1798
|
-
--version Show version
|
|
1799
|
-
|
|
1800
|
-
Commands:
|
|
1801
|
-
review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]
|
|
1802
|
-
review show <id|last> [--comments] [--todos]
|
|
1803
|
-
review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]
|
|
1804
|
-
review export <id|last> [--output <path>] [--stdout]
|
|
1805
|
-
review resolve <id|last> [--all-comments] [--yes]
|
|
1806
|
-
review status [--review <id|last>] [--source <type>]
|
|
1807
|
-
source list
|
|
1808
|
-
source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]
|
|
1809
|
-
todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]
|
|
1810
|
-
todo add --text <text> [--review <id>]
|
|
1811
|
-
todo done <id>
|
|
1812
|
-
todo undone <id>
|
|
1813
|
-
todo move <id> --position <n>
|
|
1814
|
-
todo remove <id> [--yes]
|
|
1815
|
-
todo clear [--review <id>] [--done-only] [--all] [--yes]
|
|
1816
|
-
export <id|last> [--output <path>] [--stdout]
|
|
1817
|
-
`;
|
|
1818
|
-
const HELP_TOPICS = {
|
|
1819
|
-
data: `ringi data
|
|
1820
|
-
|
|
1821
|
-
Usage:
|
|
1822
|
-
ringi data migrate
|
|
1823
|
-
ringi data reset [--yes] [--keep-exports]
|
|
1824
|
-
`,
|
|
1825
|
-
review: `ringi review
|
|
1826
|
-
|
|
1827
|
-
Usage:
|
|
1828
|
-
ringi review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]
|
|
1829
|
-
ringi review show <id|last> [--comments] [--todos]
|
|
1830
|
-
ringi review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]
|
|
1831
|
-
ringi review export <id|last> [--output <path>] [--stdout]
|
|
1832
|
-
ringi review resolve <id|last> [--all-comments] [--yes]
|
|
1833
|
-
ringi review status [--review <id|last>] [--source <type>]
|
|
1834
|
-
`,
|
|
1835
|
-
source: `ringi source
|
|
1836
|
-
|
|
1837
|
-
Usage:
|
|
1838
|
-
ringi source list
|
|
1839
|
-
ringi source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]
|
|
1840
|
-
`,
|
|
1841
|
-
todo: `ringi todo
|
|
1842
|
-
|
|
1843
|
-
Usage:
|
|
1844
|
-
ringi todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]
|
|
1845
|
-
ringi todo add --text <text> [--review <id>] [--position <n>]
|
|
1846
|
-
ringi todo done <id>
|
|
1847
|
-
ringi todo undone <id>
|
|
1848
|
-
ringi todo move <id> --position <n>
|
|
1849
|
-
ringi todo remove <id> [--yes]
|
|
1850
|
-
ringi todo clear [--review <id>] [--done-only] [--all] [--yes]
|
|
1851
|
-
`
|
|
1852
|
-
};
|
|
1853
|
-
const renderHelp = (command) => {
|
|
1854
|
-
if (command.kind !== "help") return ROOT_HELP;
|
|
1855
|
-
const [topic] = command.topic;
|
|
1856
|
-
return (topic && HELP_TOPICS[topic]) ?? ROOT_HELP;
|
|
1857
|
-
};
|
|
1858
|
-
const writeJson = (payload) => {
|
|
1859
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1860
|
-
};
|
|
1861
|
-
const writeHuman = (text) => {
|
|
1862
|
-
if (text && text.length > 0) process.stdout.write(`${text}\n`);
|
|
1863
|
-
};
|
|
1864
|
-
/** Maps exit codes to error categories and retryable status. */
|
|
1865
|
-
const EXIT_CODE_META = {
|
|
1866
|
-
[ExitCode.AuthFailure]: {
|
|
1867
|
-
category: "auth",
|
|
1868
|
-
code: "AUTH_FAILURE",
|
|
1869
|
-
retryable: false
|
|
1870
|
-
},
|
|
1871
|
-
[ExitCode.ResourceNotFound]: {
|
|
1872
|
-
category: "not_found",
|
|
1873
|
-
code: "RESOURCE_NOT_FOUND",
|
|
1874
|
-
retryable: false
|
|
1875
|
-
},
|
|
1876
|
-
[ExitCode.RuntimeFailure]: {
|
|
1877
|
-
category: "server",
|
|
1878
|
-
code: "RUNTIME_FAILURE",
|
|
1879
|
-
retryable: true
|
|
1880
|
-
},
|
|
1881
|
-
[ExitCode.StateUnavailable]: {
|
|
1882
|
-
category: "config",
|
|
1883
|
-
code: "STATE_UNAVAILABLE",
|
|
1884
|
-
retryable: false
|
|
1885
|
-
},
|
|
1886
|
-
[ExitCode.UsageError]: {
|
|
1887
|
-
category: "validation",
|
|
1888
|
-
code: "USAGE_ERROR",
|
|
1889
|
-
retryable: false
|
|
1890
|
-
}
|
|
1891
|
-
};
|
|
1892
|
-
const mapFailure = (error) => {
|
|
1893
|
-
if (error instanceof CliFailure) {
|
|
1894
|
-
const meta = EXIT_CODE_META[error.exitCode] ?? {
|
|
1895
|
-
category: "server",
|
|
1896
|
-
code: "UNKNOWN",
|
|
1897
|
-
retryable: false
|
|
1898
|
-
};
|
|
1899
|
-
return {
|
|
1900
|
-
category: meta.category,
|
|
1901
|
-
code: meta.code,
|
|
1902
|
-
exitCode: error.exitCode,
|
|
1903
|
-
message: error.message,
|
|
1904
|
-
retryable: meta.retryable,
|
|
1905
|
-
verbose: error.details
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
if (error instanceof ReviewNotFound || error instanceof TodoNotFound) return {
|
|
1909
|
-
category: "not_found",
|
|
1910
|
-
code: "RESOURCE_NOT_FOUND",
|
|
1911
|
-
exitCode: ExitCode.ResourceNotFound,
|
|
1912
|
-
message: error.message,
|
|
1913
|
-
retryable: false,
|
|
1914
|
-
verbose: error.stack
|
|
1915
|
-
};
|
|
1916
|
-
if (error instanceof Error) return {
|
|
1917
|
-
category: "server",
|
|
1918
|
-
code: "RUNTIME_FAILURE",
|
|
1919
|
-
exitCode: ExitCode.RuntimeFailure,
|
|
1920
|
-
message: error.message,
|
|
1921
|
-
retryable: true,
|
|
1922
|
-
verbose: error.stack
|
|
1923
|
-
};
|
|
1924
|
-
return {
|
|
1925
|
-
category: "server",
|
|
1926
|
-
code: "UNKNOWN_FAILURE",
|
|
1927
|
-
exitCode: ExitCode.RuntimeFailure,
|
|
1928
|
-
message: "Unknown CLI failure.",
|
|
1929
|
-
retryable: false
|
|
1930
|
-
};
|
|
1931
|
-
};
|
|
1932
|
-
/** Actionable fix guidance based on error category. */
|
|
1933
|
-
const FIX_GUIDANCE = {
|
|
1934
|
-
auth: "Check authentication credentials or run 'ringi serve --auth' with valid credentials.",
|
|
1935
|
-
config: "Run 'ringi serve' once to initialize local state, or check --repo and --db-path flags.",
|
|
1936
|
-
conflict: "Resolve the conflict and retry the operation.",
|
|
1937
|
-
connection: "Ensure the Ringi server is running: ringi serve",
|
|
1938
|
-
not_found: "Verify the resource ID. Use 'ringi review list' or 'ringi todo list' to find valid IDs.",
|
|
1939
|
-
server: "Retry the command. If the error persists, check 'ringi serve' logs.",
|
|
1940
|
-
validation: "Check command usage with 'ringi --help'. Verify flag names and values."
|
|
1941
|
-
};
|
|
1942
|
-
/** Build recovery next_actions based on error category. */
|
|
1943
|
-
const errorNextActions = (commandStr, normalized) => {
|
|
1944
|
-
const actions = [];
|
|
1945
|
-
if (normalized.retryable) actions.push({
|
|
1946
|
-
command: commandStr,
|
|
1947
|
-
description: "Retry the failed command"
|
|
868
|
+
})).pipe(Command.withDescription("Show diff for a source"));
|
|
869
|
+
const source = Command.make("source").pipe(Command.withDescription("Source management commands"), Command.withSubcommands([sourceList, sourceDiff]));
|
|
870
|
+
const todoList = Command.make("list", {
|
|
871
|
+
reviewId: Flag.string("review").pipe(Flag.withDescription("Filter by review ID"), Flag.optional),
|
|
872
|
+
status: Flag.choice("status", [
|
|
873
|
+
"pending",
|
|
874
|
+
"done",
|
|
875
|
+
"all"
|
|
876
|
+
]).pipe(Flag.withDefault("pending"), Flag.withDescription("Filter by status")),
|
|
877
|
+
limit: Flag.integer("limit").pipe(Flag.withDescription("Max results"), Flag.optional),
|
|
878
|
+
offset: Flag.integer("offset").pipe(Flag.withDefault(0), Flag.withDescription("Results offset"))
|
|
879
|
+
}, (config) => Effect.gen(function* () {
|
|
880
|
+
yield* ensureDatabaseExists;
|
|
881
|
+
const result = yield* (yield* TodoService).list({
|
|
882
|
+
completed: config.status === "all" ? void 0 : config.status === "done",
|
|
883
|
+
limit: Option.getOrUndefined(config.limit),
|
|
884
|
+
offset: config.offset,
|
|
885
|
+
reviewId: Option.getOrUndefined(config.reviewId)
|
|
1948
886
|
});
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
887
|
+
const nextActions = [];
|
|
888
|
+
if (Option.isSome(config.reviewId)) nextActions.push({
|
|
889
|
+
command: `ringi review show ${config.reviewId.value}`,
|
|
890
|
+
description: "View the associated review"
|
|
1952
891
|
});
|
|
1953
|
-
|
|
1954
|
-
command: "ringi
|
|
1955
|
-
description: "
|
|
892
|
+
nextActions.push({
|
|
893
|
+
command: "ringi todo add --text <text>",
|
|
894
|
+
description: "Add a new todo"
|
|
1956
895
|
}, {
|
|
1957
|
-
command: "ringi
|
|
1958
|
-
description: "List
|
|
896
|
+
command: "ringi review list",
|
|
897
|
+
description: "List reviews"
|
|
1959
898
|
});
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
899
|
+
yield* emitOutput("ringi todo list", {
|
|
900
|
+
data: result,
|
|
901
|
+
human: result.data.length === 0 ? "No todos found." : result.data.map((todo) => `- [${todo.completed ? "x" : " "}] (${todo.position + 1}) ${todo.content}`).join("\n"),
|
|
902
|
+
nextActions
|
|
1963
903
|
});
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
};
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
* assets). From the bundled CLI at `dist/cli.mjs`, the package root is
|
|
1996
|
-
* `import.meta.dirname/..`.
|
|
1997
|
-
* 2. **Monorepo development** — `apps/web/.output/server/index.mjs`
|
|
1998
|
-
* During local development the web build output lives inside the web
|
|
1999
|
-
* workspace. We resolve it relative to the CLI package root.
|
|
2000
|
-
* 3. **CWD fallback** — `.output/server/index.mjs` relative to the current
|
|
2001
|
-
* working directory, for running the server in a standalone checkout.
|
|
2002
|
-
*/
|
|
904
|
+
})).pipe(Command.withDescription("List todo items"));
|
|
905
|
+
const todoAdd = Command.make("add", {
|
|
906
|
+
text: Flag.string("text").pipe(Flag.withDescription("Todo text")),
|
|
907
|
+
reviewId: Flag.string("review").pipe(Flag.withDescription("Associate with review"), Flag.optional),
|
|
908
|
+
position: Flag.integer("position").pipe(Flag.withDescription("Insert position"), Flag.optional)
|
|
909
|
+
}, (_config) => requireServerMode("ringi todo add")).pipe(Command.withDescription("Add a todo item"));
|
|
910
|
+
const todoDone = Command.make("done", { id: Argument.string("id") }, (_config) => requireServerMode("ringi todo done")).pipe(Command.withDescription("Mark a todo as done"));
|
|
911
|
+
const todoUndone = Command.make("undone", { id: Argument.string("id") }, (_config) => requireServerMode("ringi todo undone")).pipe(Command.withDescription("Reopen a completed todo"));
|
|
912
|
+
const todoMove = Command.make("move", {
|
|
913
|
+
id: Argument.string("id"),
|
|
914
|
+
position: Flag.integer("position").pipe(Flag.withDescription("Target position"))
|
|
915
|
+
}, (_config) => requireServerMode("ringi todo move")).pipe(Command.withDescription("Move a todo to a position"));
|
|
916
|
+
const todoRemove = Command.make("remove", {
|
|
917
|
+
id: Argument.string("id"),
|
|
918
|
+
yes: Flag.boolean("yes").pipe(Flag.withDefault(false), Flag.withDescription("Skip confirmation"))
|
|
919
|
+
}, (_config) => requireServerMode("ringi todo remove")).pipe(Command.withDescription("Remove a todo"));
|
|
920
|
+
const todoClear = Command.make("clear", {
|
|
921
|
+
reviewId: Flag.string("review").pipe(Flag.withDescription("Scope to review"), Flag.optional),
|
|
922
|
+
doneOnly: Flag.boolean("done-only").pipe(Flag.withDefault(true), Flag.withDescription("Clear only completed")),
|
|
923
|
+
all: Flag.boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Clear all todos")),
|
|
924
|
+
yes: Flag.boolean("yes").pipe(Flag.withDefault(false), Flag.withDescription("Skip confirmation"))
|
|
925
|
+
}, (_config) => requireServerMode("ringi todo clear")).pipe(Command.withDescription("Clear completed todos"));
|
|
926
|
+
const todo = Command.make("todo").pipe(Command.withDescription("Todo management commands"), Command.withSubcommands([
|
|
927
|
+
todoList,
|
|
928
|
+
todoAdd,
|
|
929
|
+
todoDone,
|
|
930
|
+
todoUndone,
|
|
931
|
+
todoMove,
|
|
932
|
+
todoRemove,
|
|
933
|
+
todoClear
|
|
934
|
+
]));
|
|
2003
935
|
const resolveServerEntry = () => {
|
|
2004
936
|
const candidates = [];
|
|
2005
937
|
if (import.meta.dirname) {
|
|
@@ -2010,122 +942,126 @@ const resolveServerEntry = () => {
|
|
|
2010
942
|
candidates.push(resolve(process.cwd(), ".output", "server", "index.mjs"));
|
|
2011
943
|
return candidates.find((candidate) => existsSync(candidate));
|
|
2012
944
|
};
|
|
2013
|
-
const
|
|
945
|
+
const serve = Command.make("serve", {
|
|
946
|
+
host: Flag.string("host").pipe(Flag.withDefault("127.0.0.1"), Flag.withDescription("Bind host")),
|
|
947
|
+
port: Flag.integer("port").pipe(Flag.withDefault(3e3), Flag.withDescription("Port number")),
|
|
948
|
+
https: Flag.boolean("https").pipe(Flag.withDefault(false), Flag.withDescription("Enable HTTPS")),
|
|
949
|
+
cert: Flag.string("cert").pipe(Flag.withDescription("SSL certificate path"), Flag.optional),
|
|
950
|
+
key: Flag.string("key").pipe(Flag.withDescription("SSL key path"), Flag.optional),
|
|
951
|
+
auth: Flag.boolean("auth").pipe(Flag.withDefault(false), Flag.withDescription("Enable authentication")),
|
|
952
|
+
noOpen: Flag.boolean("no-open").pipe(Flag.withDefault(false), Flag.withDescription("Don't open browser"))
|
|
953
|
+
}, (config) => Effect.callback((resume) => {
|
|
2014
954
|
const serverEntry = resolveServerEntry();
|
|
2015
955
|
if (!serverEntry) {
|
|
2016
956
|
const hint = import.meta.dirname && !import.meta.dirname.includes("apps/cli/") ? "The installed package is missing its server assets. Try reinstalling: npm install -g @sanurb/ringi" : "Run 'pnpm build' at the monorepo root, then 'pnpm --filter @sanurb/ringi build:server' to copy the server assets.";
|
|
2017
|
-
|
|
2018
|
-
|
|
957
|
+
resume(Effect.fail(new CliFailure({
|
|
958
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
959
|
+
message: `No built server found.\n${hint}`
|
|
960
|
+
})));
|
|
961
|
+
return;
|
|
2019
962
|
}
|
|
2020
963
|
const env = {
|
|
2021
964
|
...process.env,
|
|
2022
|
-
NITRO_HOST:
|
|
2023
|
-
NITRO_PORT: String(
|
|
965
|
+
NITRO_HOST: config.host,
|
|
966
|
+
NITRO_PORT: String(config.port)
|
|
2024
967
|
};
|
|
2025
|
-
if (
|
|
2026
|
-
env.NITRO_SSL_CERT =
|
|
2027
|
-
env.NITRO_SSL_KEY =
|
|
968
|
+
if (config.https && Option.isSome(config.cert) && Option.isSome(config.key)) {
|
|
969
|
+
env.NITRO_SSL_CERT = config.cert.value;
|
|
970
|
+
env.NITRO_SSL_KEY = config.key.value;
|
|
2028
971
|
}
|
|
2029
|
-
const url = `${
|
|
972
|
+
const url = `${config.https ? "https" : "http"}://${config.host === "0.0.0.0" ? "localhost" : config.host}:${config.port}`;
|
|
2030
973
|
process.stderr.write(`ringi server starting on ${url}\n`);
|
|
2031
974
|
const child = fork(serverEntry, [], {
|
|
2032
975
|
env,
|
|
2033
976
|
execArgv: [],
|
|
2034
977
|
stdio: "inherit"
|
|
2035
978
|
});
|
|
2036
|
-
if (!
|
|
979
|
+
if (!config.noOpen) setTimeout(() => {
|
|
2037
980
|
let openCmd = "xdg-open";
|
|
2038
981
|
if (process.platform === "darwin") openCmd = "open";
|
|
2039
982
|
else if (process.platform === "win32") openCmd = "start";
|
|
2040
983
|
exec(`${openCmd} ${url}`, () => {});
|
|
2041
984
|
}, 1500);
|
|
2042
|
-
const shutdown = () =>
|
|
2043
|
-
child.kill("SIGTERM");
|
|
2044
|
-
};
|
|
985
|
+
const shutdown = () => child.kill("SIGTERM");
|
|
2045
986
|
process.once("SIGINT", shutdown);
|
|
2046
987
|
process.once("SIGTERM", shutdown);
|
|
2047
988
|
child.on("exit", (code) => {
|
|
2048
989
|
process.off("SIGINT", shutdown);
|
|
2049
990
|
process.off("SIGTERM", shutdown);
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
const normalized = mapFailure(opts.error);
|
|
2056
|
-
if (opts.json) writeJson(buildErrorEnvelope(opts.cmdStr, normalized));
|
|
2057
|
-
process.stderr.write(`${normalized.message}\n`);
|
|
2058
|
-
if (opts.verbose && normalized.verbose) process.stderr.write(`${normalized.verbose}\n`);
|
|
2059
|
-
return process.exit(normalized.exitCode);
|
|
2060
|
-
};
|
|
2061
|
-
const main = async () => {
|
|
2062
|
-
const argv = process.argv.slice(2);
|
|
2063
|
-
const parseResult = parseCliArgs(argv);
|
|
2064
|
-
if (Result.isFailure(parseResult)) return failAndExit({
|
|
2065
|
-
cmdStr: "ringi",
|
|
2066
|
-
error: parseResult.failure,
|
|
2067
|
-
json: argv.includes("--json"),
|
|
2068
|
-
verbose: false
|
|
2069
|
-
});
|
|
2070
|
-
const { command, options } = parseResult.success;
|
|
2071
|
-
if (command.kind === "help") {
|
|
2072
|
-
if (options.json) writeJson(success("ringi", COMMAND_TREE, ROOT_NEXT_ACTIONS));
|
|
2073
|
-
else writeHuman(renderHelp(command));
|
|
2074
|
-
process.exit(ExitCode.Success);
|
|
2075
|
-
}
|
|
2076
|
-
if (command.kind === "version") {
|
|
2077
|
-
if (options.json) writeJson(success("ringi --version", { version: CLI_VERSION }));
|
|
2078
|
-
else writeHuman(CLI_VERSION);
|
|
2079
|
-
process.exit(ExitCode.Success);
|
|
2080
|
-
}
|
|
2081
|
-
if (command.kind === "serve") {
|
|
2082
|
-
runServe(command);
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
const runtimeResources = createCliRuntimeResources(command, {
|
|
2086
|
-
color: options.color,
|
|
2087
|
-
dbPath: options.dbPath,
|
|
2088
|
-
quiet: options.quiet,
|
|
2089
|
-
repo: options.repo,
|
|
2090
|
-
verbose: options.verbose
|
|
2091
|
-
});
|
|
2092
|
-
if (runtimeResources === null) process.exit(ExitCode.Success);
|
|
2093
|
-
const cmdStr = commandLabel(command);
|
|
2094
|
-
if (runtimeResources instanceof CliFailure) return failAndExit({
|
|
2095
|
-
cmdStr,
|
|
2096
|
-
error: runtimeResources,
|
|
2097
|
-
json: options.json,
|
|
2098
|
-
verbose: options.verbose
|
|
991
|
+
if (code === 0) resume(Effect.void);
|
|
992
|
+
else resume(Effect.fail(new CliFailure({
|
|
993
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
994
|
+
message: `Server exited with code ${code}`
|
|
995
|
+
})));
|
|
2099
996
|
});
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
} catch (error) {
|
|
2122
|
-
failAndExit({
|
|
2123
|
-
cmdStr: "ringi",
|
|
2124
|
-
error,
|
|
2125
|
-
json: process.argv.slice(2).includes("--json"),
|
|
2126
|
-
verbose: false
|
|
997
|
+
})).pipe(Command.withDescription("Start the local Ringi server"));
|
|
998
|
+
const mcp = Command.make("mcp", {
|
|
999
|
+
readonly: Flag.boolean("readonly").pipe(Flag.withDefault(false), Flag.withDescription("Read-only mode")),
|
|
1000
|
+
logLevel: Flag.choice("log-level", [
|
|
1001
|
+
"debug",
|
|
1002
|
+
"info",
|
|
1003
|
+
"error",
|
|
1004
|
+
"silent"
|
|
1005
|
+
]).pipe(Flag.withDefault("error"), Flag.withDescription("MCP log level"))
|
|
1006
|
+
}, (_config) => Effect.fail(new CliFailure({
|
|
1007
|
+
exitCode: ExitCode.UsageError,
|
|
1008
|
+
message: "ringi mcp is a runtime command. Use the MCP server entry point directly."
|
|
1009
|
+
}))).pipe(Command.withDescription("Start the MCP stdio server"));
|
|
1010
|
+
const doctor = Command.make("doctor", {}, () => Effect.gen(function* () {
|
|
1011
|
+
yield* emitOutput("ringi doctor", {
|
|
1012
|
+
data: {
|
|
1013
|
+
checks: [],
|
|
1014
|
+
ok: true
|
|
1015
|
+
},
|
|
1016
|
+
human: "ringi doctor: not yet implemented.",
|
|
1017
|
+
nextActions: []
|
|
2127
1018
|
});
|
|
2128
|
-
}
|
|
1019
|
+
})).pipe(Command.withDescription("Run local diagnostics"));
|
|
1020
|
+
const events = Command.make("events", { type: Flag.choice("type", [
|
|
1021
|
+
"reviews",
|
|
1022
|
+
"comments",
|
|
1023
|
+
"todos",
|
|
1024
|
+
"files"
|
|
1025
|
+
]).pipe(Flag.withDescription("Filter event type"), Flag.optional) }, (_config) => requireServerMode("ringi events")).pipe(Command.withDescription("Tail server events"));
|
|
1026
|
+
const dataMigrate = Command.make("migrate", {}, () => requireServerMode("ringi data migrate")).pipe(Command.withDescription("Run database migrations"));
|
|
1027
|
+
const dataReset = Command.make("reset", {
|
|
1028
|
+
yes: Flag.boolean("yes").pipe(Flag.withDefault(false), Flag.withDescription("Skip confirmation")),
|
|
1029
|
+
keepExports: Flag.boolean("keep-exports").pipe(Flag.withDefault(false), Flag.withDescription("Preserve export files"))
|
|
1030
|
+
}, (_config) => requireServerMode("ringi data reset")).pipe(Command.withDescription("Reset local data"));
|
|
1031
|
+
const data = Command.make("data").pipe(Command.withDescription("Data management commands"), Command.withSubcommands([dataMigrate, dataReset]));
|
|
1032
|
+
const reviewWithCore = provideCoreLayer(review);
|
|
1033
|
+
const sourceWithGit = provideGitLayer(source);
|
|
1034
|
+
const todoWithCore = provideCoreLayer(todo);
|
|
1035
|
+
const doctorWithCore = provideCoreLayer(doctor);
|
|
1036
|
+
const ringiCommand = Command.make("ringi").pipe(Command.withDescription("ringi — local-first code review CLI"), Command.withGlobalFlags([
|
|
1037
|
+
JsonSetting,
|
|
1038
|
+
QuietSetting,
|
|
1039
|
+
RepoSetting,
|
|
1040
|
+
DbPathSetting
|
|
1041
|
+
]), Command.withSubcommands([
|
|
1042
|
+
reviewWithCore,
|
|
1043
|
+
sourceWithGit,
|
|
1044
|
+
todoWithCore,
|
|
1045
|
+
serve,
|
|
1046
|
+
mcp,
|
|
1047
|
+
doctorWithCore,
|
|
1048
|
+
events,
|
|
1049
|
+
data
|
|
1050
|
+
]));
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/cli/main.ts
|
|
1053
|
+
/**
|
|
1054
|
+
* Ringi CLI entrypoint.
|
|
1055
|
+
*
|
|
1056
|
+
* Uses `effect/unstable/cli` for command parsing, help generation, shell
|
|
1057
|
+
* completions, and version display. Each command is defined as a typed
|
|
1058
|
+
* `Command` with its own flags/arguments and handler.
|
|
1059
|
+
*
|
|
1060
|
+
* The old hand-rolled parser, imperative main loop, and manual help text
|
|
1061
|
+
* are replaced by the Effect CLI framework.
|
|
1062
|
+
*/
|
|
1063
|
+
const program = Command.run(ringiCommand, { version: "0.3.3" }).pipe(Effect.provide(NodeServices.layer));
|
|
1064
|
+
NodeRuntime.runMain(program);
|
|
2129
1065
|
//#endregion
|
|
2130
1066
|
export {};
|
|
2131
1067
|
|