flowcollab 0.3.22 → 0.3.24
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/_commands.mjs +4 -2
- package/bin/scaffold-ci.mjs +82 -0
- package/bin/scan.mjs +59 -4
- package/package.json +4 -23
- package/LICENSE +0 -6
package/bin/_commands.mjs
CHANGED
|
@@ -66,8 +66,10 @@ export const CLI_COMMANDS = [
|
|
|
66
66
|
usage: 'flow-project [--project=<node_id>] — list GitHub Projects v2 items' },
|
|
67
67
|
{ name: 'archive', file: 'archive.mjs', summary: 'Archive or unarchive a task (--undo to restore)',
|
|
68
68
|
usage: 'flow-archive <id> [--undo] — archive/unarchive a task (owner only)' },
|
|
69
|
-
{ name: 'scan', file: 'scan.mjs', summary: 'Audit for TODOs,
|
|
70
|
-
usage: 'flow-scan [--todos] [--issues] [--prs] [--security] — audit codebase for untracked work' },
|
|
69
|
+
{ name: 'scan', file: 'scan.mjs', summary: 'Audit for TODOs, Issues, PRs, security patterns, untested changes',
|
|
70
|
+
usage: 'flow-scan [--todos] [--issues] [--prs] [--security] [--untested] — audit codebase for untracked work' },
|
|
71
|
+
{ name: 'scaffold-ci', file: 'scaffold-ci.mjs', summary: 'Drop a starter CI workflow + test skeleton into the repo',
|
|
72
|
+
usage: 'flow-scaffold-ci [--force] [--print] — write .github/workflows/ci.yml + a test skeleton (Theme 35 P4)' },
|
|
71
73
|
{ name: 'close-sprint', file: 'close-sprint.mjs', summary: 'Close a sprint milestone (owner only)',
|
|
72
74
|
usage: 'flow-close-sprint --milestone=<name> [--archive-done] — owner only' },
|
|
73
75
|
{ name: 'whoami', file: 'whoami.mjs', summary: 'Verify token and show current identity',
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* flow:scaffold-ci — drop a starter CI workflow + a test skeleton into the current repo, so a
|
|
3
|
+
Flow team gets a test/CI safety net by default instead of discovering it has none. Mirrors the
|
|
4
|
+
pattern Flow itself dogfoods: `node --check` over the code surface + `node --test`.
|
|
5
|
+
|
|
6
|
+
Usage: flow-scaffold-ci [--force] [--print]
|
|
7
|
+
--print write nothing; print the files to stdout
|
|
8
|
+
--force overwrite existing files (default: skip + warn)
|
|
9
|
+
Purely local — writes files, makes no network/API calls.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
|
+
|
|
15
|
+
function die(msg) { process.stderr.write('flow-scaffold-ci: ' + msg + '\n'); process.exit(1); }
|
|
16
|
+
|
|
17
|
+
const CI_YML = `name: CI
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
branches: [main]
|
|
21
|
+
pull_request:
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
test:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: 22
|
|
31
|
+
- name: Install deps
|
|
32
|
+
if: \${{ hashFiles('package-lock.json') != '' }}
|
|
33
|
+
run: npm ci
|
|
34
|
+
- name: Syntax-check JS
|
|
35
|
+
run: git ls-files '*.js' '*.mjs' | xargs -r -n1 node --check
|
|
36
|
+
- name: Test
|
|
37
|
+
run: node --test
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const SMOKE_TEST = `/* Smoke test — the seed of your suite. Runs with \`node --test\` (no deps).
|
|
41
|
+
Replace with real tests as you build; \`flow scan --untested\` flags changed
|
|
42
|
+
files that still have none. */
|
|
43
|
+
import { test } from 'node:test';
|
|
44
|
+
import assert from 'node:assert/strict';
|
|
45
|
+
|
|
46
|
+
test('the test runner works', () => {
|
|
47
|
+
assert.equal(1 + 1, 2);
|
|
48
|
+
});
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const FILES = [
|
|
52
|
+
{ path: '.github/workflows/ci.yml', body: CI_YML },
|
|
53
|
+
{ path: 'test/smoke.test.mjs', body: SMOKE_TEST },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function main() {
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
|
|
59
|
+
if (args.includes('--print')) {
|
|
60
|
+
for (const f of FILES) process.stdout.write(`\n===== ${f.path} =====\n${f.body}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const force = args.includes('--force');
|
|
65
|
+
let wrote = 0, skipped = 0;
|
|
66
|
+
for (const f of FILES) {
|
|
67
|
+
if (existsSync(f.path) && !force) {
|
|
68
|
+
process.stdout.write(`skip ${f.path} (exists — pass --force to overwrite)\n`);
|
|
69
|
+
skipped++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
mkdirSync(dirname(f.path), { recursive: true });
|
|
73
|
+
writeFileSync(f.path, f.body);
|
|
74
|
+
process.stdout.write(`write ${f.path}\n`);
|
|
75
|
+
wrote++;
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write(`\n${wrote} written, ${skipped} skipped.`);
|
|
78
|
+
if (wrote) process.stdout.write(` Ensure package.json has a "test" script (or rely on \`node --test\`), commit, and push.`);
|
|
79
|
+
process.stdout.write('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try { main(); } catch (e) { die(e.message || e); }
|
package/bin/scan.mjs
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
Outputs numbered suggestions with ready-to-run flow-create commands.
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
|
-
flow-scan [--todos] [--issues] [--prs] [--security]
|
|
6
|
+
flow-scan [--todos] [--issues] [--prs] [--security] [--untested]
|
|
7
7
|
[--dir=<path>] default: cwd
|
|
8
8
|
[--repo=<owner/repo>] default: FLOW_GITHUB_REPO env var
|
|
9
|
-
(no flags = run all
|
|
9
|
+
(no flags = run all scans)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import 'dotenv/config';
|
|
13
13
|
import { flowFetch, arg, die, sanitizeText } from './_client.mjs';
|
|
14
14
|
import { githubHeaders } from './_github.mjs';
|
|
15
15
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
16
17
|
import { join, extname, relative } from 'node:path';
|
|
17
18
|
|
|
18
19
|
// ── File walker ────────────────────────────────────────────────────────────────
|
|
@@ -209,6 +210,52 @@ function scanSecurity(dir) {
|
|
|
209
210
|
return out;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
// ── Untested-changes scan (Theme 35 P4) ─────────────────────────────────────────
|
|
214
|
+
// Flag changed source files that have no corresponding test. "Covered" = a *.test.* / *.spec.*
|
|
215
|
+
// file, or any file under a test/tests/__tests__/spec dir, whose basename matches the source's.
|
|
216
|
+
function isTestFile(p) {
|
|
217
|
+
return /\.(test|spec)\.[cm]?[jt]sx?$/i.test(p) || /(^|\/)(test|tests|__tests__|spec)\//i.test(p);
|
|
218
|
+
}
|
|
219
|
+
function coveredBasename(p) {
|
|
220
|
+
const base = p.split('/').pop();
|
|
221
|
+
// drop the test/spec suffix, then the extension, then a leading underscore (the `_helper.mjs`
|
|
222
|
+
// convention) so `_commands.mjs` matches `commands.test.js`.
|
|
223
|
+
return base.replace(/\.(test|spec)\.[cm]?[jt]sx?$/i, '').replace(/\.[cm]?[jt]sx?$/i, '').replace(/^_+/, '').toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
function gitLines(dir, args) {
|
|
226
|
+
const r = spawnSync('git', args, { cwd: dir, encoding: 'utf8' });
|
|
227
|
+
return r.status === 0 ? r.stdout.split('\n').map(s => s.trim()).filter(Boolean) : null;
|
|
228
|
+
}
|
|
229
|
+
function changedFiles(dir) {
|
|
230
|
+
// prefer the diff vs the default branch's merge-base (the branch's work); fall back to the working tree
|
|
231
|
+
const base = (gitLines(dir, ['merge-base', 'HEAD', 'origin/main']) || gitLines(dir, ['merge-base', 'HEAD', 'main']) || [])[0];
|
|
232
|
+
if (base) { const d = gitLines(dir, ['diff', '--name-only', `${base}...HEAD`]); if (d && d.length) return d; }
|
|
233
|
+
const staged = gitLines(dir, ['diff', '--name-only', '--cached']) || [];
|
|
234
|
+
const unstaged = gitLines(dir, ['diff', '--name-only']) || [];
|
|
235
|
+
return [...new Set([...staged, ...unstaged])];
|
|
236
|
+
}
|
|
237
|
+
function scanUntested(dir) {
|
|
238
|
+
const changed = changedFiles(dir).map(f => f.replace(/\\/g, '/'));
|
|
239
|
+
const src = changed.filter(f => CODE_EXTS.has(extname(f)) && !isTestFile(f));
|
|
240
|
+
if (!src.length) return [];
|
|
241
|
+
// basenames that already have a test somewhere in the repo
|
|
242
|
+
const covered = new Set(
|
|
243
|
+
walkFiles(dir, () => true).map(f => relative(dir, f).replace(/\\/g, '/')).filter(isTestFile).map(coveredBasename),
|
|
244
|
+
);
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const f of src) {
|
|
247
|
+
const bn = coveredBasename(f);
|
|
248
|
+
if (covered.has(bn)) continue;
|
|
249
|
+
const title = `Add tests for ${f.split('/').pop()}`.replace(/"/g, '\\"');
|
|
250
|
+
out.push({
|
|
251
|
+
label: `No test for ${f}`,
|
|
252
|
+
detail: ` ${f} — changed, but no *.test.* / test-dir file covers "${bn}"`,
|
|
253
|
+
cmd: `flow-create --title="${title}" --type=chore --area=infra`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out.slice(0, 25);
|
|
257
|
+
}
|
|
258
|
+
|
|
212
259
|
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
213
260
|
function printSection(heading, suggestions) {
|
|
214
261
|
process.stdout.write(`\n${heading} (${suggestions.length})\n`);
|
|
@@ -227,7 +274,8 @@ async function main() {
|
|
|
227
274
|
const doIssues = argv.includes('--issues');
|
|
228
275
|
const doPRs = argv.includes('--prs');
|
|
229
276
|
const doSecurity = argv.includes('--security');
|
|
230
|
-
const
|
|
277
|
+
const doUntested = argv.includes('--untested');
|
|
278
|
+
const all = !doTodos && !doIssues && !doPRs && !doSecurity && !doUntested;
|
|
231
279
|
|
|
232
280
|
const dir = arg('dir') || process.cwd();
|
|
233
281
|
const repo = arg('repo') || process.env.FLOW_GITHUB_REPO;
|
|
@@ -279,8 +327,15 @@ async function main() {
|
|
|
279
327
|
else if (all) process.stdout.write('\n[security] No common patterns found.\n');
|
|
280
328
|
}
|
|
281
329
|
|
|
330
|
+
if (all || doUntested) {
|
|
331
|
+
process.stdout.write(' [untested] checking changed files for tests...\n');
|
|
332
|
+
const s = scanUntested(dir);
|
|
333
|
+
if (doUntested || (all && s.length)) printSection('Changed files with no test', s); // CLI-2: flags combine
|
|
334
|
+
else if (all) process.stdout.write('\n[untested] All changed source files have a test (or no changes detected).\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
282
337
|
if (all) {
|
|
283
|
-
process.stdout.write('\nRun "flow-scan --todos / --issues / --prs / --security" for focused output.\n');
|
|
338
|
+
process.stdout.write('\nRun "flow-scan --todos / --issues / --prs / --security / --untested" for focused output.\n');
|
|
284
339
|
process.stdout.write('Use any "$ flow-create ..." command above to create a tracking task.\n');
|
|
285
340
|
}
|
|
286
341
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowcollab",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Multi-Claude coordination layer — shared task board
|
|
3
|
+
"version": "0.3.24",
|
|
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/"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"flow-close-sprint": "bin/close-sprint.mjs",
|
|
32
32
|
"flow-review": "bin/review.mjs",
|
|
33
33
|
"flow-verify": "bin/verify.mjs",
|
|
34
|
+
"flow-scaffold-ci": "bin/scaffold-ci.mjs",
|
|
34
35
|
"flow-login": "bin/login.mjs",
|
|
35
36
|
"flow-edit": "bin/edit.mjs",
|
|
36
37
|
"flow-unblock": "bin/unblock.mjs",
|
|
@@ -39,28 +40,8 @@
|
|
|
39
40
|
"flow-archive": "bin/archive.mjs",
|
|
40
41
|
"flow-scan": "bin/scan.mjs"
|
|
41
42
|
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "node scripts/build.mjs",
|
|
44
|
-
"dev": "node scripts/build.mjs --watch",
|
|
45
|
-
"start": "node scripts/build.mjs && node --use-system-ca server.js",
|
|
46
|
-
"start:prod": "node scripts/build.mjs && node server.js",
|
|
47
|
-
"test": "node --test test/*.test.js",
|
|
48
|
-
"test:integration": "node --test test/*.integration.test.mjs",
|
|
49
|
-
"test:ui": "node --test test/*.browser.test.mjs"
|
|
50
|
-
},
|
|
51
43
|
"dependencies": {
|
|
52
|
-
"dotenv": "^16.4.7"
|
|
53
|
-
"web-push": "^3.6.7"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@supabase/supabase-js": "^2.49.4",
|
|
57
|
-
"esbuild": "^0.28.0",
|
|
58
|
-
"express": "^4.0.0 || ^5.0.0",
|
|
59
|
-
"express-rate-limit": "^7.5.0",
|
|
60
|
-
"helmet": "^8.0.0",
|
|
61
|
-
"playwright": "^1.60.0",
|
|
62
|
-
"stripe": "^22.2.0",
|
|
63
|
-
"zod": "^3.24.1"
|
|
44
|
+
"dotenv": "^16.4.7"
|
|
64
45
|
},
|
|
65
46
|
"engines": {
|
|
66
47
|
"node": ">=22"
|
package/LICENSE
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2026 Greylock Labs. All rights reserved.
|
|
2
|
-
|
|
3
|
-
Permission is granted to download and use this CLI software solely for the
|
|
4
|
-
purpose of accessing the Flow hosted service. No part of this software may be
|
|
5
|
-
reproduced, distributed, modified, or transmitted in any form or by any means
|
|
6
|
-
without the prior written permission of the copyright holder.
|