-
Notifications
You must be signed in to change notification settings - Fork 2
fix: align create/update flag surface across issue, issue-template, action, calendar (HULY-8) #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -698,6 +698,7 @@ export async function updateEvent( | |
| opts: { | ||
| title?: string | ||
| description?: string | ||
| body?: string | ||
| start?: string | ||
| end?: string | ||
| allDay?: boolean | ||
|
|
@@ -720,7 +721,10 @@ export async function updateEvent( | |
| if (!doc) throw new CliError(ExitCode.NotFound, `event ${ref} not found`) | ||
| const ops: Record<string, unknown> = {} | ||
| if (opts.title) ops.title = opts.title | ||
| if (opts.description !== undefined) ops.description = opts.description | ||
| // HULY-8: --body takes precedence over --description, mirroring createEvent | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: |
||
| // (`description: opts.description ?? opts.body ?? ''`, line 629). | ||
| if (opts.body !== undefined) ops.description = opts.body | ||
| else if (opts.description !== undefined) ops.description = opts.description | ||
| if (opts.start) { | ||
| const sd = parseDate(opts.start, '--start') | ||
| ops.startDate = sd | ||
|
|
@@ -737,7 +741,7 @@ export async function updateEvent( | |
| throw new CliError( | ||
| ExitCode.Validation, | ||
| 'nothing to update', | ||
| 'pass --title/--description/--start/--end/--all-day/--location/--attendee', | ||
| 'pass --title/--description/--body/--start/--end/--all-day/--location/--attendee', | ||
| ) | ||
| if (opts.dryRun) { | ||
| console.log(`would update event ${id}:`) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,6 +204,7 @@ export async function updateIssueTemplate( | |
| title?: string | ||
| description?: string | ||
| body?: string | ||
| bodyFile?: string | ||
| json?: boolean | ||
| ci?: boolean | ||
| dryRun?: boolean | ||
|
|
@@ -223,10 +224,22 @@ export async function updateIssueTemplate( | |
| if (!doc) throw new CliError(ExitCode.NotFound, `issue-template ${ref} not found`) | ||
| const ops: Record<string, unknown> = {} | ||
| if (opts.title) ops.title = opts.title | ||
| if (opts.body) ops.description = opts.body | ||
| // HULY-8: --body-file mirrors the file-read path used by createIssueTemplate | ||
| // (lines 165-167). --body still wins if both are passed (matches create). | ||
| // Test `!== undefined` rather than truthiness so an explicit `--body ""` | ||
| // is honored as "clear the description" instead of silently falling | ||
| // through to opts.description. | ||
| if (opts.bodyFile) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: Same HULY-20 bug pattern — |
||
| const fs = await import('node:fs/promises') | ||
| ops.description = (await fs.readFile(opts.bodyFile, 'utf8')).trim() | ||
| } else if (opts.body !== undefined) ops.description = opts.body | ||
| else if (opts.description !== undefined) ops.description = opts.description ? opts.description : '' | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if (Object.keys(ops).length === 0) | ||
| throw new CliError(ExitCode.Validation, 'nothing to update', 'pass --title, --description, or --body') | ||
| throw new CliError( | ||
| ExitCode.Validation, | ||
| 'nothing to update', | ||
| 'pass --title, --description, --body, or --body-file', | ||
| ) | ||
| if (opts.dryRun) { | ||
| console.log(`would update issue-template ${id}:`) | ||
| console.log( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,14 +89,19 @@ function stripStatusCategoryPrefix(cat: string): string { | |
| } | ||
|
|
||
| async function readBody(opts: { body?: string; bodyFile?: string }): Promise<string | undefined> { | ||
| if (opts.body && opts.bodyFile) { | ||
| if (opts.body !== undefined && opts.bodyFile !== undefined) { | ||
| throw new CliError(ExitCode.Validation, 'ambiguous body input', 'pass only one of --body or --body-file') | ||
| } | ||
| if (opts.bodyFile) { | ||
| if (opts.bodyFile !== undefined) { | ||
| const fs = await import('node:fs/promises') | ||
| return (await fs.readFile(opts.bodyFile, 'utf8')).trim() | ||
| } | ||
| if (opts.body) return opts.body | ||
| // HULY-8 (post-review): test `!== undefined` rather than truthiness so an | ||
| // explicit `--body ""` is preserved as a clear intent on issue update | ||
| // instead of silently falling through to opts.description / being treated | ||
| // as "no body provided". Matches the symmetric check added to | ||
| // issue-template.ts updateIssueTemplate. | ||
| if (opts.body !== undefined) return opts.body | ||
| return undefined | ||
| } | ||
|
|
||
|
|
@@ -1080,7 +1085,12 @@ export async function updateIssue( | |
| assignee?: string | ||
| title?: string | ||
| description?: string | ||
| body?: string | ||
| bodyFile?: string | ||
| taskType?: string | ||
| kind?: string | ||
| due?: string | ||
| label?: string[] | ||
| dryRun?: boolean | ||
| minimal?: boolean | ||
| workspace?: string | ||
|
|
@@ -1142,48 +1152,79 @@ export async function updateIssue( | |
| workspace: opts.workspace, | ||
| })) as Ref<Doc> | ||
| if (opts.title) ops.title = opts.title | ||
| if (opts.description !== undefined) { | ||
| // Update only the ydoc (issue #3). The ydoc is the source of truth | ||
| // for collaborative reads; uploading a new JSON blob on every update | ||
| // leaves orphaned blobs in MinIO and risks partial-write failures | ||
| // (issue #12). Empty string is a deliberate clear and is forwarded | ||
| // to updateMarkup (which treats undefined as no-op). | ||
| await updateMarkup( | ||
| client, | ||
| CLASS.Issue as Ref<Class<Doc>>, | ||
| issue._id as Ref<Doc>, | ||
| 'description', | ||
| opts.description, | ||
| 'markup', | ||
| ) | ||
| markupUpdated = true | ||
| } | ||
| // HULY-8: --body / --body-file take precedence over --description, | ||
| // mirroring createIssue's `descriptionSource = body ?? opts.description` | ||
| // (line 786). Resolve the markup source here but defer the upload until | ||
| // after the dry-run guard below — otherwise `--body ... --dry-run` would | ||
| // persist. The --kind / --due resolve calls are intentionally kept where | ||
| // they are (matches the existing resolveStatus / resolvePriority pattern). | ||
| const body = await readBody(opts) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION:
Meanwhile, on Either mirror the fix by changing Reply with |
||
| const markupBody = body ?? opts.description | ||
| const markupRequested = markupBody !== undefined | ||
| if (opts.taskType) ops.kind = await resolveTaskType(client, opts.taskType) | ||
| else if (opts.kind) { | ||
| // HULY-8: --kind <ref> lets power users select any TaskType. Same | ||
| // validation as --task-type but skips the by-name lookup. Mirrors | ||
| // the --kind branch in createIssue. | ||
| ops.kind = await resolveKindByRef(client, opts.kind) | ||
| } | ||
| if (opts.due !== undefined) { | ||
| // HULY-8: --due <iso> on update, mirroring createIssue (line 779). | ||
| // Empty string clears the due date; ISO string sets it; undefined | ||
| // (flag absent) leaves the existing due date untouched. | ||
| ops.dueDate = opts.due === '' ? null : parseDate(opts.due, '--due') | ||
| } | ||
| // HULY-8: --label replaces the labels array, matching issue create. | ||
| // Use 'issue label add' / 'issue label remove' for additive/subtractive | ||
| // semantics — those go through the TagReference collection, not this | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: |
||
| // direct field set. Filter empty/whitespace entries so `--label ""` and | ||
| // accidental `--label " "` don't silently create bogus tags on the issue. | ||
| if (opts.label !== undefined) { | ||
| const cleaned = opts.label.map((l) => l.trim()).filter((l) => l.length > 0) | ||
| if (cleaned.length > 0) ops.labels = cleaned | ||
| else if (opts.label.length > 0) | ||
| // All entries were blank — user clearly meant "no labels". Surface | ||
| // that intent rather than silently no-op'ing (which would mislead the | ||
| // "nothing to update" guard below). | ||
| ops.labels = [] | ||
| } | ||
|
|
||
| // --minimal means "I know what I'm doing with --set/--unset, don't | ||
| // second-guess me." It only suppresses the empty-ops guard below, | ||
| // so a minimal but explicit --set still sends through. Was previously | ||
| // a dead flag; now actually does something safe. | ||
| if (Object.keys(ops).length === 0 && !markupUpdated && !opts.minimal) { | ||
| if (Object.keys(ops).length === 0 && !markupRequested && !opts.minimal) { | ||
| throw new CliError( | ||
| ExitCode.Validation, | ||
| 'nothing to update', | ||
| 'pass --set/--unset, --status, --priority, --assignee, --title, --description, --task-type, or --kind (with --status-category to pick a workflow stage)', | ||
| 'pass --set/--unset, --status, --priority, --assignee, --title, --description, --body, --body-file, --task-type, --kind, --due, or --label (with --status-category to pick a workflow stage)', | ||
| ) | ||
| } | ||
|
|
||
| if (opts.dryRun) { | ||
| console.log(`would update issue ${issue.identifier} (${issue._id}):`) | ||
| console.log( | ||
| JSON.stringify( | ||
| { _class: CLASS.Issue, objectId: issue._id, space: issue.space, ops, markupUpdated }, | ||
| { _class: CLASS.Issue, objectId: issue._id, space: issue.space, ops, markupRequested }, | ||
| null, | ||
| 2, | ||
| ), | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| if (markupRequested) { | ||
| await updateMarkup( | ||
| client, | ||
| CLASS.Issue as Ref<Class<Doc>>, | ||
| issue._id as Ref<Doc>, | ||
| 'description', | ||
| markupBody, | ||
| 'markup', | ||
| ) | ||
| markupUpdated = true | ||
| } | ||
|
|
||
| const hasOps = Object.keys(ops).length > 0 | ||
| if (hasOps) { | ||
| await withSpinner('Updating…', () => | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Help text is misleading — it claims reparenting is supported, then immediately says it isn't exposed.
The first sentence ("Reparenting is supported via 'removeCollection' on the old parent + 'addCollection' on the new parent…") reads like a feature description, but the CLI just throws
"reparenting an action via update is not yet supported"whenever these flags are passed. The WorkSlot preservation and cascade semantics are implementation details the user can't act on. Consider rewriting to lead with the rejection: e.g., "Reparenting is not supported. To move an action, delete it and recreate it under the new parent. (The new action will receive a new_id; downstream references will break.)"