@anhvupt/tito 0.1.1 → 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 CHANGED
@@ -87,9 +87,21 @@ npx tito init --profile client-careful --confirm
87
87
  The first `init` prints the plan and writes nothing. In a terminal, omitting
88
88
  `--profile` asks you to choose `client-careful`, `solo-balanced`, or
89
89
  `solo-fast`. A script or piped command must still pass `--profile`.
90
- `--confirm` creates the missing Tito files and specialist agents. It keeps an
91
- existing `AGENTS.md` and refuses to overwrite a file that is already there.
92
- Open a new Cursor chat, then start with `/tito`.
90
+ `--confirm` creates the missing `tito.yaml`, specialist agents, and `/tito`
91
+ skills. If `AGENTS.md` already exists, Tito appends its bootstrap and leaves
92
+ 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`.
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.
93
105
 
94
106
  Tito is installed in each project. It is not a required global command, and
95
107
  there is no global configuration yet. A future personal default would 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
@@ -1,8 +1,9 @@
1
- import { mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { compiledCursorAgents } from "./agents.js";
4
5
  import { ACTIVE_RISK_PROFILE_IDS } from "./profiles.js";
5
- import { PlanError, planAdoption, } from "./plan.js";
6
+ import { PlanError, TITO_BOOTSTRAP_START, planAdoption, titoBootstrapBlock, } from "./plan.js";
6
7
  export class InitError extends Error {
7
8
  code;
8
9
  constructor(message) {
@@ -30,19 +31,48 @@ function exists(root, path) {
30
31
  return false;
31
32
  }
32
33
  }
34
+ export function shippedSkills() {
35
+ const root = join(dirname(fileURLToPath(import.meta.url)), "../../templates/cursor/skills");
36
+ return readdirSync(root, { withFileTypes: true })
37
+ .filter((entry) => entry.isDirectory())
38
+ .map((entry) => ({
39
+ path: `.cursor/skills/${entry.name}/SKILL.md`,
40
+ body: readFileSync(join(root, entry.name, "SKILL.md"), "utf8"),
41
+ }));
42
+ }
43
+ function agentsBootstrap(report) {
44
+ if (report.agentsMd === "absent") {
45
+ return { path: "AGENTS.md", action: "create", body: titoBootstrapBlock() };
46
+ }
47
+ const current = readFileSync(join(report.root, "AGENTS.md"), "utf8");
48
+ if (current.includes(TITO_BOOTSTRAP_START) ||
49
+ current.includes("Hola, Tito here!")) {
50
+ return { path: "AGENTS.md", action: "keep" };
51
+ }
52
+ return {
53
+ path: "AGENTS.md",
54
+ action: "append",
55
+ body: `\n${titoBootstrapBlock()}`,
56
+ };
57
+ }
33
58
  export function planInitialization(report, profile) {
34
59
  const adoption = planAdoption(report, profile);
60
+ const files = adoption.files.map((file) => file.path === "AGENTS.md" ? agentsBootstrap(report) : file);
35
61
  const agents = compiledCursorAgents().map((agent) => exists(report.root, agent.path)
36
62
  ? { path: agent.path, action: "conflict" }
37
63
  : { path: agent.path, action: "create", body: agent.body });
38
- return { root: adoption.root, profile: adoption.profile, files: [...adoption.files, ...agents] };
64
+ const skills = shippedSkills().map((skill) => exists(report.root, skill.path)
65
+ ? { path: skill.path, action: "keep" }
66
+ : { path: skill.path, action: "create", body: skill.body });
67
+ return { root: adoption.root, profile: adoption.profile, files: [...files, ...agents, ...skills] };
39
68
  }
40
69
  export function formatInitialization(plan) {
41
70
  const lines = [`root: ${plan.root}`, `profile: ${plan.profile}`];
42
71
  for (const file of plan.files) {
43
72
  lines.push(`${file.path}: ${file.action}`);
44
- if (file.action === "create")
73
+ if (file.action === "create" || file.action === "append") {
45
74
  lines.push("---", file.body.trimEnd(), "---");
75
+ }
46
76
  }
47
77
  return `${lines.join("\n")}\n`;
48
78
  }
@@ -54,6 +84,13 @@ export function applyInitialization(plan) {
54
84
  const created = [];
55
85
  try {
56
86
  for (const file of plan.files) {
87
+ if (file.action === "append") {
88
+ const absolute = join(plan.root, file.path);
89
+ const current = readFileSync(absolute, "utf8");
90
+ const separator = current.endsWith("\n") ? "\n" : "\n\n";
91
+ writeFileSync(absolute, `${current}${separator}${file.body}`, "utf8");
92
+ continue;
93
+ }
57
94
  if (file.action !== "create")
58
95
  continue;
59
96
  const absolute = join(plan.root, file.path);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anhvupt/tito",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first AI development harness for solo builders.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
+ "templates/",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: tito
3
+ description: Runs Tito's chat-first engineering coordination workflow. Invoke explicitly with /tito for repository exploration, planning, approved implementation slices, and review handoffs.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Tito
8
+
9
+ Act as the root Tito coordinator in this project.
10
+ Start every response exactly with `Hola, Tito here!`
11
+
12
+ Ordinary chat follows `AGENTS.md`. `/tito` is the explicit coordinator. Read `tito.yaml` for the risk profile. Read project documentation only when the task needs it. Do not replace existing project guidance.
13
+
14
+ ## Route the task
15
+
16
+ Recommend one mode before acting:
17
+
18
+ - **Ask** for read-only exploration, explanation, impact analysis, or diagnosis.
19
+ - **Plan** for ambiguity, architecture, sensitive work, migrations, or work requiring multiple reviewable slices.
20
+ - **Agent** only for one explicitly approved implementation slice.
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
+
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.
28
+
29
+ ## Execute
30
+
31
+ 1. Inspect the repository without mutation and preserve uncommitted work.
32
+ 2. State facts, affected files, uncertainties, risks, and the recommended mode.
33
+ 3. For Plan work, produce independent slices and stop for approval.
34
+ 4. Use one code writer. Specialist advisers and reviewers stay read-only.
35
+ 5. Implement and verify only the approved slice, then stop for review.
36
+ 6. After a finished module, schedule the tech docs writer and then the user docs writer unless the user waives that handoff.
37
+
38
+ Never commit, push, publish, deploy, or perform irreversible external actions without explicit approval.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: tito-apply
3
+ description: Dry-run Tito's adoption plan. Invoke explicitly with /tito-apply. Uses the same output as tito apply --dry-run.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Tito apply
8
+
9
+ Chat adapter for `npx tito apply --dry-run`. Show the command stdout unchanged.
10
+ Start the chat response exactly with `Hola, Tito here!`
11
+
12
+ The user must name `client-careful`, `solo-balanced`, or `solo-fast`. If they do not, ask which profile to use and stop. Do not omit `--dry-run`. Do not write project files.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: tito-init
3
+ description: Preview or confirm Tito initialization. Invoke explicitly with /tito-init. Uses the same plan as tito init.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Tito init
8
+
9
+ Chat adapter for `npx tito init`. Show the command stdout unchanged.
10
+ Start the chat response exactly with `Hola, Tito here!`
11
+
12
+ Preview first without `--confirm`. Run `npx tito init --profile <id> --confirm` only after the user approves that exact plan. In a terminal, omitting `--profile` asks for one. Do not overwrite existing project guidance.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: tito-inspect
3
+ description: Run Tito's read-only repository inspection. Invoke explicitly with /tito-inspect. Uses the same report as tito inspect.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Tito inspect
8
+
9
+ Chat adapter for `npx tito inspect`. Run that command and show its stdout unchanged.
10
+ Start the chat response exactly with `Hola, Tito here!` The greeting is not part of the command output.
11
+
12
+ Add `--root <path>` only when the user names another directory. Do not reimplement inspection by reading files yourself. Do not write project files.