-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Contributor-facing reference: how tuck is laid out on disk, which module owns what, the exact shape of every on-disk format, and how a command flows from CLI entry to a git commit.
If you're here to learn tuck's features or commands, start with Command Reference or Getting Started. Architecture lives here so that page-one contributors don't have to reverse-engineer it from 25 library modules.
For agent-specific conventions (CLAUDE Code + subagent routing) see CLAUDE.md and AGENTS.md in the repo root — this page is language- and agent-neutral.
Per-host working directory (~/.tuck/ by default, override via -d/--dir):
~/.tuck/
├── .tuckmanifest.json # tracked files + group tags — committed to git
├── .tuckrc.json # shared config — committed to git
├── .tuckrc.local.json # per-host config — gitignored
├── .bootstrap-state.json # per-host bootstrap install state — gitignored
├── .tuckignore # patterns skipped by tuck scan / tuck add
├── .gitignore # includes local files above
├── .git/ # tuck repo (ordinary git working copy)
├── bootstrap.toml # declarative tool catalog (optional)
└── files/
├── shell/.zshrc
├── editor/init.lua
└── <category>/<path>
Per-host backup directory — intentionally outside ~/.tuck/ so snapshots survive a catastrophic rm -rf ~/.tuck/:
~/.tuck-backups/
└── <YYYY-MM-DD-HHMMSS>/
├── metadata.json # SnapshotMetadata (see Data formats)
└── <file-tree-copies> # backed-up content
Source tree:
src/
├── commands/ # One file per top-level CLI verb (init, sync, add, …)
├── lib/ # Core library modules (25 files + 4 subdirs)
├── ui/ # Terminal UI primitives (prompts, logger, theme)
├── schemas/ # Zod schemas for config + manifest + bootstrap + secrets
├── errors.ts # Custom error classes (NotInitializedError, GitError, …)
├── constants.ts # App constants (DEFAULT_TUCK_DIR, LOCAL_CONFIG_FILE, …)
├── types.ts # Shared TS types
└── index.ts # Entry point — Commander setup + command registration
src/lib/ holds the non-CLI logic. Grouped by responsibility:
Storage & tracking
-
manifest.ts—.tuckmanifest.jsonread/write,getAllGroups, file lookup by group. -
config.ts—.tuckrc.json+.tuckrc.local.jsonload/save;loadLocalConfigandsaveLocalConfigfor per-host-only keys. -
files.ts— file copy/symlink primitives, binary-safe reads, permission preservation. -
fileTracking.ts,trackPipeline.ts— high-level "track this file" orchestration (categorize → copy → add to manifest → stage). -
paths.ts—expandPath()/collapsePath(),getTuckDir(),getConfigPath(),getManifestPath(),getBackupsDir(). Every path goes through here — never assume~resolution. -
tuckignore.ts—.tuckignoreparser and glob matcher.
Git & providers
-
git.ts—simple-gitwrapper:cloneRepo,stageAll,commit,push,pull(with--rebase --autostash),getAheadBehind. -
providers/— git-host abstraction (see Provider abstraction). -
providerSetup.ts— interactive provider selection + auth wizard used bytuck init. -
remoteChecks.ts— pre-sync "is the remote reachable?" probes. -
github.ts— GitHub CLI (gh) integration beyond the generic provider interface (dotfiles-repo search, SSH-key UX, credential helpers).
Scan & detection
-
detect.ts— heuristic dotfile discovery under$HOMEwith a category map (shell/editor/git/…). -
binary.ts— binary-file detection by content sniff. -
syntaxHighlight.ts— per-language tokenizer fortuck diff. Line-oriented, state-free (multi-line block comments are a known gap — see TASK-052-FOLLOWUP). -
osDetect.ts—/etc/os-release→ canonical group name (ubuntu / kali / arch / …) used by the init-time prompt.
Safety
-
timemachine.ts— snapshot create/list/restore/prune. The load-bearing module behind every destructive operation —tuck apply,tuck restore,tuck sync,tuck remove --delete,tuck clean,tuck validate --fix,tuck optimize --autoall callcreateSnapshotbefore writing. -
merge.ts— smart-merge for shell config files (append-block markers, dedup, diff preview). -
validation.ts— input validation + error-to-message rendering. -
groupFilter.ts— manifest → per-group file filter.
Diagnostics
-
doctor.ts—tuck doctorcheck groups (env / repo / manifest / security / hooks). -
audit.ts— internal audit log used by safety-gated commands. -
validators/—tuck validatedispatch (json, toml, shell, lua, fixers). -
shellProfiler/—tuck optimizextrace parser + rule engine (parser, rules, runner).
Other
-
hooks.ts—preSync/postSync/preRestore/postRestoreexecution with env-var shape. -
platform.ts— Windows-vs-POSIX forks (symlink permissions, path separators, junctions). -
updater.ts—update-notifierglue for the "new version available" banner. -
bootstrap/—bootstrap.tomlparser, tool runner, dependency resolver, state persistence.
Where to look when adding a new command:
-
src/commands/<name>.ts— implement + Commander wire. -
src/commands/index.ts+src/index.ts— register. -
tests/commands/<name>.test.ts— safety-invariant tests. -
docs/wiki/Command-Reference.md— entry under the right section. - Corresponding topic page in
docs/wiki/if the command has a concept the reference alone doesn't cover.
Canonical schema in src/schemas/manifest.schema.ts. Shape:
-
version: "2.0.0"— bumped to add host groups. Old 1.x manifests load with emptygroupsand tripMigrationRequiredError;tuck migratebackfills. -
Post-migration invariant: every file has at least one group.
tuck group rmrefuses to remove the last remaining group. -
Keys in
filesare the originalsourcepaths (~/intentionally preserved as a string — stored not expanded).destinationis a repo-relative path under~/.tuck/files/using forward slashes (Windows-safe).
Canonical schemas in src/schemas/config.schema.ts.
.tuckrc.json (shared, committed) holds fields that every host needs to agree on:
-
repository.*(branch, autoCommit, autoPush) -
files.*(strategy, backupOnRestore, backupDir) -
ignore,categories,readOnlyGroups,remote,security,snapshots,encryption,ui -
hooks(fallback hooks; local can override per-type)
.tuckrc.local.json (per-host, gitignored) holds fields that vary by host:
-
defaultGroups— which groups this host belongs to -
hooks— per-host overrides, merged over shared per-type
Precedence: defaults → .tuckrc.json → .tuckrc.local.json. loadConfig() returns the merged view. Callers that need to distinguish (e.g. the init-time prompt) use loadLocalConfig() to read only the local file.
Local-only keys route through saveLocalConfig() — writing them to shared would leak host-specific state across every clone (see the defaultGroups bug fixed in v2.22.2).
Canonical schema in src/lib/bootstrap/state.ts. Shape:
{
"version": 1,
"tools": {
"ripgrep": {
"installedAt": "2026-04-20T...",
"version": "14.1.0",
"definitionHash": "sha256:…"
}
}
}-
Per-host, never committed. Lives at
~/.tuck/.bootstrap-state.json. Gitignore entry is auto-appended on first save. -
definitionHashis the SHA-256 of the normalized tool definition inbootstrap.toml. When the definition changes (version bump, install-command edit), the hash moves andtuck bootstrap updateflags the tool as drift-detected. -
STATE_VERSION = 1. Missing file is indistinguishable from "nothing installed yet" — the common first-run state. -
No concurrency:
tuck bootstrapprocesses tools sequentially, so load-then-save doesn't race. If that ever changes, revisit — a naive last-writer-wins would clobber entries.
Each destructive operation takes a snapshot in ~/.tuck-backups/<YYYY-MM-DD-HHMMSS>/ via createSnapshot(paths, reason, { kind }) in src/lib/timemachine.ts:
~/.tuck-backups/2026-04-24-163050/
├── metadata.json
└── <file-tree-copies>
metadata.json shape:
{
"id": "2026-04-24-163050",
"timestamp": "2026-04-24T16:30:50.123Z",
"reason": "tuck restore -g kubuntu",
"machine": "kubuntu-desktop",
"profile": "kubuntu",
"kind": "restore",
"files": [
{ "originalPath": "/home/me/.zshrc", "backupPath": "home/me/.zshrc", "existed": true },
{ "originalPath": "/home/me/.tmux.conf", "backupPath": "home/me/.tmux.conf", "existed": false }
]
}SnapshotKind (authoritative list in timemachine.ts):
| Kind | Emitted by |
|---|---|
apply |
tuck apply |
restore |
tuck restore |
sync |
tuck sync (before writes) |
remove |
tuck remove --delete |
clean |
tuck clean |
manual |
tuck snapshot create (direct user ask) |
validate-fix |
tuck validate --fix |
optimize-auto |
tuck optimize --auto |
tuck undo surfaces snapshots grouped by kind so users can scan for the right roll-back point. Adding a new destructive command means adding a new SnapshotKind literal and a formatSnapshotKind case.
Retention — pruneSnapshotsByRetention runs after each new snapshot. Defaults from .tuckrc.json: snapshots.maxCount: 50, snapshots.maxAgeDays: 30. Either set to 0 disables that dimension.
Git-host integration is behind the GitProvider interface in src/lib/providers/types.ts. Four implementations today: github, gitlab, custom (Gitea / Bitbucket / SourceHut / self-hosted), local (no remote).
Interface surface (condensed):
interface GitProvider {
readonly mode: 'github' | 'gitlab' | 'local' | 'custom';
readonly displayName: string;
readonly cliName: string | null;
readonly requiresRemote: boolean;
// Detection & auth
isCliInstalled(): Promise<boolean>;
isAuthenticated(): Promise<boolean>;
getUser(): Promise<ProviderUser | null>;
detect(): Promise<ProviderDetection>;
// Repo ops
repoExists(repoName: string): Promise<boolean>;
createRepo(options: CreateRepoOptions): Promise<ProviderRepo>;
getRepoInfo(repoName: string): Promise<ProviderRepo | null>;
cloneRepo(repoName: string, targetDir: string): Promise<void>;
findDotfilesRepo(username?: string): Promise<string | null>;
// URL utilities
getPreferredRepoUrl(repo: ProviderRepo): Promise<string>;
validateUrl(url: string): boolean;
buildRepoUrl(username: string, repoName: string, protocol: 'ssh' | 'https'): string;
// Setup help
getSetupInstructions(): string;
getAltAuthInstructions(): string;
}The RemoteConfig record in .tuckrc.json is what loadConfig() hands off to getProvider(mode):
"remote": {
"mode": "custom",
"url": "https://gitea.stanley.cloud/me/dotfiles.git"
}Adding a new provider:
- Implement
GitProviderinsrc/lib/providers/<name>.ts. - Register it in
src/lib/providers/index.ts'sgetProvider()switch. - Add a
ProviderModeliteral insrc/lib/providers/types.ts. - Update
Git Providerswiki page + Command Reference for theremote.modevalue.
local provider is the "no remote" shape — every method no-ops or throws LocalModeError. Useful as a reference for providers that don't expose an API (e.g., a bare-git over SSH setup).
-
loadManifest()+loadConfig(). - Filter manifest by
defaultGroups(from local config) — files outside the host's groups are skipped. - Optional
preSynchook viahooks.ts. - For each tracked file: read system version, compare to repo copy by checksum.
- If changes →
createSnapshot(paths, 'tuck sync', { kind: 'sync' })before any write. - Write-back to repo (
files.copy), stage viagit.stageAll, commit viagit.commit. - Pull-rebase + push if
repository.autoPush—git.pull()uses--rebase --autostashto survive incidental working-tree dirt. - Optional
postSynchook.
-
loadManifest()+ filter by-g <group>. - Optional
preRestorehook. -
createSnapshot(paths, 'tuck restore', { kind: 'restore' })covering every destination. - For each tracked file: materialize onto the system via
files.copyorfiles.symlinkbased onstrategy. - Optional
postRestorehook. - If
--bootstrap→ chain intorunBootstrap(): loadbootstrap.toml, diff against.bootstrap-state.json, install deltas, save state.
- Parse
bootstrap.tomlviabootstrap/parser.ts→ validatedBootstrapConfig. - Load
.bootstrap-state.jsonviabootstrap/state.ts(empty on first run). - Resolve
bundle/-gfilters viabootstrap/resolver.ts→ ordered list of tools. - For each tool:
detect()runs the tool's detect command; if installed at the right version with matchingdefinitionHash, skip. - Otherwise run install command via
bootstrap/runner.ts, confirm via detect, update state. -
saveBootstrapState()at the end — one write per bootstrap run.
- Command Reference — every command + flags + examples
-
Configuration Reference — per-field walk of
.tuckrc.json+.tuckrc.local.json - Time Machine & Undo — snapshot-driven recovery recipes
-
Host Groups —
defaultGroups/readOnlyGroupssemantics - Git Providers — per-provider feature matrix
- CLAUDE.md + AGENTS.md — agent-facing conventions
{ "version": "2.0.0", "created": "2026-04-01T...", "updated": "2026-04-24T...", "machine": "kubuntu-desktop", "files": { "~/.zshrc": { "source": "~/.zshrc", "destination": "shell/zshrc", "category": "shell", "strategy": "copy", "encrypted": false, "added": "2026-04-01T...", "modified": "2026-04-24T...", "checksum": "sha256:…", "groups": ["kubuntu"] } } }