flowcollab 0.1.9 → 0.2.1

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.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /* flow-archive — archive or unarchive a task.
3
+ Archived tasks are hidden from GET /tasks by default; use flow-pull or
4
+ board "Show archived" toggle to see them.
5
+
6
+ Usage:
7
+ flow-archive <task-id> # archive
8
+ flow-archive <task-id> --undo # unarchive
9
+ */
10
+
11
+ import { flowFetch, RESOLVED_ACTOR } from './_client.mjs';
12
+
13
+ const [,, taskId, ...rest] = process.argv;
14
+ const undo = rest.includes('--undo');
15
+
16
+ if (!taskId || taskId.startsWith('-')) {
17
+ process.stderr.write('Usage: flow-archive <task-id> [--undo]\n');
18
+ process.exit(1);
19
+ }
20
+
21
+ const endpoint = undo
22
+ ? `/api/flow/tasks/${encodeURIComponent(taskId)}/unarchive`
23
+ : `/api/flow/tasks/${encodeURIComponent(taskId)}/archive`;
24
+
25
+ const res = await flowFetch(endpoint, { method: 'POST' }).catch(e => {
26
+ process.stderr.write('flow-archive: ' + (e.message || e) + '\n');
27
+ process.exit(1);
28
+ });
29
+
30
+ const t = res.task;
31
+ const ref = `#${t.issue_num ?? taskId.slice(0, 6)}`;
32
+ const verb = undo ? 'Unarchived' : 'Archived';
33
+ process.stdout.write(`${verb} ${ref}: ${t.title}\n`);
@@ -22,7 +22,9 @@ async function main() {
22
22
  process.exit(1);
23
23
  }
24
24
 
25
- const res = await flowFetch(`/api/flow/milestones/${encodeURIComponent(milestone)}/close`, { method: 'POST' });
25
+ const archiveDone = process.argv.includes('--archive-done');
26
+ const qs = archiveDone ? '?archive=true' : '';
27
+ const res = await flowFetch(`/api/flow/milestones/${encodeURIComponent(milestone)}/close${qs}`, { method: 'POST' });
26
28
 
27
29
  const { completed = [], carried_forward = [] } = res;
28
30
  const total = completed.length + carried_forward.length;
@@ -51,7 +53,10 @@ async function main() {
51
53
  out.push('');
52
54
  }
53
55
 
54
- out.push('Done tasks archived (milestone cleared). Carried tasks keep the milestone for the next sprint.');
56
+ const doneLabel = archiveDone
57
+ ? 'Done tasks archived (milestone cleared + archived_at set). Carried tasks keep the milestone for the next sprint.'
58
+ : 'Done tasks archived (milestone cleared). Carried tasks keep the milestone for the next sprint.'
59
+ out.push(doneLabel);
55
60
  process.stdout.write(out.join('\n') + '\n');
56
61
  }
57
62
 
@@ -9,10 +9,10 @@
9
9
  import { positional, die } from './_client.mjs';
10
10
 
11
11
  const COMMANDS = [
12
- 'pull', 'claim', 'comment', 'close', 'create', 'propose', 'approve', 'reject',
13
- 'assign', 'decisions', 'ping', 'whoami', 'heartbeat', 'sync', 'status', 'handoff',
14
- 'standup', 'search', 'init', 'project', 'close-sprint', 'review', 'login',
15
- 'edit', 'unblock', 'log', 'completion',
12
+ 'login', 'pull', 'claim', 'comment', 'close', 'create', 'edit', 'unblock',
13
+ 'propose', 'approve', 'reject', 'assign', 'decisions', 'handoff', 'review',
14
+ 'search', 'status', 'standup', 'sync', 'log', 'heartbeat', 'ping', 'pr',
15
+ 'project', 'archive', 'close-sprint', 'whoami', 'completion',
16
16
  ];
17
17
 
18
18
  const FLAGS = {
@@ -26,29 +26,18 @@ const FLAGS = {
26
26
  review: ['--pr=', '--reviewer=', '--context='],
27
27
  edit: ['--title=', '--priority=', '--area=', '--due=', '--milestone=', '--blocked-by='],
28
28
  log: ['--task=', '--limit='],
29
- 'close-sprint': ['--milestone='],
29
+ archive: ['--undo'],
30
+ 'close-sprint': ['--milestone=', '--archive-done'],
30
31
  };
31
32
 
32
33
  function bashScript() {
33
- const cmds = COMMANDS.map(c => `flow-${c}`).join(' ');
34
+ const hyphenated = COMMANDS.map(c => `flow-${c}`).join(' ');
35
+ const subcmds = COMMANDS.join(' ');
34
36
  return `# Flow CLI bash completion
35
37
  # Source this file or add to ~/.bash_completion.d/flow
36
38
 
37
- _flow_complete() {
38
- local cur prev words
39
- COMPREPLY=()
40
- cur="\${COMP_WORDS[COMP_CWORD]}"
41
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
42
-
43
- local commands="${cmds}"
44
-
45
- if [[ \${COMP_CWORD} -eq 1 ]]; then
46
- COMPREPLY=( \$(compgen -W "\$commands" "\$cur") )
47
- return
48
- fi
49
-
50
- local cmd="\${COMP_WORDS[0]}"
51
- local subcmd="\${cmd#flow-}"
39
+ _flow_subcmd_flags() {
40
+ local subcmd="\$1" cur="\$2"
52
41
  case "\$subcmd" in
53
42
  ${Object.entries(FLAGS).map(([cmd, flags]) => ` ${cmd})
54
43
  COMPREPLY=( \$(compgen -W "${flags.join(' ')}" "\$cur") )
@@ -56,35 +45,85 @@ ${Object.entries(FLAGS).map(([cmd, flags]) => ` ${cmd})
56
45
  esac
57
46
  }
58
47
 
48
+ _flow_complete() {
49
+ local cur prev
50
+ COMPREPLY=()
51
+ cur="\${COMP_WORDS[COMP_CWORD]}"
52
+ local cmd="\${COMP_WORDS[0]}"
53
+
54
+ if [[ "\$cmd" == "flow" ]]; then
55
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
56
+ COMPREPLY=( \$(compgen -W "${subcmds}" "\$cur") )
57
+ return
58
+ fi
59
+ _flow_subcmd_flags "\${COMP_WORDS[1]}" "\$cur"
60
+ else
61
+ local subcmd="\${cmd#flow-}"
62
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
63
+ COMPREPLY=( \$(compgen -W "${hyphenated}" "\$cur") )
64
+ return
65
+ fi
66
+ _flow_subcmd_flags "\$subcmd" "\$cur"
67
+ fi
68
+ }
69
+
70
+ complete -F _flow_complete flow
59
71
  ${COMMANDS.map(c => `complete -F _flow_complete flow-${c}`).join('\n')}
60
72
  `;
61
73
  }
62
74
 
63
75
  function zshScript() {
64
76
  const cmds = COMMANDS.map(c => `flow-${c}`);
65
- return `#compdef ${cmds.join(' ')}
77
+ const subcmdList = COMMANDS.map(c => `'${c}'`).join(' ');
78
+ return `#compdef flow ${cmds.join(' ')}
66
79
  # Flow CLI zsh completion
67
80
  # Place in ~/.zfunc/_flow and add fpath=(~/.zfunc $fpath) to ~/.zshrc
68
81
 
69
- _flow_args() {
70
- local cmd="\${words[1]#flow-}"
71
- case "\$cmd" in
82
+ _flow_flags() {
83
+ local subcmd="\$1"
84
+ case "\$subcmd" in
72
85
  ${Object.entries(FLAGS).map(([cmd, flags]) => ` ${cmd})
73
86
  local opts=(${flags.map(f => `'${f}'`).join(' ')})
74
87
  _arguments "\${opts[@]}"
75
- ;;`).join('\n')}
76
- *)
77
- _arguments '*:arg:'
78
- ;;
88
+ return ;;`).join('\n')}
79
89
  esac
90
+ _arguments '*:arg:'
91
+ }
92
+
93
+ _flow() {
94
+ local state
95
+ if [[ "\${words[1]}" == "flow" ]]; then
96
+ _arguments '1:subcommand:(${subcmdList})' '*:: :->args'
97
+ case "\$state" in args) _flow_flags "\${words[1]}" ;; esac
98
+ else
99
+ _flow_flags "\${words[1]#flow-}"
100
+ fi
80
101
  }
81
102
 
82
- ${cmds.map(c => `compdef _flow_args ${c}`).join('\n')}
103
+ compdef _flow flow
104
+ ${cmds.map(c => `compdef _flow ${c}`).join('\n')}
83
105
  `;
84
106
  }
85
107
 
86
108
  function fishScript() {
87
109
  const lines = [];
110
+ // Parent 'flow' command: subcommand completions
111
+ lines.push('# flow (parent command)');
112
+ for (const cmd of COMMANDS) {
113
+ lines.push(`complete -c flow -f -n '__fish_use_subcommand' -a ${cmd}`);
114
+ }
115
+ lines.push('');
116
+ // Per-subcommand flags for 'flow <sub>' form
117
+ for (const [cmd, flags] of Object.entries(FLAGS)) {
118
+ lines.push(`# flow ${cmd}`);
119
+ for (const f of flags) {
120
+ const name = f.replace(/^--/, '').replace(/=$/, '');
121
+ const hasArg = f.endsWith('=');
122
+ lines.push(`complete -c flow -n '__fish_seen_subcommand_from ${cmd}' -l ${name}${hasArg ? ' -r' : ''}`);
123
+ }
124
+ }
125
+ lines.push('');
126
+ // Hyphenated aliases
88
127
  for (const cmd of COMMANDS) {
89
128
  lines.push(`# flow-${cmd}`);
90
129
  const flags = FLAGS[cmd] || [];
package/bin/flow.mjs ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /* flow — unified CLI router.
3
+ Usage: flow <command> [args]
4
+ Run 'flow --help' for the full command list.
5
+ */
6
+
7
+ import { spawnSync } from 'child_process';
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname } from 'path';
11
+
12
+ // Mirror the _client.mjs TLS re-spawn so the child sees --use-system-ca
13
+ // and doesn't re-spawn a third time (checks process.execArgv).
14
+ if (process.platform === 'win32' && !process.execArgv.includes('--use-system-ca')) {
15
+ const r = spawnSync(process.execPath, ['--use-system-ca', ...process.argv.slice(1)], {
16
+ stdio: 'inherit',
17
+ env: process.env,
18
+ });
19
+ process.exit(r.status ?? 1);
20
+ }
21
+
22
+ const __dir = dirname(fileURLToPath(import.meta.url));
23
+
24
+ // ── Command registry ────────────────────────────────────────────────────────
25
+ const CMDS = [
26
+ ['login', 'login.mjs', 'Authorize the CLI via browser (device flow)'],
27
+ ['pull', 'pull.mjs', 'Fetch board snapshot + @mentions (start of day)'],
28
+ ['claim', 'claim.mjs', 'Claim a task and mark it in-progress'],
29
+ ['comment', 'comment.mjs', 'Post a comment on a task'],
30
+ ['close', 'close.mjs', 'Mark a task done with a summary'],
31
+ ['create', 'create.mjs', 'Create a new task'],
32
+ ['edit', 'edit.mjs', 'Update task fields (title, priority, area, due…)'],
33
+ ['unblock', 'unblock.mjs', 'Clear a task\'s blocked-by dependency'],
34
+ ['propose', 'propose.mjs', 'Propose a sub-task for human approval'],
35
+ ['approve', 'approve.mjs', 'Approve a pending proposal'],
36
+ ['reject', 'reject.mjs', 'Reject a pending proposal with a reason'],
37
+ ['assign', 'assign.mjs', 'Assign a task to another actor'],
38
+ ['decisions', 'decisions.mjs', 'List pending proposals awaiting approval'],
39
+ ['handoff', 'handoff.mjs', 'Hand off a task to another agent'],
40
+ ['review', 'review.mjs', 'Request code review; moves task to in-review'],
41
+ ['search', 'search.mjs', 'Search tasks by text, status, area, or assignee'],
42
+ ['status', 'status.mjs', 'Quick board summary'],
43
+ ['standup', 'standup.mjs', 'Done/in-progress digest; --velocity for trend chart'],
44
+ ['sync', 'sync.mjs', 'Delta sync since N minutes (default 60)'],
45
+ ['log', 'log.mjs', 'Tail recent timeline events'],
46
+ ['heartbeat', 'heartbeat.mjs', 'Send a presence ping (~60s while active)'],
47
+ ['ping', 'ping.mjs', 'Health check or send a direct agent message'],
48
+ ['pr', 'pr.mjs', 'Record a PR link on a task timeline'],
49
+ ['project', 'project.mjs', 'List GitHub Projects v2 items'],
50
+ ['archive', 'archive.mjs', 'Archive or unarchive a task (--undo to restore)'],
51
+ ['close-sprint', 'close-sprint.mjs', 'Close a sprint milestone (owner only)'],
52
+ ['whoami', 'whoami.mjs', 'Verify token and show current identity'],
53
+ ['completion', 'completion.mjs', 'Print shell completion script (bash/zsh/fish)'],
54
+ ];
55
+
56
+ const CMD_MAP = new Map(CMDS.map(([name, file]) => [name, file]));
57
+
58
+ // ── Help ────────────────────────────────────────────────────────────────────
59
+ function printHelp() {
60
+ let version = '';
61
+ try {
62
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
63
+ version = ` v${pkg.version}`;
64
+ } catch {}
65
+
66
+ process.stdout.write(`flow${version} — FlowCollab CLI\n\n`);
67
+ process.stdout.write(`Usage: flow <command> [options]\n\n`);
68
+ process.stdout.write(`Commands:\n`);
69
+ const pad = Math.max(...CMDS.map(([n]) => n.length)) + 2;
70
+ for (const [name, , desc] of CMDS) {
71
+ process.stdout.write(` ${name.padEnd(pad)}${desc}\n`);
72
+ }
73
+ process.stdout.write(`\nRun 'flow <command> --help' for command-specific flags.\n`);
74
+ process.stdout.write(`Hyphenated aliases (flow-login, flow-pull, …) remain supported.\n`);
75
+ }
76
+
77
+ // ── Dispatch ────────────────────────────────────────────────────────────────
78
+ const [, , sub, ...rest] = process.argv;
79
+
80
+ if (!sub || sub === '--help' || sub === '-h') {
81
+ printHelp();
82
+ process.exit(0);
83
+ }
84
+
85
+ if (sub === '--version' || sub === '-v') {
86
+ try {
87
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
88
+ process.stdout.write(`${version}\n`);
89
+ } catch {
90
+ process.stdout.write('unknown\n');
91
+ }
92
+ process.exit(0);
93
+ }
94
+
95
+ const target = CMD_MAP.get(sub);
96
+ if (!target) {
97
+ process.stderr.write(`flow: unknown command '${sub}'\nRun 'flow --help' for the command list.\n`);
98
+ process.exit(1);
99
+ }
100
+
101
+ // Splice the subcommand out of argv so target module sees clean args.
102
+ // e.g. [node, flow.mjs, pull, --focus] → [node, flow.mjs, --focus]
103
+ // _client.mjs's arg()/positional() use process.argv.slice(2), so they
104
+ // read [--focus] — identical to what flow-pull sees when called directly.
105
+ process.argv.splice(2, 1);
106
+
107
+ await import(new URL(target, import.meta.url));
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "flowcollab",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Multi-Claude coordination layer — shared task board + CLI for teams running Claude Code",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "bin/"
8
8
  ],
9
9
  "bin": {
10
+ "flow": "bin/flow.mjs",
10
11
  "flow-create": "bin/create.mjs",
11
12
  "flow-pull": "bin/pull.mjs",
12
13
  "flow-claim": "bin/claim.mjs",
@@ -33,7 +34,8 @@
33
34
  "flow-edit": "bin/edit.mjs",
34
35
  "flow-unblock": "bin/unblock.mjs",
35
36
  "flow-log": "bin/log.mjs",
36
- "flow-completion": "bin/completion.mjs"
37
+ "flow-completion": "bin/completion.mjs",
38
+ "flow-archive": "bin/archive.mjs"
37
39
  },
38
40
  "scripts": {
39
41
  "build": "node scripts/build.mjs",