Skip to content

feat: config module + dennoh init / config CLI (T1) - #2

Merged
goofmint merged 4 commits into
mainfrom
phase-1
Jun 13, 2026
Merged

feat: config module + dennoh init / config CLI (T1)#2
goofmint merged 4 commits into
mainfrom
phase-1

Conversation

@goofmint

@goofmint goofmint commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implements T1 (Configuration & Initialization) from .tmp/tasks.md:

  • Config persistence at ~/Library/Application Support/dennoh/config.json
  • dennoh init interactive setup (vault + .dennoh/ + git init + .gitignore + config)
  • dennoh config get / set / list with DENNOH_LANG env override

All five subtasks (T1.1〜T1.5) are checked off in .tmp/tasks.md.

Architecture

Config module (src/config/index.ts)

  • Lang = 'ja' | 'en', Config = { vaultPath, lang }, DEFAULT_CONFIG
  • readConfig validates JSON shape and rejects unknown lang or non-string vaultPath
  • writeConfig creates the parent directory recursively
  • resolveLang priority: DENNOH_LANG env > config file > default 'ja'

CLI shell (src/cli/)

  • types.ts: CliIO interface so handlers don't touch process.stdout / process.exit directly
  • main.ts: argv routing, gated by import.meta.main so tests can import main safely
  • commands/config.ts: configGet / configSet / configList / configCommand
  • commands/init.ts: composable steps (expandTilde, resolveVaultPath, ensureVaultDirs, initGitRepo, updateGitignore, detectCloudSync, formatCloudWarning) + initCommand that sequences them with an injectable prompt

Why dependency injection

  • configCommand(args, io) returns an exit code instead of calling process.exit so tests can capture stdout/stderr without subprocess overhead
  • initCommand({ io, promptVaultPath }) lets tests skip interactive input entirely; production wires up defaultPromptVaultPath (node:readline/promises)

Cloud sync detection

detectCloudSync recognizes:

  • ~/Library/Mobile Documents/* (iCloud Drive)
  • ~/Dropbox/*
  • ~/OneDrive/*
  • ~/Library/CloudStorage/OneDrive*/*

For each, formatCloudWarning writes a service-specific exclusion tip (.nosync / .dropboxignore / OneDrive client) to stderr — informational, never blocking.

Test coverage

55 → 92 tests total (+37 new):

  • tests/config.test.ts (10): round-trip, recursive mkdir, every read-error path, env precedence
  • tests/cli/config.test.ts (18): set→get round-trip, list formatting + env annotation, every error path, routing
  • tests/cli/init.test.ts (17): full happy path, ~ expansion, whitespace trim, existing .git preservation, .gitignore append / dedupe / bare-form, four cloud paths + non-cloud baseline, validation failures

All tests redirect os.homedir() to a temp directory via spyOn, so the developer's real ~/Library/Application Support/dennoh/config.json is never touched.

Test plan

  • bun install
  • bun run lint — clean
  • bun run typecheck — clean
  • bun run test — 92 pass
  • bun run build — produces dist/cli.js
  • bun dist/cli.js --version0.0.0
  • echo /tmp/dennoh-vault | bun dist/cli.js init creates vault + .dennoh/ + .git/ + .gitignore
  • bun dist/cli.js config list shows the new config
  • DENNOH_LANG=en bun dist/cli.js config list annotates lang=en (from DENNOH_LANG)
  • Cleanup: rm -rf /tmp/dennoh-vault ~/Library/Application\ Support/dennoh/config.json

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • CLI に vault 初期化(init)と設定管理(config)を追加。初期化時の環境チェック・.git 管理・クラウド同期警告、設定の get/set/list をサポート。エントリーポイントでヘルプ/バージョン出力を追加。
  • Config

    • 永続化設定と言語解決を導入(環境変数優先の言語判定、読み書きと検証を実装)。
  • Tests

    • init/config 周りの包括的な自動テストを追加。
  • Chores

    • タスクチェックリストを完了に更新。

goofmint and others added 2 commits June 13, 2026 09:19
- Lang union ('ja' | 'en'), Config interface, DEFAULT_CONFIG export
- readConfig / writeConfig: ~/Library/Application Support/dennoh/config.json,
  recursive mkdir on write, schema validation on read (object shape,
  vaultPath: string, lang in {ja,en})
- resolveLang: DENNOH_LANG env > config file lang > DEFAULT_CONFIG.lang
- tests/config.test.ts: 10 cases covering round-trip, dir creation,
  missing-file/invalid-lang/invalid-vaultPath/non-object errors, env
  precedence, invalid env fallback, missing config fallback, full
  priority chain
- tests/smoke.test.ts: include @/config alongside other subpackages

Tests use spyOn(os, "homedir") to redirect to a temp dir, so real
user config is never touched during tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLI infrastructure
- src/cli/types.ts: CliIO interface (stdout/stderr writers)
- src/cli/main.ts: argv routing for --help / --version / init /
  config / serve, async, gated by import.meta.main so tests can
  import without exiting

config commands (T1.3)
- configGet: reads vaultPath / lang from disk; lang respects
  DENNOH_LANG env via resolveLang
- configSet: validates lang in {ja,en} and rejects empty vaultPath
  before writing; requires existing config (caller must run init)
- configList: emits key=value lines; annotates lang with
  "(from DENNOH_LANG)" when the env override is active
- exit 1 + stderr message for unknown key / invalid value /
  missing config / usage errors

init command (T1.4)
- Composable steps: expandTilde, resolveVaultPath, ensureVaultDirs,
  initGitRepo, updateGitignore, detectCloudSync, formatCloudWarning
- Prompt fn is injected (defaultPromptVaultPath uses node:readline/
  promises) so tests can drive it deterministically
- mkdir -p vault + vault/.dennoh; skip git init when .git/ exists;
  append .dennoh/ to .gitignore without duplicating .dennoh /
  .dennoh/ / .dennoh/*; write { vaultPath, lang: "ja" } config
- Cloud sync detection for iCloud Drive, Dropbox, ~/OneDrive, and
  ~/Library/CloudStorage/OneDrive*; service-specific advisory text
  goes to stderr without blocking init

Tests (37 new cases)
- tests/cli/config.test.ts: set->get round-trip, list formatting,
  env annotation, every error path
- tests/cli/init.test.ts: full happy path, ~ expansion, whitespace
  trim, pre-existing .git preservation, .gitignore append /
  dedupe / bare-form recognition, four cloud paths + non-cloud
  baseline, validation failures

.tmp/tasks.md: mark T1.1-T1.5 as done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b1184de3-2af0-4bdf-b7dd-2f8929168b5d

📥 Commits

Reviewing files that changed from the base of the PR and between b8606da and 558adcd.

📒 Files selected for processing (1)
  • src/cli/commands/config.ts

📝 Walkthrough

Walkthrough

このPRはCLIにvault初期化(init)と設定管理(config)機能、設定の永続化と言語解決、CLIエントリポイント、およびそれらを検証するテスト群を追加します。

Changes

CLI と設定管理の統合実装

Layer / File(s) Summary
設定管理の基礎実装
src/config/index.ts, tests/config.test.ts
Config型定義、JSON形式の読み書き(readConfig/writeConfig)、DENNOH_LANG環境変数優先の言語解決(resolveLang)。設定ディレクトリは ~/.dennoh 配下。
CLI基盤と公開API
src/cli/types.ts, src/cli/index.ts
CliIO型で stdout/stderr コールバック関数を定義。CLI各モジュールの型・関数を再エクスポート。
Config コマンド実装
src/cli/commands/config.ts, tests/cli/config.test.ts
configGet/configSet/configList サブコマンド。langja/en のみ許可、vaultPath は空文字拒否。DENNOH_LANG を優先参照し、read/write のエラーと終了コードを制御。
Init コマンド実装
src/cli/commands/init.ts, tests/cli/init.test.ts
ボルト初期化フロー:プロンプト→~ 展開・trim→絶対パス解決→ディレクトリ作成→.git 初期化(既存は保持)→.gitignore 更新→クラウド同期検出・警告→writeConfig
CLI メインエントリポイント
src/cli/main.ts
--help/-h--version/-vinit/config/serve コマンド分岐。未知コマンド/serve は未実装エラー。Bun 実行時ブートストラップを含む。
タスク・スモークテスト更新
.tmp/tasks.md, tests/smoke.test.ts
T1.1〜T1.5 設定・初期化タスク完了マーク。@/config モジュールをスモーク読み込みに追加。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • goofmint/dennoh#1: スキャフォールドPR。tests/smoke.test.ts の初期スモークテスト追加に関連。

Poem

🐰 ボルト掘って、設定詰めて
~ 展開で道は真っすぐに
ja/en 決めて、ファイル書き込むよ
クラウド見つけたら「気をつけて」と一声
テストで跳ねて、安心で乾杯!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRのタイトルは、設定モジュール + CLI (init/config) コマンドの実装という、変更セットの主要な内容を正確に要約している。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-1

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/config.ts`:
- Around line 61-63: writeConfig(next) can throw and currently escapes the CLI's
exit-code contract; wrap the call to writeConfig(next) inside a try/catch (for
both places where next.lang = value; writeConfig(next); return 0; appears) and
on error write a clear message to stderr (e.g., console.error or
process.stderr.write) and return 1 so the CLI exits with a consistent non-zero
code instead of bubbling an uncaught exception.
- Around line 15-23: When handling the "lang" branch, don't force readConfig()
before honoring the DENNOH_LANG environment variable; instead, first check
whether process.env.DENNOH_LANG is present and valid (or call a helper that
validates it) and if so skip readConfig(), write resolveLang() to stdout and
return 0; only call readConfig() (and handle its error with io.stderr + return
1) when the env var is absent or invalid. Update the key === "lang" block to
prefer the env var, then fall back to readConfig() and finally
io.stdout(`${resolveLang()}\n`) with the same exit codes.

In `@src/cli/commands/init.ts`:
- Around line 50-53: The .git existence check uses fs.existsSync(gitDir) which
returns true for files as well, causing initialization to be skipped
incorrectly; change the check to verify that gitDir is a directory (use
fs.lstatSync or fs.statSync and call isDirectory()) before treating it as an
existing repo so the early return that logs "Using existing git repository at
${vaultPath}." only runs when gitDir is actually a directory.
- Around line 59-62: The error construction using implicit fallback must be
replaced with an explicit branch: in the git init failure handling (the block
that checks result.exitCode in src/cli/commands/init.ts), call new
TextDecoder().decode(result.stderr), assign to a variable (e.g., stderr),
compute trimmed = stderr.trim(), then if trimmed.length > 0 use that in the
thrown Error message and otherwise use `exit code ${result.exitCode}` (or throw
a distinct error when both are absent); do this inside the same failure branch
that currently references result to keep behavior unchanged.

In `@tests/cli/init.test.ts`:
- Around line 154-184: The cloud-warning tests only assert stderr text but not
the process exit status; update each test that calls initCommand({ io, ... })
(e.g., the tests using makeIO, stderr(), and initCommand) to capture the
returned result from initCommand and assert the exit code is 0 (e.g., const
result = await initCommand({...}); expect(result.code).toBe(0)); keep the
existing stderr() assertions as-is while adding the code === 0 assertion in each
cloud-related test.

In `@tests/config.test.ts`:
- Around line 25-29: Extract the environment restore logic into a shared helper
(e.g., restoreDennohLang) and call that from both tests instead of leaving an if
in each test; the helper should accept the captured originalLang and perform the
proper action on process.env.DENNOH_LANG (delete when originalLang is undefined,
otherwise restore the value), replacing the inline if block that references
originalLang and process.env.DENNOH_LANG in the tests.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ec32a991-4f0d-422c-803d-a3daebca89f3

📥 Commits

Reviewing files that changed from the base of the PR and between 2e6368e and 5a2e09b.

📒 Files selected for processing (11)
  • .tmp/tasks.md
  • src/cli/commands/config.ts
  • src/cli/commands/init.ts
  • src/cli/index.ts
  • src/cli/main.ts
  • src/cli/types.ts
  • src/config/index.ts
  • tests/cli/config.test.ts
  • tests/cli/init.test.ts
  • tests/config.test.ts
  • tests/smoke.test.ts

Comment thread src/cli/commands/config.ts
Comment thread src/cli/commands/config.ts
Comment thread src/cli/commands/init.ts Outdated
Comment thread src/cli/commands/init.ts
Comment thread tests/cli/init.test.ts
Comment thread tests/config.test.ts Outdated
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 4 file(s) based on 6 unresolved review comments.

Files modified:

  • src/cli/commands/config.ts
  • src/cli/commands/init.ts
  • tests/cli/init.test.ts
  • tests/config.test.ts

Commit: b8606da393af210d03f4990a90d5e17519f9915a

The changes have been pushed to the phase-1 branch.

Time taken: 4m 9s

Fixed 4 file(s) based on 6 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/commands/config.ts (1)

15-29: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

lang 取得ロジックが冗長で、設定を二重に読み込む可能性があります。

現在のコード:

  • 16-20行目: envLang が有効な場合、resolveLang() を呼び出していますが、resolveLang() 内で envLang を再度検証するため冗長です。
  • 21-28行目: envLang が無効な場合、22行目で readConfig() を呼び出した後、27行目で resolveLang() を呼び出すと、resolveLang() 内で再度 readConfig() が実行されます。

より明確で効率的な実装:

♻️ 推奨リファクタリング
   if (key === "lang") {
     const envLang = process.env.DENNOH_LANG;
     if (envLang === "ja" || envLang === "en") {
-      io.stdout(`${resolveLang()}\n`);
+      io.stdout(`${envLang}\n`);
       return 0;
     }
     try {
-      readConfig();
+      const cfg = readConfig();
+      io.stdout(`${cfg.lang}\n`);
+      return 0;
     } catch (e) {
       io.stderr(`${readError(e)}\n`);
       return 1;
     }
-    io.stdout(`${resolveLang()}\n`);
-    return 0;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/config.ts` around lines 15 - 29, When handling the "lang"
key, avoid calling resolveLang() twice and prevent double-reading config: if
process.env.DENNOH_LANG is "ja" or "en", write that value directly to io.stdout
and return; otherwise call readConfig() once inside the try/catch and pass the
loaded config into resolveLang (or add an overload/variant of resolveLang that
accepts the config) so resolveLang does not call readConfig() again; ensure you
still catch and log readConfig() errors via readError(e) and return the proper
exit code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/cli/commands/config.ts`:
- Around line 15-29: When handling the "lang" key, avoid calling resolveLang()
twice and prevent double-reading config: if process.env.DENNOH_LANG is "ja" or
"en", write that value directly to io.stdout and return; otherwise call
readConfig() once inside the try/catch and pass the loaded config into
resolveLang (or add an overload/variant of resolveLang that accepts the config)
so resolveLang does not call readConfig() again; ensure you still catch and log
readConfig() errors via readError(e) and return the proper exit code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 04a33e85-2caa-4f1c-a365-c4b18b33c44d

📥 Commits

Reviewing files that changed from the base of the PR and between 5a2e09b and b8606da.

📒 Files selected for processing (4)
  • src/cli/commands/config.ts
  • src/cli/commands/init.ts
  • tests/cli/init.test.ts
  • tests/config.test.ts

When DENNOH_LANG is invalid the old path called readConfig() to verify
existence, then resolveLang() — which calls readConfig() again. Read once
inside the try/catch and use the loaded config directly. Drop the now-unused
resolveLang import.

When DENNOH_LANG is valid, output it directly instead of going through
resolveLang (which only re-evaluates the same env var).

Behavior is identical; existing tests cover both branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant