tiller-ai 0.1.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 +138 -0
- package/dist/index.js +1183 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# tiller-ai
|
|
2
|
+
|
|
3
|
+
Scaffold Claude Code projects with a structured vibe loop — branch, build, commit, land.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
Tiller is a thin scaffold for Claude Code that turns a blank repo into a project Claude knows how to navigate. It installs a set of slash commands (skills), two `CLAUDE.md` files (one user-facing, one Tiller-managed), hooks for formatting and secret scanning, and shared tracking files. Once scaffolded, you describe work with `/vibe`, save checkpoints with `/save`, and ship with `/land` — and Claude follows the loop without you having to re-explain your workflow every session.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Scaffold a new project
|
|
13
|
+
npx tiller-ai init
|
|
14
|
+
|
|
15
|
+
# 2. Open in Claude Code and run setup
|
|
16
|
+
/setup
|
|
17
|
+
|
|
18
|
+
# 3. Start working
|
|
19
|
+
/vibe add a login page
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Skills
|
|
23
|
+
|
|
24
|
+
| Command | What it does |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `/setup` | First-run: understand the project and fill in `CLAUDE.md` |
|
|
27
|
+
| `/vibe [idea]` | Start or continue work on something |
|
|
28
|
+
| `/save` | Commit current progress on the feature branch |
|
|
29
|
+
| `/land` | Merge completed feature to main (solo) or open a PR (team) |
|
|
30
|
+
| `/recap` | Read-only status — active feature, notes |
|
|
31
|
+
|
|
32
|
+
## Modes
|
|
33
|
+
|
|
34
|
+
Tiller has two modes that control how Claude communicates:
|
|
35
|
+
|
|
36
|
+
**`simple`** — for non-technical users. Claude builds without narrating steps, surfaces only blockers, and keeps responses short and outcome-focused.
|
|
37
|
+
|
|
38
|
+
**`detailed`** — for technical users. Claude proposes an approach and waits for confirmation before touching files, narrates decisions, and surfaces trade-offs.
|
|
39
|
+
|
|
40
|
+
Switch modes at any time:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx tiller-ai mode simple
|
|
44
|
+
npx tiller-ai mode detailed
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use `--project` to update the shared project default instead of your personal override:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx tiller-ai mode detailed --project
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or just update the `Mode:` line in your root `CLAUDE.md`.
|
|
54
|
+
|
|
55
|
+
## Workflows
|
|
56
|
+
|
|
57
|
+
Tiller supports two workflows that affect how `/land` behaves:
|
|
58
|
+
|
|
59
|
+
**`solo`** — single developer. `/land` merges the feature branch into main locally and deletes the branch.
|
|
60
|
+
|
|
61
|
+
**`team`** — multiple developers. `/land` pushes the branch and opens a PR (via `gh` CLI if available, otherwise prints the URL). The branch is not deleted locally.
|
|
62
|
+
|
|
63
|
+
The workflow is set during `init` and stored in `.claude/.tiller.json`. Each developer can override it locally in `.tiller.local.json` (gitignored).
|
|
64
|
+
|
|
65
|
+
## What gets scaffolded
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
your-project/
|
|
69
|
+
├── CLAUDE.md # User-facing: project context, verify command, mode, workflow
|
|
70
|
+
├── .gitignore # Ignores vibestate.md, .tiller.local.json, common build artifacts
|
|
71
|
+
├── changelog.md # Shared done log — updated by /land on each merge
|
|
72
|
+
├── vibestate.md # Per-dev: active feature, milestone checklist, notes (gitignored)
|
|
73
|
+
├── .claude/
|
|
74
|
+
│ ├── CLAUDE.md # Tiller-managed: vibe loop rules, skill docs
|
|
75
|
+
│ ├── settings.json # Hook registrations (PostToolUse, PreToolUse, UserPromptSubmit)
|
|
76
|
+
│ ├── .tiller.json # Manifest: version, mode, workflow, runCommand, managedFiles
|
|
77
|
+
│ ├── hooks/
|
|
78
|
+
│ │ ├── post-write.sh # PostToolUse: run formatter after file writes
|
|
79
|
+
│ │ ├── secret-scan.sh # PreToolUse: block writes containing secrets
|
|
80
|
+
│ │ └── session-resume.sh # UserPromptSubmit: orient Claude at session start
|
|
81
|
+
│ └── skills/
|
|
82
|
+
│ ├── setup/SKILL.md # /setup skill
|
|
83
|
+
│ ├── vibe/SKILL.md # /vibe skill
|
|
84
|
+
│ ├── save/SKILL.md # /save skill
|
|
85
|
+
│ ├── land/SKILL.md # /land skill
|
|
86
|
+
│ └── recap/SKILL.md # /recap skill
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Per-dev local overrides** — `.tiller.local.json` (gitignored, not scaffolded) lets individual developers override `mode` and `workflow` without touching shared files.
|
|
90
|
+
|
|
91
|
+
## The vibe loop
|
|
92
|
+
|
|
93
|
+
Every piece of work follows this loop:
|
|
94
|
+
|
|
95
|
+
1. **Orient** — Claude reads `CLAUDE.md` and `vibestate.md` to understand project state and pick up any in-progress work
|
|
96
|
+
2. **Confirm** — in `detailed` mode, Claude writes out the proposed approach and waits for a go-ahead before touching files
|
|
97
|
+
3. **Build** — Claude implements, runs the verify command after each chunk, and fixes failures before moving on
|
|
98
|
+
4. **Save** — Claude reminds you to `/save` when stable and `/land` when the feature is done
|
|
99
|
+
|
|
100
|
+
`vibestate.md` tracks the active branch, milestone checklist, and session notes. `changelog.md` is the shared done log — updated by `/land` whenever a feature merges, so team members can see what's been shipped.
|
|
101
|
+
|
|
102
|
+
## CLI reference
|
|
103
|
+
|
|
104
|
+
### `npx tiller-ai init`
|
|
105
|
+
|
|
106
|
+
Scaffold a new project interactively. Prompts for project name, description, run/verify command, mode, and workflow. Writes all files and makes an initial git commit.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx tiller-ai init
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `npx tiller-ai upgrade`
|
|
113
|
+
|
|
114
|
+
Update Tiller-managed files (`.claude/CLAUDE.md`, `settings.json`, hooks, skills) to the latest version without touching your `CLAUDE.md`, `vibestate.md`, or `changelog.md`.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx tiller-ai upgrade
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `npx tiller-ai mode <mode>`
|
|
121
|
+
|
|
122
|
+
Switch the project mode between `simple` and `detailed`. Without `--project`, writes to `.tiller.local.json` (your personal override). With `--project`, updates the shared `CLAUDE.md`.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx tiller-ai mode simple
|
|
126
|
+
npx tiller-ai mode detailed
|
|
127
|
+
npx tiller-ai mode detailed --project # update shared project default
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Node 22+
|
|
133
|
+
- [Claude Code](https://claude.ai/code)
|
|
134
|
+
- git
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { intro, outro, spinner } from "@clack/prompts";
|
|
8
|
+
import { resolve, basename } from "path";
|
|
9
|
+
|
|
10
|
+
// src/scaffold/index.ts
|
|
11
|
+
import { join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/fs.ts
|
|
14
|
+
import { mkdir, writeFile as fsWriteFile } from "fs/promises";
|
|
15
|
+
import { dirname } from "path";
|
|
16
|
+
async function ensureDir(dirPath) {
|
|
17
|
+
await mkdir(dirPath, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
async function writeFile(filePath, content) {
|
|
20
|
+
await ensureDir(dirname(filePath));
|
|
21
|
+
await fsWriteFile(filePath, content, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/utils/git.ts
|
|
25
|
+
import { execSync } from "child_process";
|
|
26
|
+
import { existsSync } from "fs";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
function exec(cmd, cwd) {
|
|
29
|
+
execSync(cmd, { cwd, stdio: "pipe" });
|
|
30
|
+
}
|
|
31
|
+
function isGitRepo(cwd) {
|
|
32
|
+
return existsSync(join(cwd, ".git"));
|
|
33
|
+
}
|
|
34
|
+
function gitInit(cwd) {
|
|
35
|
+
exec("git init", cwd);
|
|
36
|
+
try {
|
|
37
|
+
exec("git checkout -b main", cwd);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function gitAdd(cwd, pattern = "-A") {
|
|
42
|
+
exec(`git add ${pattern}`, cwd);
|
|
43
|
+
}
|
|
44
|
+
function gitCommit(cwd, message) {
|
|
45
|
+
exec(`git commit -m "${message.replace(/"/g, '\\"')}"`, cwd);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/scaffold/claude-md.ts
|
|
49
|
+
function generateRootClaudeMd(config) {
|
|
50
|
+
const description = config.description || "<!-- Run /setup to fill this in -->";
|
|
51
|
+
const runCommand = config.runCommand || "# not set \u2014 run /setup to configure";
|
|
52
|
+
const modeLabel = config.mode === "detailed" ? "detailed" : "simple";
|
|
53
|
+
const workflowLabel = config.workflow === "team" ? "team" : "solo";
|
|
54
|
+
return `# ${config.projectName}
|
|
55
|
+
|
|
56
|
+
${description}
|
|
57
|
+
|
|
58
|
+
## Verify command
|
|
59
|
+
|
|
60
|
+
\`\`\`
|
|
61
|
+
${runCommand}
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Mode
|
|
65
|
+
|
|
66
|
+
${modeLabel}
|
|
67
|
+
|
|
68
|
+
## Workflow
|
|
69
|
+
|
|
70
|
+
${workflowLabel}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
function generateDotClaudeMd(_config) {
|
|
74
|
+
return `# Tiller \u2014 How to work
|
|
75
|
+
|
|
76
|
+
> These rules are managed by Tiller. Do not edit manually.
|
|
77
|
+
|
|
78
|
+
## Modes
|
|
79
|
+
|
|
80
|
+
The mode is set in CLAUDE.md (or overridden locally in \`.tiller.local.json\`). Read it at the start of every session.
|
|
81
|
+
|
|
82
|
+
### simple
|
|
83
|
+
|
|
84
|
+
The user is non-technical. They want to describe what they want and have it built for them.
|
|
85
|
+
|
|
86
|
+
- Do not explain your technical decisions unless asked
|
|
87
|
+
- Do not narrate steps as you do them
|
|
88
|
+
- Do not ask about tooling, frameworks, file structure, or verify commands
|
|
89
|
+
- When something goes wrong, fix it yourself first \u2014 only surface it if you can't resolve it
|
|
90
|
+
- Keep all communication short and outcome-focused: "Done. Here's what changed."
|
|
91
|
+
|
|
92
|
+
### detailed
|
|
93
|
+
|
|
94
|
+
The user is technical and wants to stay in control.
|
|
95
|
+
|
|
96
|
+
- Before touching files: write out your proposed approach, list files you'll create or modify, wait for explicit confirmation
|
|
97
|
+
- Narrate what you're doing and why
|
|
98
|
+
- Surface decisions and trade-offs before making them
|
|
99
|
+
- When something goes wrong, explain what happened and what you plan to do
|
|
100
|
+
|
|
101
|
+
## Workflows
|
|
102
|
+
|
|
103
|
+
The workflow is set in CLAUDE.md (or overridden locally in \`.tiller.local.json\`).
|
|
104
|
+
|
|
105
|
+
### solo
|
|
106
|
+
|
|
107
|
+
Single developer or local-only flow. /land merges directly to main.
|
|
108
|
+
|
|
109
|
+
### team
|
|
110
|
+
|
|
111
|
+
Multiple developers. /land pushes the feature branch and opens a PR. Merging happens on GitHub/GitLab after review and CI.
|
|
112
|
+
|
|
113
|
+
## Vibe loop
|
|
114
|
+
|
|
115
|
+
Every piece of work follows this loop:
|
|
116
|
+
|
|
117
|
+
1. **Orient** \u2014 read CLAUDE.md, vibestate.md (local), and changelog.md (shared)
|
|
118
|
+
2. **Confirm** \u2014 in detailed mode, enter plan mode with milestones and wait for approval
|
|
119
|
+
3. **Build** \u2014 implement milestone by milestone; each milestone includes tests, verify, and auto-commit
|
|
120
|
+
4. **Complete** \u2014 announce feature done, suggest /land
|
|
121
|
+
|
|
122
|
+
## File discipline
|
|
123
|
+
|
|
124
|
+
- Never commit directly to main
|
|
125
|
+
- Always work on a feature branch (feature/<name>)
|
|
126
|
+
- Run the verify command before every snapshot and land
|
|
127
|
+
- \`vibestate.md\` is gitignored \u2014 it tracks your local active feature state
|
|
128
|
+
- \`changelog.md\` is committed and shared \u2014 it tracks the project's done log
|
|
129
|
+
|
|
130
|
+
## Per-dev overrides
|
|
131
|
+
|
|
132
|
+
Create \`.tiller.local.json\` (gitignored) to override mode or workflow personally:
|
|
133
|
+
\`\`\`json
|
|
134
|
+
{ "mode": "simple", "workflow": "solo" }
|
|
135
|
+
\`\`\`
|
|
136
|
+
Skills read \`.tiller.local.json\` first, then fall back to CLAUDE.md.
|
|
137
|
+
|
|
138
|
+
## Skills
|
|
139
|
+
|
|
140
|
+
- **/setup** \u2014 first-run: understand the project and configure CLAUDE.md
|
|
141
|
+
- **/vibe** [idea] \u2014 milestone-driven development: plan, build, test, auto-commit. Every 3 landed features, automatically runs a background tech debt cleanup before planning.
|
|
142
|
+
- **/snapshot** \u2014 commit current progress on the feature branch
|
|
143
|
+
- **/land** \u2014 merge or PR depending on workflow
|
|
144
|
+
- **/recap** \u2014 read-only status of all work
|
|
145
|
+
|
|
146
|
+
## Rules
|
|
147
|
+
|
|
148
|
+
- Do not skip the verify step
|
|
149
|
+
- Do not touch unrelated files
|
|
150
|
+
- Do not make architectural changes without explicit confirmation in detailed mode
|
|
151
|
+
- Keep commits atomic and descriptive
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/scaffold/vibestate.ts
|
|
156
|
+
function generateVibestate(_config) {
|
|
157
|
+
return `# vibestate.md \u2014 local only, do not commit
|
|
158
|
+
|
|
159
|
+
> Tracks your active feature work. Gitignored \u2014 each dev has their own copy.
|
|
160
|
+
|
|
161
|
+
## Active feature
|
|
162
|
+
|
|
163
|
+
None \u2014 on main, ready to start something.
|
|
164
|
+
|
|
165
|
+
## Notes
|
|
166
|
+
|
|
167
|
+
<!-- Add context, gotchas, decisions here -->
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/scaffold/changelog.ts
|
|
172
|
+
function generateChangelog(config) {
|
|
173
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
174
|
+
return `# changelog.md \u2014 ${config.projectName}
|
|
175
|
+
|
|
176
|
+
> Shared project history. Updated by /vibe, /snapshot, and /land. Committed and shared.
|
|
177
|
+
|
|
178
|
+
## Done
|
|
179
|
+
|
|
180
|
+
- [${today}] v0 \u2014 initial scaffold
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/scaffold/settings-json.ts
|
|
185
|
+
function generateSettingsJson(_config) {
|
|
186
|
+
const settings = {
|
|
187
|
+
hooks: {
|
|
188
|
+
PostToolUse: [
|
|
189
|
+
{
|
|
190
|
+
matcher: "Edit|Write|MultiEdit",
|
|
191
|
+
hooks: [
|
|
192
|
+
{
|
|
193
|
+
type: "command",
|
|
194
|
+
command: 'bash .claude/hooks/post-write.sh "$CLAUDE_FILE_PATHS"'
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
PreToolUse: [
|
|
200
|
+
{
|
|
201
|
+
matcher: "Bash",
|
|
202
|
+
hooks: [
|
|
203
|
+
{
|
|
204
|
+
type: "command",
|
|
205
|
+
command: "bash .claude/hooks/secret-scan.sh"
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
SessionStart: [
|
|
211
|
+
{
|
|
212
|
+
matcher: "clear",
|
|
213
|
+
hooks: [
|
|
214
|
+
{
|
|
215
|
+
type: "command",
|
|
216
|
+
command: "bash .claude/hooks/session-resume.sh"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
permissions: {
|
|
223
|
+
allow: [
|
|
224
|
+
"Bash(git:*)",
|
|
225
|
+
"Bash(npm:*)",
|
|
226
|
+
"Bash(npx:*)",
|
|
227
|
+
"Bash(node:*)",
|
|
228
|
+
"Bash(echo:*)"
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
return JSON.stringify(settings, null, 2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/scaffold/gitignore.ts
|
|
236
|
+
function generateGitignore(_config) {
|
|
237
|
+
return `# Dependencies
|
|
238
|
+
node_modules/
|
|
239
|
+
|
|
240
|
+
# Build output
|
|
241
|
+
dist/
|
|
242
|
+
build/
|
|
243
|
+
.next/
|
|
244
|
+
out/
|
|
245
|
+
|
|
246
|
+
# Environment
|
|
247
|
+
.env
|
|
248
|
+
.env.local
|
|
249
|
+
.env.*.local
|
|
250
|
+
|
|
251
|
+
# Editor
|
|
252
|
+
.DS_Store
|
|
253
|
+
*.swp
|
|
254
|
+
*.swo
|
|
255
|
+
.idea/
|
|
256
|
+
.vscode/
|
|
257
|
+
|
|
258
|
+
# Logs
|
|
259
|
+
*.log
|
|
260
|
+
npm-debug.log*
|
|
261
|
+
|
|
262
|
+
# TypeScript
|
|
263
|
+
*.tsbuildinfo
|
|
264
|
+
|
|
265
|
+
# Tiller \u2014 local-only files (per-dev, not shared)
|
|
266
|
+
vibestate.md
|
|
267
|
+
.tiller.local.json
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/scaffold/tiller-manifest.ts
|
|
272
|
+
var MANAGED_FILES = [
|
|
273
|
+
".claude/CLAUDE.md",
|
|
274
|
+
".claude/settings.json",
|
|
275
|
+
".claude/hooks/post-write.sh",
|
|
276
|
+
".claude/hooks/secret-scan.sh",
|
|
277
|
+
".claude/skills/setup/SKILL.md",
|
|
278
|
+
".claude/skills/vibe/SKILL.md",
|
|
279
|
+
".claude/skills/snapshot/SKILL.md",
|
|
280
|
+
".claude/skills/recap/SKILL.md",
|
|
281
|
+
".claude/skills/land/SKILL.md",
|
|
282
|
+
".claude/skills/tech-debt/SKILL.md"
|
|
283
|
+
];
|
|
284
|
+
function generateTillerManifest(config, version) {
|
|
285
|
+
const manifest = {
|
|
286
|
+
version,
|
|
287
|
+
mode: config.mode,
|
|
288
|
+
workflow: config.workflow,
|
|
289
|
+
runCommand: config.runCommand,
|
|
290
|
+
managedFiles: MANAGED_FILES
|
|
291
|
+
};
|
|
292
|
+
return JSON.stringify(manifest, null, 2);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/scaffold/hooks/post-write.ts
|
|
296
|
+
function generatePostWriteHook(_config) {
|
|
297
|
+
return `#!/usr/bin/env bash
|
|
298
|
+
# post-write.sh \u2014 auto-format after file edits
|
|
299
|
+
# Managed by Tiller. Silent fail if formatter not available.
|
|
300
|
+
|
|
301
|
+
set -euo pipefail
|
|
302
|
+
|
|
303
|
+
FILES="$1"
|
|
304
|
+
|
|
305
|
+
if [ -z "$FILES" ]; then
|
|
306
|
+
exit 0
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Try prettier
|
|
310
|
+
if command -v prettier &>/dev/null; then
|
|
311
|
+
echo "$FILES" | tr ' ' '\\n' | xargs prettier --write --ignore-unknown 2>/dev/null || true
|
|
312
|
+
exit 0
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Try biome
|
|
316
|
+
if command -v biome &>/dev/null; then
|
|
317
|
+
echo "$FILES" | tr ' ' '\\n' | xargs biome format --write 2>/dev/null || true
|
|
318
|
+
exit 0
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# No formatter found \u2014 that's fine
|
|
322
|
+
exit 0
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/scaffold/hooks/secret-scan.ts
|
|
327
|
+
function generateSecretScanHook(_config) {
|
|
328
|
+
return `#!/usr/bin/env bash
|
|
329
|
+
# secret-scan.sh \u2014 block commits that contain secrets
|
|
330
|
+
# Managed by Tiller. Called as PreToolUse hook on Bash commands.
|
|
331
|
+
|
|
332
|
+
set -euo pipefail
|
|
333
|
+
|
|
334
|
+
# Read the tool input from stdin
|
|
335
|
+
INPUT=$(cat)
|
|
336
|
+
|
|
337
|
+
# Only run on git commit commands
|
|
338
|
+
COMMAND=$(echo "$INPUT" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('command', ''))" 2>/dev/null || echo "")
|
|
339
|
+
|
|
340
|
+
if [[ "$COMMAND" != *"git commit"* ]]; then
|
|
341
|
+
exit 0
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# Get staged files
|
|
345
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null || true)
|
|
346
|
+
|
|
347
|
+
if [ -z "$STAGED" ]; then
|
|
348
|
+
exit 0
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# Patterns that look like secrets
|
|
352
|
+
SECRET_PATTERNS=(
|
|
353
|
+
'sk-[a-zA-Z0-9]{20,}'
|
|
354
|
+
'ghp_[a-zA-Z0-9]{36}'
|
|
355
|
+
'gho_[a-zA-Z0-9]{36}'
|
|
356
|
+
'github_pat_[a-zA-Z0-9_]{82}'
|
|
357
|
+
'xoxb-[0-9]+-[a-zA-Z0-9]+'
|
|
358
|
+
'AKIA[0-9A-Z]{16}'
|
|
359
|
+
'eyJ[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+'
|
|
360
|
+
'AIza[0-9A-Za-z_-]{35}'
|
|
361
|
+
'password\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
|
|
362
|
+
'secret\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
|
|
363
|
+
'api[_-]?key\\s*=\\s*["\\'\\''"][^"\\'']+["\\'\\''"]'
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
FOUND=""
|
|
367
|
+
for PATTERN in "\${SECRET_PATTERNS[@]}"; do
|
|
368
|
+
MATCHES=$(git diff --cached | grep -iE "$PATTERN" 2>/dev/null || true)
|
|
369
|
+
if [ -n "$MATCHES" ]; then
|
|
370
|
+
FOUND="$FOUND\\n$MATCHES"
|
|
371
|
+
fi
|
|
372
|
+
done
|
|
373
|
+
|
|
374
|
+
if [ -n "$FOUND" ]; then
|
|
375
|
+
echo '{"permissionDecision":"deny","denyReason":"Potential secrets detected in staged changes. Remove secrets before committing."}'
|
|
376
|
+
exit 0
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
exit 0
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/scaffold/hooks/session-resume.ts
|
|
384
|
+
function generateSessionResumeHook(_config) {
|
|
385
|
+
return `#!/usr/bin/env bash
|
|
386
|
+
# Injected into context on SessionStart(clear)
|
|
387
|
+
if grep -q "Status: executing" vibestate.md 2>/dev/null; then
|
|
388
|
+
PLAN_FILE=$(grep "Plan:" vibestate.md | head -1 | sed 's/Plan: //')
|
|
389
|
+
echo "You are in the middle of a /vibe milestone build."
|
|
390
|
+
echo "Read vibestate.md for milestone progress and \${PLAN_FILE:-the plan file} for full context."
|
|
391
|
+
echo "Resume the milestone loop from where you left off. Auto-commit after each milestone."
|
|
392
|
+
fi
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/scaffold/skills/vibe.ts
|
|
397
|
+
function generateVibeSkill(config) {
|
|
398
|
+
return `---
|
|
399
|
+
name: vibe
|
|
400
|
+
description: Start or continue working on an idea. Usage: /vibe [idea description]
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
# /vibe \u2014 Start or continue work
|
|
404
|
+
|
|
405
|
+
## Step 1: Orient
|
|
406
|
+
|
|
407
|
+
Read \`CLAUDE.md\`, \`vibestate.md\`, and \`changelog.md\` to understand current state.
|
|
408
|
+
Run \`git branch\` and \`git status\`.
|
|
409
|
+
|
|
410
|
+
State the current mode from CLAUDE.md: "Mode: <mode>".
|
|
411
|
+
|
|
412
|
+
**If mode is simple:** Do not narrate the orient step.
|
|
413
|
+
**If mode is detailed:** Summarize the current state in 2-3 sentences.
|
|
414
|
+
|
|
415
|
+
## Step 2: Branch routing
|
|
416
|
+
|
|
417
|
+
**$ARGUMENTS provided** \u2192 check if a branch named \`feature/<kebab-case-of-arguments>\` already exists locally or remotely.
|
|
418
|
+
- If it exists: switch to it. Read \`vibestate.md\` for current state. Ask: "Found existing branch feature/<name>. Continue where we left off, or do you want to revisit the plan first?"
|
|
419
|
+
- Continue \u2192 pick up from the next unchecked milestone
|
|
420
|
+
- Revisit \u2192 summarize what's done so far, discuss before building
|
|
421
|
+
- If it doesn't exist: create it from main.
|
|
422
|
+
- **simple:** Say: "On it."
|
|
423
|
+
- **detailed:** State: "Starting work on: <idea>"
|
|
424
|
+
|
|
425
|
+
**Already on a feature branch** \u2192 continue.
|
|
426
|
+
- **simple:** Say nothing unless asked.
|
|
427
|
+
- **detailed:** State: "Continuing work on: <branch-name>"
|
|
428
|
+
|
|
429
|
+
**Neither** \u2192 list open feature branches briefly, ask what to work on.
|
|
430
|
+
|
|
431
|
+
## Step 2.5: Tech debt check
|
|
432
|
+
|
|
433
|
+
Before planning, check if a tech debt cleanup is due:
|
|
434
|
+
|
|
435
|
+
1. Count lines in \`changelog.md\` matching the pattern \`- [[^]]*] landed feature/\` \u2014 this is \`landedCount\`
|
|
436
|
+
2. Read \`.claude/.tiller-tech-debt.json\` \u2014 get \`lastTechDebtAtFeature\` and \`threshold\` (default threshold: 3)
|
|
437
|
+
3. If \`(landedCount - lastTechDebtAtFeature) >= threshold\`:
|
|
438
|
+
- Use the **Task tool** (foreground, \`subagent_type: "general-purpose"\`) with the contents of \`.claude/skills/tech-debt/SKILL.md\` as the prompt
|
|
439
|
+
- Wait for the agent to complete before continuing
|
|
440
|
+
4. Continue to Step 3 regardless of whether the tech debt agent ran
|
|
441
|
+
|
|
442
|
+
## Step 3: Plan milestones
|
|
443
|
+
|
|
444
|
+
**If mode is simple:** Explore the codebase and break the work into 2\u20135 milestones internally. Do not show this plan to the user.
|
|
445
|
+
|
|
446
|
+
**If mode is detailed:** Call \`EnterPlanMode\`. In the plan file, write:
|
|
447
|
+
- High-level approach (2\u20133 sentences)
|
|
448
|
+
- 2\u20135 numbered milestones, each with: what gets built + what gets tested
|
|
449
|
+
- Files to create or modify
|
|
450
|
+
- Any trade-offs worth noting
|
|
451
|
+
- **Execution rules** (embed verbatim): After plan approval, read \`vibestate.md\` to find the milestone checklist, then execute the milestone loop: for each remaining milestone, announce "Milestone X/N: <description>", build functionality, add or update tests, run \`${config.runCommand}\` and fix failures, run \`git add -A && git commit -m "<milestone>"\`, update \`vibestate.md\` checkboxes and \`changelog.md\` Done section then amend commit, report "Saved: <description> (X/N)". When all milestones are done, summarize what was built and suggest \`/land\`.
|
|
452
|
+
|
|
453
|
+
Before exiting plan mode, write the milestone checklist to the \`Active feature\` section of \`vibestate.md\` with \`Status: executing\` and the plan file path.
|
|
454
|
+
|
|
455
|
+
## Step 4: Build milestone by milestone
|
|
456
|
+
|
|
457
|
+
For each milestone:
|
|
458
|
+
1. **detailed only:** Announce: "Milestone X/N: <description>"
|
|
459
|
+
2. Build the functionality
|
|
460
|
+
3. Add or update tests for what was built
|
|
461
|
+
4. Run \`${config.runCommand}\` \u2014 **simple:** fix failures silently. **detailed:** fix before continuing.
|
|
462
|
+
5. \`git add -A && git commit -m "<milestone description>"\`
|
|
463
|
+
6. Update \`vibestate.md\` milestone checkboxes (detailed) and add entry to \`changelog.md\` Done section. Amend: \`git commit --amend --no-edit\`
|
|
464
|
+
7. **simple:** Say: "Saved: <what changed>". **detailed:** Report: "Saved: <description> (X/N)"
|
|
465
|
+
|
|
466
|
+
## Step 5: Complete
|
|
467
|
+
|
|
468
|
+
**simple:** Say: "Feature complete. Type /land when ready to merge."
|
|
469
|
+
**detailed:** Summarize everything that was built across all milestones. Suggest \`/land\` to merge.
|
|
470
|
+
|
|
471
|
+
## If something goes wrong
|
|
472
|
+
|
|
473
|
+
**simple:** Fix it yourself first. Only tell the user if you genuinely can't resolve it.
|
|
474
|
+
**detailed:** Explain what happened and what you plan to do.
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/scaffold/skills/snapshot.ts
|
|
479
|
+
function generateSnapshotSkill(config) {
|
|
480
|
+
return `---
|
|
481
|
+
name: snapshot
|
|
482
|
+
description: Save current progress with a commit on the feature branch
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
# /snapshot \u2014 Save progress
|
|
486
|
+
|
|
487
|
+
## Step 1: Check branch
|
|
488
|
+
|
|
489
|
+
Run \`git branch --show-current\`.
|
|
490
|
+
|
|
491
|
+
If on \`main\`:
|
|
492
|
+
- **simple:** Say: "You're on main \u2014 use /vibe to start a feature first." Stop.
|
|
493
|
+
- **detailed:** Warn: "You're on main. Snapshot is for feature branches. Use /vibe to start a feature branch first." Stop.
|
|
494
|
+
|
|
495
|
+
## Step 2: Run verify
|
|
496
|
+
|
|
497
|
+
Run \`${config.runCommand}\`
|
|
498
|
+
|
|
499
|
+
If it fails:
|
|
500
|
+
- **simple:** Say: "Something's broken, let me fix it first." Fix it, then continue.
|
|
501
|
+
- **detailed:** Show the error output. Do NOT commit. Say: "Verify failed. Fix the errors and try /snapshot again." Stop.
|
|
502
|
+
|
|
503
|
+
## Step 3: Describe changes
|
|
504
|
+
|
|
505
|
+
If $ARGUMENTS is provided, use that as the commit message.
|
|
506
|
+
|
|
507
|
+
Otherwise, run \`git diff --stat HEAD\` and infer a short, descriptive commit message.
|
|
508
|
+
|
|
509
|
+
Format: \`<verb> <what> \u2014 <brief detail if needed>\`
|
|
510
|
+
|
|
511
|
+
## Step 4: Commit
|
|
512
|
+
|
|
513
|
+
\`\`\`
|
|
514
|
+
git add -A
|
|
515
|
+
git commit -m "<message>"
|
|
516
|
+
\`\`\`
|
|
517
|
+
|
|
518
|
+
## Step 5: Update changelog.md
|
|
519
|
+
|
|
520
|
+
Add an entry to the Done section of \`changelog.md\`:
|
|
521
|
+
\`\`\`
|
|
522
|
+
- [YYYY-MM-DD] <message>
|
|
523
|
+
\`\`\`
|
|
524
|
+
|
|
525
|
+
Run \`git add changelog.md && git commit --amend --no-edit\`.
|
|
526
|
+
|
|
527
|
+
## Step 6: Confirm
|
|
528
|
+
|
|
529
|
+
- **simple:** Say: "Saved. Keep going or type /land when you're done."
|
|
530
|
+
- **detailed:** Say: "Snapshot saved: <message>. Use /land when this feature is ready to merge."
|
|
531
|
+
`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/scaffold/skills/recap.ts
|
|
535
|
+
function generateRecapSkill(_config) {
|
|
536
|
+
return `---
|
|
537
|
+
name: recap
|
|
538
|
+
description: Read-only status of all work \u2014 completed and in progress
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
# /recap \u2014 Project status
|
|
542
|
+
|
|
543
|
+
> Read-only. No file modifications. No suggestions. No internal monologue.
|
|
544
|
+
|
|
545
|
+
## Gather everything silently first
|
|
546
|
+
|
|
547
|
+
Run all of these before writing any output:
|
|
548
|
+
|
|
549
|
+
1. Read \`vibestate.md\` (active feature, local state)
|
|
550
|
+
2. Read \`changelog.md\` (shared done log)
|
|
551
|
+
3. \`git log main --oneline\`
|
|
552
|
+
4. \`git branch --list 'feature/*'\`
|
|
553
|
+
5. For each feature branch: \`git log main..<branch> --oneline\`
|
|
554
|
+
|
|
555
|
+
## Then produce output based on mode
|
|
556
|
+
|
|
557
|
+
Read mode from CLAUDE.md.
|
|
558
|
+
|
|
559
|
+
**If mode is simple:** Translate everything into plain English. No hashes, no branch names, no git jargon.
|
|
560
|
+
|
|
561
|
+
Format:
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
**Working on:** <what's currently being built, from vibestate.md \u2014 or "nothing, ready to start">
|
|
566
|
+
|
|
567
|
+
**Done**
|
|
568
|
+
- <plain English description of what was built>
|
|
569
|
+
...
|
|
570
|
+
|
|
571
|
+
**In progress**
|
|
572
|
+
- <plain English description of what's being worked on>
|
|
573
|
+
...
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
If there's nothing in progress: omit the "In progress" section.
|
|
578
|
+
If there's nothing done yet: say "Nothing completed yet."
|
|
579
|
+
|
|
580
|
+
**If mode is detailed:** Include technical details.
|
|
581
|
+
|
|
582
|
+
Format:
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
**Active:** <active feature from vibestate.md, or "none">
|
|
587
|
+
|
|
588
|
+
**Completed (main)**
|
|
589
|
+
<hash> <message>
|
|
590
|
+
...
|
|
591
|
+
|
|
592
|
+
**In progress**
|
|
593
|
+
feature/<name>
|
|
594
|
+
<hash> <message>
|
|
595
|
+
...
|
|
596
|
+
|
|
597
|
+
<X> features landed, <Y> in progress.
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
Nothing else. No commentary, no second-guessing, no re-running commands.
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/scaffold/skills/land.ts
|
|
606
|
+
function generateLandSkill(config) {
|
|
607
|
+
return `---
|
|
608
|
+
name: land
|
|
609
|
+
description: Merge completed feature to main and clean up the branch
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
# /land \u2014 Merge feature to main
|
|
613
|
+
|
|
614
|
+
## Step 1: Check branch
|
|
615
|
+
|
|
616
|
+
Run \`git branch --show-current\`.
|
|
617
|
+
|
|
618
|
+
If on \`main\`:
|
|
619
|
+
- **simple:** Say: "You're already on main." Stop.
|
|
620
|
+
- **detailed:** Error: "You're already on main. Switch to the feature branch you want to land." Stop.
|
|
621
|
+
|
|
622
|
+
Save the current branch name as \`<feature-branch>\`.
|
|
623
|
+
|
|
624
|
+
## Step 2: Run verify
|
|
625
|
+
|
|
626
|
+
Run \`${config.runCommand}\`
|
|
627
|
+
|
|
628
|
+
If it fails:
|
|
629
|
+
- **simple:** Say: "Something's not working, let me sort it out." Fix it first.
|
|
630
|
+
- **detailed:** Show the error output. Do NOT proceed. Say: "Verify failed. Fix the errors and try /land again." Stop.
|
|
631
|
+
|
|
632
|
+
## Step 3: Commit any uncommitted changes
|
|
633
|
+
|
|
634
|
+
Run \`git status --porcelain\`.
|
|
635
|
+
|
|
636
|
+
If there are uncommitted changes:
|
|
637
|
+
\`\`\`
|
|
638
|
+
git add -A
|
|
639
|
+
git commit -m "wip: save before landing"
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
## Step 4: Check workflow
|
|
643
|
+
|
|
644
|
+
Read workflow from \`.tiller.local.json\` if it exists, otherwise from CLAUDE.md or \`.tiller.json\`. Default: solo.
|
|
645
|
+
|
|
646
|
+
**If workflow is solo** \u2192 go to Step 5a (local merge).
|
|
647
|
+
**If workflow is team** \u2192 go to Step 5b (open PR).
|
|
648
|
+
|
|
649
|
+
## Step 5a: Solo \u2014 merge to main
|
|
650
|
+
|
|
651
|
+
\`\`\`
|
|
652
|
+
git checkout main
|
|
653
|
+
git merge --no-ff <feature-branch> -m "land: <feature-branch>"
|
|
654
|
+
git branch -d <feature-branch>
|
|
655
|
+
\`\`\`
|
|
656
|
+
|
|
657
|
+
Then go to Step 6.
|
|
658
|
+
|
|
659
|
+
## Step 5b: Team \u2014 open PR
|
|
660
|
+
|
|
661
|
+
Push the branch:
|
|
662
|
+
\`\`\`
|
|
663
|
+
git push origin <feature-branch>
|
|
664
|
+
\`\`\`
|
|
665
|
+
|
|
666
|
+
Check if \`gh\` CLI is available: run \`which gh\`.
|
|
667
|
+
|
|
668
|
+
**If gh is available:**
|
|
669
|
+
\`\`\`
|
|
670
|
+
gh pr create --fill
|
|
671
|
+
\`\`\`
|
|
672
|
+
Print the PR URL. Say: "PR opened. Merge happens on GitHub after review and CI."
|
|
673
|
+
|
|
674
|
+
**If gh is not available:**
|
|
675
|
+
Run \`git remote get-url origin\` to get the remote URL. Convert to a browser URL if needed.
|
|
676
|
+
Say: "Push done. Open a PR at: <remote-url>/compare/<feature-branch>"
|
|
677
|
+
|
|
678
|
+
Then go to Step 6 (do NOT delete the branch locally \u2014 it will be deleted after the PR merges remotely).
|
|
679
|
+
|
|
680
|
+
## Step 6: Update changelog.md
|
|
681
|
+
|
|
682
|
+
1. Add an entry to the Done section of \`changelog.md\`:
|
|
683
|
+
- **solo:** \`- [YYYY-MM-DD] landed <feature-branch>\`
|
|
684
|
+
- **team:** \`- [YYYY-MM-DD] PR opened: <feature-branch>\`
|
|
685
|
+
2. Clear the \`Active feature\` section of \`vibestate.md\`: set it to "None \u2014 on main, ready to start something."
|
|
686
|
+
3. Commit:
|
|
687
|
+
\`\`\`
|
|
688
|
+
git add changelog.md vibestate.md && git commit -m "update changelog: landed <feature-branch>"
|
|
689
|
+
\`\`\`
|
|
690
|
+
**team:** commit to the feature branch before pushing (or amend if already pushed).
|
|
691
|
+
|
|
692
|
+
## Step 7: Confirm
|
|
693
|
+
|
|
694
|
+
- **simple:** Say: "Done. Run \`/clear\` to reset context before starting your next feature, then \`/vibe\` to continue."
|
|
695
|
+
- **detailed:** Say: "Feature landed on main. Run \`/clear\` to reset context before your next feature, then \`/vibe\` to continue."
|
|
696
|
+
`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/scaffold/skills/setup.ts
|
|
700
|
+
function generateSetupSkill(_config) {
|
|
701
|
+
return `---
|
|
702
|
+
name: setup
|
|
703
|
+
description: First-run setup \u2014 understand the project and configure CLAUDE.md
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
# /setup \u2014 Configure this project
|
|
707
|
+
|
|
708
|
+
> Run this once after \`tiller-ai init\`.
|
|
709
|
+
|
|
710
|
+
## Step 1: Ask about mode first
|
|
711
|
+
|
|
712
|
+
Ask only this question to start:
|
|
713
|
+
|
|
714
|
+
> "How do you want to work together?
|
|
715
|
+
> - **Simple** \u2014 just tell me what to build, I'll handle all the technical decisions.
|
|
716
|
+
> - **Detailed** \u2014 I'll explain my thinking and check in with you before making decisions."
|
|
717
|
+
|
|
718
|
+
Wait for their answer. Default to simple if they're unsure.
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## If mode is SIMPLE
|
|
723
|
+
|
|
724
|
+
The user is non-technical. Handle everything yourself, ask as little as possible.
|
|
725
|
+
|
|
726
|
+
**Step 2: Ask what the project is**
|
|
727
|
+
|
|
728
|
+
Ask one question: "What are you building?"
|
|
729
|
+
|
|
730
|
+
Listen to their answer. That's enough \u2014 use it as the description.
|
|
731
|
+
|
|
732
|
+
**Step 3: Derive the verify command yourself**
|
|
733
|
+
|
|
734
|
+
Look at the project files silently. Do not ask the user about this.
|
|
735
|
+
|
|
736
|
+
- \`package.json\` with a \`test\` script \u2192 \`npm test\`
|
|
737
|
+
- \`package.json\` without a \`test\` script \u2192 \`npm run build\` if build script exists, else \`node --check index.js\`
|
|
738
|
+
- \`pyproject.toml\` or \`setup.py\` \u2192 \`pytest\`
|
|
739
|
+
- \`Cargo.toml\` \u2192 \`cargo test\`
|
|
740
|
+
- \`go.mod\` \u2192 \`go test ./...\`
|
|
741
|
+
- \`Makefile\` with a \`test\` target \u2192 \`make test\`
|
|
742
|
+
- Nothing recognizable \u2192 \`echo ok\` (can be updated later)
|
|
743
|
+
|
|
744
|
+
**Step 4: Update CLAUDE.md and commit**
|
|
745
|
+
|
|
746
|
+
Rewrite \`CLAUDE.md\` with the project name, their description, the verify command you derived, and mode: simple.
|
|
747
|
+
|
|
748
|
+
Update \`runCommand\` and \`mode\` in \`.claude/.tiller.json\`.
|
|
749
|
+
|
|
750
|
+
\`\`\`
|
|
751
|
+
git add CLAUDE.md .claude/.tiller.json
|
|
752
|
+
git commit -m "chore: configure project via /setup"
|
|
753
|
+
\`\`\`
|
|
754
|
+
|
|
755
|
+
Say: "All set. Just tell me what you want to build and I'll take it from there."
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## If mode is DETAILED
|
|
760
|
+
|
|
761
|
+
The user is technical and wants to stay in the loop.
|
|
762
|
+
|
|
763
|
+
**Step 2: Ask about the project**
|
|
764
|
+
|
|
765
|
+
Ask: "What are you building?" \u2014 let them describe it.
|
|
766
|
+
|
|
767
|
+
If there's already a README or package.json, read it first and confirm your understanding instead of asking blind.
|
|
768
|
+
|
|
769
|
+
**Step 3: Ask about the verify command**
|
|
770
|
+
|
|
771
|
+
Ask: "What command should I run to verify everything's working after a change?"
|
|
772
|
+
|
|
773
|
+
Give suggestions based on what you see in the project. If they don't know yet, suggest \`echo ok\` as a placeholder.
|
|
774
|
+
|
|
775
|
+
**Step 4: Update CLAUDE.md and commit**
|
|
776
|
+
|
|
777
|
+
Rewrite \`CLAUDE.md\` with the project name, description, verify command, and mode: detailed.
|
|
778
|
+
|
|
779
|
+
Update \`runCommand\` and \`mode\` in \`.claude/.tiller.json\`.
|
|
780
|
+
|
|
781
|
+
\`\`\`
|
|
782
|
+
git add CLAUDE.md .claude/.tiller.json
|
|
783
|
+
git commit -m "chore: configure project via /setup"
|
|
784
|
+
\`\`\`
|
|
785
|
+
|
|
786
|
+
Say: "All set. Use /vibe to start working."
|
|
787
|
+
`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/scaffold/skills/tech-debt.ts
|
|
791
|
+
function generateTechDebtSkill(config) {
|
|
792
|
+
return `---
|
|
793
|
+
name: tech-debt
|
|
794
|
+
description: Internal skill \u2014 spawned by /vibe to fix one small tech debt item. Not user-invocable directly.
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
# Tech Debt Agent
|
|
798
|
+
|
|
799
|
+
You are a focused tech debt cleanup agent. Your job: find and fix **one small, non-invasive tech debt item**, merge it to main, then exit cleanly.
|
|
800
|
+
|
|
801
|
+
## What to look for
|
|
802
|
+
|
|
803
|
+
Scan the codebase for ONE item from these categories (pick the smallest/safest):
|
|
804
|
+
|
|
805
|
+
- **Clutter:** dead code, unused imports/exports, stale TODOs, commented-out code
|
|
806
|
+
- **Duplication:** duplicated logic extractable into a shared helper
|
|
807
|
+
- **Readability:** functions with high cognitive complexity, unclear names, missing comments on non-obvious logic
|
|
808
|
+
- **Correctness risks:** unhandled promise rejections, missing \`await\`, unsafe type assertions (\`as X\`), hardcoded values that should be constants
|
|
809
|
+
- **Inconsistency:** patterns done differently in one place vs. the rest of the codebase
|
|
810
|
+
- **Test health:** happy-path-only tests missing obvious error cases
|
|
811
|
+
- **Dependency hygiene:** packages imported but barely used (could be inlined or removed)
|
|
812
|
+
|
|
813
|
+
## Guardrails \u2014 you MUST NOT
|
|
814
|
+
|
|
815
|
+
- Touch more than 3 files
|
|
816
|
+
- Change any public API or exported interface
|
|
817
|
+
- Refactor anything that changes observable behavior
|
|
818
|
+
- Split, merge, or move files (structural changes)
|
|
819
|
+
- Rename anything used across many files
|
|
820
|
+
- Modify CI/CD, build config, or dependency versions
|
|
821
|
+
- If nothing safe is found, skip entirely and report "codebase is clean"
|
|
822
|
+
|
|
823
|
+
## Steps
|
|
824
|
+
|
|
825
|
+
1. Explore the codebase and identify the single smallest, most non-invasive item to fix
|
|
826
|
+
2. If nothing safe is found: skip to the "Report" step with "codebase is clean"
|
|
827
|
+
3. Note the current branch: \`git branch --show-current\`
|
|
828
|
+
4. Stash any uncommitted work: \`git stash\`
|
|
829
|
+
5. \`git checkout main\`
|
|
830
|
+
6. Create a chore branch: \`git checkout -b chore/tech-debt-<short-desc>\` (use kebab-case, max 4 words)
|
|
831
|
+
7. Fix the item
|
|
832
|
+
8. Run \`${config.runCommand}\` \u2014 if it fails, revert the change and skip (report "skipped \u2014 verify failed")
|
|
833
|
+
9. \`git add -A && git commit -m "chore: tech debt \u2014 <short-desc>"\`
|
|
834
|
+
10. \`git checkout main && git merge --no-ff chore/tech-debt-<short-desc> -m "chore: tech debt \u2014 <short-desc>"\`
|
|
835
|
+
11. \`git branch -d chore/tech-debt-<short-desc>\`
|
|
836
|
+
12. \`git checkout <original-branch> && git stash pop\` (restore original state)
|
|
837
|
+
13. Update \`.claude/.tiller-tech-debt.json\`: set \`lastTechDebtAtFeature\` to the current count of landed features (lines matching \`- [.*] landed feature/\` in \`changelog.md\`)
|
|
838
|
+
|
|
839
|
+
## Report
|
|
840
|
+
|
|
841
|
+
**simple mode:** "Cleaned up a bit."
|
|
842
|
+
**detailed mode:** "Tech debt fixed: <what was done and why it mattered>"
|
|
843
|
+
|
|
844
|
+
If skipped: **simple:** say nothing. **detailed:** "Tech debt check: codebase is clean."
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/scaffold/tech-debt-state.ts
|
|
849
|
+
function generateTechDebtState() {
|
|
850
|
+
return JSON.stringify(
|
|
851
|
+
{
|
|
852
|
+
lastTechDebtAtFeature: 0,
|
|
853
|
+
threshold: 3
|
|
854
|
+
},
|
|
855
|
+
null,
|
|
856
|
+
2
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/scaffold/index.ts
|
|
861
|
+
var TILLER_VERSION = "0.1.0";
|
|
862
|
+
async function scaffold(config, targetDir) {
|
|
863
|
+
const p = (rel) => join2(targetDir, rel);
|
|
864
|
+
await writeFile(p("CLAUDE.md"), generateRootClaudeMd(config));
|
|
865
|
+
await writeFile(p(".gitignore"), generateGitignore(config));
|
|
866
|
+
await writeFile(p("changelog.md"), generateChangelog(config));
|
|
867
|
+
await writeFile(p("vibestate.md"), generateVibestate(config));
|
|
868
|
+
await writeFile(p(".claude/CLAUDE.md"), generateDotClaudeMd(config));
|
|
869
|
+
await writeFile(p(".claude/settings.json"), generateSettingsJson(config));
|
|
870
|
+
await writeFile(p(".claude/.tiller.json"), generateTillerManifest(config, TILLER_VERSION));
|
|
871
|
+
await writeFile(p(".claude/.tiller-tech-debt.json"), generateTechDebtState());
|
|
872
|
+
await writeFile(p(".claude/hooks/post-write.sh"), generatePostWriteHook(config));
|
|
873
|
+
await writeFile(p(".claude/hooks/secret-scan.sh"), generateSecretScanHook(config));
|
|
874
|
+
await writeFile(p(".claude/hooks/session-resume.sh"), generateSessionResumeHook(config));
|
|
875
|
+
await writeFile(p(".claude/skills/setup/SKILL.md"), generateSetupSkill(config));
|
|
876
|
+
await writeFile(p(".claude/skills/vibe/SKILL.md"), generateVibeSkill(config));
|
|
877
|
+
await writeFile(p(".claude/skills/snapshot/SKILL.md"), generateSnapshotSkill(config));
|
|
878
|
+
await writeFile(p(".claude/skills/recap/SKILL.md"), generateRecapSkill(config));
|
|
879
|
+
await writeFile(p(".claude/skills/land/SKILL.md"), generateLandSkill(config));
|
|
880
|
+
await writeFile(p(".claude/skills/tech-debt/SKILL.md"), generateTechDebtSkill(config));
|
|
881
|
+
if (!isGitRepo(targetDir)) {
|
|
882
|
+
gitInit(targetDir);
|
|
883
|
+
}
|
|
884
|
+
gitAdd(targetDir);
|
|
885
|
+
gitCommit(targetDir, "chore: initial tiller scaffold");
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/commands/init.ts
|
|
889
|
+
async function initCommand() {
|
|
890
|
+
const targetDir = resolve(process.cwd());
|
|
891
|
+
const projectName = basename(targetDir);
|
|
892
|
+
intro("tiller-ai init");
|
|
893
|
+
const config = {
|
|
894
|
+
projectName,
|
|
895
|
+
description: "",
|
|
896
|
+
runCommand: "",
|
|
897
|
+
mode: "simple",
|
|
898
|
+
workflow: "solo"
|
|
899
|
+
};
|
|
900
|
+
const s = spinner();
|
|
901
|
+
s.start("Scaffolding...");
|
|
902
|
+
try {
|
|
903
|
+
await scaffold(config, targetDir);
|
|
904
|
+
s.stop("Done.");
|
|
905
|
+
} catch (err) {
|
|
906
|
+
s.stop("Failed.");
|
|
907
|
+
throw err;
|
|
908
|
+
}
|
|
909
|
+
outro(
|
|
910
|
+
`Scaffolded in ./${projectName}
|
|
911
|
+
|
|
912
|
+
claude
|
|
913
|
+
|
|
914
|
+
Then run /setup to configure the project with AI assistance.`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/upgrade.ts
|
|
919
|
+
import { intro as intro2, outro as outro2, confirm, spinner as spinner2, isCancel, cancel } from "@clack/prompts";
|
|
920
|
+
import { readFile } from "fs/promises";
|
|
921
|
+
import { existsSync as existsSync2 } from "fs";
|
|
922
|
+
import { resolve as resolve2 } from "path";
|
|
923
|
+
var TILLER_VERSION2 = "0.1.0";
|
|
924
|
+
async function upgradeCommand() {
|
|
925
|
+
intro2("tiller-ai upgrade \u2014 update hooks and skills");
|
|
926
|
+
const manifestPath = resolve2(process.cwd(), ".claude/.tiller.json");
|
|
927
|
+
if (!existsSync2(manifestPath)) {
|
|
928
|
+
cancel("No .claude/.tiller.json found. Is this a Tiller project? Run tiller-ai init to start a new project.");
|
|
929
|
+
process.exit(1);
|
|
930
|
+
}
|
|
931
|
+
let manifest;
|
|
932
|
+
try {
|
|
933
|
+
const raw = await readFile(manifestPath, "utf-8");
|
|
934
|
+
manifest = JSON.parse(raw);
|
|
935
|
+
} catch {
|
|
936
|
+
cancel("Failed to read .claude/.tiller.json. The file may be corrupted.");
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
const go = await confirm({
|
|
940
|
+
message: `Upgrade Tiller files from v${manifest.version} to v${TILLER_VERSION2}? (managed files will be overwritten)`
|
|
941
|
+
});
|
|
942
|
+
if (isCancel(go) || !go) {
|
|
943
|
+
cancel("Upgrade cancelled.");
|
|
944
|
+
process.exit(0);
|
|
945
|
+
}
|
|
946
|
+
const config = {
|
|
947
|
+
projectName: "",
|
|
948
|
+
description: "",
|
|
949
|
+
runCommand: manifest.runCommand,
|
|
950
|
+
mode: manifest.mode,
|
|
951
|
+
workflow: manifest.workflow ?? "solo"
|
|
952
|
+
};
|
|
953
|
+
const s = spinner2();
|
|
954
|
+
s.start("Upgrading...");
|
|
955
|
+
try {
|
|
956
|
+
await writeFile(".claude/CLAUDE.md", generateDotClaudeMd(config));
|
|
957
|
+
await writeFile(".claude/hooks/post-write.sh", generatePostWriteHook(config));
|
|
958
|
+
await writeFile(".claude/hooks/secret-scan.sh", generateSecretScanHook(config));
|
|
959
|
+
await writeFile(".claude/skills/setup/SKILL.md", generateSetupSkill(config));
|
|
960
|
+
await writeFile(".claude/skills/vibe/SKILL.md", generateVibeSkill(config));
|
|
961
|
+
await writeFile(".claude/skills/snapshot/SKILL.md", generateSnapshotSkill(config));
|
|
962
|
+
await writeFile(".claude/skills/recap/SKILL.md", generateRecapSkill(config));
|
|
963
|
+
await writeFile(".claude/skills/land/SKILL.md", generateLandSkill(config));
|
|
964
|
+
await writeFile(".claude/skills/tech-debt/SKILL.md", generateTechDebtSkill(config));
|
|
965
|
+
await writeFile(".claude/.tiller.json", generateTillerManifest(config, TILLER_VERSION2));
|
|
966
|
+
s.stop("Done!");
|
|
967
|
+
} catch (err) {
|
|
968
|
+
s.stop("Failed.");
|
|
969
|
+
throw err;
|
|
970
|
+
}
|
|
971
|
+
outro2(`Upgraded to v${TILLER_VERSION2}. Managed files: ${MANAGED_FILES.length}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/commands/mode.ts
|
|
975
|
+
import { intro as intro3, outro as outro3, spinner as spinner3, cancel as cancel2 } from "@clack/prompts";
|
|
976
|
+
import { readFile as readFile2, writeFile as fsWriteFile2 } from "fs/promises";
|
|
977
|
+
import { existsSync as existsSync3 } from "fs";
|
|
978
|
+
import { resolve as resolve3 } from "path";
|
|
979
|
+
var TILLER_VERSION3 = "0.1.0";
|
|
980
|
+
async function modeCommand(newMode, options) {
|
|
981
|
+
intro3("tiller-ai mode \u2014 switch between simple and detailed");
|
|
982
|
+
if (newMode !== "simple" && newMode !== "detailed") {
|
|
983
|
+
cancel2('Mode must be "simple" or "detailed".');
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
const manifestPath = resolve3(process.cwd(), ".claude/.tiller.json");
|
|
987
|
+
if (!existsSync3(manifestPath)) {
|
|
988
|
+
cancel2("No .claude/.tiller.json found. Is this a Tiller project?");
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
let manifest;
|
|
992
|
+
try {
|
|
993
|
+
const raw = await readFile2(manifestPath, "utf-8");
|
|
994
|
+
manifest = JSON.parse(raw);
|
|
995
|
+
} catch {
|
|
996
|
+
cancel2("Failed to read .claude/.tiller.json.");
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const localPath = resolve3(process.cwd(), ".tiller.local.json");
|
|
1000
|
+
const isProjectScope = options.project === true;
|
|
1001
|
+
if (!isProjectScope) {
|
|
1002
|
+
let localMode;
|
|
1003
|
+
if (existsSync3(localPath)) {
|
|
1004
|
+
try {
|
|
1005
|
+
const raw = await readFile2(localPath, "utf-8");
|
|
1006
|
+
const local = JSON.parse(raw);
|
|
1007
|
+
localMode = local.mode;
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (localMode === newMode) {
|
|
1012
|
+
outro3(`Already in ${newMode} mode (local override).`);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
if (manifest.mode === newMode) {
|
|
1017
|
+
outro3(`Already in ${newMode} mode (project default).`);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const rootClaudePath = resolve3(process.cwd(), "CLAUDE.md");
|
|
1022
|
+
let projectName = manifest.projectName ?? "";
|
|
1023
|
+
let description = manifest.description ?? "";
|
|
1024
|
+
if (existsSync3(rootClaudePath)) {
|
|
1025
|
+
try {
|
|
1026
|
+
const raw = await readFile2(rootClaudePath, "utf-8");
|
|
1027
|
+
const nameMatch = raw.match(/^# (.+)$/m);
|
|
1028
|
+
const descMatch = raw.match(/^# .+\n\n(.+)/m);
|
|
1029
|
+
if (nameMatch) projectName = nameMatch[1];
|
|
1030
|
+
if (descMatch) description = descMatch[1];
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const s = spinner3();
|
|
1035
|
+
if (isProjectScope) {
|
|
1036
|
+
s.start(`Switching project default to ${newMode} mode...`);
|
|
1037
|
+
const config = {
|
|
1038
|
+
projectName,
|
|
1039
|
+
description,
|
|
1040
|
+
runCommand: manifest.runCommand,
|
|
1041
|
+
mode: newMode,
|
|
1042
|
+
workflow: manifest.workflow ?? "solo"
|
|
1043
|
+
};
|
|
1044
|
+
try {
|
|
1045
|
+
await writeFile("CLAUDE.md", generateRootClaudeMd(config));
|
|
1046
|
+
await writeFile(".claude/.tiller.json", generateTillerManifest(config, TILLER_VERSION3));
|
|
1047
|
+
s.stop("Done!");
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
s.stop("Failed.");
|
|
1050
|
+
throw err;
|
|
1051
|
+
}
|
|
1052
|
+
outro3(`Project default mode set to ${newMode}. Commit CLAUDE.md and .tiller.json to share with the team.`);
|
|
1053
|
+
} else {
|
|
1054
|
+
s.start(`Setting personal mode to ${newMode}...`);
|
|
1055
|
+
let existing = {};
|
|
1056
|
+
if (existsSync3(localPath)) {
|
|
1057
|
+
try {
|
|
1058
|
+
const raw = await readFile2(localPath, "utf-8");
|
|
1059
|
+
existing = JSON.parse(raw);
|
|
1060
|
+
} catch {
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const local = { ...existing, mode: newMode };
|
|
1064
|
+
try {
|
|
1065
|
+
await fsWriteFile2(localPath, JSON.stringify(local, null, 2), "utf-8");
|
|
1066
|
+
s.stop("Done!");
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
s.stop("Failed.");
|
|
1069
|
+
throw err;
|
|
1070
|
+
}
|
|
1071
|
+
outro3(`Personal mode set to ${newMode} in .tiller.local.json (gitignored). Use --project to change the shared default.`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/commands/workflow.ts
|
|
1076
|
+
import { intro as intro4, outro as outro4, spinner as spinner4, cancel as cancel3 } from "@clack/prompts";
|
|
1077
|
+
import { readFile as readFile3, writeFile as fsWriteFile3 } from "fs/promises";
|
|
1078
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1079
|
+
import { resolve as resolve4 } from "path";
|
|
1080
|
+
var TILLER_VERSION4 = "0.1.0";
|
|
1081
|
+
async function workflowCommand(newWorkflow, options) {
|
|
1082
|
+
intro4("tiller-ai workflow \u2014 switch between solo and team");
|
|
1083
|
+
if (newWorkflow !== "solo" && newWorkflow !== "team") {
|
|
1084
|
+
cancel3('Workflow must be "solo" or "team".');
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
const manifestPath = resolve4(process.cwd(), ".claude/.tiller.json");
|
|
1088
|
+
if (!existsSync4(manifestPath)) {
|
|
1089
|
+
cancel3("No .claude/.tiller.json found. Is this a Tiller project?");
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
let manifest;
|
|
1093
|
+
try {
|
|
1094
|
+
const raw = await readFile3(manifestPath, "utf-8");
|
|
1095
|
+
manifest = JSON.parse(raw);
|
|
1096
|
+
} catch {
|
|
1097
|
+
cancel3("Failed to read .claude/.tiller.json.");
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
const localPath = resolve4(process.cwd(), ".tiller.local.json");
|
|
1101
|
+
const isProjectScope = options.project === true;
|
|
1102
|
+
if (!isProjectScope) {
|
|
1103
|
+
let localWorkflow;
|
|
1104
|
+
if (existsSync4(localPath)) {
|
|
1105
|
+
try {
|
|
1106
|
+
const raw = await readFile3(localPath, "utf-8");
|
|
1107
|
+
const local = JSON.parse(raw);
|
|
1108
|
+
localWorkflow = local.workflow;
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (localWorkflow === newWorkflow) {
|
|
1113
|
+
outro4(`Already on ${newWorkflow} workflow (local override).`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
1117
|
+
if ((manifest.workflow ?? "solo") === newWorkflow) {
|
|
1118
|
+
outro4(`Already on ${newWorkflow} workflow (project default).`);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const rootClaudePath = resolve4(process.cwd(), "CLAUDE.md");
|
|
1123
|
+
let projectName = manifest.projectName ?? "";
|
|
1124
|
+
let description = manifest.description ?? "";
|
|
1125
|
+
if (existsSync4(rootClaudePath)) {
|
|
1126
|
+
try {
|
|
1127
|
+
const raw = await readFile3(rootClaudePath, "utf-8");
|
|
1128
|
+
const nameMatch = raw.match(/^# (.+)$/m);
|
|
1129
|
+
const descMatch = raw.match(/^# .+\n\n(.+)/m);
|
|
1130
|
+
if (nameMatch) projectName = nameMatch[1];
|
|
1131
|
+
if (descMatch) description = descMatch[1];
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const s = spinner4();
|
|
1136
|
+
if (isProjectScope) {
|
|
1137
|
+
s.start(`Switching project default to ${newWorkflow} workflow...`);
|
|
1138
|
+
const config = {
|
|
1139
|
+
projectName,
|
|
1140
|
+
description,
|
|
1141
|
+
runCommand: manifest.runCommand,
|
|
1142
|
+
mode: manifest.mode ?? "detailed",
|
|
1143
|
+
workflow: newWorkflow
|
|
1144
|
+
};
|
|
1145
|
+
try {
|
|
1146
|
+
await fsWriteFile3(rootClaudePath, generateRootClaudeMd(config), "utf-8");
|
|
1147
|
+
await fsWriteFile3(manifestPath, generateTillerManifest(config, TILLER_VERSION4), "utf-8");
|
|
1148
|
+
s.stop("Done!");
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
s.stop("Failed.");
|
|
1151
|
+
throw err;
|
|
1152
|
+
}
|
|
1153
|
+
outro4(`Project default workflow set to ${newWorkflow}. Commit CLAUDE.md and .tiller.json to share with the team.`);
|
|
1154
|
+
} else {
|
|
1155
|
+
s.start(`Setting personal workflow to ${newWorkflow}...`);
|
|
1156
|
+
let existing = {};
|
|
1157
|
+
if (existsSync4(localPath)) {
|
|
1158
|
+
try {
|
|
1159
|
+
const raw = await readFile3(localPath, "utf-8");
|
|
1160
|
+
existing = JSON.parse(raw);
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const local = { ...existing, workflow: newWorkflow };
|
|
1165
|
+
try {
|
|
1166
|
+
await fsWriteFile3(localPath, JSON.stringify(local, null, 2), "utf-8");
|
|
1167
|
+
s.stop("Done!");
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
s.stop("Failed.");
|
|
1170
|
+
throw err;
|
|
1171
|
+
}
|
|
1172
|
+
outro4(`Personal workflow set to ${newWorkflow} in .tiller.local.json (gitignored). Use --project to change the shared default.`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/index.ts
|
|
1177
|
+
var program = new Command();
|
|
1178
|
+
program.name("tiller").description("Scaffold Claude Code projects with a structured vibe loop").version("0.1.0");
|
|
1179
|
+
program.command("init").description("Scaffold Tiller files in the current directory").action(initCommand);
|
|
1180
|
+
program.command("upgrade").description("Update hooks and skills in an existing Tiller project").action(upgradeCommand);
|
|
1181
|
+
program.command("mode").description("Switch between simple and detailed mode").argument("<mode>", "Mode: simple or detailed").option("--project", "Set the shared project default instead of your personal override").action(modeCommand);
|
|
1182
|
+
program.command("workflow").description("Switch between solo and team workflow").argument("<workflow>", "Workflow: solo or team").option("--project", "Set the shared project default instead of your personal override").action(workflowCommand);
|
|
1183
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tiller-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold Claude Code projects with a structured vibe loop",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tiller-ai": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"regen": "tsx scripts/regen-skills.ts"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^0.9.1",
|
|
22
|
+
"commander": "^12.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"tsup": "^8.3.0",
|
|
27
|
+
"tsx": "^4.21.0",
|
|
28
|
+
"typescript": "^5.6.0",
|
|
29
|
+
"vitest": "^2.1.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.0.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"claude",
|
|
36
|
+
"claude-code",
|
|
37
|
+
"ai",
|
|
38
|
+
"ai-workflow",
|
|
39
|
+
"scaffold",
|
|
40
|
+
"cli",
|
|
41
|
+
"vibe-coding",
|
|
42
|
+
"developer-tools",
|
|
43
|
+
"code-generation",
|
|
44
|
+
"project-scaffold"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|