@anhvupt/tito 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,12 +3,13 @@
3
3
  **Quiet orchestration. Trusted continuity.**
4
4
 
5
5
  Tito is a chat-first engineering coordinator for solo builders. You talk to one
6
- coordinator; Tito explores the repository, chooses the right working mode,
7
- turns larger work into reviewable slices, and keeps implementation inside the
8
- scope you approved.
6
+ coordinator. Tito confirms what you want, writes the plan as the spec, and
7
+ sends only the approved slice to a sub-agent.
9
8
 
10
- It is local-first, Git-friendly, and designed to add coordination without
11
- replacing your project documentation, rules, or specialists.
9
+ The spec records the decisions and the conventions behind them, the test cases
10
+ you review before they are written, and the docs that must change. Tito is
11
+ local-first and Git-friendly. It coordinates your documentation, rules, and
12
+ specialists, and it updates the docs named in the plan before a pull request.
12
13
 
13
14
  ## What Tito helps with
14
15
 
@@ -17,9 +18,16 @@ replacing your project documentation, rules, or specialists.
17
18
  - **The right amount of process:** Tito recommends Ask for discovery, Plan for
18
19
  uncertain or critical work, and Agent only for an approved implementation
19
20
  slice.
20
- - **Plans that make decisions:** the planner owns technical choices, records
21
- important trade-offs, and includes signatures, schemas, pseudocode, or
22
- guidance code when that makes implementation clearer.
21
+ - **Plans that make decisions:** Tito confirms the goal and what is out of
22
+ scope, then asks one batch of clarifying questions, lighter on `solo-fast`.
23
+ One obvious reading continues after a one-line confirmation. Tito still asks
24
+ before locking a technical decision or a product-vision change. The plan
25
+ records that decision and includes signatures, schemas, pseudocode, or
26
+ guidance code when that makes implementation clearer. Decisions name the
27
+ convention they follow. Test cases carry a layer tag: `[unit]`,
28
+ `[integration]`, or `[e2e]`. The plan also lists its docs impact.
29
+ - **A light joke, sometimes:** after `Hola, Tito here!`, Tito may add one short
30
+ joke. It does not replace the answer, and it stays out of the CLI.
23
31
  - **Smaller reviews:** work is split into bounded slices with acceptance
24
32
  criteria, tests, forbidden changes, risks, and a stop condition.
25
33
  - **Safer changes:** Tito preserves uncommitted work, refuses silent overwrites,
@@ -29,9 +37,11 @@ replacing your project documentation, rules, or specialists.
29
37
  rules, skills, BMad workflows, and project-owned specialists rather than
30
38
  replacing them.
31
39
  - **Controlled model use:** Tito recommends the least expensive capable tier,
32
- using stronger reasoning models for architecture, ambiguity, migrations,
33
- security, and other high-consequence planning. If a requested model is
34
- unavailable, Tito asks instead of substituting one silently.
40
+ and each launch names the selected model. Explore, docs, and ordinary work
41
+ use the cheap host family (Grok on Cursor, Sonnet on Claude). Core work uses
42
+ the stronger coding family. Architecture, security, and ERP work stay on the
43
+ reasoning family. If a requested model is unavailable, Tito asks instead of
44
+ substituting one silently.
35
45
 
36
46
  ## Specialists
37
47
 
@@ -52,16 +62,25 @@ mode decides whether it may change anything:
52
62
  | Tech docs writer | — | Update technical documentation |
53
63
  | User docs writer | — | Update user documentation |
54
64
 
55
- Several read-only specialists may work together. Only one mutating mode may be
56
- active. Backend and frontend work is split into sequential slices. After a
57
- module is finished, Tito schedules the tech docs writer and then the user docs
58
- writer, one at a time, before calling that module done. Skip that handoff only
59
- when you explicitly waive it for that module. Each specialist carries triggers,
65
+ Several read-only specialists may work together. Coding sub-agents follow the
66
+ profile cap: `client-careful` 3, `solo-balanced` 6, and `solo-fast` 12. Each
67
+ has its own plan and branch. Tito does not code in the chat. Every change,
68
+ including a small one, goes to a sub-agent, and Tito returns to the user.
69
+ When two or more slices are independent, Tito sends those sub-agents together,
70
+ each on its own branch, up to that cap, and does not line them up one after
71
+ another. Check out each branch only after you accept that branch.
72
+ Documentation writers stay one at a time. Backend and frontend work can run
73
+ together inside that cap. Docs named in
74
+ the plan are updated, or you waive them, before Tito offers a pull request.
75
+ After a module is finished, Tito still schedules the tech docs writer and then
76
+ the user docs writer, one at a time, before calling that module done. Skip
77
+ that handoff only when you explicitly waive it for that module. Each specialist carries triggers,
60
78
  knowledge references, handoffs, and a stop condition. The detailed prompt stays
61
79
  lean and loads knowledge only when that mode needs it.
62
80
 
63
- Cursor agent files such as `.cursor/agents/tito-frontend.md` are not generated
64
- yet. The roster above is the contract those files will follow.
81
+ Stack knowledge points to the stack owner's official resource for the project's version and is never copied in.
82
+
83
+ `tito init` writes Cursor agent files such as `.cursor/agents/tito-frontend.md` from the roster above, and `tito upgrade` refreshes them.
65
84
 
66
85
  ## Chat first, CLI when useful
67
86
 
@@ -90,7 +109,18 @@ The first `init` prints the plan and writes nothing. In a terminal, omitting
90
109
  `--confirm` creates the missing `tito.yaml`, specialist agents, and `/tito`
91
110
  skills. If `AGENTS.md` already exists, Tito appends its bootstrap and leaves
92
111
  the existing guidance in place. It refuses to overwrite a Tito file that is
93
- already there. Open a new Cursor chat, then start with `/tito`.
112
+ already there. In a terminal, `tito init --confirm` then asks for the git base
113
+ (`dev`, `develop`, `main`, or `master`), screen language (`vi` or `en`),
114
+ tenancy (`single` or `multi`), optional comma-separated surface ids, and
115
+ whether to auto-create a pull request after implementation. That question is
116
+ `Auto-create a pull request after implementation? [Y/n]`. Enter, `y`, or `yes`
117
+ stores yes. `n` or `no` stores no. Empty surface ids omit `product.surfaces`.
118
+ A non-interactive init with `--profile` does not ask; it stores
119
+ `git.autoPullRequest: true` and does not invent the other product fields.
120
+ Preview without `--confirm` does not ask and does not write. An existing
121
+ `tito.yaml` is left unchanged. Storing the answer does not open a pull request.
122
+ Errors on a terminal are red, and `NO_COLOR` turns that off.
123
+ Open a new Cursor chat, then start with `/tito`.
94
124
 
95
125
  Upgrade an existing project after installing a newer Tito:
96
126
 
@@ -101,12 +131,13 @@ npx tito upgrade --confirm
101
131
 
102
132
  `--confirm` installs the latest `@anhvupt/tito` and replaces Tito-owned
103
133
  agents, skills, and the Tito section of `AGENTS.md`. Consumer rules, consumer
104
- agents, and other project skills stay untouched.
134
+ agents, and other project skills stay untouched. In a terminal, `tito upgrade --confirm` asks only for settings missing from `tito.yaml`: git base, screen language, tenancy, surfaces, and auto-create pull request. Enter on that last question still stores yes. It does not change a value that is already present, and it does not change the profile. A non-interactive upgrade does not ask. If `tito.yaml` is missing, it skips those questions.
105
135
 
106
- Tito is installed in each project. It is not a required global command, and
107
- there is no global configuration yet. A future personal default would be
108
- overridden by that project's `tito.yaml`. The safety floor still cannot be
109
- weakened.
136
+ Tito is installed in each project. It is not a required global command.
137
+ An opt-in local admin index (`tito admin add|list|remove|refresh`) can register
138
+ repos under `~/.config/tito/admin/` and store recent commit subjects, dates, and
139
+ touched paths — never diffs. A project `tito.yaml` still overrides any personal
140
+ default. The safety floor cannot be weakened.
110
141
 
111
142
  ## What works today
112
143
 
@@ -142,6 +173,11 @@ node dist/cli.js inspect
142
173
  node dist/cli.js inspect --root .
143
174
  node dist/cli.js apply --dry-run --profile solo-balanced
144
175
  node dist/cli.js init --profile solo-balanced
176
+ node dist/cli.js admin add
177
+ node dist/cli.js admin list
178
+ node dist/cli.js admin refresh
179
+ node dist/cli.js admin remove --root /path/to/repo
180
+ node dist/cli.js admin retro --week 2026-W40
145
181
  ```
146
182
 
147
183
  `inspect` reads `tito.yaml` when it exists and checks whether `AGENTS.md` is
@@ -155,16 +191,65 @@ specialist agent files. It writes those files only with `--confirm`, and it
155
191
  refuses when an existing file would be overwritten. In chat, `/tito-init` runs
156
192
  this same command.
157
193
 
194
+ `admin` is opt-in. `add` registers the current repo (or `--root`), `list` prints
195
+ registered paths, `remove` drops one path, and `refresh` writes local branch plus
196
+ the last 50 commit subjects, dates, and file paths (secrets like `.env` skipped;
197
+ no diffs) for chat and CLI to share.
198
+
199
+ `retro` builds that week's report from local git and saves `retros/<week>.json`
200
+ and `retros/<week>.md` under the admin root. Review-fix counts, docs-blank, and
201
+ merge duration say unavailable until GitHub data is added.
202
+
158
203
  ## How work moves
159
204
 
160
205
  The normal careful path is:
161
206
 
162
207
  `DISCOVERY → PLANNED → APPROVED → IMPLEMENTING → READY_FOR_REVIEW → APPROVED_FOR_COMMIT → DONE`
163
208
 
209
+ Work follows clarify, then domain, then the spec doc, then failing tests, then code. Discover clarifies. The plan opens with Domain. In Code, the agent writes the spec doc named in Docs impact first, then the approved tests, confirms they fail, then implements until they pass.
210
+
211
+ Six working principles guide that path: research, then judge; fixed floor, flexible tools; adopt before building; business before tech; clarify, domain, document, test, then code; and it depends, so show it.
212
+
164
213
  A small, obvious change can move from discovery directly to an explicitly
165
214
  approved slice. Review feedback can return an in-scope slice to implementation.
166
215
  Other lifecycle skips are rejected.
167
216
 
217
+ ## Git flow
218
+
219
+ Tito suggests the branch before checkout, for example `Suggested branch: feat/short-slug from develop`. You accept it or name another base or type. The base is `dev`, `develop`, `main`, or `master`. `dev` and `develop` are interchangeable. `main` and `master` are interchangeable. The type is `feat`, `fix`, `hot-fix`, `chores`, `refactor`, or `debug`.
220
+
221
+ `tito.yaml` may set the default base:
222
+
223
+ ```yaml
224
+ schemaVersion: 1
225
+ profile: client-careful
226
+ git:
227
+ defaultBase: develop
228
+ autoPullRequest: true
229
+ ```
230
+
231
+ `git.autoPullRequest` is an optional boolean. Missing it is valid. A non-boolean value is rejected. Storing it does not open a pull request.
232
+
233
+ `product.screenLanguage` is optional and accepts only `vi` or `en`. A missing product block is valid. A missing `screenLanguage` is valid. Unknown product fields are rejected. `product.tenancy` is optional (`single` or `multi`). `product.surfaces` is an optional list of `{ id }` entries. Missing `tenancy` and missing `surfaces` stay valid.
234
+
235
+ ```yaml
236
+ product:
237
+ screenLanguage: vi
238
+ tenancy: single
239
+ surfaces:
240
+ - id: admin
241
+ ```
242
+
243
+ - Code stays English.
244
+ - In a Vietnamese app (`screenLanguage: vi`), routes and slugs are Vietnamese first. The public path is native Vietnamese, for example `/tien-ich/ca-phe`. Do not invent that Vietnamese by translating an English slug word for word. If the product is bilingual, the English route comes second.
245
+ - An English app (`screenLanguage: en`) keeps English routes and slugs.
246
+ - In a Vietnamese app (`screenLanguage: vi`), every string a person reads is Vietnamese: tables, labels, buttons, headings, and messages. Write native Vietnamese first. Do not invent it by translating English word for word. English may exist as a second field only when the product is bilingual, and it comes after the Vietnamese.
247
+ - An English app (`screenLanguage: en`) stays English on screen.
248
+ - When `screenLanguage` is missing and the screen language is unclear, Tito asks once. One obvious reading continues without a question.
249
+ - Globalized apps store timestamps in UTC and show them in the user's timezone. There is no switch to turn that off.
250
+
251
+ A commit subject is `<type>: <sentence>`. The type is the same token as the branch type: `feat`, `fix`, `hot-fix`, `chores`, `refactor`, or `debug`. Examples: `feat: add commit message rule`, `fix: reject a duplicate surface id`, `chores: record the commit prefix rule`, and `hot-fix: stop a bad release build`. The words after the colon are one finished sentence of at most 70 words. The type is not counted in those 70 words. The body is a separate description. When you review a plan, Tito saves that plan and your edit as separate files under `.tito/feedback/<slug>/`. `review.md` starts with front matter `outcome: accepted | edited | expanded`. Tito indexes only the plan name. The plan body stays in Cursor's plan file. After an approved slice is coded, every review fix goes into one plan named `review/<slug>` on the same branch. Tito asks before opening a pull request only after that plan is coded, or when you accept the code with no changes. The pull request description has four parts within 2 to 50 lines: a one-line problem, what changed, review fixes, and checks for lint, code quality, conventions, tests, build, and docs. When the user comments on an open pull request, Tito updates that pull request's description in the same turn. Review fixes lists every comment, including the earlier ones. A thread reply does not replace that update. Init creates `.github/pull_request_template.md` from Tito's template when it is missing. Upgrade replaces that file with Tito's template. Tito does not approve a pull request. Tito merges only when you call for the merge and the pull request already has an approval. Do not merge a slice branch into `main`, `master`, `dev`, or `develop`, on the machine or on the remote, unless the user calls for that merge and the pull request already has an approval. Finishing a slice leaves the branch as it is. After a pull request is merged, Tito asks before the next slice. Tito switches back to the base branch only when you say so clearly. Tito pushes directly to the base branch only when you clearly instruct that push.
252
+
168
253
  ## Development
169
254
 
170
255
  Requirements:
package/dist/cli.js CHANGED
@@ -2,13 +2,17 @@
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
- import { readFileSync } from "node:fs";
5
+ import { readFileSync, writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import { InspectionError, formatInspection, inspectRepository, } from "./core/inspect.js";
9
9
  import { PlanError, formatAdoptionPlan, planAdoption } from "./core/plan.js";
10
- import { InitError, applyInitialization, formatInitialization, planInitialization, promptForProfile, } from "./core/init.js";
11
- import { applyUpgrade, formatUpgrade, planUpgrade, titoOwnedFiles, } from "./core/upgrade.js";
10
+ import { ConfigValidationError, parseConfig } from "./core/config.js";
11
+ import { INIT_SETTING_KEYS, InitError, applyInitialization, formatInitialization, planInitialization, promptForProfile, promptForSettings, } from "./core/init.js";
12
+ import { applyUpgrade, formatUpgrade, mergeSettings, missingSettings, planUpgrade, titoOwnedFiles, } from "./core/upgrade.js";
13
+ import { AdminError, addRepo, formatContexts, formatRepos, listRepos, refreshRepos, removeRepo, resolveAdminRoot, } from "./core/admin.js";
14
+ import { RetroError, buildRetro, formatRetro, retroJson, saveRetro, } from "./core/retro.js";
15
+ import { formatInitError } from "./core/terminal.js";
12
16
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
13
17
  const help = `Tito — quiet orchestration for solo builders
14
18
 
@@ -18,6 +22,11 @@ Usage:
18
22
  tito apply --dry-run --profile <id> [--root <path>]
19
23
  tito init [--profile <id>] [--root <path>] [--confirm]
20
24
  tito upgrade [--root <path>] [--confirm]
25
+ tito admin add [--root <path>] [--admin-root <path>]
26
+ tito admin list [--admin-root <path>]
27
+ tito admin remove --root <path> [--admin-root <path>]
28
+ tito admin refresh [--admin-root <path>]
29
+ tito admin retro [--week <YYYY-Www>] [--timezone <IANA>] [--json] [--admin-root <path>]
21
30
 
22
31
  Options:
23
32
  -h, --help Show help
@@ -28,11 +37,19 @@ Commands:
28
37
  apply Dry-run an adoption plan. Writes are not available.
29
38
  init Install Tito files and specialist agents. Confirm before writing.
30
39
  upgrade Install the latest Tito and replace Tito-owned files only.
40
+ admin Opt-in local repo index: add, list, remove, refresh, retro.
31
41
  `;
32
42
  function fail(message) {
33
43
  process.stderr.write(`${message}\n`);
34
44
  process.exitCode = 1;
35
45
  }
46
+ function failInit(message) {
47
+ process.stderr.write(formatInitError(message, {
48
+ tty: process.stderr.isTTY === true,
49
+ noColor: process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "",
50
+ }));
51
+ process.exitCode = 1;
52
+ }
36
53
  function runInspect(args) {
37
54
  if (args.includes("-h") || args.includes("--help")) {
38
55
  process.stdout.write(help);
@@ -139,12 +156,12 @@ async function runInit(args) {
139
156
  root = values.root;
140
157
  }
141
158
  catch (error) {
142
- fail(error instanceof Error ? error.message : "Invalid init arguments.");
159
+ failInit(error instanceof Error ? error.message : "Invalid init arguments.");
143
160
  return;
144
161
  }
145
162
  if (profile === undefined) {
146
163
  if (!input.isTTY) {
147
- fail("Missing required option --profile.");
164
+ failInit("Missing required option --profile.");
148
165
  return;
149
166
  }
150
167
  const prompts = createInterface({ input, output });
@@ -156,24 +173,39 @@ async function runInit(args) {
156
173
  }
157
174
  }
158
175
  try {
159
- const plan = planInitialization(inspectRepository(root), profile);
176
+ const report = inspectRepository(root);
177
+ const creatingYaml = report.titoYaml.status === "absent";
178
+ let settings;
179
+ if (creatingYaml && !input.isTTY)
180
+ settings = { autoPullRequest: true };
181
+ let plan = planInitialization(report, profile, settings);
160
182
  if (!confirm) {
161
183
  process.stdout.write(formatInitialization(plan));
162
184
  return;
163
185
  }
186
+ if (creatingYaml && input.isTTY) {
187
+ const prompts = createInterface({ input, output });
188
+ try {
189
+ settings = await promptForSettings((prompt) => prompts.question(prompt), INIT_SETTING_KEYS);
190
+ }
191
+ finally {
192
+ prompts.close();
193
+ }
194
+ plan = planInitialization(report, profile, settings);
195
+ }
164
196
  applyInitialization(plan);
165
197
  process.stdout.write(formatInitialization(plan));
166
198
  }
167
199
  catch (error) {
168
200
  if (error instanceof InspectionError || error instanceof PlanError || error instanceof InitError) {
169
- fail(`${error.code}: ${error.message}`);
201
+ failInit(`${error.code}: ${error.message}`);
170
202
  return;
171
203
  }
172
204
  if (typeof error === "object" &&
173
205
  error !== null &&
174
206
  "code" in error &&
175
207
  error.code === "EPERM") {
176
- fail("filesystem: Tito could not create .cursor/agents.");
208
+ failInit("filesystem: Tito could not create .cursor/agents.");
177
209
  return;
178
210
  }
179
211
  throw error;
@@ -212,6 +244,8 @@ async function runUpgrade(args) {
212
244
  process.stdout.write(formatUpgrade(root, files));
213
245
  return;
214
246
  }
247
+ if (!filesOnly && input.isTTY)
248
+ await fillMissingSettings(root);
215
249
  if (!filesOnly && !isTitoSource(root)) {
216
250
  const install = spawnSync("npm", ["install", "-D", "@anhvupt/tito@latest"], { cwd: root, encoding: "utf8" });
217
251
  if (install.status !== 0) {
@@ -229,6 +263,34 @@ async function runUpgrade(args) {
229
263
  applyUpgrade(root, files);
230
264
  process.stdout.write(formatUpgrade(root, files));
231
265
  }
266
+ async function fillMissingSettings(root) {
267
+ let text;
268
+ try {
269
+ text = readFileSync(join(root, "tito.yaml"), "utf8");
270
+ }
271
+ catch {
272
+ return;
273
+ }
274
+ let missing;
275
+ try {
276
+ missing = missingSettings(parseConfig(text));
277
+ }
278
+ catch (error) {
279
+ if (error instanceof ConfigValidationError)
280
+ return;
281
+ throw error;
282
+ }
283
+ if (missing.length === 0)
284
+ return;
285
+ const prompts = createInterface({ input, output });
286
+ try {
287
+ const answers = await promptForSettings((prompt) => prompts.question(prompt), missing);
288
+ writeFileSync(join(root, "tito.yaml"), mergeSettings(text, answers), "utf8");
289
+ }
290
+ finally {
291
+ prompts.close();
292
+ }
293
+ }
232
294
  function isTitoSource(root) {
233
295
  try {
234
296
  const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
@@ -238,6 +300,127 @@ function isTitoSource(root) {
238
300
  return false;
239
301
  }
240
302
  }
303
+ function runAdmin(args) {
304
+ if (args.includes("-h") || args.includes("--help") || args.length === 0) {
305
+ process.stdout.write(help);
306
+ return;
307
+ }
308
+ const action = args[0];
309
+ const rest = args.slice(1);
310
+ if (action === "retro") {
311
+ runRetro(rest);
312
+ return;
313
+ }
314
+ if (action !== "add" &&
315
+ action !== "list" &&
316
+ action !== "remove" &&
317
+ action !== "refresh") {
318
+ fail(`Unknown admin action: ${action}`);
319
+ return;
320
+ }
321
+ let root = process.cwd();
322
+ let adminRoot = resolveAdminRoot();
323
+ let rootProvided = false;
324
+ try {
325
+ const { values } = parseArgs({
326
+ args: rest,
327
+ options: {
328
+ root: { type: "string" },
329
+ "admin-root": { type: "string" },
330
+ },
331
+ strict: true,
332
+ allowPositionals: false,
333
+ });
334
+ if (values.root !== undefined) {
335
+ root = values.root;
336
+ rootProvided = true;
337
+ }
338
+ if (values["admin-root"] !== undefined) {
339
+ adminRoot = resolveAdminRoot(values["admin-root"]);
340
+ }
341
+ }
342
+ catch (error) {
343
+ fail(error instanceof Error ? error.message : "Invalid admin arguments.");
344
+ return;
345
+ }
346
+ try {
347
+ if (action === "add") {
348
+ const result = addRepo(adminRoot, root);
349
+ process.stdout.write(result.created
350
+ ? `added: ${result.repo.path}\n`
351
+ : `kept: ${result.repo.path}\n`);
352
+ return;
353
+ }
354
+ if (action === "list") {
355
+ process.stdout.write(formatRepos(listRepos(adminRoot)));
356
+ return;
357
+ }
358
+ if (action === "remove") {
359
+ if (!rootProvided) {
360
+ fail("Missing required option --root.");
361
+ return;
362
+ }
363
+ const removed = removeRepo(adminRoot, root);
364
+ process.stdout.write(`removed: ${removed.path}\n`);
365
+ return;
366
+ }
367
+ process.stdout.write(formatContexts(refreshRepos(adminRoot)));
368
+ }
369
+ catch (error) {
370
+ if (error instanceof AdminError) {
371
+ fail(`${error.code}: ${error.message}`);
372
+ return;
373
+ }
374
+ throw error;
375
+ }
376
+ }
377
+ function runRetro(args) {
378
+ let week;
379
+ let timezone;
380
+ let json = false;
381
+ let adminRoot = resolveAdminRoot();
382
+ try {
383
+ const { values } = parseArgs({
384
+ args,
385
+ options: {
386
+ week: { type: "string" },
387
+ timezone: { type: "string" },
388
+ json: { type: "boolean" },
389
+ "admin-root": { type: "string" },
390
+ },
391
+ strict: true,
392
+ allowPositionals: false,
393
+ });
394
+ if (values.week !== undefined)
395
+ week = values.week;
396
+ if (values.timezone !== undefined)
397
+ timezone = values.timezone;
398
+ json = values.json === true;
399
+ if (values["admin-root"] !== undefined) {
400
+ adminRoot = resolveAdminRoot(values["admin-root"]);
401
+ }
402
+ }
403
+ catch (error) {
404
+ fail(error instanceof Error ? error.message : "Invalid admin arguments.");
405
+ return;
406
+ }
407
+ try {
408
+ const report = buildRetro({
409
+ adminRoot,
410
+ ...(week !== undefined ? { week } : {}),
411
+ ...(timezone !== undefined ? { timezone } : {}),
412
+ });
413
+ saveRetro(adminRoot, report);
414
+ process.stdout.write(json ? retroJson(report) : formatRetro(report));
415
+ }
416
+ catch (error) {
417
+ if (error instanceof RetroError || error instanceof AdminError) {
418
+ fail(`${error.code}: ${error.message}`);
419
+ return;
420
+ }
421
+ throw error;
422
+ }
423
+ }
241
424
  async function run(args) {
242
425
  if (args[0] === "inspect") {
243
426
  runInspect(args.slice(1));
@@ -253,6 +436,10 @@ async function run(args) {
253
436
  if (args[0] === "upgrade") {
254
437
  return runUpgrade(args.slice(1));
255
438
  }
439
+ if (args[0] === "admin") {
440
+ runAdmin(args.slice(1));
441
+ return;
442
+ }
256
443
  let helpRequested = false;
257
444
  let versionRequested = false;
258
445
  try {