create-pmos 0.188.0 → 0.189.0
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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-report-setup-test.js +1 -1
- package/workspace/c3-scan-fixture.js +247 -5
- package/workspace/c3-scan-test.js +453 -106
- package/workspace/guide/horizon-scan.md +45 -36
- package/workspace/lib/headless.js +19 -5
- package/workspace/lib/pmos-verbs.js +2 -2
- package/workspace/lib/route-modules.js +1 -1
- package/workspace/lib/routes-boards.js +29 -12
- package/workspace/lib/routes-scan.js +224 -44
- package/workspace/lib/scan-cli.js +143 -45
- package/workspace/lib/scan-run.js +242 -87
- package/workspace/lib/scan.js +623 -79
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-doc-sheet.js +7 -1
- package/workspace/public/pm/c3-initiatives.js +5 -4
- package/workspace/public/pm/c3-scan-api.js +167 -0
- package/workspace/public/pm/c3-scan-next.js +248 -0
- package/workspace/public/pm/c3-scan-setup.js +317 -0
- package/workspace/public/pm/c3-scan-words.js +34 -0
- package/workspace/public/pm/c3-scan.css +58 -9
- package/workspace/public/pm/c3-scan.js +36 -208
- package/workspace/public/pm/c3-toast.js +1 -0
- package/workspace/server.js +7 -4
- package/workspace/skills/horizon-scan-repo.skill +26 -10
- package/workspace/skills/horizon-scan-research.skill +44 -0
- package/workspace/skills/horizon-scan-web.skill +12 -5
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/* scan-cli.js — `pmos scan …`: the Horizon scan from a terminal (
|
|
2
|
+
/* scan-cli.js — `pmos scan …`: the Horizon scan from a terminal (initiatives horizon-scan and horizon-scan-portfolio).
|
|
3
3
|
*
|
|
4
4
|
* pmos scan file --scan <id> --title "<finding>" --why "<one line>" --source label=ref … [--match <initiative>] [--origin web|repo]
|
|
5
5
|
* an agent files one finding. It answers `filed` or `duplicate` and nothing else about earlier findings.
|
|
6
6
|
* pmos scan file --scan <id> --pages <n> the web run says how many public pages it read
|
|
7
|
-
* pmos scan
|
|
7
|
+
* pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"] (text on stdin when --body is absent)
|
|
8
|
+
* a run writes one section of a report document. A web section cites only public pages the run read;
|
|
9
|
+
* a repository section cites paths, Jira keys or notes. lib/scan.js assembles the file when the scan ends.
|
|
10
|
+
* pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …
|
|
11
|
+
* create: --board <id> --lane <lane> --anchor <KR|health:id> --intent "<one paragraph>"
|
|
12
|
+
* research: --question "<public question>" [--depth quick|standard|deep] attach: --initiative <id>
|
|
13
|
+
* the repository run proposes a next step. The PM approves, changes or dismisses it. It answers `filed` or `duplicate`.
|
|
14
|
+
* pmos scan list --scan <id> [--sections] the findings of one scan (the repository run reads the web run's), and its report sections
|
|
8
15
|
* pmos scan match <finding> <initiative> --scan <id> the repository run marks a finding that matches an initiative
|
|
9
|
-
* pmos scan suggest --
|
|
10
|
-
* the suggest run files one watch list suggestion (at most 8
|
|
11
|
-
* pmos scan status
|
|
12
|
-
* pmos scan cadence
|
|
13
|
-
* pmos scan
|
|
14
|
-
* pmos scan watch
|
|
15
|
-
* pmos scan watch
|
|
16
|
+
* pmos scan suggest [--boards <id,id>] --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>
|
|
17
|
+
* the suggest run files one watch list suggestion (at most 8 a run)
|
|
18
|
+
* pmos scan status the setting, the watch list, the last scan and the proposals waiting (read)
|
|
19
|
+
* pmos scan setting [--cadence daily|weekly|off] [--repo on|off] [--types benchmark,competitive,insight,productivity] PM only
|
|
20
|
+
* pmos scan cadence <daily|weekly|off> PM only (the first scan's verb; it sets the workspace cadence)
|
|
21
|
+
* pmos scan watch keep|remove <item> PM only
|
|
22
|
+
* pmos scan watch edit <item> [--name …] [--kind …] [--why …] [--boards a,b] PM only
|
|
23
|
+
* pmos scan watch add --kind … --name … [--why …] [--boards a,b] PM only
|
|
16
24
|
* every command: [--json] [--root <dir>]
|
|
17
25
|
*
|
|
18
|
-
* Who wrote a line is the call path (lib/actor.js), never an argument. The scan runs set
|
|
26
|
+
* Who wrote a line is the call path (lib/actor.js), never an argument. The scan runs set variables the model cannot change: PMOS_SCAN_FILE
|
|
19
27
|
* (the ledger, so the web run, which has no repository, still finds it) and PMOS_SCAN_ORIGIN (web or repo, so a run cannot file as the other).
|
|
20
28
|
* PMOS_SCAN_RUN names the run, so a finding carries the run that filed it.
|
|
21
29
|
* `file` answers only `duplicate` for a repeat: it never prints the earlier finding. Nothing here sends anything outward.
|
|
@@ -28,25 +36,32 @@ const actorLib = require('./actor.js');
|
|
|
28
36
|
const D = require('./driver.js');
|
|
29
37
|
const S = require('./scan.js');
|
|
30
38
|
|
|
31
|
-
const COMMANDS = { scan: ['file', 'list', 'match', 'suggest', 'status', 'cadence', 'watch'] };
|
|
39
|
+
const COMMANDS = { scan: ['file', 'section', 'propose', 'list', 'match', 'suggest', 'status', 'setting', 'cadence', 'watch'] };
|
|
32
40
|
const USAGE = [
|
|
33
|
-
'usage: pmos scan file|list|match|suggest|status|cadence|watch — the Horizon scan',
|
|
41
|
+
'usage: pmos scan file|section|propose|list|match|suggest|status|setting|cadence|watch — the Horizon scan',
|
|
34
42
|
' pmos scan file --scan <id> --title "<finding>" --why "<one line>" --source label=ref … [--match <initiative>]',
|
|
35
43
|
' a web finding needs one source that is a public page you read; a repository finding needs a pointer (path, Jira key, note)',
|
|
36
44
|
' pmos scan file --scan <id> --pages <n> how many public pages the web run read',
|
|
37
|
-
' pmos scan
|
|
45
|
+
' pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"] the text is read from stdin without --body',
|
|
46
|
+
' a web section cites public pages you read; a repository section cites paths, Jira keys or notes',
|
|
47
|
+
' pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …',
|
|
48
|
+
' create: --board <id> --lane <lane> --anchor <KR|health:id> --intent "<one paragraph>"',
|
|
49
|
+
' research: --question "<public question>" [--depth quick|standard|deep] attach: --initiative <id>',
|
|
50
|
+
' pmos scan list --scan <id> [--sections] the findings of a scan, and its report sections',
|
|
38
51
|
' pmos scan match <finding> <initiative> --scan <id> a finding matches an existing initiative',
|
|
39
|
-
' pmos scan suggest --
|
|
40
|
-
' pmos scan status
|
|
41
|
-
' pmos scan cadence
|
|
42
|
-
' pmos scan
|
|
43
|
-
' pmos scan watch
|
|
44
|
-
' pmos scan watch
|
|
52
|
+
' pmos scan suggest [--boards a,b] --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>',
|
|
53
|
+
' pmos scan status',
|
|
54
|
+
' pmos scan setting [--cadence daily|weekly|off] [--repo on|off] [--types a,b] PM only',
|
|
55
|
+
' pmos scan cadence <daily|weekly|off> PM only',
|
|
56
|
+
' pmos scan watch keep|remove <item> PM only',
|
|
57
|
+
' pmos scan watch edit <item> [--name …] [--kind …] [--why …] [--boards a,b] PM only',
|
|
58
|
+
' pmos scan watch add --kind … --name … [--why …] [--boards a,b] PM only',
|
|
45
59
|
' every command: [--json] [--root <dir>]',
|
|
46
|
-
'A scan reads and files. It sends nothing and creates no initiative: each
|
|
60
|
+
'A scan reads and files. It sends nothing and creates no initiative: each proposal is a step the PM approves, changes or dismisses.',
|
|
47
61
|
];
|
|
48
|
-
const BOOL = new Set(['json', 'help']);
|
|
49
|
-
const VALUE = new Set(['scan', 'title', 'why', 'source', 'match', 'origin', 'pages', 'board', 'kind', 'name', 'from', 'root'
|
|
62
|
+
const BOOL = new Set(['json', 'help', 'sections']);
|
|
63
|
+
const VALUE = new Set(['scan', 'title', 'why', 'source', 'match', 'origin', 'pages', 'board', 'boards', 'kind', 'name', 'from', 'root', 'report', 'heading', 'body', 'finding', 'lane', 'anchor',
|
|
64
|
+
'intent', 'question', 'depth', 'initiative', 'cadence', 'repo', 'types']);
|
|
50
65
|
|
|
51
66
|
function parse(argv) {
|
|
52
67
|
const pos = []; const flags = {}; const sources = [];
|
|
@@ -78,7 +93,22 @@ function sourcesFrom(list) {
|
|
|
78
93
|
return { sources: out };
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
/** The text a section is made of: the `stdin` option (tests), else standard input. Nothing on standard input for 8 seconds is an empty text, not a hang. */
|
|
97
|
+
function readStdin(given) {
|
|
98
|
+
if (typeof given === 'string') return Promise.resolve(given);
|
|
99
|
+
if (process.stdin.isTTY) return Promise.resolve('');
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let data = ''; let got = false;
|
|
102
|
+
const quiet = setTimeout(() => { if (!got) { process.stdin.pause(); resolve(''); } }, 8000);
|
|
103
|
+
process.stdin.setEncoding('utf8');
|
|
104
|
+
process.stdin.on('data', (c) => { got = true; data += c; });
|
|
105
|
+
process.stdin.on('end', () => { clearTimeout(quiet); resolve(data); });
|
|
106
|
+
process.stdin.on('error', () => { clearTimeout(quiet); resolve(data); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const listOf = (v) => String(v || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
110
|
+
|
|
111
|
+
async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now, stdin } = {}) {
|
|
82
112
|
const args = argv[0] === 'scan' ? argv.slice(1) : argv.slice();
|
|
83
113
|
const verb = args[0];
|
|
84
114
|
const { pos, flags, sources } = parse(args.slice(1));
|
|
@@ -129,7 +159,7 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
|
|
|
129
159
|
const count = sc.findings.filter((id) => state.findings.get(id).origin === origin).length;
|
|
130
160
|
const limit = origin === 'web' ? S.LIMITS.web : S.LIMITS.repo;
|
|
131
161
|
// A duplicate is answered before the limit: a repeat costs nothing, and it is counted as skipped.
|
|
132
|
-
const dup = S.duplicateOf(state,
|
|
162
|
+
const dup = S.duplicateOf(state, null, c.finding); // the whole portfolio: the same page or title on any board
|
|
133
163
|
if (dup) {
|
|
134
164
|
S.append(file, { ev: 'skip', board: sc.board, scan: sc.scan, origin, of: dup.id }, { now });
|
|
135
165
|
if (flags.pages !== undefined && Number.isInteger(Number(flags.pages))) S.append(file, { ev: 'pages', board: sc.board, scan: sc.scan, pages: Number(flags.pages) }, { now });
|
|
@@ -143,15 +173,69 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
|
|
|
143
173
|
return done({ result: 'filed', id }, `filed ${id}`);
|
|
144
174
|
}
|
|
145
175
|
|
|
176
|
+
if (verb === 'section') {
|
|
177
|
+
if (!file) return noState();
|
|
178
|
+
if (!flags.report) { say('usage: pmos scan section --report <id> --heading "<h>" --source label=ref … [--body "<text>"]'); return 2; }
|
|
179
|
+
const state = S.fold(S.readLines(file));
|
|
180
|
+
const rep = state.reports.get(flags.report);
|
|
181
|
+
const sc = rep ? state.scans.get(rep.scan) : null;
|
|
182
|
+
if (!rep) return fail('no such report in this ledger');
|
|
183
|
+
if (!sc || sc.end) return fail('the scan of that report is over; file no more');
|
|
184
|
+
const origin = (E && (E.PMOS_SCAN_ORIGIN === 'web' || E.PMOS_SCAN_ORIGIN === 'repo')) ? E.PMOS_SCAN_ORIGIN : flags.origin;
|
|
185
|
+
if (!S.ORIGINS.includes(origin)) { say('say where the section comes from: --origin web or --origin repo'); return 2; }
|
|
186
|
+
if (rep.type === 'productivity' && origin === 'web') return fail('the productivity report comes from the repository run only');
|
|
187
|
+
const s = sourcesFrom(sources);
|
|
188
|
+
if (s.error) return fail(s.error, 2);
|
|
189
|
+
const body = flags.body !== undefined ? flags.body : await readStdin(stdin);
|
|
190
|
+
const c = S.checkSection({ heading: flags.heading, body, sources: s.sources }, origin, { state, scanId: sc.scan });
|
|
191
|
+
if (!c.ok) return fail(c.error);
|
|
192
|
+
if (rep.sections.some((x) => x.origin === origin && x.heading.toLowerCase() === c.section.heading.toLowerCase())) return fail('that report has a section with that heading already');
|
|
193
|
+
if (rep.sections.filter((x) => x.origin === origin).length >= S.LIMITS.sections) return fail(`this report holds ${S.LIMITS.sections} sections from the ${origin === 'web' ? 'web' : 'repository'} run already; write no more`);
|
|
194
|
+
const run = E && S.RUN_RE.test(String(E.PMOS_SCAN_RUN || '')) ? E.PMOS_SCAN_RUN : null;
|
|
195
|
+
S.append(file, { ev: 'section', board: sc.board, scan: sc.scan, report: rep.id, ...c.section, by: actor.by, ...(run ? { run } : {}) }, { now });
|
|
196
|
+
return done({ result: 'filed', report: rep.id }, `filed section "${c.section.heading}"`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (verb === 'propose') {
|
|
200
|
+
if (!file) return noState();
|
|
201
|
+
if (!flags.report) { say('usage: pmos scan propose --report <id> --kind create|research|attach --title "<what>" --why "<one line>" --finding "<which finding>" --source label=ref …'); return 2; }
|
|
202
|
+
const origin = (E && (E.PMOS_SCAN_ORIGIN === 'web' || E.PMOS_SCAN_ORIGIN === 'repo')) ? E.PMOS_SCAN_ORIGIN : flags.origin;
|
|
203
|
+
if (origin !== 'repo') return fail('only the repository run proposes next steps; the web run files findings and sections');
|
|
204
|
+
const state = S.fold(S.readLines(file));
|
|
205
|
+
const rep = state.reports.get(flags.report);
|
|
206
|
+
const sc = rep ? state.scans.get(rep.scan) : null;
|
|
207
|
+
if (!rep) return fail('no such report in this ledger');
|
|
208
|
+
if (!sc || sc.end) return fail('the scan of that report is over; file no more');
|
|
209
|
+
const s = sourcesFrom(sources);
|
|
210
|
+
if (s.error) return fail(s.error, 2);
|
|
211
|
+
const fields = flags.kind === 'create' ? { board: flags.board, lane: flags.lane, anchor: flags.anchor, intent: flags.intent }
|
|
212
|
+
: flags.kind === 'research' ? { question: flags.question, ...(flags.depth !== undefined ? { depth: flags.depth } : {}) } : { initiative: flags.initiative };
|
|
213
|
+
const c = S.checkProposal({ kind: flags.kind, title: flags.title, why: flags.why, finding: flags.finding, evidence: s.sources, fields });
|
|
214
|
+
if (!c.ok) return fail(c.error);
|
|
215
|
+
// A repeat is answered before the limit, and says nothing about the earlier proposal.
|
|
216
|
+
if (S.duplicateProposal(state, c.proposal.title)) return done({ result: 'duplicate' }, 'duplicate');
|
|
217
|
+
if (S.proposalsOf(state, rep.id).length >= S.LIMITS.proposals) return fail(`this report holds ${S.LIMITS.proposals} proposals already; file no more`);
|
|
218
|
+
const id = S.newId('p', 5);
|
|
219
|
+
const run = E && S.RUN_RE.test(String(E.PMOS_SCAN_RUN || '')) ? E.PMOS_SCAN_RUN : null;
|
|
220
|
+
S.append(file, { ev: 'proposal', board: sc.board, scan: sc.scan, report: rep.id, id, ...c.proposal, by: actor.by, ...(run ? { run } : {}) }, { now });
|
|
221
|
+
return done({ result: 'filed', id }, `filed ${id}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
146
224
|
if (verb === 'list') {
|
|
147
225
|
if (!file) return noState();
|
|
148
226
|
const state = S.fold(S.readLines(file));
|
|
149
227
|
const sc = flags.scan ? state.scans.get(flags.scan) : null;
|
|
150
228
|
if (!sc) return fail('no scan under that id');
|
|
151
229
|
const rows = sc.findings.map((id) => state.findings.get(id)).map((f) => ({ id: f.id, origin: f.origin, title: f.title, why: f.why, sources: f.sources, match: f.match }));
|
|
152
|
-
|
|
230
|
+
const reps = flags.sections ? sc.reports.map((id) => state.reports.get(id)).map((r) => ({ id: r.id, type: r.type, title: r.title, origin: r.origin,
|
|
231
|
+
sections: r.sections.map((x) => ({ heading: x.heading, origin: x.origin, body: x.body, sources: x.sources })) })) : null;
|
|
232
|
+
if (json) { say(JSON.stringify({ ok: true, scan: sc.scan, board: sc.board, findings: rows, ...(reps ? { reports: reps } : {}) })); return 0; }
|
|
153
233
|
for (const f of rows) say(`${f.id} · ${f.origin} · ${f.title} — ${f.why}${f.match ? ` · matches ${f.match}` : ''}${f.sources.length ? ` · ${f.sources.map((x) => x.ref).join(', ')}` : ''}`);
|
|
154
234
|
if (!rows.length) say('no findings yet');
|
|
235
|
+
for (const r of reps || []) {
|
|
236
|
+
say(`report ${r.id} · ${r.type} · ${r.title}`);
|
|
237
|
+
for (const x of r.sections) say(` [${x.origin}] ${x.heading}: ${x.body.replace(/\s+/g, ' ').slice(0, 300)}${x.sources.length ? ` · ${x.sources.map((y) => y.ref).join(', ')}` : ''}`);
|
|
238
|
+
}
|
|
155
239
|
return 0;
|
|
156
240
|
}
|
|
157
241
|
|
|
@@ -172,53 +256,67 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
|
|
|
172
256
|
|
|
173
257
|
if (verb === 'suggest') {
|
|
174
258
|
if (!file) return noState();
|
|
175
|
-
if (!flags.board) { say('usage: pmos scan suggest --board <id> --kind product|domain|approach --name "<what to watch>" --why "<one line>" --from <initiative>'); return 2; }
|
|
176
259
|
if (!flags.from || !S.ID_RE.test(flags.from)) return fail('--from names the initiative this suggestion came from');
|
|
177
260
|
const c = S.checkWatchItem({ kind: flags.kind, name: flags.name, why: flags.why });
|
|
178
261
|
if (!c.ok) return fail(c.error);
|
|
179
262
|
if (!c.item.why) return fail('--why is one line that says why this is worth watching');
|
|
263
|
+
const on = listOf(flags.boards || flags.board);
|
|
264
|
+
if (on.some((b) => !S.ID_RE.test(b)) || on.length > 20) return fail('--boards is a list of board ids');
|
|
180
265
|
const state = S.fold(S.readLines(file));
|
|
181
|
-
const open = S.scansOf(state,
|
|
182
|
-
if (!open) return fail('no suggest run is open
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
if (b && [...b.items.values()].some((i) => i.name.toLowerCase() === c.item.name.toLowerCase())) return done({ filed: false, result: 'duplicate' }, 'duplicate');
|
|
266
|
+
const open = S.scansOf(state, null, 'suggest').find((x) => !x.end);
|
|
267
|
+
if (!open) return fail('no suggest run is open');
|
|
268
|
+
const mine = [...state.itemById.values()].filter((i) => i.scan === open.scan).length;
|
|
269
|
+
if (mine >= S.LIMITS.suggestions) return fail(`a suggest run files at most ${S.LIMITS.suggestions} suggestions`);
|
|
270
|
+
if ([...state.itemById.values()].some((i) => i.name.toLowerCase() === c.item.name.toLowerCase())) return done({ filed: false, result: 'duplicate' }, 'duplicate');
|
|
187
271
|
const id = S.newId('w');
|
|
188
|
-
S.append(file, { ev: 'watch-suggest',
|
|
272
|
+
S.append(file, { ev: 'watch-suggest', scan: open.scan, boards: on, id, ...c.item, ini: flags.from, by: actor.by }, { now });
|
|
189
273
|
return done({ filed: true, id }, `filed ${id}`);
|
|
190
274
|
}
|
|
191
275
|
|
|
192
276
|
if (verb === 'status') {
|
|
193
277
|
if (!file) return noState();
|
|
194
278
|
const state = S.fold(S.readLines(file));
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
279
|
+
const w = S.watchOf(state, null);
|
|
280
|
+
const last = S.lastScan(state, null);
|
|
281
|
+
const out = { setting: S.settingOf(state), kept: w.kept.length, suggested: w.suggested.length, proposals_waiting: S.waitingProposals(state).length,
|
|
282
|
+
last: last ? { scan: last.scan, at: last.end, found: last.found, skipped: last.skipped, pages: last.pages, ok: last.ok } : null };
|
|
283
|
+
if (json) { say(JSON.stringify({ ok: true, ...out })); return 0; }
|
|
284
|
+
say(`${out.setting.cadence} · repository ${out.setting.repo ? 'on' : 'off'} · reports ${out.setting.types.join(', ') || 'none'} · ${out.kept} kept, ${out.suggested} suggested · ${out.proposals_waiting} proposals waiting · ${out.last ? `last scan ${out.last.at}: ${out.last.found} found, ${out.last.skipped} skipped` : 'no scan yet'}`);
|
|
201
285
|
return 0;
|
|
202
286
|
}
|
|
203
287
|
|
|
204
288
|
// ── the PM's verbs ────────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
if (verb === 'setting') {
|
|
290
|
+
const g = pmOnly('change the scan setting');
|
|
291
|
+
if (!g.ok) return fail(g.error);
|
|
292
|
+
if (!file) return noState();
|
|
293
|
+
const input = {};
|
|
294
|
+
if (flags.cadence !== undefined) input.cadence = flags.cadence;
|
|
295
|
+
if (flags.repo !== undefined) { if (!/^(on|off)$/.test(flags.repo)) return fail('--repo is on or off', 2); input.repo = flags.repo === 'on'; }
|
|
296
|
+
if (flags.types !== undefined) input.types = listOf(flags.types);
|
|
297
|
+
const r = S.setSetting(file, input, actor, { now });
|
|
298
|
+
if (!r.ok) return fail(r.error);
|
|
299
|
+
return done({ setting: r.setting }, `✓ ${r.setting.cadence} · repository ${r.setting.repo ? 'on' : 'off'} · reports ${r.setting.types.join(', ') || 'none'}`);
|
|
300
|
+
}
|
|
205
301
|
if (verb === 'cadence') {
|
|
206
302
|
const g = pmOnly('change the scan cadence');
|
|
207
303
|
if (!g.ok) return fail(g.error);
|
|
208
|
-
if (!pos[0]
|
|
304
|
+
if (!pos[0]) { say('usage: pmos scan cadence <daily|weekly|off>'); return 2; }
|
|
209
305
|
if (!file) return noState();
|
|
210
|
-
const r = S.setCadence(file, flags.board, pos[0], actor, { now });
|
|
306
|
+
const r = S.setCadence(file, flags.board || null, pos[0], actor, { now });
|
|
211
307
|
if (!r.ok) return fail(r.error);
|
|
212
|
-
return done({
|
|
308
|
+
return done({ cadence: r.cadence }, `✓ the scan runs ${r.cadence === 'off' ? 'never' : r.cadence}`);
|
|
213
309
|
}
|
|
214
310
|
// watch
|
|
215
311
|
const op = pos[0];
|
|
216
|
-
if (!['keep', 'edit', 'remove', 'add'].includes(op)
|
|
312
|
+
if (!['keep', 'edit', 'remove', 'add'].includes(op)) { say('usage: pmos scan watch keep|edit|remove|add [<item>]'); return 2; }
|
|
217
313
|
const g = pmOnly('change the watch list');
|
|
218
314
|
if (!g.ok) return fail(g.error);
|
|
219
315
|
if (!file) return noState();
|
|
220
|
-
const
|
|
221
|
-
const
|
|
316
|
+
const on = flags.boards !== undefined ? listOf(flags.boards) : undefined;
|
|
317
|
+
const input = op === 'add' ? { kind: flags.kind, name: flags.name, why: flags.why, ...(on ? { boards: on } : {}) }
|
|
318
|
+
: { id: pos[1], ...(flags.name !== undefined ? { name: flags.name } : {}), ...(flags.kind !== undefined ? { kind: flags.kind } : {}), ...(flags.why !== undefined ? { why: flags.why } : {}), ...(on ? { boards: on } : {}) };
|
|
319
|
+
const r = S.watchOp(file, op, flags.board || null, input, actor, { now });
|
|
222
320
|
if (!r.ok) return fail(r.error);
|
|
223
321
|
return done({ id: r.id }, `✓ ${op} ${r.id}`);
|
|
224
322
|
}
|