@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 +109 -24
- package/dist/cli.js +195 -8
- package/dist/core/admin.js +241 -0
- package/dist/core/agents.js +4 -1
- package/dist/core/config.js +225 -3
- package/dist/core/feedback.js +68 -0
- package/dist/core/git-flow.js +174 -0
- package/dist/core/init.js +124 -3
- package/dist/core/models.js +116 -0
- package/dist/core/plan.js +4 -0
- package/dist/core/profiles.js +6 -6
- package/dist/core/retro.js +442 -0
- package/dist/core/specialists.js +27 -15
- package/dist/core/terminal.js +9 -0
- package/dist/core/upgrade.js +65 -2
- package/dist/core/work-plan.js +6 -5
- package/package.json +2 -2
- package/templates/cursor/skills/tito/SKILL.md +73 -14
- package/templates/cursor/skills/tito-admin-retro/SKILL.md +12 -0
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
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|