@anhvupt/tito 0.1.2 → 0.1.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/README.md +11 -0
- package/dist/cli.js +67 -0
- package/dist/core/init.js +6 -5
- package/dist/core/plan.js +5 -0
- package/dist/core/upgrade.js +72 -0
- package/package.json +1 -1
- package/templates/cursor/skills/tito/SKILL.md +5 -0
package/README.md
CHANGED
|
@@ -92,6 +92,17 @@ skills. If `AGENTS.md` already exists, Tito appends its bootstrap and leaves
|
|
|
92
92
|
the existing guidance in place. It refuses to overwrite a Tito file that is
|
|
93
93
|
already there. Open a new Cursor chat, then start with `/tito`.
|
|
94
94
|
|
|
95
|
+
Upgrade an existing project after installing a newer Tito:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npx tito upgrade
|
|
99
|
+
npx tito upgrade --confirm
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`--confirm` installs the latest `@anhvupt/tito` and replaces Tito-owned
|
|
103
|
+
agents, skills, and the Tito section of `AGENTS.md`. Consumer rules, consumer
|
|
104
|
+
agents, and other project skills stay untouched.
|
|
105
|
+
|
|
95
106
|
Tito is installed in each project. It is not a required global command, and
|
|
96
107
|
there is no global configuration yet. A future personal default would be
|
|
97
108
|
overridden by that project's `tito.yaml`. The safety floor still cannot be
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
5
7
|
import { parseArgs } from "node:util";
|
|
6
8
|
import { InspectionError, formatInspection, inspectRepository, } from "./core/inspect.js";
|
|
7
9
|
import { PlanError, formatAdoptionPlan, planAdoption } from "./core/plan.js";
|
|
8
10
|
import { InitError, applyInitialization, formatInitialization, planInitialization, promptForProfile, } from "./core/init.js";
|
|
11
|
+
import { applyUpgrade, formatUpgrade, planUpgrade, titoOwnedFiles, } from "./core/upgrade.js";
|
|
9
12
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
10
13
|
const help = `Tito — quiet orchestration for solo builders
|
|
11
14
|
|
|
@@ -14,6 +17,7 @@ Usage:
|
|
|
14
17
|
tito inspect [--root <path>]
|
|
15
18
|
tito apply --dry-run --profile <id> [--root <path>]
|
|
16
19
|
tito init [--profile <id>] [--root <path>] [--confirm]
|
|
20
|
+
tito upgrade [--root <path>] [--confirm]
|
|
17
21
|
|
|
18
22
|
Options:
|
|
19
23
|
-h, --help Show help
|
|
@@ -23,6 +27,7 @@ Commands:
|
|
|
23
27
|
inspect Report tito.yaml and AGENTS.md. Reads only.
|
|
24
28
|
apply Dry-run an adoption plan. Writes are not available.
|
|
25
29
|
init Install Tito files and specialist agents. Confirm before writing.
|
|
30
|
+
upgrade Install the latest Tito and replace Tito-owned files only.
|
|
26
31
|
`;
|
|
27
32
|
function fail(message) {
|
|
28
33
|
process.stderr.write(`${message}\n`);
|
|
@@ -174,6 +179,65 @@ async function runInit(args) {
|
|
|
174
179
|
throw error;
|
|
175
180
|
}
|
|
176
181
|
}
|
|
182
|
+
async function runUpgrade(args) {
|
|
183
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
184
|
+
process.stdout.write(help);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let root = process.cwd();
|
|
188
|
+
let confirm = false;
|
|
189
|
+
let filesOnly = false;
|
|
190
|
+
try {
|
|
191
|
+
const { values } = parseArgs({
|
|
192
|
+
args,
|
|
193
|
+
options: {
|
|
194
|
+
confirm: { type: "boolean" },
|
|
195
|
+
root: { type: "string" },
|
|
196
|
+
"files-only": { type: "boolean" },
|
|
197
|
+
},
|
|
198
|
+
strict: true,
|
|
199
|
+
allowPositionals: false,
|
|
200
|
+
});
|
|
201
|
+
confirm = values.confirm === true;
|
|
202
|
+
filesOnly = values["files-only"] === true;
|
|
203
|
+
if (values.root !== undefined)
|
|
204
|
+
root = values.root;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
fail(error instanceof Error ? error.message : "Invalid upgrade arguments.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const files = planUpgrade(root, titoOwnedFiles());
|
|
211
|
+
if (!confirm) {
|
|
212
|
+
process.stdout.write(formatUpgrade(root, files));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!filesOnly && !isTitoSource(root)) {
|
|
216
|
+
const install = spawnSync("npm", ["install", "-D", "@anhvupt/tito@latest"], { cwd: root, encoding: "utf8" });
|
|
217
|
+
if (install.status !== 0) {
|
|
218
|
+
fail(install.stderr || "Could not install the latest Tito.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const installed = join(root, "node_modules/@anhvupt/tito/dist/cli.js");
|
|
222
|
+
const child = spawnSync(process.execPath, [installed, "upgrade", "--confirm", "--files-only", "--root", root], { encoding: "utf8" });
|
|
223
|
+
process.stdout.write(child.stdout ?? "");
|
|
224
|
+
if (child.stderr)
|
|
225
|
+
process.stderr.write(child.stderr);
|
|
226
|
+
process.exitCode = child.status ?? 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
applyUpgrade(root, files);
|
|
230
|
+
process.stdout.write(formatUpgrade(root, files));
|
|
231
|
+
}
|
|
232
|
+
function isTitoSource(root) {
|
|
233
|
+
try {
|
|
234
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
235
|
+
return manifest.name === "@anhvupt/tito";
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
177
241
|
async function run(args) {
|
|
178
242
|
if (args[0] === "inspect") {
|
|
179
243
|
runInspect(args.slice(1));
|
|
@@ -186,6 +250,9 @@ async function run(args) {
|
|
|
186
250
|
if (args[0] === "init") {
|
|
187
251
|
return runInit(args.slice(1));
|
|
188
252
|
}
|
|
253
|
+
if (args[0] === "upgrade") {
|
|
254
|
+
return runUpgrade(args.slice(1));
|
|
255
|
+
}
|
|
189
256
|
let helpRequested = false;
|
|
190
257
|
let versionRequested = false;
|
|
191
258
|
try {
|
package/dist/core/init.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { compiledCursorAgents } from "./agents.js";
|
|
5
5
|
import { ACTIVE_RISK_PROFILE_IDS } from "./profiles.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PlanError, TITO_BOOTSTRAP_START, planAdoption, titoBootstrapBlock, } from "./plan.js";
|
|
7
7
|
export class InitError extends Error {
|
|
8
8
|
code;
|
|
9
9
|
constructor(message) {
|
|
@@ -31,7 +31,7 @@ function exists(root, path) {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function shippedSkills() {
|
|
34
|
+
export function shippedSkills() {
|
|
35
35
|
const root = join(dirname(fileURLToPath(import.meta.url)), "../../templates/cursor/skills");
|
|
36
36
|
return readdirSync(root, { withFileTypes: true })
|
|
37
37
|
.filter((entry) => entry.isDirectory())
|
|
@@ -42,16 +42,17 @@ function shippedSkills() {
|
|
|
42
42
|
}
|
|
43
43
|
function agentsBootstrap(report) {
|
|
44
44
|
if (report.agentsMd === "absent") {
|
|
45
|
-
return { path: "AGENTS.md", action: "create", body:
|
|
45
|
+
return { path: "AGENTS.md", action: "create", body: titoBootstrapBlock() };
|
|
46
46
|
}
|
|
47
47
|
const current = readFileSync(join(report.root, "AGENTS.md"), "utf8");
|
|
48
|
-
if (current.includes(
|
|
48
|
+
if (current.includes(TITO_BOOTSTRAP_START) ||
|
|
49
|
+
current.includes("Hola, Tito here!")) {
|
|
49
50
|
return { path: "AGENTS.md", action: "keep" };
|
|
50
51
|
}
|
|
51
52
|
return {
|
|
52
53
|
path: "AGENTS.md",
|
|
53
54
|
action: "append",
|
|
54
|
-
body: `\n${
|
|
55
|
+
body: `\n${titoBootstrapBlock()}`,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
export function planInitialization(report, profile) {
|
package/dist/core/plan.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
2
|
+
export const TITO_BOOTSTRAP_START = "<!-- tito:bootstrap start -->";
|
|
3
|
+
export const TITO_BOOTSTRAP_END = "<!-- tito:bootstrap end -->";
|
|
2
4
|
export const ADOPTION_AGENTS_MD = `# Tito bootstrap
|
|
3
5
|
|
|
4
6
|
This project uses Tito as its root engineering coordinator.
|
|
@@ -15,6 +17,9 @@ This project uses Tito as its root engineering coordinator.
|
|
|
15
17
|
|
|
16
18
|
Keep this bootstrap compact. Load project documentation only when the task needs it.
|
|
17
19
|
`;
|
|
20
|
+
export function titoBootstrapBlock() {
|
|
21
|
+
return `${TITO_BOOTSTRAP_START}\n${ADOPTION_AGENTS_MD.trim()}\n${TITO_BOOTSTRAP_END}\n`;
|
|
22
|
+
}
|
|
18
23
|
export class PlanError extends Error {
|
|
19
24
|
code;
|
|
20
25
|
constructor(code, message) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { compiledCursorAgents } from "./agents.js";
|
|
4
|
+
import { shippedSkills } from "./init.js";
|
|
5
|
+
import { TITO_BOOTSTRAP_END, TITO_BOOTSTRAP_START, titoBootstrapBlock, } from "./plan.js";
|
|
6
|
+
function readIfFile(root, path) {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(join(root, path), "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function ownedText(current, next) {
|
|
15
|
+
if (current === null)
|
|
16
|
+
return "create";
|
|
17
|
+
return current === next ? "keep" : "replace";
|
|
18
|
+
}
|
|
19
|
+
export function refreshAgentsBootstrap(current) {
|
|
20
|
+
const block = titoBootstrapBlock().trimEnd();
|
|
21
|
+
const start = current.indexOf(TITO_BOOTSTRAP_START);
|
|
22
|
+
const end = current.indexOf(TITO_BOOTSTRAP_END);
|
|
23
|
+
if (start >= 0 && end > start) {
|
|
24
|
+
return `${current.slice(0, start)}${block}${current.slice(end + TITO_BOOTSTRAP_END.length)}`;
|
|
25
|
+
}
|
|
26
|
+
const heading = current.indexOf("# Tito bootstrap");
|
|
27
|
+
if (heading >= 0) {
|
|
28
|
+
const prefix = current.slice(0, heading).trimEnd();
|
|
29
|
+
return prefix.length === 0 ? `${block}\n` : `${prefix}\n\n${block}\n`;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = current.trimEnd();
|
|
32
|
+
return `${trimmed}\n\n${block}\n`;
|
|
33
|
+
}
|
|
34
|
+
export function planUpgrade(root, ownedFiles) {
|
|
35
|
+
const files = ownedFiles.map((file) => {
|
|
36
|
+
const current = readIfFile(root, file.path);
|
|
37
|
+
const action = ownedText(current, file.body);
|
|
38
|
+
return action === "keep"
|
|
39
|
+
? { path: file.path, action }
|
|
40
|
+
: { path: file.path, action, body: file.body };
|
|
41
|
+
});
|
|
42
|
+
const agents = readIfFile(root, "AGENTS.md");
|
|
43
|
+
if (agents === null) {
|
|
44
|
+
files.push({ path: "AGENTS.md", action: "create", body: titoBootstrapBlock() });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const next = refreshAgentsBootstrap(agents);
|
|
48
|
+
files.push(next === agents
|
|
49
|
+
? { path: "AGENTS.md", action: "keep" }
|
|
50
|
+
: { path: "AGENTS.md", action: "replace", body: next });
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
export function titoOwnedFiles() {
|
|
55
|
+
return [...compiledCursorAgents(), ...shippedSkills()];
|
|
56
|
+
}
|
|
57
|
+
export function formatUpgrade(root, files) {
|
|
58
|
+
const lines = [`root: ${root}`, "consumer rules: untouched"];
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
lines.push(`${file.path}: ${file.action}`);
|
|
61
|
+
}
|
|
62
|
+
return `${lines.join("\n")}\n`;
|
|
63
|
+
}
|
|
64
|
+
export function applyUpgrade(root, files) {
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (file.action === "keep" || file.body === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const absolute = join(root, file.path);
|
|
69
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
70
|
+
writeFileSync(absolute, file.body, "utf8");
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -19,6 +19,11 @@ Recommend one mode before acting:
|
|
|
19
19
|
- **Plan** for ambiguity, architecture, sensitive work, migrations, or work requiring multiple reviewable slices.
|
|
20
20
|
- **Agent** only for one explicitly approved implementation slice.
|
|
21
21
|
|
|
22
|
+
A new command, a new write behavior, or any technical choice is always Plan
|
|
23
|
+
first. That response contains the plan only. No source edits. "No need to plan"
|
|
24
|
+
applies only to the slice named in that message. Implementation starts only
|
|
25
|
+
after that plan is approved.
|
|
26
|
+
|
|
22
27
|
For Plan work, the planner decides the technical approach and includes guidance code when it removes ambiguity. Ask the user only for a genuine product or business choice.
|
|
23
28
|
|
|
24
29
|
## Execute
|