flowcollab 0.3.15 → 0.3.17

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/_client.mjs CHANGED
@@ -110,9 +110,11 @@ export async function flowFetch(path, { method = 'GET', body } = {}) {
110
110
  'content-type': 'application/json',
111
111
  };
112
112
  if (actingViaClaude()) headers['x-flow-acting-via'] = 'claude';
113
- const org = process.env.FLOW_ORG_ID || _gc.orgId || '';
113
+ // CLI-5 (audit): config-first then env (consistent with token at 94 + actor at 138) so a stale env
114
+ // var can't override a logged-in session's org/project.
115
+ const org = _gc.orgId || process.env.FLOW_ORG_ID || '';
114
116
  if (org) headers['x-flow-org'] = org;
115
- const proj = process.env.FLOW_PROJECT_ID || _gc.projectId || '';
117
+ const proj = _gc.projectId || process.env.FLOW_PROJECT_ID || '';
116
118
  if (proj) headers['x-flow-project'] = proj;
117
119
 
118
120
  const res = await fetch(url, {
@@ -179,7 +181,9 @@ export function arg(name, fallback) {
179
181
  }
180
182
 
181
183
  export function positional(idx) {
182
- const pos = process.argv.slice(2).filter(a => !a.startsWith('--'));
184
+ // CLI-3 (audit): only filter REAL flag tokens (--word / --word=value), not a free-text positional that
185
+ // merely begins with '--' (e.g. a flow-comment body "-- reverted the change" was being dropped).
186
+ const pos = process.argv.slice(2).filter(a => !/^--[a-zA-Z][\w-]*(=|$)/.test(a));
183
187
  return pos[idx];
184
188
  }
185
189
 
package/bin/create.mjs CHANGED
@@ -29,7 +29,12 @@ async function fetchGithubIssue(issueNum) {
29
29
  if (!repo) die('FLOW_GITHUB_REPO is not set. Set it in .env to use --from-issue.');
30
30
  const url = `https://api.github.com/repos/${repo}/issues/${issueNum}`;
31
31
  const res = await fetch(url, { headers: githubHeaders() });
32
- if (res.status === 404) die(`GitHub Issue #${issueNum} not found in ${repo}.`);
32
+ // CLI-4 (audit): a PRIVATE repo with no token returns 404 (unauthenticated) — make that legible
33
+ // instead of a misleading "not found" (githubHeaders() omits Authorization when no token is set).
34
+ if (res.status === 404) {
35
+ const hint = process.env.FLOW_GITHUB_TOKEN ? '' : ' — if it is a PRIVATE repo, set FLOW_GITHUB_TOKEN (none is set)';
36
+ die(`GitHub Issue #${issueNum} not found in ${repo}${hint}.`);
37
+ }
33
38
  if (!res.ok) die(`GitHub API error ${res.status} fetching issue #${issueNum}.`);
34
39
  return res.json();
35
40
  }
package/bin/edit.mjs CHANGED
@@ -20,7 +20,7 @@ const FIELD_MAP = {
20
20
  title: v => ({ title: v }),
21
21
  priority: v => ({ priority: normalizePriority(v) }),
22
22
  area: v => ({ area: v }),
23
- due: v => ({ due_at: /^\d{4}-\d{2}-\d{2}$/.test(v) ? v + 'T12:00:00Z' : v }),
23
+ due: v => { if (!/^\d{4}-\d{2}-\d{2}$/.test(v)) die(`--due must be YYYY-MM-DD (got: ${v})`); return { due_at: v + 'T12:00:00Z' }; }, // CLI-7 (audit): validate instead of sending a bad string (e.g. "tomorrow") through raw
24
24
  milestone: v => ({ milestone: v }),
25
25
  'blocked-by': v => ({ blocked_by: v === 'null' ? [] : v.split(',').map(s => s.trim()).filter(Boolean) }), // B9: split the comma list; prefixes are resolved after the loop
26
26
  };
package/bin/pull.mjs CHANGED
@@ -420,15 +420,19 @@ async function main() {
420
420
  process.stderr.write(`flow-pull: --sync-md failed: ${e.message}\n`);
421
421
  }
422
422
 
423
- // Write project_id to global config (atomic, 0o600-enforced — see saveGlobalConfig)
424
- try {
425
- const { projects } = await flowFetch('/api/flow/projects');
426
- const defaultProject = projects?.find(p => p.slug === 'main') || projects?.[0];
427
- if (defaultProject?.id) {
428
- saveGlobalConfig({ projectId: defaultProject.id });
429
- process.stdout.write(` project_id ~/.flow/config.json (project: ${defaultProject.slug})\n`);
430
- }
431
- } catch { /* non-fatal */ }
423
+ // Write project_id to global config (atomic, 0o600-enforced — see saveGlobalConfig).
424
+ // CLI-1 (audit): only SEED it when none is resolved yet — don't clobber a project the user already
425
+ // selected (the old code overwrote it with 'main'/the oldest project on every --sync-md).
426
+ if (!RESOLVED_PROJECT) {
427
+ try {
428
+ const { projects } = await flowFetch('/api/flow/projects');
429
+ const defaultProject = projects?.find(p => p.slug === 'main') || projects?.[0];
430
+ if (defaultProject?.id) {
431
+ saveGlobalConfig({ projectId: defaultProject.id });
432
+ process.stdout.write(` project_id → ~/.flow/config.json (project: ${defaultProject.slug})\n`);
433
+ }
434
+ } catch { /* non-fatal */ }
435
+ }
432
436
  }
433
437
  }
434
438
 
package/bin/scan.mjs CHANGED
@@ -228,8 +228,9 @@ async function main() {
228
228
  if (all || doTodos) {
229
229
  process.stdout.write(' [todos] scanning...\n');
230
230
  const s = scanTodos(files, dir);
231
- if (doTodos) { printSection('TODO / FIXME items', s); return; }
232
- if (all && s.length) printSection('TODO / FIXME items', s);
231
+ // CLI-2 (audit): print when explicitly requested OR (all + results); NO early return, so combining
232
+ // focused flags (e.g. `flow-scan --todos --issues`) runs every requested scan, not only the first.
233
+ if (doTodos || (all && s.length)) printSection('TODO / FIXME items', s);
233
234
  else if (all) process.stdout.write('\n[todos] None found.\n');
234
235
  }
235
236
 
@@ -241,10 +242,8 @@ async function main() {
241
242
  // no FLOW_GITHUB_REPO reports "Skipped" instead of a misleading "Untracked Issues (0) None found".
242
243
  if (s === null) process.stdout.write('\n[issues] Skipped — FLOW_GITHUB_REPO not set.\n');
243
244
  else if (issueErr) process.stdout.write('\n[issues] Scan failed — check FLOW_GITHUB_REPO and token.\n');
244
- else if (doIssues) { printSection('Untracked GitHub Issues', s); return; }
245
- else if (all && s.length) printSection('Untracked GitHub Issues', s);
245
+ else if (doIssues || (all && s.length)) printSection('Untracked GitHub Issues', s); // CLI-2 (audit): no early return — flags combine
246
246
  else if (all) process.stdout.write('\n[issues] All open Issues are tracked in Flow.\n');
247
- if (doIssues) return; // explicit --issues exits after its own report (Skipped/error already printed)
248
247
  }
249
248
 
250
249
  if (all || doPRs) {
@@ -254,17 +253,14 @@ async function main() {
254
253
  // B14 (audit): null/error before the explicit-flag return (see the issues block above).
255
254
  if (s === null) process.stdout.write('\n[prs] Skipped — FLOW_GITHUB_REPO not set.\n');
256
255
  else if (prErr) process.stdout.write('\n[prs] Scan failed — check FLOW_GITHUB_REPO and token.\n');
257
- else if (doPRs) { printSection('Unlinked open PRs', s); return; }
258
- else if (all && s.length) printSection('Unlinked open PRs', s);
256
+ else if (doPRs || (all && s.length)) printSection('Unlinked open PRs', s); // CLI-2 (audit): no early return — flags combine
259
257
  else if (all) process.stdout.write('\n[prs] All open PRs are linked to Flow tasks.\n');
260
- if (doPRs) return; // explicit --prs exits after its own report
261
258
  }
262
259
 
263
260
  if (all || doSecurity) {
264
261
  process.stdout.write(' [security] scanning patterns...\n');
265
262
  const s = scanSecurity(dir);
266
- if (doSecurity) { printSection('Security concerns', s); return; }
267
- if (all && s.length) printSection('Security concerns', s);
263
+ if (doSecurity || (all && s.length)) printSection('Security concerns', s); // CLI-2 (audit): no early return — flags combine
268
264
  else if (all) process.stdout.write('\n[security] No common patterns found.\n');
269
265
  }
270
266
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowcollab",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "Multi-Claude coordination layer — shared task board + CLI for teams running Claude Code",
5
5
  "type": "module",
6
6
  "files": [