Compare commits
6 Commits
1a3aaf2dd9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df7df3d65c | ||
| d34a4f9363 | |||
| d9f476ff79 | |||
|
|
4b4b1df5db | ||
| 0cfa0d9b3c | |||
|
|
4b344621bc |
Submodule .claude/worktrees/agent-a27fe5f2 deleted from 0d60583c65
@@ -1,4 +1,4 @@
|
|||||||
# Handoff — notfiles (2026-07-07)
|
# Handoff — notfiles (2026-07-14)
|
||||||
|
|
||||||
**Branch:** main | **Build:** unknown | **Tests:** unknown
|
**Branch:** main | **Build:** unknown | **Tests:** unknown
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
## Log
|
## Log
|
||||||
|
|
||||||
- 20260606.135918: done=13 running=0 pending=0 blocked=0
|
- 20260606.135918: done=13 running=0 pending=0 blocked=0
|
||||||
|
- 2026-07-08: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
|
||||||
- 2026-04-17: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
|
- 2026-04-17: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
|
||||||
- 2026-04-15: Hardened bootstrap and hook-state failure handling, made notfiles FileStore usage real, and fixed copy-drift safety coverage
|
- 2026-04-15: Hardened bootstrap and hook-state failure handling, made notfiles FileStore usage real, and fixed copy-drift safety coverage
|
||||||
- 2026-04-15: Pruned completed items from handoff; GitHub issues plus doob are now the source of truth for active work
|
- 2026-04-15: Pruned completed items from handoff; GitHub issues plus doob are now the source of truth for active work
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
project: notfiles
|
project: notfiles
|
||||||
id: notfile
|
id: notfile
|
||||||
updated: 2026-07-08
|
updated: 2026-07-14
|
||||||
items:
|
items:
|
||||||
- id: uncommitted-work
|
- id: uncommitted-work
|
||||||
doob_uuid: null
|
doob_uuid: null
|
||||||
@@ -20,14 +20,12 @@ items:
|
|||||||
completed: null
|
completed: null
|
||||||
extra: []
|
extra: []
|
||||||
log:
|
log:
|
||||||
- date: 2026-07-08
|
|
||||||
summary: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
|
|
||||||
commits:
|
|
||||||
- 6427d18
|
|
||||||
- 25624c9
|
|
||||||
- date: '20260606.135918'
|
- date: '20260606.135918'
|
||||||
summary: done=13 running=0 pending=0 blocked=0
|
summary: done=13 running=0 pending=0 blocked=0
|
||||||
commits: []
|
commits: []
|
||||||
|
- date: 2026-07-08
|
||||||
|
summary: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
|
||||||
|
commits: []
|
||||||
- date: 2026-04-17
|
- date: 2026-04-17
|
||||||
summary: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
|
summary: Added notnet crate with YubikeySource and threaded Tailscale into notstrap; design spec complete
|
||||||
commits: []
|
commits: []
|
||||||
|
|||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -32,6 +32,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.112.2
|
||||||
- uses: taiki-e/install-action@v2
|
- uses: taiki-e/install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
.venv/
|
.venv/
|
||||||
.envrc
|
.envrc
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
.claude/worktrees/
|
||||||
.claude.local.md
|
.claude.local.md
|
||||||
docs/*
|
docs/*
|
||||||
!docs/superpowers/
|
!docs/superpowers/
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<h2>notfiles + Tailscale — Bootstrap Architecture</h2>
|
|
||||||
<p class="subtitle">How the crates fit together for ephemeral machine setup</p>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div style="background:#1a1a2e;border-radius:12px;padding:24px;font-family:monospace;font-size:13px;line-height:2;color:#e0e0e0;">
|
|
||||||
|
|
||||||
<div style="display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap;">
|
|
||||||
|
|
||||||
<!-- Fresh machine -->
|
|
||||||
<div style="border:2px solid #4a9eff;border-radius:8px;padding:16px;min-width:160px;background:#0d1b2a;">
|
|
||||||
<div style="color:#4a9eff;font-weight:bold;margin-bottom:8px;">New Machine</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">TS_AUTHKEY env var</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">YubiKey (PIV slot 9d)</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">→ interactive prompt</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="color:#888;font-size:20px;margin-top:28px;">→</div>
|
|
||||||
|
|
||||||
<!-- notstrap -->
|
|
||||||
<div style="border:2px solid #f0c040;border-radius:8px;padding:16px;min-width:160px;background:#1a1500;">
|
|
||||||
<div style="color:#f0c040;font-weight:bold;margin-bottom:8px;">notstrap</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">1. prereqs check</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">2. → <span style="color:#ff7eb3">notnet</span></div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">3. clone Gitea</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">4. → <span style="color:#7effa0">notsecrets</span></div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">5. link dotfiles</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">6. run hooks</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:12px;margin-top:0;">
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;">
|
|
||||||
<div style="color:#888;font-size:20px;">→</div>
|
|
||||||
<!-- notnet -->
|
|
||||||
<div style="border:2px solid #ff7eb3;border-radius:8px;padding:16px;min-width:160px;background:#1a0a14;">
|
|
||||||
<div style="color:#ff7eb3;font-weight:bold;margin-bottom:8px;">notnet <span style="color:#666;font-size:10px;">NEW</span></div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">detect existing Tailscale</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">install if missing</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">resolve auth key</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">join tailnet</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">verify peer reachable</div>
|
|
||||||
</div>
|
|
||||||
<div style="color:#888;font-size:20px;">→</div>
|
|
||||||
<div style="border:2px solid #7effa0;border-radius:8px;padding:12px;min-width:120px;background:#001a08;">
|
|
||||||
<div style="color:#7effa0;font-weight:bold;margin-bottom:6px;">Tailnet</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">minibox (VPS)</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">Gitea repo</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;">
|
|
||||||
<div style="color:#888;font-size:20px;">→</div>
|
|
||||||
<!-- notsecrets -->
|
|
||||||
<div style="border:2px solid #7effa0;border-radius:8px;padding:16px;min-width:160px;background:#001a08;">
|
|
||||||
<div style="color:#7effa0;font-weight:bold;margin-bottom:8px;">notsecrets <span style="color:#666;font-size:10px;">+YubiKey</span></div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">YubikeySource (PIV 9c)</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">BitwardenSource</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">FileSource</div>
|
|
||||||
<div style="color:#aaa;font-size:12px;">PromptSource</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #333;color:#888;font-size:11px;">
|
|
||||||
Already on Tailnet? notstrap detects active Tailscale → skips notnet entirely
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"reason":"idle timeout","timestamp":1776399095365}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{"type":"server-started","port":63245,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:63245","screen_dir":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/content","state_dir":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/state"}
|
|
||||||
{"type":"screen-added","file":"/Users/joe/dev/notfiles/.superpowers/brainstorm/72758-1776397235/content/architecture.html"}
|
|
||||||
{"type":"server-stopped","reason":"idle timeout"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
72766
|
|
||||||
32
CLAUDE.md
32
CLAUDE.md
@@ -1,5 +1,11 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
|
<preflight>
|
||||||
|
Read:
|
||||||
|
- @docs/DESIGN.md
|
||||||
|
- @.ctx/HANDOFF.md
|
||||||
|
</preflight>
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## What is notfiles?
|
## What is notfiles?
|
||||||
@@ -24,19 +30,19 @@ cargo fmt --check # check formatting
|
|||||||
|
|
||||||
This is a Cargo workspace with 7 crates under `crates/`:
|
This is a Cargo workspace with 7 crates under `crates/`:
|
||||||
|
|
||||||
| Crate | Purpose |
|
| Crate | Purpose |
|
||||||
| ------------ | -------------------------------------------------------------------------- |
|
| ------------ | ----------------------------------------------------------------------- |
|
||||||
| `notcore` | Shared types: `Config`, `NotfilesError`, `Reporter`, `LinkEvent`, |
|
| `notcore` | Shared types: `Config`, `NotfilesError`, `Reporter`, `LinkEvent`, |
|
||||||
| | `expand_tilde`, `suggest_package`, `HookPhase`, `HookSpec`, `Report` |
|
| | `expand_tilde`, `suggest_package`, `HookPhase`, `HookSpec`, `Report` |
|
||||||
| `notfiles` | Dotfiles linker — lib + `notfiles` binary. Subcommands: `init`, `link`, |
|
| `notfiles` | Dotfiles linker — lib + `notfiles` binary. Subcommands: `init`, `link`, |
|
||||||
| | `unlink`, `status`, `check`, `diff`, `adopt`, `completions` |
|
| | `unlink`, `status`, `check`, `diff`, `adopt`, `completions` |
|
||||||
| `notsecrets` | Multi-provider secret resolution (`SecretResolver`), age encryption/ |
|
| `notsecrets` | Multi-provider secret resolution (`SecretResolver`), age encryption/ |
|
||||||
| | decryption, identity management |
|
| | decryption, identity management |
|
||||||
| `nothooks` | Nushell hook runner with dot/setup phases and state persistence |
|
| `nothooks` | Nushell hook runner with dot/setup phases and state persistence |
|
||||||
| `notnet` | Network utilities — Tailscale integration, YubikeySource |
|
| `notnet` | Network utilities — Tailscale integration, YubikeySource |
|
||||||
| `notstrap` | New-machine bootstrap orchestrator — ties all crates together |
|
| `notstrap` | New-machine bootstrap orchestrator — ties all crates together |
|
||||||
| `notgraph` | Dependency/import graph for Rust files — HTML/MD/JSON/Mermaid output, |
|
| `notgraph` | Dependency/import graph for Rust files — HTML/MD/JSON/Mermaid output, |
|
||||||
| | heatmap, cycle detection |
|
| | heatmap, cycle detection |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -1084,7 +1084,6 @@ dependencies = [
|
|||||||
name = "notcore"
|
name = "notcore"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"dirs",
|
"dirs",
|
||||||
"serde",
|
"serde",
|
||||||
"strsim",
|
"strsim",
|
||||||
@@ -1114,9 +1113,7 @@ dependencies = [
|
|||||||
name = "notforge"
|
name = "notforge"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
|
||||||
"miette",
|
"miette",
|
||||||
"notcore",
|
"notcore",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1173,7 +1170,6 @@ dependencies = [
|
|||||||
"bech32",
|
"bech32",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dirs",
|
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac",
|
"hmac",
|
||||||
@@ -1188,7 +1184,6 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
"yubikey",
|
"yubikey",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
25
Cargo.toml
25
Cargo.toml
@@ -5,6 +5,7 @@ homepage = "https://github.com/89jobrien/notfiles"
|
|||||||
keywords = ["dotfiles", "symlink", "stow", "config"]
|
keywords = ["dotfiles", "symlink", "stow", "config"]
|
||||||
categories = ["command-line-utilities", "filesystem"]
|
categories = ["command-line-utilities", "filesystem"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
rust-version = "1.87.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -22,31 +23,33 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
assert_fs = "1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
globset = "0.4"
|
globset = "0.4"
|
||||||
|
miette = { version = "7", features = ["derive"] }
|
||||||
|
notcore = { path = "crates/notcore" }
|
||||||
|
notfiles = { path = "crates/notfiles" }
|
||||||
|
notforge = { path = "crates/notforge" }
|
||||||
|
nothooks = { path = "crates/nothooks" }
|
||||||
|
notnet = { path = "crates/notnet" }
|
||||||
|
notsecrets = { path = "crates/notsecrets" }
|
||||||
|
notstrap = { path = "crates/notstrap", version = "^0.1.0" } #unified
|
||||||
|
rpassword = "7"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
strsim = "0.11"
|
strsim = "0.11"
|
||||||
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
miette = { version = "7", features = ["derive"] }
|
|
||||||
ureq = { version = "2", features = ["json"] }
|
ureq = { version = "2", features = ["json"] }
|
||||||
which = "7"
|
which = "7"
|
||||||
rpassword = "7"
|
|
||||||
tempfile = "3"
|
|
||||||
assert_fs = "1"
|
|
||||||
notcore = { path = "crates/notcore" }
|
|
||||||
notfiles = { path = "crates/notfiles" }
|
|
||||||
notsecrets = { path = "crates/notsecrets" }
|
|
||||||
nothooks = { path = "crates/nothooks" }
|
|
||||||
notnet = { path = "crates/notnet" }
|
|
||||||
notforge = { path = "crates/notforge" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
24
HANDOFF.md
24
HANDOFF.md
@@ -1,24 +0,0 @@
|
|||||||
# HANDOFF.md
|
|
||||||
|
|
||||||
Legacy note as of 2026-04-15.
|
|
||||||
|
|
||||||
This repo no longer uses `HANDOFF.md` as a running history log.
|
|
||||||
|
|
||||||
Active workflow:
|
|
||||||
|
|
||||||
1. Sync GitHub issues into `doob`
|
|
||||||
2. Read short-lived handoff context from `.ctx/HANDOFF.*.*.yaml`
|
|
||||||
3. Update issue state in `doob`
|
|
||||||
4. Sync `doob` back to GitHub issues
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Handoff context should be brief and ephemeral
|
|
||||||
- Only open work should appear in handoff files
|
|
||||||
- Closed issues should be removed from handoff files after sync
|
|
||||||
- Historical status belongs in GitHub issues and git history, not here
|
|
||||||
|
|
||||||
Current state:
|
|
||||||
|
|
||||||
- No open handoff items are tracked locally
|
|
||||||
- Use GitHub issues plus `doob` as the task source of truth
|
|
||||||
17
README.md
17
README.md
@@ -265,6 +265,23 @@ Migration is gradual — shell scripts are replaced as Rust equivalents ship:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Absorbing `pj`
|
||||||
|
|
||||||
|
`pj` (89jobrien/pj) was a standalone portable dev-environment bootstrap CLI. Its functionality overlaps directly with this workspace:
|
||||||
|
|
||||||
|
| `pj` command | `notfiles` equivalent |
|
||||||
|
|---|---|
|
||||||
|
| `pj dot` | `notfiles` (symlink engine) |
|
||||||
|
| `pj doctor` | `notstrap` prerequisites check |
|
||||||
|
| `pj sync` | `notstrap run` |
|
||||||
|
| `pj secret` | `notsecrets` |
|
||||||
|
| `pj cache` | planned `notcache` or Cargo workspace tooling |
|
||||||
|
| `pj tui` | planned TUI layer on `notstrap` |
|
||||||
|
|
||||||
|
`pj` is archived. Its remaining functionality (cache management, TUI dashboard, git-config helper) will be folded in as those areas of the workspace mature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
93
cliff.toml
Normal file
93
cliff.toml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# git-cliff ~ configuration file
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
|
||||||
|
[remote.github]
|
||||||
|
owner = "89jobrien"
|
||||||
|
repo = "notfiles"
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## Unreleased
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\
|
||||||
|
{{ commit.message | split(pat="\n") | first | upper_first | trim_end }}\
|
||||||
|
{% if commit.links %} \
|
||||||
|
({% for link in commit.links %}[#{{ link.text }}]({{ link.href }}){% if not loop.last %}, {% endif %}{% endfor %})\
|
||||||
|
{% endif %}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
|
||||||
|
footer = """
|
||||||
|
{%- macro remote_url() -%}
|
||||||
|
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{% for release in releases -%}
|
||||||
|
{% if release.version -%}
|
||||||
|
{% if release.previous.version -%}
|
||||||
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
|
{{ self::remote_url() }}/compare/{{ release.previous.version }}...{{ release.version }}
|
||||||
|
{% endif -%}
|
||||||
|
{% else -%}
|
||||||
|
[unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}...HEAD
|
||||||
|
{% endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
|
||||||
|
trim = true
|
||||||
|
render_always = true
|
||||||
|
output = "CHANGELOG.md"
|
||||||
|
|
||||||
|
postprocessors = [
|
||||||
|
{ pattern = '<REPO>', replace = "https://github.com/89jobrien/notfiles" },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = true
|
||||||
|
require_conventional = false
|
||||||
|
split_commits = false
|
||||||
|
protect_breaking_commits = false
|
||||||
|
filter_commits = false
|
||||||
|
fail_on_unmatched_commit = false
|
||||||
|
use_branch_tags = false
|
||||||
|
topo_order = false
|
||||||
|
topo_order_commits = true
|
||||||
|
sort_commits = "oldest"
|
||||||
|
recurse_submodules = false
|
||||||
|
|
||||||
|
commit_preprocessors = [
|
||||||
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))" },
|
||||||
|
]
|
||||||
|
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Features" },
|
||||||
|
{ message = "^fix", group = "Bug Fixes" },
|
||||||
|
{ message = "^refactor", group = "Refactor" },
|
||||||
|
{ message = "^doc", group = "Documentation" },
|
||||||
|
{ message = "^perf", group = "Performance" },
|
||||||
|
{ message = "^style", group = "Styling" },
|
||||||
|
{ message = "^test", group = "Testing" },
|
||||||
|
{ message = "^chore\\(release\\):.*", skip = true },
|
||||||
|
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||||
|
{ message = "^chore\\(pr\\)", skip = true },
|
||||||
|
{ message = "^chore\\(pull\\)", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "Miscellaneous" },
|
||||||
|
{ body = ".*security", group = "Security" },
|
||||||
|
{ message = "^revert", group = "Reverts" },
|
||||||
|
{ message = ".*", group = "Other" },
|
||||||
|
]
|
||||||
|
|
||||||
|
link_parsers = [
|
||||||
|
{ pattern = "#(\\d+)", href = "https://github.com/89jobrien/notfiles/issues/$1", text = "$1" },
|
||||||
|
]
|
||||||
@@ -10,7 +10,6 @@ categories.workspace = true
|
|||||||
description = "Shared types and config for the notfiles dotfiles manager"
|
description = "Shared types and config for the notfiles dotfiles manager"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
strsim = { workspace = true }
|
strsim = { workspace = true }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::NotfilesError;
|
use crate::NotfilesError;
|
||||||
|
|
||||||
@@ -102,13 +102,28 @@ where
|
|||||||
toml::from_str(&content).map_err(|err| parse_error(path, err))
|
toml::from_str(&content).map_err(|err| parse_error(path, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the default config path: `~/.config/notfiles/notfiles.toml`.
|
||||||
|
pub fn default_config_path() -> PathBuf {
|
||||||
|
dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("notfiles")
|
||||||
|
.join("notfiles.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default dotfiles directory: `~/.notfiles`.
|
||||||
|
pub fn default_dotfiles_dir() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~"))
|
||||||
|
.join(".notfiles")
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
/// Load config from an explicit path. Returns `Config::default()` if the file does not exist.
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
pub fn load_from(config_path: &Path) -> Result<Self, NotfilesError> {
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
return Ok(Config::default());
|
return Ok(Config::default());
|
||||||
}
|
}
|
||||||
let content = std::fs::read_to_string(&config_path).map_err(|e| {
|
let content = std::fs::read_to_string(config_path).map_err(|e| {
|
||||||
NotfilesError::Config(format!("reading {}: {e}", config_path.display()))
|
NotfilesError::Config(format!("reading {}: {e}", config_path.display()))
|
||||||
})?;
|
})?;
|
||||||
let config: Config = toml::from_str(&content).map_err(|e| {
|
let config: Config = toml::from_str(&content).map_err(|e| {
|
||||||
@@ -117,6 +132,12 @@ impl Config {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load config from `<dotfiles_dir>/notfiles.toml` (legacy location).
|
||||||
|
/// Prefer [`Config::load_from`] with an explicit path in new call sites.
|
||||||
|
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
||||||
|
Self::load_from(&dotfiles_dir.join("notfiles.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn method_for(&self, package: &str) -> Method {
|
pub fn method_for(&self, package: &str) -> Method {
|
||||||
self.packages
|
self.packages
|
||||||
.get(package)
|
.get(package)
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ use std::path::PathBuf;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "notfiles", about = "A modern dotfiles manager")]
|
#[command(name = "notfiles", about = "A modern dotfiles manager")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Path to the dotfiles directory (default: current directory)
|
/// Path to the dotfiles directory (default: ~/.notfiles)
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
pub dir: Option<PathBuf>,
|
pub dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Path to the config file (default: ~/.config/notfiles/notfiles.toml)
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
/// Show what would be done without making changes
|
/// Show what would be done without making changes
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
|||||||
@@ -15,10 +15,26 @@ fn main() -> Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let dotfiles_dir = cli
|
let dotfiles_dir = cli
|
||||||
.dir
|
.dir
|
||||||
.unwrap_or_else(|| std::env::current_dir().expect("cannot determine current directory"));
|
.unwrap_or_else(notcore::config::default_dotfiles_dir);
|
||||||
|
|
||||||
|
// Init is the only command allowed when the dotfiles dir doesn't exist yet.
|
||||||
|
if matches!(cli.command, Command::Init) {
|
||||||
|
return cmd_init(&dotfiles_dir, cli.config.as_deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dotfiles_dir.exists() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"dotfiles directory not found: {}\nRun `notfiles init` to create it.",
|
||||||
|
dotfiles_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
let dotfiles_dir = fs::canonicalize(&dotfiles_dir)
|
let dotfiles_dir = fs::canonicalize(&dotfiles_dir)
|
||||||
.with_context(|| format!("dotfiles directory not found: {}", dotfiles_dir.display()))?;
|
.with_context(|| format!("dotfiles directory not found: {}", dotfiles_dir.display()))?;
|
||||||
|
|
||||||
|
let config_path = cli
|
||||||
|
.config
|
||||||
|
.unwrap_or_else(notcore::config::default_config_path);
|
||||||
|
|
||||||
let reporter: Box<dyn Reporter> = if cli.json {
|
let reporter: Box<dyn Reporter> = if cli.json {
|
||||||
Box::new(JsonReporter)
|
Box::new(JsonReporter)
|
||||||
} else {
|
} else {
|
||||||
@@ -37,14 +53,14 @@ fn main() -> Result<()> {
|
|||||||
detect::print_detected(&managers);
|
detect::print_detected(&managers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Init => cmd_init(&dotfiles_dir)?,
|
Command::Init => unreachable!("handled above"),
|
||||||
Command::Link {
|
Command::Link {
|
||||||
force,
|
force,
|
||||||
no_backup,
|
no_backup,
|
||||||
packages,
|
packages,
|
||||||
} => {
|
} => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -89,7 +105,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Unlink { packages } => {
|
Command::Unlink { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
let _ = &config; // loaded but not needed for unlink
|
let _ = &config; // loaded but not needed for unlink
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = if packages.is_empty() {
|
let pkgs = if packages.is_empty() {
|
||||||
@@ -145,7 +161,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Check => {
|
Command::Check => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let all = resolve_packages_filtered_with_store(&dotfiles_dir, &[], &config, fs)?;
|
let all = resolve_packages_filtered_with_store(&dotfiles_dir, &[], &config, fs)?;
|
||||||
|
|
||||||
@@ -171,7 +187,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Diff { packages } => {
|
Command::Diff { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let state = State::load(&dotfiles_dir, fs)?;
|
let state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -183,7 +199,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Adopt { package, files } => {
|
Command::Adopt { package, files } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let opts = LinkOptions {
|
let opts = LinkOptions {
|
||||||
force: false,
|
force: false,
|
||||||
@@ -217,7 +233,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Status { packages } => {
|
Command::Status { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let state = State::load(&dotfiles_dir, fs)?;
|
let state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -236,13 +252,29 @@ fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_init(dotfiles_dir: &std::path::Path) -> Result<()> {
|
fn cmd_init(
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
dotfiles_dir: &std::path::Path,
|
||||||
|
config_override: Option<&std::path::Path>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !dotfiles_dir.exists() {
|
||||||
|
fs::create_dir_all(dotfiles_dir)
|
||||||
|
.with_context(|| format!("creating dotfiles dir: {}", dotfiles_dir.display()))?;
|
||||||
|
println!("Created {}", dotfiles_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = config_override
|
||||||
|
.map(std::path::Path::to_path_buf)
|
||||||
|
.unwrap_or_else(notcore::config::default_config_path);
|
||||||
|
|
||||||
if config_path.exists() {
|
if config_path.exists() {
|
||||||
println!("notfiles.toml already exists.");
|
println!("{} already exists.", config_path.display());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("creating config dir: {}", parent.display()))?;
|
||||||
|
}
|
||||||
fs::write(&config_path, notcore::config::starter_toml())?;
|
fs::write(&config_path, notcore::config::starter_toml())?;
|
||||||
println!("Created notfiles.toml");
|
println!("Created {}", config_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ fn notfiles_bin() -> std::path::PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(dotfiles: &Path, args: &[&str]) -> (String, String, bool) {
|
fn run(dotfiles: &Path, args: &[&str]) -> (String, String, bool) {
|
||||||
|
let config = dotfiles.join("notfiles.toml");
|
||||||
let output = Command::new(notfiles_bin())
|
let output = Command::new(notfiles_bin())
|
||||||
.arg("--dir")
|
.arg("--dir")
|
||||||
.arg(dotfiles)
|
.arg(dotfiles)
|
||||||
|
.arg("--config")
|
||||||
|
.arg(&config)
|
||||||
.args(args)
|
.args(args)
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to run notfiles");
|
.expect("failed to run notfiles");
|
||||||
@@ -61,7 +64,7 @@ fn test_init_creates_config() {
|
|||||||
|
|
||||||
let (stdout, _, ok) = run(&dotfiles, &["init"]);
|
let (stdout, _, ok) = run(&dotfiles, &["init"]);
|
||||||
assert!(ok);
|
assert!(ok);
|
||||||
assert!(stdout.contains("Created notfiles.toml"));
|
assert!(stdout.contains("Created"), "expected Created in: {stdout}");
|
||||||
assert!(dotfiles.join("notfiles.toml").exists());
|
assert!(dotfiles.join("notfiles.toml").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ name = "notforge"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
clap = { workspace = true }
|
|
||||||
miette = { workspace = true }
|
miette = { workspace = true }
|
||||||
notcore = { workspace = true }
|
notcore = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ base64 = "0.22"
|
|||||||
bech32 = "0.11"
|
bech32 = "0.11"
|
||||||
chacha20poly1305 = "0.10"
|
chacha20poly1305 = "0.10"
|
||||||
curve25519-dalek = "4"
|
curve25519-dalek = "4"
|
||||||
dirs = { workspace = true }
|
|
||||||
ed25519-dalek = "2"
|
ed25519-dalek = "2"
|
||||||
hkdf = "0.12"
|
hkdf = "0.12"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
@@ -24,7 +23,6 @@ thiserror = { workspace = true }
|
|||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||||
yubikey = { version = "0.8", optional = true }
|
yubikey = { version = "0.8", optional = true }
|
||||||
zeroize = "1"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
18
preflight.md
18
preflight.md
@@ -3,7 +3,7 @@
|
|||||||
A `rust-script` preflight that surfaces repo context in one shot: shell detection, git state,
|
A `rust-script` preflight that surfaces repo context in one shot: shell detection, git state,
|
||||||
tracked-file tree, and HANDOFF document read/update.
|
tracked-file tree, and HANDOFF document read/update.
|
||||||
|
|
||||||
```rust
|
````rust
|
||||||
#!/usr/bin/env rust-script
|
#!/usr/bin/env rust-script
|
||||||
//! ```cargo
|
//! ```cargo
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
@@ -238,7 +238,7 @@ fn main() -> Result<()> {
|
|||||||
println!("\n{}", "╚══ done ══╝".bold().yellow());
|
println!("\n{}", "╚══ done ══╝".bold().yellow());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
````
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -253,13 +253,13 @@ rust-script preflight.rs
|
|||||||
|
|
||||||
## What it surfaces
|
## What it surfaces
|
||||||
|
|
||||||
| Section | Details |
|
| Section | Details |
|
||||||
|---------|---------|
|
| ----------------- | ------------------------------------------------------------------------------------------------------ |
|
||||||
| Environment | Current shell (`$SHELL`), cwd, git root |
|
| Environment | Current shell (`$SHELL`), cwd, git root |
|
||||||
| Git status | Branch name, short status (clean or dirty file list) |
|
| Git status | Branch name, short status (clean or dirty file list) |
|
||||||
| Git history | Last 7 commits, one-line with decoration |
|
| Git history | Last 7 commits, one-line with decoration |
|
||||||
| Tracked file tree | `git ls-files` output rendered as an indented tree, depth ≤ 3 |
|
| Tracked file tree | `git ls-files` output rendered as an indented tree, depth ≤ 3 |
|
||||||
| HANDOFF docs | Full content (first 40 lines) of any `HANDOFF.*` file at repo root; stamps `last_preflight:` timestamp |
|
| HANDOFF docs | Full content (first 40 lines) of any `HANDOFF.*` file at repo root; stamps `last_preflight:` timestamp |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "integration"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
publish = true
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "bootstrap"
|
name = "bootstrap"
|
||||||
@@ -16,9 +16,9 @@ path = "tests/cross_crate.rs"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
assert_fs = { workspace = true }
|
assert_fs = { workspace = true }
|
||||||
notcore = { path = "../../crates/notcore" }
|
notcore = { workspace = true } #unified
|
||||||
notfiles = { path = "../../crates/notfiles" }
|
notfiles = { workspace = true } #unified
|
||||||
nothooks = { path = "../../crates/nothooks" }
|
nothooks = { workspace = true } #unified
|
||||||
notsecrets = { path = "../../crates/notsecrets" }
|
notsecrets = { workspace = true } #unified
|
||||||
notstrap = { path = "../../crates/notstrap" }
|
notstrap = { workspace = true } #unified
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|||||||
Reference in New Issue
Block a user