Skip to content

Commit 9a9b75a

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/commit-create-name-description
# Conflicts: # cmd/commit.go
2 parents f195dac + 78779c7 commit 9a9b75a

65 files changed

Lines changed: 3747 additions & 295 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ vers status
5454
vers status -q
5555

5656
# Full JSON output
57-
vers status --format json
57+
vers status --json
5858

5959
# Detailed metadata for a VM (IP, lineage, timestamps)
60-
vers info <vm-id>
61-
vers info --format json
60+
vers get <vm-id>
61+
vers get --json
6262

6363
# Execute a command on a VM
6464
vers execute <vm-id> <command> [args...]
@@ -91,7 +91,7 @@ vers commit <vm-id>
9191
# List your commits
9292
vers commit list
9393
vers commit list -q # just IDs
94-
vers commit list --format json
94+
vers commit list --json
9595
vers commit list --public # public commits
9696

9797
# View commit history (parent chain)
@@ -118,7 +118,7 @@ vers tag create production abc-123 -d "stable release"
118118
# List all tags
119119
vers tag list
120120
vers tag list -q # just names
121-
vers tag list --format json
121+
vers tag list --json
122122

123123
# Get tag details
124124
vers tag get <name>
@@ -132,6 +132,29 @@ vers tag delete <name>
132132
vers tag delete <name-1> <name-2>
133133
```
134134

135+
### Feedback
136+
137+
Report friction (or anything else) about the CLI. Entries are appended to a
138+
local JSONL journal at `~/.vers/feedback.jsonl`.
139+
140+
```bash
141+
# Record locally
142+
vers feedback "the --tier flag rejects 'enterprise' but docs list it as valid"
143+
144+
# List recent entries
145+
vers feedback list
146+
vers feedback list --limit 5 --json
147+
148+
# Opt-in upstream delivery: when VERS_FEEDBACK_ENDPOINT is set, the entry is
149+
# also POSTed there (application/json, 5s timeout). Failures are logged to
150+
# stderr but the local journal entry is still written.
151+
VERS_FEEDBACK_ENDPOINT=https://example.com/cli-feedback \
152+
vers feedback "race condition in --wait when job completes during first poll"
153+
```
154+
155+
Override the journal path with `VERS_FEEDBACK_PATH` (primarily for testing).
156+
157+
135158
### Shell Composition
136159

137160
Commands with `-q` output are designed to compose with standard Unix tools:
@@ -147,18 +170,36 @@ vers commit delete $(vers commit list -q)
147170
vers tag delete $(vers tag list -q)
148171

149172
# Get info on the first VM
150-
vers info $(vers status -q | head -1)
173+
vers get $(vers status -q | head -1)
151174

152175
# JSON piped to jq
153-
vers status --format json | jq '.[].vm_id'
154-
vers info <vm-id> --format json | jq '.ip'
176+
vers status --json | jq '.[].vm_id'
177+
vers get <vm-id> --json | jq '.ip'
155178
```
156179

157180
`ps` is an alias for `status`:
158181
```bash
159182
vers ps -q
160183
```
161184

185+
### Job Ledger
186+
187+
Every `--wait` invocation of `run`, `branch`, `deploy`, `resume`, or `run-commit`
188+
appends an entry to a durable JSONL ledger at `~/.vers/jobs.jsonl` (override
189+
with `VERS_JOBS_DIR`). Use `vers jobs` to introspect:
190+
191+
```bash
192+
vers jobs list --json # all jobs as JSON
193+
vers jobs list --status failed # only failed jobs
194+
vers jobs get job_<id> # full record for one job
195+
vers jobs prune --older-than 7d # trim entries older than 7 days
196+
vers jobs prune --all --dry-run # preview clearing the ledger
197+
```
198+
199+
Phase 1 ships journaling only. The ledger is written best-effort: a write
200+
failure never causes the underlying command to fail. Resumption of in-flight
201+
jobs is not yet implemented.
202+
162203

163204
## Configuration
164205

cmd/agent_context.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/pflag"
11+
)
12+
13+
// agentContextSchemaVersion is bumped whenever the agent-context JSON shape
14+
// changes in a backwards-incompatible way.
15+
const agentContextSchemaVersion = "1"
16+
17+
// agentContextEnumAnnotationPrefix is the cobra Annotations key prefix used to
18+
// declare a known enum set for a flag. Example:
19+
//
20+
// cmd.Annotations["vers:enum:visibility"] = "public,private,unlisted"
21+
const agentContextEnumAnnotationPrefix = "vers:enum:"
22+
23+
var agentContextPretty bool
24+
25+
var agentContextCmd = &cobra.Command{
26+
Use: "agent-context",
27+
Short: "Emit a versioned JSON description of the CLI for agent consumption",
28+
Long: `Emit a versioned, machine-readable JSON description of every command,
29+
subcommand, and flag exposed by the CLI.
30+
31+
Agents should consume this output instead of parsing --help text. The top-level
32+
"schema_version" field lets consumers detect breaking shape changes.`,
33+
SilenceUsage: true,
34+
SilenceErrors: true,
35+
Run: func(cmd *cobra.Command, args []string) {
36+
doc := buildAgentContext(cmd.Root())
37+
38+
var (
39+
out []byte
40+
err error
41+
)
42+
if agentContextPretty {
43+
out, err = json.MarshalIndent(doc, "", " ")
44+
} else {
45+
out, err = json.Marshal(doc)
46+
}
47+
if err != nil {
48+
// Per spec this command must never fail; fall back to a minimal
49+
// stub document and exit 0.
50+
fmt.Fprintln(os.Stdout, `{"schema_version":"`+agentContextSchemaVersion+`","commands":{}}`)
51+
return
52+
}
53+
fmt.Fprintln(os.Stdout, string(out))
54+
},
55+
}
56+
57+
func init() {
58+
agentContextCmd.Flags().BoolVar(&agentContextPretty, "pretty", false, "Emit indented JSON instead of compact JSON")
59+
rootCmd.AddCommand(agentContextCmd)
60+
}
61+
62+
// agentContextDoc is the top-level JSON shape emitted by `vers agent-context`.
63+
type agentContextDoc struct {
64+
SchemaVersion string `json:"schema_version"`
65+
CLI agentContextCLI `json:"cli"`
66+
Commands map[string]*agentContextCommand `json:"commands"`
67+
AvailableProfiles []string `json:"available_profiles"`
68+
Feedback agentContextFeedback `json:"feedback"`
69+
}
70+
71+
type agentContextCLI struct {
72+
Name string `json:"name"`
73+
Version string `json:"version"`
74+
Description string `json:"description"`
75+
}
76+
77+
type agentContextCommand struct {
78+
Use string `json:"use"`
79+
Short string `json:"short"`
80+
Long string `json:"long,omitempty"`
81+
Aliases []string `json:"aliases,omitempty"`
82+
Args agentContextArgs `json:"args"`
83+
Async bool `json:"async"`
84+
Flags map[string]*agentContextFlag `json:"flags"`
85+
Subcommands map[string]*agentContextCommand `json:"subcommands,omitempty"`
86+
}
87+
88+
type agentContextArgs struct {
89+
Min int `json:"min"`
90+
Max int `json:"max"`
91+
}
92+
93+
type agentContextFlag struct {
94+
Shorthand string `json:"shorthand,omitempty"`
95+
Type string `json:"type"`
96+
Default string `json:"default"`
97+
Usage string `json:"usage"`
98+
Required bool `json:"required"`
99+
Enum []string `json:"enum,omitempty"`
100+
}
101+
102+
type agentContextFeedback struct {
103+
LocalPath string `json:"local_path"`
104+
EndpointConfigured bool `json:"endpoint_configured"`
105+
}
106+
107+
func buildAgentContext(root *cobra.Command) *agentContextDoc {
108+
doc := &agentContextDoc{
109+
SchemaVersion: agentContextSchemaVersion,
110+
CLI: agentContextCLI{
111+
Name: root.Name(),
112+
Version: Version,
113+
Description: strings.TrimSpace(root.Long),
114+
},
115+
Commands: map[string]*agentContextCommand{},
116+
AvailableProfiles: []string{},
117+
Feedback: agentContextFeedback{
118+
LocalPath: "~/.vers/feedback.jsonl",
119+
EndpointConfigured: os.Getenv("VERS_FEEDBACK_ENDPOINT") != "",
120+
},
121+
}
122+
123+
for _, c := range root.Commands() {
124+
if shouldSkipCommand(c) {
125+
continue
126+
}
127+
doc.Commands[c.Name()] = describeCommand(c)
128+
}
129+
return doc
130+
}
131+
132+
func shouldSkipCommand(c *cobra.Command) bool {
133+
if c.Hidden {
134+
return true
135+
}
136+
switch c.Name() {
137+
case "help", "completion":
138+
return true
139+
}
140+
return false
141+
}
142+
143+
func describeCommand(c *cobra.Command) *agentContextCommand {
144+
out := &agentContextCommand{
145+
Use: c.Use,
146+
Short: c.Short,
147+
Long: strings.TrimSpace(c.Long),
148+
Args: agentContextArgs{Min: 0, Max: -1},
149+
Flags: map[string]*agentContextFlag{},
150+
}
151+
if len(c.Aliases) > 0 {
152+
out.Aliases = append(out.Aliases, c.Aliases...)
153+
}
154+
155+
// Collect flags (local + inherited from parents), excluding hidden &
156+
// deprecated. Inherited flags give agents a complete picture without
157+
// having to re-walk the parent chain themselves.
158+
c.Flags().VisitAll(func(f *pflag.Flag) {
159+
if entry := describeFlag(c, f); entry != nil {
160+
out.Flags["--"+f.Name] = entry
161+
}
162+
})
163+
164+
// async = visible --wait flag exists.
165+
if w := c.Flags().Lookup("wait"); w != nil && !w.Hidden && len(w.Deprecated) == 0 {
166+
out.Async = true
167+
}
168+
169+
for _, sub := range c.Commands() {
170+
if shouldSkipCommand(sub) {
171+
continue
172+
}
173+
if out.Subcommands == nil {
174+
out.Subcommands = map[string]*agentContextCommand{}
175+
}
176+
out.Subcommands[sub.Name()] = describeCommand(sub)
177+
}
178+
179+
return out
180+
}
181+
182+
func describeFlag(parent *cobra.Command, f *pflag.Flag) *agentContextFlag {
183+
if f.Hidden || len(f.Deprecated) > 0 {
184+
return nil
185+
}
186+
entry := &agentContextFlag{
187+
Shorthand: f.Shorthand,
188+
Type: f.Value.Type(),
189+
Default: f.DefValue,
190+
Usage: f.Usage,
191+
}
192+
if _, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok {
193+
entry.Required = true
194+
}
195+
if parent != nil && parent.Annotations != nil {
196+
if raw, ok := parent.Annotations[agentContextEnumAnnotationPrefix+f.Name]; ok && raw != "" {
197+
parts := strings.Split(raw, ",")
198+
vals := make([]string, 0, len(parts))
199+
for _, p := range parts {
200+
if v := strings.TrimSpace(p); v != "" {
201+
vals = append(vals, v)
202+
}
203+
}
204+
if len(vals) > 0 {
205+
entry.Enum = vals
206+
}
207+
}
208+
}
209+
return entry
210+
}

0 commit comments

Comments
 (0)