flowcollab 0.3.6 → 0.3.8
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.
- package/bin/comment.mjs +29 -5
- package/bin/decisions.mjs +101 -5
- package/bin/flow.mjs +2 -2
- package/package.json +1 -1
package/bin/comment.mjs
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* flow:comment — append a free-form comment to a task's timeline.
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
flow-comment <task_id> "<body>"
|
|
6
|
+
flow-comment <task_id> "<body>" --commit=<sha> link a specific commit
|
|
7
|
+
flow-comment <task_id> "<body>" --commit link the current HEAD
|
|
8
|
+
|
|
9
|
+
When a commit is linked, the comment renders a clickable "View changes" diff
|
|
10
|
+
in the board timeline (Theme 34). The commit must be pushed to GitHub to be
|
|
11
|
+
viewable.
|
|
4
12
|
*/
|
|
5
13
|
|
|
6
|
-
import {
|
|
14
|
+
import { execFileSync } from 'child_process';
|
|
15
|
+
import { flowFetch, resolveTaskId, positional, arg, die } from './_client.mjs';
|
|
16
|
+
|
|
17
|
+
function headSha() {
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
20
|
+
} catch {
|
|
21
|
+
die('--commit with no value needs a git repo (could not run `git rev-parse HEAD`). Pass --commit=<sha> instead.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
7
24
|
|
|
8
25
|
async function main() {
|
|
9
26
|
const raw = positional(0);
|
|
10
27
|
const body = positional(1);
|
|
11
|
-
if (!raw || !body) die('Usage: flow-comment <task_id> "<body>"');
|
|
28
|
+
if (!raw || !body) die('Usage: flow-comment <task_id> "<body>" [--commit[=<sha>]]');
|
|
29
|
+
|
|
30
|
+
let commit;
|
|
31
|
+
if (process.argv.includes('--commit') || process.argv.some(a => a.startsWith('--commit='))) {
|
|
32
|
+
commit = arg('commit') || headSha();
|
|
33
|
+
if (!/^[0-9a-f]{7,64}$/i.test(commit)) die(`Invalid commit SHA: ${commit}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
const id = await resolveTaskId(raw);
|
|
13
|
-
await flowFetch('/api/flow/comment', { method: 'POST', body: { task_id: id, body } });
|
|
14
|
-
process.stdout.write('Comment posted.\n');
|
|
37
|
+
await flowFetch('/api/flow/comment', { method: 'POST', body: { task_id: id, body, ...(commit ? { commit } : {}) } });
|
|
38
|
+
process.stdout.write(commit ? `Comment posted (linked commit ${commit.slice(0, 7)}).\n` : 'Comment posted.\n');
|
|
15
39
|
}
|
|
16
40
|
|
|
17
41
|
main().catch(e => die(e.message || e));
|
package/bin/decisions.mjs
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* flow:decisions — list pending decisions.
|
|
3
|
-
|
|
2
|
+
/* flow:decisions — list pending decisions, or watch one until it resolves.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
flow-decisions list pending decisions
|
|
6
|
+
flow-decisions --watch <id> block until decision <id> resolves
|
|
7
|
+
|
|
8
|
+
--watch exit codes (so the Claude Code harness can branch on resume):
|
|
9
|
+
0 approved — proceed with the proposed work
|
|
10
|
+
1 rejected — do not proceed; read the rejection reason
|
|
11
|
+
2 timed out — --timeout elapsed before a human decided
|
|
12
|
+
3 expired — the decision auto-expired before a human decided
|
|
13
|
+
4 error — auth / network / decision not found
|
|
14
|
+
130 interrupted (Ctrl-C)
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
--watch <id> decision id (6-char prefix or full UUID)
|
|
18
|
+
--interval=<sec> poll interval, default 5
|
|
19
|
+
--timeout=<sec> give up after N seconds, default 1800 (30 min); 0 = forever
|
|
4
20
|
*/
|
|
5
21
|
|
|
6
|
-
import { flowFetch, die } from './_client.mjs';
|
|
22
|
+
import { flowFetch, resolveTaskId, arg, positional, die } from './_client.mjs';
|
|
7
23
|
|
|
8
|
-
|
|
24
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
25
|
+
|
|
26
|
+
async function list() {
|
|
9
27
|
const [decRes, tasksRes] = await Promise.all([
|
|
10
28
|
flowFetch('/api/flow/decisions'),
|
|
11
29
|
flowFetch('/api/flow/tasks?limit=500'),
|
|
@@ -30,4 +48,82 @@ async function main() {
|
|
|
30
48
|
}
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
async function watch(rawId) {
|
|
52
|
+
const interval = Math.max(1, Number(arg('interval')) || 5);
|
|
53
|
+
const timeout = arg('timeout') !== undefined ? Math.max(0, Number(arg('timeout'))) : 1800;
|
|
54
|
+
|
|
55
|
+
// Resolve the prefix to a full UUID once, up front, while the decision is
|
|
56
|
+
// still pending (resolveTaskId can only resolve prefixes via the pending
|
|
57
|
+
// list). After this we poll by full UUID, which survives resolution.
|
|
58
|
+
let id;
|
|
59
|
+
try {
|
|
60
|
+
id = await resolveTaskId(rawId);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
die(e.message || String(e), 4);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// No custom SIGINT handler: registering one and then calling process.exit()
|
|
66
|
+
// trips a libuv async-handle assertion on Windows (async.c). Default Ctrl-C
|
|
67
|
+
// behaviour already exits 130, which is the contract we want.
|
|
68
|
+
//
|
|
69
|
+
// We never call process.exit() from inside the loop either — we set
|
|
70
|
+
// process.exitCode and `return`, letting the event loop drain cleanly. An
|
|
71
|
+
// abrupt process.exit() mid-teardown is what triggers the same Windows crash.
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
const short = `#${id.slice(0, 6)}`;
|
|
74
|
+
process.stdout.write(`Watching decision ${short} — polling every ${interval}s${timeout ? `, timeout ${timeout}s` : ' (no timeout)'}.\n`);
|
|
75
|
+
|
|
76
|
+
for (;;) {
|
|
77
|
+
let decision;
|
|
78
|
+
try {
|
|
79
|
+
const r = await flowFetch(`/api/flow/decisions/${id}`);
|
|
80
|
+
decision = r.decision;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// 404 means the decision is gone (deleted/org mismatch) — treat as error.
|
|
83
|
+
process.stderr.write(`flow: watch failed for ${short}: ${e.message || e}\n`);
|
|
84
|
+
process.exitCode = 4;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const status = decision?.status;
|
|
89
|
+
if (status === 'approved') {
|
|
90
|
+
process.stdout.write(`✓ ${short} approved — proceed.\n`);
|
|
91
|
+
process.exitCode = 0;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (status === 'rejected') {
|
|
95
|
+
const reason = decision.rejected_reason ? ` Reason: ${decision.rejected_reason}` : '';
|
|
96
|
+
process.stdout.write(`✗ ${short} rejected — do not proceed.${reason}\n`);
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (status === 'expired') {
|
|
101
|
+
process.stdout.write(`⌛ ${short} expired before a human decided — do not proceed.\n`);
|
|
102
|
+
process.exitCode = 3;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (timeout && (Date.now() - startedAt) / 1000 >= timeout) {
|
|
107
|
+
process.stdout.write(`⌛ ${short} still pending after ${timeout}s — giving up (timed out).\n`);
|
|
108
|
+
process.exitCode = 2;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await sleep(interval * 1000);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
const isWatch = process.argv.slice(2).some(a => a === '--watch' || a.startsWith('--watch='));
|
|
118
|
+
if (isWatch) {
|
|
119
|
+
// arg('watch') handles --watch=<id> and --watch <id>; positional(0) is a
|
|
120
|
+
// fallback for when the value after a bare --watch is itself a flag.
|
|
121
|
+
const watchId = arg('watch') || positional(0);
|
|
122
|
+
if (!watchId) die('Usage: flow-decisions --watch <id>', 4);
|
|
123
|
+
await watch(watchId);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await list();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch(e => die(e.message || e, 4));
|
package/bin/flow.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const CMDS = [
|
|
|
26
26
|
['login', 'login.mjs', 'Authorize the CLI via browser (device flow)'],
|
|
27
27
|
['pull', 'pull.mjs', 'Fetch board snapshot + @mentions (start of day)'],
|
|
28
28
|
['claim', 'claim.mjs', 'Claim a task and mark it in-progress'],
|
|
29
|
-
['comment', 'comment.mjs', 'Post a comment
|
|
29
|
+
['comment', 'comment.mjs', 'Post a comment (--commit[=<sha>] links a clickable diff)'],
|
|
30
30
|
['close', 'close.mjs', 'Mark a task done with a summary'],
|
|
31
31
|
['create', 'create.mjs', 'Create a new task'],
|
|
32
32
|
['edit', 'edit.mjs', 'Update task fields (title, priority, area, due…)'],
|
|
@@ -35,7 +35,7 @@ const CMDS = [
|
|
|
35
35
|
['approve', 'approve.mjs', 'Approve a pending proposal'],
|
|
36
36
|
['reject', 'reject.mjs', 'Reject a pending proposal with a reason'],
|
|
37
37
|
['assign', 'assign.mjs', 'Assign a task to another actor'],
|
|
38
|
-
['decisions', 'decisions.mjs', 'List pending proposals
|
|
38
|
+
['decisions', 'decisions.mjs', 'List pending proposals (--watch <id> blocks until resolved)'],
|
|
39
39
|
['handoff', 'handoff.mjs', 'Hand off a task to another agent'],
|
|
40
40
|
['review', 'review.mjs', 'Request code review; moves task to in-review'],
|
|
41
41
|
['search', 'search.mjs', 'Search tasks by text, status, area, or assignee'],
|