flowcollab 0.3.10 → 0.3.12
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 +27 -3
- package/bin/completion.mjs +1 -1
- package/bin/decisions.mjs +8 -1
- package/bin/flow.mjs +9 -4
- package/bin/login.mjs +8 -2
- package/bin/pull.mjs +20 -10
- package/package.json +6 -2
package/bin/_client.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import 'dotenv/config';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
|
|
13
13
|
// On Windows, Node's bundled CA store doesn't include certs added by corporate/AV
|
|
@@ -15,10 +15,16 @@ import { join } from 'path';
|
|
|
15
15
|
// --use-system-ca so Node trusts the Windows certificate store instead.
|
|
16
16
|
// Requires Node 22+; on older Node the child exits with "bad option" which tells
|
|
17
17
|
// the user to upgrade.
|
|
18
|
-
|
|
18
|
+
// FLOW_TLS_RESPAWNED guards against an infinite re-spawn loop: if a Node build
|
|
19
|
+
// ever drops --use-system-ca from execArgv, the execArgv check alone would
|
|
20
|
+
// re-spawn forever. The env flag survives across the spawn, so we only ever
|
|
21
|
+
// re-spawn once.
|
|
22
|
+
if (process.platform === 'win32'
|
|
23
|
+
&& !process.execArgv.includes('--use-system-ca')
|
|
24
|
+
&& process.env.FLOW_TLS_RESPAWNED !== '1') {
|
|
19
25
|
const r = spawnSync(process.execPath, ['--use-system-ca', ...process.argv.slice(1)], {
|
|
20
26
|
stdio: 'inherit',
|
|
21
|
-
env: process.env,
|
|
27
|
+
env: { ...process.env, FLOW_TLS_RESPAWNED: '1' },
|
|
22
28
|
});
|
|
23
29
|
process.exit(r.status ?? 1);
|
|
24
30
|
}
|
|
@@ -54,6 +60,24 @@ function loadGlobalConfig() {
|
|
|
54
60
|
}
|
|
55
61
|
const _gc = loadGlobalConfig();
|
|
56
62
|
|
|
63
|
+
// Merge `patch` into ~/.flow/config.json and persist atomically. writeFileSync's
|
|
64
|
+
// `mode` is only honoured when the file is *created*, so an existing config keeps
|
|
65
|
+
// its old (possibly world-readable) perms — we chmod 0o600 explicitly. The temp +
|
|
66
|
+
// rename keeps the file from being truncated to garbage if the process dies mid-write.
|
|
67
|
+
export function saveGlobalConfig(patch) {
|
|
68
|
+
const dir = join(homedir(), '.flow');
|
|
69
|
+
const target = join(dir, 'config.json');
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
let existing = {};
|
|
72
|
+
try { existing = JSON.parse(readFileSync(target, 'utf8')); } catch {}
|
|
73
|
+
const merged = { ...existing, ...patch };
|
|
74
|
+
const tmp = join(dir, `config.json.${process.pid}.tmp`);
|
|
75
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
76
|
+
try { chmodSync(tmp, 0o600); } catch {}
|
|
77
|
+
renameSync(tmp, target);
|
|
78
|
+
return merged;
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
// flow-login writes apiBase + token to ~/.flow/config.json — both take precedence over
|
|
58
82
|
// env vars so a stale FLOW_API_BASE / FLOW_API_TOKEN_* doesn't silently override the
|
|
59
83
|
// user's authenticated session. Env vars are still honoured when config.json has no value.
|
package/bin/completion.mjs
CHANGED
|
@@ -16,7 +16,7 @@ const COMMANDS = [
|
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
const FLAGS = {
|
|
19
|
-
pull: ['--focus', '--milestone=', '--sync-md'],
|
|
19
|
+
pull: ['--focus', '--milestone=', '--sync-md', '--force'],
|
|
20
20
|
claim: ['--files=', '--watch'],
|
|
21
21
|
create: ['--title=', '--type=', '--area=', '--priority=', '--due=', '--blocked-by=', '--milestone=', '--from-issue=', '--from-project-item=', '--template='],
|
|
22
22
|
handoff: ['--task=', '--to=', '--context=', '--branch=', '--questions=', '--next-step='],
|
package/bin/decisions.mjs
CHANGED
|
@@ -66,7 +66,14 @@ async function watch(rawId) {
|
|
|
66
66
|
try {
|
|
67
67
|
id = await resolveTaskId(rawId);
|
|
68
68
|
} catch (e) {
|
|
69
|
-
|
|
69
|
+
// B5: resolveTaskId only sees PENDING decisions, so a prefix for an ALREADY-RESOLVED
|
|
70
|
+
// decision wasn't found → exit 4 instead of its real status. Fall back to the server,
|
|
71
|
+
// which resolves a prefix among all the org's decisions regardless of status.
|
|
72
|
+
try {
|
|
73
|
+
const r = await flowFetch(`/api/flow/decisions/${encodeURIComponent(rawId.replace(/^#/, ''))}`);
|
|
74
|
+
id = r.decision?.id;
|
|
75
|
+
} catch { /* fall through to the original resolve error below */ }
|
|
76
|
+
if (!id) die(e.message || String(e), 4);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
// No custom SIGINT handler: registering one and then calling process.exit()
|
package/bin/flow.mjs
CHANGED
|
@@ -9,12 +9,17 @@ import { readFileSync } from 'fs';
|
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname } from 'path';
|
|
11
11
|
|
|
12
|
-
// Mirror the _client.mjs TLS re-spawn so the child sees --use-system-ca
|
|
13
|
-
//
|
|
14
|
-
if
|
|
12
|
+
// Mirror the _client.mjs TLS re-spawn so the child sees --use-system-ca.
|
|
13
|
+
// FLOW_TLS_RESPAWNED is a belt-and-suspenders guard against an infinite loop
|
|
14
|
+
// if a Node build ever drops --use-system-ca from execArgv (the env flag
|
|
15
|
+
// survives the spawn, so we re-spawn at most once). The router child also
|
|
16
|
+
// hands this flag to the subcommand, so _client.mjs won't re-spawn a third time.
|
|
17
|
+
if (process.platform === 'win32'
|
|
18
|
+
&& !process.execArgv.includes('--use-system-ca')
|
|
19
|
+
&& process.env.FLOW_TLS_RESPAWNED !== '1') {
|
|
15
20
|
const r = spawnSync(process.execPath, ['--use-system-ca', ...process.argv.slice(1)], {
|
|
16
21
|
stdio: 'inherit',
|
|
17
|
-
env: process.env,
|
|
22
|
+
env: { ...process.env, FLOW_TLS_RESPAWNED: '1' },
|
|
18
23
|
});
|
|
19
24
|
process.exit(r.status ?? 1);
|
|
20
25
|
}
|
package/bin/login.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
flow-login [--server=https://flow-production-84b7.up.railway.app]
|
|
5
5
|
*/
|
|
6
6
|
import { homedir } from 'os';
|
|
7
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync, chmodSync, renameSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { exec } from 'child_process';
|
|
10
10
|
|
|
@@ -74,11 +74,17 @@ async function main() {
|
|
|
74
74
|
const configPath = join(dir, 'config.json');
|
|
75
75
|
let existing = {};
|
|
76
76
|
try { existing = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
77
|
+
// Atomic write + explicit chmod: writeFileSync's `mode` is ignored when the
|
|
78
|
+
// file already exists, so an old config would keep looser perms; the temp +
|
|
79
|
+
// rename avoids a truncated config if the process dies mid-write.
|
|
80
|
+
const tmp = join(dir, `config.json.${process.pid}.tmp`);
|
|
77
81
|
writeFileSync(
|
|
78
|
-
|
|
82
|
+
tmp,
|
|
79
83
|
JSON.stringify({ ...existing, apiBase: api_base || base, token, actorId: actor_id, orgId: org_id }, null, 2) + '\n',
|
|
80
84
|
{ encoding: 'utf8', mode: 0o600 },
|
|
81
85
|
);
|
|
86
|
+
try { chmodSync(tmp, 0o600); } catch {}
|
|
87
|
+
renameSync(tmp, configPath);
|
|
82
88
|
process.stdout.write('\n\n');
|
|
83
89
|
process.stdout.write(`✓ Logged in as ${actor_id}\n`);
|
|
84
90
|
process.stdout.write(` Config saved to ~/.flow/config.json\n`);
|
package/bin/pull.mjs
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
3. Recent timeline activity on YOUR tasks (last 5 per task)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { flowFetch, arg, RESOLVED_ACTOR, RESOLVED_PROJECT } from './_client.mjs';
|
|
15
|
+
import { flowFetch, arg, saveGlobalConfig, RESOLVED_ACTOR, RESOLVED_PROJECT } from './_client.mjs';
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
17
|
-
import { homedir } from 'os';
|
|
18
17
|
import { join } from 'path';
|
|
19
18
|
|
|
20
19
|
// Per-status age threshold (days) before pull flags a task as aging.
|
|
@@ -399,22 +398,33 @@ async function main() {
|
|
|
399
398
|
const mdText = mdRes.raw || (typeof mdRes === 'string' ? mdRes : null);
|
|
400
399
|
if (!mdText) throw new Error('empty response');
|
|
401
400
|
const dest = join(process.cwd(), 'CLAUDE.md');
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
const force = arg('force') !== undefined;
|
|
402
|
+
// Don't blindly clobber a CLAUDE.md that has local edits — back it up first
|
|
403
|
+
// (unless --force) so a sync can never silently destroy hand-written context.
|
|
404
|
+
if (existsSync(dest) && !force) {
|
|
405
|
+
const current = readFileSync(dest, 'utf8');
|
|
406
|
+
if (current === mdText) {
|
|
407
|
+
process.stdout.write(`\n✓ CLAUDE.md already up to date (${dest})\n`);
|
|
408
|
+
} else {
|
|
409
|
+
const backup = dest + '.bak';
|
|
410
|
+
writeFileSync(backup, current, 'utf8');
|
|
411
|
+
writeFileSync(dest, mdText, 'utf8');
|
|
412
|
+
process.stdout.write(`\n✓ CLAUDE.md updated (${dest})\n Previous version backed up → ${backup} (pass --force to skip the backup)\n`);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
writeFileSync(dest, mdText, 'utf8');
|
|
416
|
+
process.stdout.write(`\n✓ CLAUDE.md ${force && existsSync(dest) ? 'overwritten (--force)' : 'written'} (${dest})\n`);
|
|
417
|
+
}
|
|
404
418
|
} catch (e) {
|
|
405
419
|
process.stderr.write(`flow-pull: --sync-md failed: ${e.message}\n`);
|
|
406
420
|
}
|
|
407
421
|
|
|
408
|
-
// Write project_id to global config
|
|
422
|
+
// Write project_id to global config (atomic, 0o600-enforced — see saveGlobalConfig)
|
|
409
423
|
try {
|
|
410
424
|
const { projects } = await flowFetch('/api/flow/projects');
|
|
411
425
|
const defaultProject = projects?.find(p => p.slug === 'main') || projects?.[0];
|
|
412
426
|
if (defaultProject?.id) {
|
|
413
|
-
|
|
414
|
-
let gc = {};
|
|
415
|
-
try { gc = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
416
|
-
gc.projectId = defaultProject.id;
|
|
417
|
-
writeFileSync(configPath, JSON.stringify(gc, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
427
|
+
saveGlobalConfig({ projectId: defaultProject.id });
|
|
418
428
|
process.stdout.write(` project_id → ~/.flow/config.json (project: ${defaultProject.slug})\n`);
|
|
419
429
|
}
|
|
420
430
|
} catch { /* non-fatal */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowcollab",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Multi-Claude coordination layer — shared task board + CLI for teams running Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -42,7 +42,10 @@
|
|
|
42
42
|
"build": "node scripts/build.mjs",
|
|
43
43
|
"dev": "node scripts/build.mjs --watch",
|
|
44
44
|
"start": "node scripts/build.mjs && node --use-system-ca server.js",
|
|
45
|
-
"start:prod": "node scripts/build.mjs && node server.js"
|
|
45
|
+
"start:prod": "node scripts/build.mjs && node server.js",
|
|
46
|
+
"test": "node --test test/*.test.js",
|
|
47
|
+
"test:integration": "node --test test/*.integration.test.mjs",
|
|
48
|
+
"test:ui": "node --test test/*.browser.test.mjs"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
51
|
"dotenv": "^16.4.7"
|
|
@@ -53,6 +56,7 @@
|
|
|
53
56
|
"express": "^4.0.0 || ^5.0.0",
|
|
54
57
|
"express-rate-limit": "^7.5.0",
|
|
55
58
|
"helmet": "^8.0.0",
|
|
59
|
+
"playwright": "^1.60.0",
|
|
56
60
|
"stripe": "^22.2.0",
|
|
57
61
|
"zod": "^3.24.1"
|
|
58
62
|
},
|