Skip to content

Commit 73dff6d

Browse files
anshulsaoclaude
andcommitted
docs+test+skill: cobra tests for new commands, README, CHANGELOG, skill content
- cmd/whoami_test.go, cmd/status_test.go, cmd/init_test.go, cmd/use_test.go - Skill content rewritten to teach AI hosts how to operate praxis (always --json, run `praxis login` yourself, exit-code semantics, what's safe to call without auth, multi-profile support). - README rewritten: AI-prompt-first onboarding, profiles section, multi-deployment support engineer flow. - CHANGELOG entry for v0.4.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fe69644 commit 73dff6d

7 files changed

Lines changed: 572 additions & 97 deletions

File tree

CHANGELOG.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,53 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9-
(Empty — see 0.3.0 below.)
9+
(Empty — see 0.4.0 below.)
10+
11+
## [0.4.0] — 2026-05-06
12+
13+
### Added
14+
- `praxis login` — browser-callback authentication. Opens the user's
15+
default browser to create an API key, captures the new key via a
16+
one-shot localhost listener, validates against `/ai-api/auth/me`,
17+
saves credentials. Supports `--profile`, `--url`, `--token`,
18+
`--timeout` flags.
19+
- `praxis whoami` — live identity check against `/ai-api/auth/me`.
20+
- `praxis status` — read-only local snapshot (active profile, URL,
21+
login state, installed skills) — designed for AI hosts to inspect.
22+
- `praxis init` — idempotent first-run bootstrap: installs the praxis
23+
skill into all detected AI hosts and reports state as JSON.
24+
- `praxis use <profile>` — kubectl-style: persist active profile to
25+
`~/.praxis/config`.
26+
- Multi-profile credentials store at `~/.praxis/credentials` in INI
27+
format matching `~/.facets/credentials`. Profile resolution
28+
priority: `--profile` flag > `PRAXIS_PROFILE` env > `~/.praxis/config`
29+
active-profile pointer > literal `default` section. Single-profile
30+
users see no behavior change.
31+
- Browser-callback flow on the agent-factory side
32+
(PR Facets-cloud/agent-factory#1048): the API-key create modal
33+
reads `cli_callback`, `cli_session`, `suggested_name` query params,
34+
auto-opens with the name pre-filled, POSTs the new key to the
35+
localhost listener after creation, redirects to `/ui/ai/cli-success`.
36+
- Named exit codes (`internal/exitcode`): 3 = auth missing/expired,
37+
4 = no config, 5 = network, 6 = no AI host. Pinned in tests.
38+
- Structured `--json` output mode + auto-non-TTY default
39+
(`internal/render`).
40+
- Skill content rewrite — the placeholder `praxis` skill now teaches
41+
AI hosts how to operate the CLI (always `--json`, run `praxis login`
42+
yourself, exit-code semantics, etc.).
43+
44+
### Changed (breaking)
45+
- `~/.praxis/credentials` format changed from a single JSON object to
46+
an INI profile map. Existing v0.3 single-credential files are not
47+
auto-migrated; re-run `praxis login`.
48+
- `praxis logout` now takes `--profile X` (default: active profile)
49+
or `--all` (wipe everything + active-profile pointer).
50+
51+
### Removed
52+
- `internal/config` package. URL is now stored per-profile inside
53+
`~/.praxis/credentials`. The legacy `~/.praxis/config.json` (if any)
54+
is ignored; `~/.praxis/config` (no .json) takes its place as the
55+
active-profile pointer file.
1056

1157
## [0.3.0] — 2026-05-06
1258

README.md

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,112 @@
11
# Praxis CLI
22

3-
> Bring your organization's Praxis cloud to any local AI host —
4-
> Claude Code, Cursor, Gemini CLI — without ever putting AWS, kubeconfig,
5-
> or Terraform credentials on your laptop.
6-
7-
## What it does
8-
9-
```
10-
Your laptop Praxis cloud
11-
─────────── ────────────
12-
PRAXIS_TOKEN ─────────────────────► Validate per-call
13-
AssumeRole into your org's AWS
14-
Claude Code ─ shells out to ─► Execute the MCP function
15-
praxis CLI Audit
16-
◄─── JSON response ───
17-
```
18-
19-
`praxis` exposes Praxis MCP functions and skills as a single-binary CLI.
20-
Your AI host calls `praxis mcp …` for any infra operation; the server
21-
runs it under the org's managed credentials and returns the result.
3+
> Bring your Praxis cloud to any local AI host (Claude Code, Codex,
4+
> Gemini CLI). Operated by your AI; you (the human) only install the
5+
> binary and click one button during login.
226
237
## Install
248

259
```bash
2610
brew install Facets-cloud/tap/praxis
27-
# or
28-
curl -fsSL https://install.askpraxis.ai | sh
2911
```
3012

31-
Verify:
13+
Or direct binary:
3214

3315
```bash
34-
praxis version
35-
praxis doctor
16+
curl -fsSL https://install.askpraxis.ai | sh # planned
17+
# or grab a release directly:
18+
# https://github.com/Facets-cloud/praxis-cli/releases/latest
3619
```
3720

38-
## Verify
21+
## Set up — paste this prompt into your AI
3922

40-
```bash
41-
praxis version
42-
praxis completion zsh > "${fpath[1]}/_praxis"
43-
```
23+
Open Claude Code, Codex, Gemini CLI, or any AI host that can run
24+
shell commands. Paste:
4425

45-
The skill / MCP commands aren't shipped yet — see "Coming in subsequent
46-
releases" below.
26+
> I just installed the `praxis` CLI on my machine. Please set it up
27+
> end-to-end:
28+
>
29+
> 1. Run `praxis init` to install the praxis skill into every AI host
30+
> on this machine. The skill teaches you how to use praxis.
31+
> 2. Run `praxis login`. A browser will open; I'll click "Create Key"
32+
> once. You handle the rest.
33+
> 3. Run `praxis status --json` and tell me what you see.
34+
>
35+
> If I have a custom Praxis deployment, use `praxis login --url <url>`.
36+
> Otherwise the default `https://askpraxis.ai` is fine.
37+
>
38+
> Always pass `--json` to praxis from now on. Read exit codes — they
39+
> tell you what to do next.
4740
48-
## Surface today (v0.3.x)
41+
That's it. Your AI now operates praxis on your behalf. Daily use:
42+
43+
> "Show me what's deployed in prod."
44+
> *(your AI runs `praxis mcp …` once those commands land)*
45+
46+
> "Debug my failed release."
47+
> *(your AI sources the release-debugging skill from praxis cloud
48+
> and walks the diagnosis with you)*
49+
50+
## Surface today (v0.4.x)
4951

5052
```
51-
praxis install-skill install the praxis skill into every detected
52-
AI host (Claude Code, Codex, Gemini CLI).
53-
User-scope only — Cursor has no user-level
54-
skill dir.
55-
praxis uninstall-skill remove from every host where installed
56-
praxis list-skills show what's installed and where
57-
praxis refresh-skills rewrite installed SKILL.md files with current
58-
content (called automatically by `praxis
59-
update`)
60-
praxis version build version, commit, date, runtime
61-
praxis update self-update + auto-refresh installed skills
62-
praxis completion <shell> bash | zsh | fish | powershell
63-
praxis logout delete ~/.praxis/credentials
64-
praxis --help / -v
53+
praxis init install skill into AI hosts +
54+
report state JSON
55+
praxis login browser-callback authentication
56+
--profile X | --url Y | --token Z
57+
praxis whoami live identity check via /auth/me
58+
praxis status local snapshot (no network call)
59+
praxis logout remove credentials
60+
--profile X | --all
61+
praxis use <profile> kubectl-style: set active profile
62+
praxis install-skill write the praxis skill into all
63+
detected AI hosts (Claude Code,
64+
Codex, Gemini CLI; user-scope only)
65+
praxis uninstall-skill remove from every host
66+
praxis list-skills what's installed and where
67+
praxis refresh-skills rewrite SKILL.md files (auto-called
68+
by `praxis update`)
69+
praxis version | update | completion | logout | --help
6570
```
6671

67-
## Try it
72+
## Multiple deployments (internal-support engineers)
6873

6974
```bash
70-
brew install Facets-cloud/tap/praxis
71-
praxis install-skill # writes the praxis skill to your AI hosts
72-
praxis list-skills # confirm where it landed
75+
praxis login # → "default"
76+
praxis login --profile acme --url https://acme.console.facets.cloud
77+
praxis login --profile vymo --url https://vymo.console.facets.cloud
78+
79+
praxis use acme # all subsequent commands use acme
80+
praxis whoami # → reports acme's identity
81+
praxis status # → reports acme's URL + auth state
82+
83+
# One-shot overrides (no `use` needed):
84+
praxis whoami --profile vymo
85+
PRAXIS_PROFILE=vymo praxis status
7386
```
7487

75-
## Coming in subsequent releases
88+
Single-profile users never see this — `praxis login` writes "default"
89+
and everything resolves there silently.
90+
91+
### Files
7692

7793
```
78-
v0.2 login | whoami | mcp list | mcp <mcp> | mcp <mcp> <fn> [--arg val …]
79-
Server-driven skill catalog (skill list / show <name>); per-repo
80-
Cursor install. The placeholder "praxis" skill gets replaced with
81-
real catalogued skills (release-debugging, k8s-investigation,
82-
terraform-plan-explain, …).
94+
~/.praxis/credentials INI format, matches ~/.facets/credentials
95+
[default] url, username, token
96+
[acme] url, username, token
97+
98+
(chmod 0600)
99+
100+
~/.praxis/config Active-profile pointer, set by `praxis use`.
101+
Doesn't exist until first `use` call.
102+
(chmod 0600)
83103
```
84104

85-
## Why a CLI (and not an MCP server)?
105+
## Why a CLI
86106

87-
CLIs run anywhere: any AI host, CI, cron, shell pipelines, your own scripts.
88-
MCP support varies by tool; `bash -c "praxis …"` doesn't. The CLI also makes
89-
auth and audit one-per-invocation, so every call is attributable.
107+
CLIs run anywhere: any AI host, CI, cron, shell pipelines. MCP support
108+
varies by tool; `bash -c "praxis …"` doesn't. The CLI also makes auth
109+
and audit per-invocation, so every call is attributable.
90110

91111
## Develop
92112

@@ -95,14 +115,15 @@ Requirements: Go 1.21+.
95115
```bash
96116
git clone https://github.com/Facets-cloud/praxis-cli.git
97117
cd praxis-cli
98-
make build # ./praxis with version stamp
99-
./praxis --help
100-
make test
118+
make build # builds ./praxis with version stamp
119+
make test # go test -race ./...
101120
make lint # gofmt + vet + test
121+
go test -cover ./... # coverage
102122
```
103123

104124
Releases are cut by tagging `v*.*.*` and pushing — GitHub Actions runs
105-
goreleaser, publishes the release, and updates the brew tap.
125+
goreleaser, publishes the GitHub Release, and updates the Brew tap
126+
formula automatically.
106127

107128
## License
108129

cmd/init_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/Facets-cloud/praxis-cli/internal/credentials"
10+
"github.com/Facets-cloud/praxis-cli/internal/harness"
11+
"github.com/Facets-cloud/praxis-cli/internal/skillinstall"
12+
)
13+
14+
func resetInitFlags() {
15+
initProfile = ""
16+
initJSON = false
17+
}
18+
19+
func TestInitCmd_NoHostsDetected(t *testing.T) {
20+
t.Setenv("HOME", t.TempDir())
21+
t.Setenv("PRAXIS_PROFILE", "")
22+
resetInitFlags()
23+
24+
withSeams(t,
25+
func() []harness.Harness { return nil }, // no hosts
26+
nil, nil, nil,
27+
)
28+
29+
var buf bytes.Buffer
30+
initCmd.SetOut(&buf)
31+
if err := initCmd.RunE(initCmd, nil); err != nil {
32+
t.Fatalf("RunE err = %v", err)
33+
}
34+
out := buf.String()
35+
if !strings.Contains(out, `"hosts_detected": null`) && !strings.Contains(out, `"hosts_detected": []`) {
36+
t.Errorf("expected empty hosts_detected, got %q", out)
37+
}
38+
if !strings.Contains(out, `"logged_in": false`) {
39+
t.Errorf("not logged in → expected logged_in: false, got %q", out)
40+
}
41+
if !strings.Contains(out, "praxis login") {
42+
t.Errorf("expected next-steps to mention praxis login, got %q", out)
43+
}
44+
}
45+
46+
func TestInitCmd_HostsDetected_InstallSkillCalled(t *testing.T) {
47+
t.Setenv("HOME", t.TempDir())
48+
t.Setenv("PRAXIS_PROFILE", "")
49+
resetInitFlags()
50+
51+
withSeams(t,
52+
func() []harness.Harness {
53+
return []harness.Harness{
54+
{Name: "claude-code", Detected: true, SkillDir: "/fake/c"},
55+
{Name: "codex", Detected: true, SkillDir: "/fake/x"},
56+
}
57+
},
58+
func(name string, hosts []harness.Harness) ([]skillinstall.Installation, error) {
59+
out := make([]skillinstall.Installation, 0, len(hosts))
60+
for _, h := range hosts {
61+
out = append(out, skillinstall.Installation{
62+
SkillName: name,
63+
Harness: h.Name,
64+
Path: h.SkillDir + "/" + name + "/SKILL.md",
65+
InstalledAt: time.Now(),
66+
})
67+
}
68+
return out, nil
69+
},
70+
nil, nil,
71+
)
72+
73+
var buf bytes.Buffer
74+
initCmd.SetOut(&buf)
75+
if err := initCmd.RunE(initCmd, nil); err != nil {
76+
t.Fatalf("RunE err = %v", err)
77+
}
78+
out := buf.String()
79+
for _, want := range []string{`"claude-code"`, `"codex"`, `"hosts_detected"`, `"skills_installed"`} {
80+
if !strings.Contains(out, want) {
81+
t.Errorf("output missing %q\nfull: %s", want, out)
82+
}
83+
}
84+
}
85+
86+
func TestInitCmd_LoggedIn_NoNextSteps(t *testing.T) {
87+
t.Setenv("HOME", t.TempDir())
88+
t.Setenv("PRAXIS_PROFILE", "")
89+
resetInitFlags()
90+
91+
_ = credentials.Put("default", credentials.Profile{
92+
URL: "https://x.test", Token: "sk_live", Username: "u@x",
93+
})
94+
95+
withSeams(t,
96+
func() []harness.Harness { return nil },
97+
nil, nil, nil,
98+
)
99+
100+
var buf bytes.Buffer
101+
initCmd.SetOut(&buf)
102+
if err := initCmd.RunE(initCmd, nil); err != nil {
103+
t.Fatalf("RunE err = %v", err)
104+
}
105+
out := buf.String()
106+
if !strings.Contains(out, `"logged_in": true`) {
107+
t.Errorf("expected logged_in: true, got %q", out)
108+
}
109+
if strings.Contains(out, "praxis login") {
110+
t.Errorf("logged-in profile should NOT trigger 'praxis login' next-step, got %q", out)
111+
}
112+
}

0 commit comments

Comments
 (0)