diff --git a/AGENTS.md b/AGENTS.md index 2ee7643..8bbe20c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,6 +224,19 @@ make release # GoReleaser dry-run make install # Install to GOPATH/bin ``` +### CLI Commands + +| Command | Purpose | +|---------|---------| +| `replicator init` | Per-repo setup: creates `.hive/` with empty `cells.json` | +| `replicator setup` | Per-machine setup: creates `~/.config/swarm-tools/` + SQLite DB | +| `replicator serve` | Start MCP JSON-RPC server on stdio | +| `replicator cells` | List hive cells (work items) | +| `replicator doctor` | Check environment health | +| `replicator stats` | Display activity summary | +| `replicator query` | Run preset SQL analytics queries | +| `replicator version` | Print version, commit, build date | + ## Project Structure ``` diff --git a/README.md b/README.md index ad9a1fa..aabf0fe 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ go install github.com/unbound-force/replicator/cmd/replicator@latest ## Usage ```bash +# Initialize a project for swarm operations +replicator init + # Start MCP server (for AI agent connections) replicator serve diff --git a/cmd/replicator/init.go b/cmd/replicator/init.go new file mode 100644 index 0000000..925d9c2 --- /dev/null +++ b/cmd/replicator/init.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +func initCmd() *cobra.Command { + var pathFlag string + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize a project directory for swarm operations", + Long: `Creates a .hive/ directory with an empty cells.json in the target +directory. Idempotent — safe to run multiple times. + +This is the per-repo initialization command. It does not require the +global database (replicator setup) or any external services.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runInit(pathFlag) + }, + } + cmd.Flags().StringVar(&pathFlag, "path", ".", "Target directory for .hive/ initialization") + return cmd +} + +// runInit creates the .hive/ directory and seeds cells.json. +func runInit(targetDir string) error { + hiveDir := filepath.Join(targetDir, ".hive") + + // Check if already initialized. + if info, err := os.Stat(hiveDir); err == nil && info.IsDir() { + fmt.Println("already initialized") + return nil + } + + // Create .hive/ directory. + if err := os.MkdirAll(hiveDir, 0o755); err != nil { + return fmt.Errorf("create .hive directory: %w", err) + } + + // Write empty cells.json. + cellsPath := filepath.Join(hiveDir, "cells.json") + if err := os.WriteFile(cellsPath, []byte("[]\n"), 0o644); err != nil { + return fmt.Errorf("write cells.json: %w", err) + } + + fmt.Println("initialized .hive/") + return nil +} diff --git a/cmd/replicator/init_test.go b/cmd/replicator/init_test.go new file mode 100644 index 0000000..ae2f5dd --- /dev/null +++ b/cmd/replicator/init_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRunInit_FreshDirectory(t *testing.T) { + dir := t.TempDir() + if err := runInit(dir); err != nil { + t.Fatalf("runInit: %v", err) + } + + hiveDir := filepath.Join(dir, ".hive") + info, err := os.Stat(hiveDir) + if err != nil { + t.Fatalf(".hive/ not created: %v", err) + } + if !info.IsDir() { + t.Fatal(".hive/ is not a directory") + } + + cellsPath := filepath.Join(hiveDir, "cells.json") + data, err := os.ReadFile(cellsPath) + if err != nil { + t.Fatalf("cells.json not created: %v", err) + } + if string(data) != "[]\n" { + t.Errorf("cells.json content = %q, want %q", string(data), "[]\n") + } +} + +func TestRunInit_AlreadyInitialized(t *testing.T) { + dir := t.TempDir() + + // First init. + if err := runInit(dir); err != nil { + t.Fatalf("first runInit: %v", err) + } + + // Write something to cells.json to verify it's not overwritten. + cellsPath := filepath.Join(dir, ".hive", "cells.json") + os.WriteFile(cellsPath, []byte(`[{"id":"test"}]`), 0o644) + + // Second init — should be idempotent. + if err := runInit(dir); err != nil { + t.Fatalf("second runInit: %v", err) + } + + // Verify cells.json was NOT overwritten. + data, _ := os.ReadFile(cellsPath) + if string(data) != `[{"id":"test"}]` { + t.Errorf("cells.json was overwritten: got %q", string(data)) + } +} + +func TestRunInit_CustomPath(t *testing.T) { + parent := t.TempDir() + target := filepath.Join(parent, "myproject") + os.MkdirAll(target, 0o755) + + if err := runInit(target); err != nil { + t.Fatalf("runInit with custom path: %v", err) + } + + cellsPath := filepath.Join(target, ".hive", "cells.json") + if _, err := os.Stat(cellsPath); err != nil { + t.Fatalf("cells.json not created at custom path: %v", err) + } +} + +func TestRunInit_InvalidPath(t *testing.T) { + err := runInit("/nonexistent/path/that/cannot/exist") + if err == nil { + t.Fatal("expected error for invalid path") + } +} diff --git a/cmd/replicator/main.go b/cmd/replicator/main.go index ad9448e..ca0a148 100644 --- a/cmd/replicator/main.go +++ b/cmd/replicator/main.go @@ -42,6 +42,7 @@ via Dewey).`, root.AddCommand(statsCmd()) root.AddCommand(queryCmd()) root.AddCommand(setupCmd()) + root.AddCommand(initCmd()) if err := root.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/openspec/changes/add-init-command/.openspec.yaml b/openspec/changes/add-init-command/.openspec.yaml new file mode 100644 index 0000000..796d2d7 --- /dev/null +++ b/openspec/changes/add-init-command/.openspec.yaml @@ -0,0 +1,2 @@ +schema: unbound-force +created: 2026-04-06 diff --git a/openspec/changes/add-init-command/design.md b/openspec/changes/add-init-command/design.md new file mode 100644 index 0000000..5d13cb5 --- /dev/null +++ b/openspec/changes/add-init-command/design.md @@ -0,0 +1,29 @@ +## Context + +Replicator has `setup` (per-machine: `~/.config/swarm-tools/` + SQLite) but no `init` (per-repo). The `uf` CLI needs `replicator init` to exist so it can delegate to it during `uf init`, following the same pattern as `dewey init`. + +The existing `setup.go` pattern: cobra command → `runSetup()` function → filesystem ops → print status. Follow this exactly. + +## Goals / Non-Goals + +### Goals +- Add `replicator init` command that creates `.hive/cells.json` in the target directory +- Support `--path` flag for specifying a non-CWD target +- Idempotent (exit 0 whether creating or already exists) +- No database, no git, no network + +### Non-Goals +- Creating `.hive/memories.jsonl` or other hive files (those are created by specific tools when needed) +- Running `git add` or `git commit` (that's `hive_sync`) +- Initializing the SQLite database (that's `replicator setup`) +- Adding `.hive/` to `.gitignore` (user decides tracking policy) + +## Decisions + +**D1: Follow the `setup.go` pattern.** New file `cmd/replicator/init.go` with `initCmd() *cobra.Command` and `runInit(targetDir string) error`. Register in `main.go` via `root.AddCommand(initCmd())`. + +**D2: Seed file is `cells.json` with `[]`.** This matches the format `hive_sync` writes. An empty JSON array signals "initialized but no cells yet" rather than the ambiguity of a missing file. + +**D3: `--path` defaults to `.` (current directory).** Same UX as `dewey init` which operates on CWD by default. + +**D4: Testable via `t.TempDir()`.** The `runInit` function takes a directory path, making it directly testable without mocking. diff --git a/openspec/changes/add-init-command/proposal.md b/openspec/changes/add-init-command/proposal.md new file mode 100644 index 0000000..8926a98 --- /dev/null +++ b/openspec/changes/add-init-command/proposal.md @@ -0,0 +1,33 @@ +## Why + +The `uf init` command delegates per-repo initialization to each sub-tool (`dewey init`, `gaze init`). Replicator lacks an `init` command, so `uf init` cannot prepare a repository for swarm operations. Without it, the `.hive/` directory only appears organically on first `hive_sync` call, leaving new projects in an ambiguous state where the directory structure doesn't signal that swarm coordination is available. + +This blocks `unbound-force/unbound-force#82` (replace Swarm plugin with Replicator in the `uf` CLI). + +## What Changes + +### New Capabilities +- `replicator init` creates a `.hive/` directory with an empty `cells.json` in the current working directory +- `replicator init --path /some/dir` creates `.hive/` at a specified location +- Idempotent: safe to run multiple times (prints "already initialized" if `.hive/` exists) + +## Impact + +- `cmd/replicator/init.go` (new file, ~40 lines) +- `cmd/replicator/main.go` (add `initCmd()` registration) +- `README.md` (add `init` to usage section) +- `AGENTS.md` (add `init` to commands table) + +## Constitution Alignment + +### I. Autonomous Collaboration +**PASS**: `init` creates a well-known directory (`.hive/`) that other tools can discover without coordination. + +### II. Composability First +**PASS**: No database or external service required. Works standalone before `replicator setup` has run. + +### III. Observable Quality +**PASS**: Prints machine-parseable status (`initialized .hive/` or `already initialized`). Exit 0 on success, exit 1 on error. + +### IV. Testability +**PASS**: Uses `t.TempDir()` for filesystem tests. No external dependencies. diff --git a/openspec/changes/add-init-command/specs/init-command.md b/openspec/changes/add-init-command/specs/init-command.md new file mode 100644 index 0000000..6855efd --- /dev/null +++ b/openspec/changes/add-init-command/specs/init-command.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Per-repo initialization + +`replicator init` MUST create a `.hive/` directory with an empty `cells.json` file in the target directory. + +#### Scenario: Fresh directory +- **GIVEN** a directory without `.hive/` +- **WHEN** `replicator init` is run +- **THEN** `.hive/cells.json` is created containing `[]` +- **AND** stdout prints `initialized .hive/` +- **AND** exit code is 0 + +#### Scenario: Already initialized +- **GIVEN** a directory with existing `.hive/` +- **WHEN** `replicator init` is run +- **THEN** no files are modified +- **AND** stdout prints `already initialized` +- **AND** exit code is 0 + +#### Scenario: Custom path +- **GIVEN** a valid directory at `/some/path` +- **WHEN** `replicator init --path /some/path` is run +- **THEN** `.hive/cells.json` is created at `/some/path/.hive/cells.json` + +#### Scenario: Invalid path +- **GIVEN** a path that does not exist and cannot be created +- **WHEN** `replicator init --path /nonexistent/path` is run +- **THEN** an error is printed +- **AND** exit code is 1 + +### Requirement: No external dependencies + +`replicator init` MUST NOT require the SQLite database, git, network access, or any other external service to function. diff --git a/openspec/changes/add-init-command/tasks.md b/openspec/changes/add-init-command/tasks.md new file mode 100644 index 0000000..c0a972b --- /dev/null +++ b/openspec/changes/add-init-command/tasks.md @@ -0,0 +1,19 @@ +## 1. Implement init command + +- [x] 1.1 Create `cmd/replicator/init.go` with `initCmd() *cobra.Command` (--path flag, default ".") and `runInit(targetDir string) error` +- [x] 1.2 In `runInit`: check if `.hive/` exists (print "already initialized", return nil), otherwise `os.MkdirAll` + `os.WriteFile("cells.json", "[]")` +- [x] 1.3 Register `initCmd()` in `cmd/replicator/main.go` via `root.AddCommand(initCmd())` + +## 2. Tests + +- [x] 2.1 Create `cmd/replicator/init_test.go` with tests: fresh dir creates `.hive/cells.json`, already-initialized is idempotent, custom --path works, cells.json content is `[]` + +## 3. Documentation + +- [x] 3.1 Add `replicator init` to the Usage section in `README.md` +- [x] 3.2 Add `make init` is NOT needed (clarify `init` is a CLI command, not a Makefile target) -- update AGENTS.md commands section to include `replicator init` + +## 4. Verify + +- [x] 4.1 Run `make check` -- all tests pass +- [x] 4.2 Run `./bin/replicator init` in a temp dir -- verify `.hive/cells.json` created