6 Commits

Author SHA1 Message Date
Joseph OBrien
df7df3d65c feat(cli): default dotfiles dir to ~/.notfiles, config to ~/.config/notfiles/notfiles.toml
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
- Add `default_dotfiles_dir()` (~/.notfiles) and `default_config_path()`
  (~/.config/notfiles/notfiles.toml) to notcore::config
- Add `Config::load_from(path)` for explicit config path; keep `Config::load(dir)`
  as the legacy/library variant (config inside dotfiles dir)
- CLI resolves config via XDG default or --config override; library functions
  stay legacy so the public API is unaffected
- `notfiles init` creates ~/.notfiles and ~/.config/notfiles/notfiles.toml
- Add --config global flag to override the config path
- Update integration tests to pass --config explicitly so they are isolated
  from any real ~/.config/notfiles/notfiles.toml on the test machine
2026-07-14 10:42:35 -04:00
d34a4f9363 chore(repo): add git-cliff config, sort deps, add rust-version
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
- Add cliff.toml with GitHub remote wiring, issue link parsers, and
  clean group names (no sort prefixes)
- Add rust-version = "1.87.0" and codegen-units = 1 to workspace
- Sort workspace.dependencies alphabetically; add notstrap path entry
- Add CLAUDE.md preflight block; reformat workspace table to 100-col
- Delete stale brainstorm artifacts and root HANDOFF.md
2026-07-14 10:14:43 -04:00
d9f476ff79 chore(repo): remove nested agent worktree marker
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
2026-07-11 23:53:42 -04:00
Joseph O'Brien
4b4b1df5db Merge pull request #20 from 89jobrien/chore/sort-cargo-manifests
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
chore(manifests): sort Cargo.toml dependencies
2026-07-11 23:49:24 -04:00
0cfa0d9b3c fix(ci): install nushell for integration tests 2026-07-11 23:44:04 -04:00
Joseph O'Brien
4b344621bc docs: note absorption of pj into notfiles workspace 2026-04-14 08:54:51 -04:00
24 changed files with 246 additions and 175 deletions

Submodule .claude/worktrees/agent-a27fe5f2 deleted from 0d60583c65

View File

@@ -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

View File

@@ -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: []

View File

@@ -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
View File

@@ -5,6 +5,7 @@
.venv/ .venv/
.envrc .envrc
.worktrees/ .worktrees/
.claude/worktrees/
.claude.local.md .claude.local.md
docs/* docs/*
!docs/superpowers/ !docs/superpowers/

View File

@@ -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>

View File

@@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1776399095365}

View File

@@ -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"}

View File

@@ -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?
@@ -25,7 +31,7 @@ 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`, |

5
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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" },
]

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(())
} }

View File

@@ -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());
} }

View File

@@ -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 }

View File

@@ -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 = []

View File

@@ -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
@@ -254,7 +254,7 @@ 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 |

View File

@@ -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 }