orizu 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,66 @@ function asRecord(value) {
73
73
  function num(value) {
74
74
  return typeof value === 'number' ? value : 0;
75
75
  }
76
+ // -- list table (ALI-1038) ----------------------------------------------------
77
+ /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
78
+ function relativeAge(iso, now) {
79
+ if (typeof iso !== 'string')
80
+ return '-';
81
+ const then = Date.parse(iso);
82
+ if (Number.isNaN(then))
83
+ return '-';
84
+ const mins = Math.max(0, Math.floor((now - then) / 60_000));
85
+ if (mins < 60)
86
+ return `${mins}m`;
87
+ const hours = Math.floor(mins / 60);
88
+ if (hours < 48)
89
+ return `${hours}h`;
90
+ return `${Math.floor(hours / 24)}d`;
91
+ }
92
+ /** One-glance change summary from the stored proposedState — no live API call. */
93
+ function changesSummary(manifest) {
94
+ if (manifest.actionType !== 'repo_merge')
95
+ return '-';
96
+ const proposed = asRecord(manifest.proposedState);
97
+ const files = num(proposed.filesChanged);
98
+ const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`;
99
+ // Instruction files steer agent behavior — reviewers should see the flag in the list.
100
+ return proposed.touches_instruction_files === true ? `${parts} !instr` : parts;
101
+ }
102
+ /** Reviewable-first (pending_approval, then draft), newest first within a group. */
103
+ function listOrder(a, b) {
104
+ const rank = (m) => m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2;
105
+ const byRank = rank(a) - rank(b);
106
+ if (byRank !== 0)
107
+ return byRank;
108
+ const created = (m) => typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0;
109
+ return created(b) - created(a);
110
+ }
111
+ /** Render manifests as the standard padEnd table (matches teams/projects list). */
112
+ function manifestsTable(manifests, now) {
113
+ if (manifests.length === 0)
114
+ return 'promotion manifests\n(none)';
115
+ const rows = [...manifests].sort(listOrder).map(manifest => ({
116
+ id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
117
+ status: typeof manifest.status === 'string' ? manifest.status : '(unknown)',
118
+ type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
119
+ changes: changesSummary(manifest),
120
+ author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
121
+ age: relativeAge(manifest.createdAt, now),
122
+ }));
123
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length));
124
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
125
+ const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length));
126
+ const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length));
127
+ const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length));
128
+ const header = `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
129
+ `${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`;
130
+ const divider = `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
131
+ `${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`;
132
+ const body = rows.map(row => `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
133
+ `${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`);
134
+ return [header, divider, ...body].join('\n');
135
+ }
76
136
  // repo_merge manifests (ALI-972) render the compare summary from stored evidence
77
137
  // — no live GitHub call — so a reviewer can approve informed without a PR.
78
138
  function repoMergeShow(manifest) {
@@ -108,7 +168,7 @@ export async function manifestsCommand(args, io) {
108
168
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`);
109
169
  const data = await requireOk(response, 'Manifest list');
110
170
  const manifests = Array.isArray(data.manifests) ? data.manifests : [];
111
- emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'));
171
+ emit(io, { manifests }, manifestsTable(manifests, Date.now()));
112
172
  return 0;
113
173
  }
114
174
  if (subcommand === 'show') {
@@ -135,7 +195,23 @@ export async function manifestsCommand(args, io) {
135
195
  });
136
196
  const data = await requireOk(response, `Manifest ${subcommand}`);
137
197
  const manifest = (data.manifest ?? {});
138
- emit(io, { manifest }, manifestLine(manifest));
198
+ const lines = [manifestLine(manifest)];
199
+ // Reject reaps the session branch best-effort; a failed reap must never be
200
+ // silent (ALI-1043) — surface it with the exact cleanup command.
201
+ if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
202
+ const outcome = asRecord(manifest.outcome);
203
+ const branch = asRecord(manifest.proposedState).branch;
204
+ if (outcome.branchDeleted === true) {
205
+ lines.push('session branch deleted');
206
+ }
207
+ else if (typeof branch === 'string' && branch.length > 0) {
208
+ lines.push(`warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`);
209
+ }
210
+ else {
211
+ lines.push('warning: the session branch could not be deleted (branch name unavailable)');
212
+ }
213
+ }
214
+ emit(io, { manifest }, lines.join('\n'));
139
215
  return 0;
140
216
  }
141
217
  io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -93,6 +93,71 @@ function num(value: unknown): number {
93
93
  return typeof value === 'number' ? value : 0
94
94
  }
95
95
 
96
+ // -- list table (ALI-1038) ----------------------------------------------------
97
+
98
+ /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
99
+ function relativeAge(iso: unknown, now: number): string {
100
+ if (typeof iso !== 'string') return '-'
101
+ const then = Date.parse(iso)
102
+ if (Number.isNaN(then)) return '-'
103
+ const mins = Math.max(0, Math.floor((now - then) / 60_000))
104
+ if (mins < 60) return `${mins}m`
105
+ const hours = Math.floor(mins / 60)
106
+ if (hours < 48) return `${hours}h`
107
+ return `${Math.floor(hours / 24)}d`
108
+ }
109
+
110
+ /** One-glance change summary from the stored proposedState — no live API call. */
111
+ function changesSummary(manifest: Record<string, unknown>): string {
112
+ if (manifest.actionType !== 'repo_merge') return '-'
113
+ const proposed = asRecord(manifest.proposedState)
114
+ const files = num(proposed.filesChanged)
115
+ const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`
116
+ // Instruction files steer agent behavior — reviewers should see the flag in the list.
117
+ return proposed.touches_instruction_files === true ? `${parts} !instr` : parts
118
+ }
119
+
120
+ /** Reviewable-first (pending_approval, then draft), newest first within a group. */
121
+ function listOrder(a: Record<string, unknown>, b: Record<string, unknown>): number {
122
+ const rank = (m: Record<string, unknown>): number =>
123
+ m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2
124
+ const byRank = rank(a) - rank(b)
125
+ if (byRank !== 0) return byRank
126
+ const created = (m: Record<string, unknown>): number =>
127
+ typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0
128
+ return created(b) - created(a)
129
+ }
130
+
131
+ /** Render manifests as the standard padEnd table (matches teams/projects list). */
132
+ function manifestsTable(manifests: Record<string, unknown>[], now: number): string {
133
+ if (manifests.length === 0) return 'promotion manifests\n(none)'
134
+ const rows = [...manifests].sort(listOrder).map(manifest => ({
135
+ id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
136
+ status: typeof manifest.status === 'string' ? manifest.status : '(unknown)',
137
+ type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
138
+ changes: changesSummary(manifest),
139
+ author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
140
+ age: relativeAge(manifest.createdAt, now),
141
+ }))
142
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
143
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
144
+ const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length))
145
+ const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length))
146
+ const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length))
147
+ const header =
148
+ `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
149
+ `${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`
150
+ const divider =
151
+ `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
152
+ `${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`
153
+ const body = rows.map(
154
+ row =>
155
+ `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
156
+ `${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`
157
+ )
158
+ return [header, divider, ...body].join('\n')
159
+ }
160
+
96
161
  // repo_merge manifests (ALI-972) render the compare summary from stored evidence
97
162
  // — no live GitHub call — so a reviewer can approve informed without a PR.
98
163
  function repoMergeShow(manifest: Record<string, unknown>): string {
@@ -130,7 +195,7 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
130
195
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`)
131
196
  const data = await requireOk(response, 'Manifest list')
132
197
  const manifests = Array.isArray(data.manifests) ? (data.manifests as Record<string, unknown>[]) : []
133
- emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'))
198
+ emit(io, { manifests }, manifestsTable(manifests, Date.now()))
134
199
  return 0
135
200
  }
136
201
 
@@ -159,7 +224,23 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
159
224
  })
160
225
  const data = await requireOk(response, `Manifest ${subcommand}`)
161
226
  const manifest = (data.manifest ?? {}) as Record<string, unknown>
162
- emit(io, { manifest }, manifestLine(manifest))
227
+ const lines = [manifestLine(manifest)]
228
+ // Reject reaps the session branch best-effort; a failed reap must never be
229
+ // silent (ALI-1043) — surface it with the exact cleanup command.
230
+ if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
231
+ const outcome = asRecord(manifest.outcome)
232
+ const branch = asRecord(manifest.proposedState).branch
233
+ if (outcome.branchDeleted === true) {
234
+ lines.push('session branch deleted')
235
+ } else if (typeof branch === 'string' && branch.length > 0) {
236
+ lines.push(
237
+ `warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`
238
+ )
239
+ } else {
240
+ lines.push('warning: the session branch could not be deleted (branch name unavailable)')
241
+ }
242
+ }
243
+ emit(io, { manifest }, lines.join('\n'))
163
244
  return 0
164
245
  }
165
246