mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
fix(cli): schema-aware --field parsing for non-string typed fields (BUG-1125)
pad item create/update --field key=value previously stored every value
as a string, so json / number / checkbox / multi_select fields were
rejected by the server-side validator. The new parseFieldFlag helper
fetches the collection schema once per command and parses each value
according to its declared field type:
- json / multi_select → json.Unmarshal
- number → strconv.ParseFloat
- checkbox → strconv.ParseBool
- text / url / select / date / relation / unknown → raw string
Schema-fetch failure degrades gracefully to pre-fix string-only behavior.
pad item list --field is unchanged (URL query param, not validator).
Verified against both repros: --field reading_time=3 on blogs (the
original number case) and --field 'arguments=[{...}]' on playbooks (the
json case that surfaced authoring the ship playbook). String fields show
no regression.
Skill update folded in: the "Authoring slug-invocable playbooks with
arguments" section in skills/pad/SKILL.md previously routed users to the
web editor as the only path for structured arguments. With this fix the
CLI handles it in one command, so the section now leads with the CLI
flow and demotes the web editor to an alternative.
This commit is contained in:
+58
-4
@@ -2799,6 +2799,42 @@ Usage:
|
||||
// v2 Commands: create, list, show, update, delete, search, status, next, collections
|
||||
// =============================================================================
|
||||
|
||||
// parseFieldFlag parses a --field key=value flag value according to the
|
||||
// field's declared schema type. JSON-typed and multi_select fields receive
|
||||
// parsed JSON values, number-typed fields receive numbers, and checkbox
|
||||
// fields receive booleans. Unknown fields (not in the schema) and string-
|
||||
// typed fields (text, url, select, date, relation) fall back to the raw
|
||||
// string. On parse failure, falls back to the raw string so the server
|
||||
// validator returns a useful error rather than the CLI silently dropping
|
||||
// data. See BUG-1125.
|
||||
func parseFieldFlag(schema models.CollectionSchema, key, raw string) any {
|
||||
for i := range schema.Fields {
|
||||
def := schema.Fields[i]
|
||||
if def.Key != key {
|
||||
continue
|
||||
}
|
||||
switch def.Type {
|
||||
case "json", "multi_select":
|
||||
var v any
|
||||
if err := json.Unmarshal([]byte(raw), &v); err == nil {
|
||||
return v
|
||||
}
|
||||
case "number":
|
||||
if f, err := strconv.ParseFloat(raw, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
case "checkbox":
|
||||
if b, err := strconv.ParseBool(raw); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
// All other types (text, url, select, date, relation) — string is correct.
|
||||
return raw
|
||||
}
|
||||
// Unknown field — let the server decide.
|
||||
return raw
|
||||
}
|
||||
|
||||
// --- create ---
|
||||
|
||||
func createCmd() *cobra.Command {
|
||||
@@ -2857,10 +2893,18 @@ Run with --help-collections to see available collections and their status values
|
||||
fields["category"] = category
|
||||
}
|
||||
|
||||
// Apply arbitrary --field key=value flags
|
||||
// Apply arbitrary --field key=value flags. Fetch the collection
|
||||
// schema so JSON / number / checkbox / multi_select fields parse
|
||||
// to their declared types (BUG-1125). Schema-fetch failure
|
||||
// degrades gracefully: all values stay as strings, matching
|
||||
// pre-fix behavior.
|
||||
var collSchema models.CollectionSchema
|
||||
if coll, err := client.GetCollection(ws, collSlug); err == nil {
|
||||
_ = json.Unmarshal([]byte(coll.Schema), &collSchema)
|
||||
}
|
||||
for _, kv := range fieldFlags {
|
||||
if idx := strings.Index(kv, "="); idx > 0 {
|
||||
fields[kv[:idx]] = kv[idx+1:]
|
||||
fields[kv[:idx]] = parseFieldFlag(collSchema, kv[:idx], kv[idx+1:])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3424,10 +3468,20 @@ Examples:
|
||||
existingFields["category"] = category
|
||||
}
|
||||
|
||||
// Apply arbitrary --field key=value flags
|
||||
// Apply arbitrary --field key=value flags. Fetch the
|
||||
// collection schema (using the item's own collection slug)
|
||||
// so JSON / number / checkbox / multi_select fields parse
|
||||
// to their declared types (BUG-1125). Schema-fetch failure
|
||||
// degrades gracefully: all values stay as strings.
|
||||
var collSchema models.CollectionSchema
|
||||
if item.CollectionSlug != "" {
|
||||
if coll, err := client.GetCollection(ws, item.CollectionSlug); err == nil {
|
||||
_ = json.Unmarshal([]byte(coll.Schema), &collSchema)
|
||||
}
|
||||
}
|
||||
for _, kv := range fieldFlags {
|
||||
if idx := strings.Index(kv, "="); idx > 0 {
|
||||
existingFields[kv[:idx]] = kv[idx+1:]
|
||||
existingFields[kv[:idx]] = parseFieldFlag(collSchema, kv[:idx], kv[idx+1:])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-21
@@ -242,34 +242,34 @@ pad item create playbook "Release checklist" \
|
||||
EOF
|
||||
```
|
||||
|
||||
**Authoring slug-invocable playbooks with arguments.** The `arguments` field on the playbooks collection is a `json` type, and `pad item create --field` only sets string values — so you can't set a structured `arguments` array directly from the CLI. Two working paths:
|
||||
**Authoring slug-invocable playbooks with arguments.** As of BUG-1125's fix, `pad item create --field` is schema-aware — pass the structured `arguments` array directly as a JSON literal and the CLI parses it into the json-typed field. The full playbook (slug + arguments + body with `## Arguments` mirror) lands in one command:
|
||||
|
||||
- **Web UI playbook editor (recommended)** at `/{username}/{workspace}/playbooks` (click "+ New Playbook"). The editor lets the user (or the agent talking the user through it) declare each argument's `name / type / required / default / description / enum` in a structured form, validates the kebab-case slug + workspace uniqueness, and round-trips the spec into the body's `## Arguments` section. Open the URL with `pad server open` if the user isn't already there. This is the canonical path — the editor exists specifically for this case.
|
||||
```bash
|
||||
pad item create playbook "Cut a release" \
|
||||
--field invocation_slug=release \
|
||||
--field trigger=manual \
|
||||
--field status=active \
|
||||
--field 'arguments=[{"name":"version","type":"string","required":true,"description":"semver, e.g. 0.5.0"},{"name":"dry-run","type":"flag","default":false,"description":"Print what would happen, don'\''t push"}]' \
|
||||
--stdin <<'EOF'
|
||||
Cut a Pad release.
|
||||
|
||||
- **CLI then web edit.** If the user wants the CLI flow, create the playbook with the body containing a `## Arguments` section and set everything BUT `arguments`:
|
||||
## Arguments
|
||||
|
||||
```bash
|
||||
pad item create playbook "Cut a release" \
|
||||
--field invocation_slug=release \
|
||||
--field trigger=manual \
|
||||
--field status=active \
|
||||
--stdin <<'EOF'
|
||||
Cut a Pad release.
|
||||
- `version` (string, required) — semver, e.g. 0.5.0
|
||||
- `dry-run` (flag, default=false) — print what would happen, don't push
|
||||
|
||||
## Arguments
|
||||
## Steps
|
||||
|
||||
- `version` (string, required) — semver, e.g. 0.5.0
|
||||
1. Verify the tree is clean and on main
|
||||
2. Run `make test`
|
||||
3. Tag with `git tag v$VERSION && git push --tags`
|
||||
4. Verify CI release workflow succeeded
|
||||
EOF
|
||||
```
|
||||
|
||||
## Steps
|
||||
The `arguments` JSON and the body's `## Arguments` section are the same contract expressed two ways — the structured field is what the strict CLI/MCP arg parser reads; the markdown is the human-readable mirror. Keep them in sync. For long argument specs, write the JSON to a file and inline it: `--field "arguments=$(cat /tmp/args.json)"`.
|
||||
|
||||
1. Verify the tree is clean and on main
|
||||
2. Run `make test`
|
||||
3. Tag with `git tag v$VERSION && git push --tags`
|
||||
4. Verify CI release workflow succeeded
|
||||
EOF
|
||||
```
|
||||
|
||||
Then open the new playbook in the web editor's structured arguments builder to declare each argument (the markdown `## Arguments` section is the human-readable mirror; the structured field is what the strict CLI parser reads, so both need to be populated). The editor's two-way binding will keep them in sync from there.
|
||||
**Web UI playbook editor** at `/{username}/{workspace}/playbooks` (click "+ New Playbook") is the alternative if the user prefers a form-based flow — kebab-case slug input with debounced uniqueness check, structured arguments builder, and two-way binding with the body's `## Arguments` section. Open with `pad server open`. Equally valid; pick whichever surface the user is already in.
|
||||
|
||||
After creation, point the user at `/pad <slug>` for the new invocation or, for trigger-only playbooks, the action that will auto-load it ("This will fire on the next `on-release` action").
|
||||
|
||||
|
||||
Reference in New Issue
Block a user