@cspeach/cli 1.1.29 → 1.1.31
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/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +127 -11
- package/dist/auth/auth-file.js +2 -2
- package/dist/cli.js +39 -13
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team.js +652 -46
- package/dist/commands/whoami.js +1 -1
- package/dist/config/paths.js +15 -1
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +21 -1
- package/dist/projects/clean-core-confirm.js +192 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +82 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +115 -15
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/validate.js +2 -1
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -120
- package/dist/register/atc-file.js +9 -1
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +82 -0
- package/dist/register/measure.js +60 -1
- package/dist/register/offer.js +125 -0
- package/dist/register/picture.js +25 -0
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +57 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +221 -0
- package/dist/register/status-file/register-model.js +341 -0
- package/dist/register/status-file/register-parts.js +555 -0
- package/dist/register/status-file/register-status-file.js +189 -0
- package/dist/register/status-file/team-css.js +5 -0
- package/dist/register/status-file-sync.js +22 -0
- package/dist/register/status-html.js +19 -0
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +124 -0
- package/dist/register/write-whole.js +22 -0
- package/dist/register/xlsx.js +127 -0
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/journey-entry.js +25 -0
- package/dist/repl.js +158 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +45 -45
- package/dist/tools/_filesystem-shared.js +190 -21
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +145 -0
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +203 -5
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +2 -2
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cspeach team discover` — the estate inventory before anyone chose a scope
|
|
3
|
+
* (spec docs/superpowers/specs/2026-10-06-cca-discovery-team-plan-design.md §5, §7).
|
|
4
|
+
*
|
|
5
|
+
* Read only: every statement is a fixed one in register/discover.ts and goes
|
|
6
|
+
* through sap-client's read-only gate. Files go into `<folder>/discovery/`,
|
|
7
|
+
* each written whole then renamed; older discoveries are never deleted (the
|
|
8
|
+
* newest stamp wins on read). On a team project folder only its lead may run
|
|
9
|
+
* it, and that is checked before SAP is even reached.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { NO_USAGE_LINE, estateSheets, estateSummaryOf, objectKey, usageLineWithWindow } from '@cspeach/register-core';
|
|
15
|
+
import { discover } from '../register/discover.js';
|
|
16
|
+
import { DISCOVERY_DIR, RegisterError, TEAM_FILE, loadState, readBaseline, readProjectFile } from '../register/store.js';
|
|
17
|
+
import { fileStamp, writeRecord } from '../register/records.js';
|
|
18
|
+
import { writeWhole } from '../register/write-whole.js';
|
|
19
|
+
import { writeXlsxFile } from '../register/xlsx.js';
|
|
20
|
+
import { HELD } from '../register/workbooks.js';
|
|
21
|
+
import { flags, list, onInterrupt, requireLead, whoAmI } from './team.js';
|
|
22
|
+
export const DISCOVER_USAGE = 'Usage: cspeach team discover [--out <folder>] [--prefixes Z,Y] [--namespaces auto|/ACME/,…] [--sap-alias S4H] '
|
|
23
|
+
+ '[--no-xref] [--no-jobs] [--max-minutes 30]';
|
|
24
|
+
const DISCOVER_OPTIONS = ['out', 'prefixes', 'namespaces', 'sap-alias', 'no-xref', 'no-jobs', 'max-minutes'];
|
|
25
|
+
const DISCOVER_FLAGS = ['no-xref', 'no-jobs'];
|
|
26
|
+
/** 2,931 — every count on the estate line reads the way a person writes it. */
|
|
27
|
+
const n = (v) => v.toLocaleString('en-US');
|
|
28
|
+
/** The usage part of the estate line, from the summary's one sentence. */
|
|
29
|
+
function usageWords(s) {
|
|
30
|
+
if (s.usageLine === NO_USAGE_LINE)
|
|
31
|
+
return 'none recorded on this system';
|
|
32
|
+
return usageLineWithWindow(s).replace(/^Usage /, '').replace(/\.$/, '');
|
|
33
|
+
}
|
|
34
|
+
export function estateLine(s) {
|
|
35
|
+
const t = s.totals;
|
|
36
|
+
return `Estate: ${n(t.packages)} packages · ${n(t.codeObjects)} code objects · ${n(t.nonCodeObjects)} non-code · `
|
|
37
|
+
+ `${n(t.local)} outside packages · usage: ${usageWords(s)} · modifications of SAP objects: ${n(t.modifications)} · unknowns: ${n(t.unknowns)}`;
|
|
38
|
+
}
|
|
39
|
+
/** The workbook sheets of an estate summary, as the xlsx writer takes them. */
|
|
40
|
+
export function estateWorkbook(s) {
|
|
41
|
+
return estateSheets(s).map((sh) => ({ name: sh.name, header: sh.table.header, rows: sh.table.rows }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The evidence rows of an objects file, for the objects a baseline holds.
|
|
45
|
+
* Rows for anything else are not written; they are counted.
|
|
46
|
+
*/
|
|
47
|
+
export function evidenceRows(objects, baselineKeys) {
|
|
48
|
+
const list = objects?.objects;
|
|
49
|
+
if (objects?.kind !== 'estate-objects' || !Array.isArray(list))
|
|
50
|
+
return null;
|
|
51
|
+
const rows = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
let outside = 0;
|
|
54
|
+
for (const o of list) {
|
|
55
|
+
if (!o || typeof o.type !== 'string' || typeof o.name !== 'string')
|
|
56
|
+
continue;
|
|
57
|
+
const key = objectKey(o.type, o.name);
|
|
58
|
+
if (!baselineKeys.has(key)) {
|
|
59
|
+
outside += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(key))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
rows.push({ key, callers: o.callers, batch: o.batch, usage: o.usage, changed: o.changed });
|
|
66
|
+
}
|
|
67
|
+
return { rows, outside };
|
|
68
|
+
}
|
|
69
|
+
export const EVIDENCE_LINE = (count) => `Evidence for ${count} objects written (callers, batch runs, usage, last change).`;
|
|
70
|
+
export const OUTSIDE_LINE = (count) => `${count === 1 ? '1 object' : `${count} objects`} in the inventory ${count === 1 ? 'is' : 'are'} outside this team project.`;
|
|
71
|
+
/** One sentence for a file that could not be written: its name and the code, never the path (it carries the user name). */
|
|
72
|
+
const cannotWrite = (name, code) => `Could not write ${name} in the discovery folder${code ? ` (${code})` : ''}.`;
|
|
73
|
+
function writeIn(name, write) {
|
|
74
|
+
try {
|
|
75
|
+
write();
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
throw new RegisterError(cannotWrite(name, e?.code ?? ''));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The stamp, or `<stamp>-2`, `-3`, … when a discovery of the same second is
|
|
83
|
+
* already there: an older discovery is history and is never overwritten.
|
|
84
|
+
*/
|
|
85
|
+
function freeStamp(folder, stamp) {
|
|
86
|
+
const taken = (s) => fs.existsSync(path.join(folder, `estate-${s}.json`)) || fs.existsSync(path.join(folder, `objects-${s}.json`));
|
|
87
|
+
if (!taken(stamp))
|
|
88
|
+
return stamp;
|
|
89
|
+
for (let i = 2;; i += 1)
|
|
90
|
+
if (!taken(`${stamp}-${i}`))
|
|
91
|
+
return `${stamp}-${i}`;
|
|
92
|
+
}
|
|
93
|
+
/** The same folder, said two ways: on Windows case does not matter, and a trailing separator never does. */
|
|
94
|
+
export function samePath(a, b) {
|
|
95
|
+
const norm = (p) => {
|
|
96
|
+
const r = path.resolve(p).replace(/[\\/]+$/, '');
|
|
97
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
98
|
+
};
|
|
99
|
+
return norm(a) === norm(b);
|
|
100
|
+
}
|
|
101
|
+
export async function teamDiscover(args, deps) {
|
|
102
|
+
const { positional, opts } = flags(args, DISCOVER_FLAGS);
|
|
103
|
+
for (const name of Object.keys(opts)) {
|
|
104
|
+
if (!DISCOVER_OPTIONS.includes(name))
|
|
105
|
+
throw new RegisterError(`Unknown option --${name}.\n${DISCOVER_USAGE}`);
|
|
106
|
+
}
|
|
107
|
+
// 1.1.31: no --out → the folder you are in (owner: "why doesn't CSPeach remember all that?").
|
|
108
|
+
if (positional.length || opts.out === 'true')
|
|
109
|
+
throw new RegisterError(DISCOVER_USAGE);
|
|
110
|
+
const maxMinutes = opts['max-minutes'] === undefined ? 30 : Number(opts['max-minutes']);
|
|
111
|
+
if (!Number.isFinite(maxMinutes) || maxMinutes <= 0)
|
|
112
|
+
throw new RegisterError(`--max-minutes needs a number of minutes, for example: --max-minutes 30\n${DISCOVER_USAGE}`);
|
|
113
|
+
const namespaces = !opts.namespaces || opts.namespaces === 'true' || opts.namespaces.trim().toLowerCase() === 'auto'
|
|
114
|
+
? 'auto' : list(opts.namespaces).map((s) => s.toUpperCase());
|
|
115
|
+
const prefixes = opts.prefixes && opts.prefixes !== 'true' ? list(opts.prefixes).map((s) => s.toUpperCase()) : ['Z', 'Y'];
|
|
116
|
+
const dir = path.resolve(opts.out ?? process.cwd());
|
|
117
|
+
// A team project folder: its lead only, and that is settled before SAP is reached.
|
|
118
|
+
const isTeam = fs.existsSync(path.join(dir, TEAM_FILE));
|
|
119
|
+
const project = isTeam ? readProjectFile(dir) : null;
|
|
120
|
+
if (project)
|
|
121
|
+
requireLead(project, deps);
|
|
122
|
+
if (deps.noSap || !deps.connect)
|
|
123
|
+
throw new RegisterError('discover needs a SAP connection to read the estate.');
|
|
124
|
+
const { sql, system } = await deps.connect(opts['sap-alias']);
|
|
125
|
+
const stop = new AbortController();
|
|
126
|
+
const off = onInterrupt(deps, () => {
|
|
127
|
+
if (stop.signal.aborted)
|
|
128
|
+
return;
|
|
129
|
+
deps.print(' Stopping. What has been read so far will still be written.');
|
|
130
|
+
stop.abort();
|
|
131
|
+
});
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
deps.print('Reading the estate from SAP (read only). Ctrl-C keeps what has been read.');
|
|
135
|
+
result = await discover(sql, {
|
|
136
|
+
prefixes, namespaces, xref: !opts['no-xref'], jobs: !opts['no-jobs'], maxMinutes,
|
|
137
|
+
signal: stop.signal, by: whoAmI(deps), ...(system ? { system } : {}),
|
|
138
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
139
|
+
onStep: (step, label) => deps.print(` Step ${step} of 12: ${label}`),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
off();
|
|
144
|
+
}
|
|
145
|
+
const folder = path.join(dir, DISCOVERY_DIR);
|
|
146
|
+
const stamp = freeStamp(folder, fileStamp(new Date(result.estate.takenAt)));
|
|
147
|
+
const objectsName = `objects-${stamp}.json`;
|
|
148
|
+
const estateName = `estate-${stamp}.json`;
|
|
149
|
+
const objectsText = `${JSON.stringify(result.objects, null, 1)}\n`;
|
|
150
|
+
// The object list first: an estate file on disk always has its object list beside it.
|
|
151
|
+
writeIn(objectsName, () => writeWhole(path.join(folder, objectsName), objectsText));
|
|
152
|
+
writeIn(estateName, () => writeWhole(path.join(folder, estateName), `${JSON.stringify(result.estate, null, 1)}\n`));
|
|
153
|
+
const summary = estateSummaryOf(result.estate, project ? readBaseline(dir) : undefined);
|
|
154
|
+
// Said as written only when it was: no summary, no workbook.
|
|
155
|
+
let workbook = !!summary;
|
|
156
|
+
let held = false;
|
|
157
|
+
try {
|
|
158
|
+
if (summary)
|
|
159
|
+
writeXlsxFile(path.join(folder, 'estate.xlsx'), estateWorkbook(summary));
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
const code = e?.code ?? '';
|
|
163
|
+
if (!HELD.has(code))
|
|
164
|
+
throw new RegisterError(cannotWrite('estate.xlsx', code));
|
|
165
|
+
workbook = false;
|
|
166
|
+
held = true;
|
|
167
|
+
}
|
|
168
|
+
if (summary)
|
|
169
|
+
deps.print(estateLine(summary));
|
|
170
|
+
deps.print(` Written in the discovery folder: ${estateName}, ${objectsName}${workbook ? ', estate.xlsx' : ''}`);
|
|
171
|
+
if (held)
|
|
172
|
+
deps.print(' ! "estate.xlsx" is open in another program, so it was not updated. Close it and run discover again.');
|
|
173
|
+
if (!project) {
|
|
174
|
+
deps.print('Next: open the folder on the team page, or start a team project here: cspeach team start <packages>');
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
// The lead's own team project folder: the inventory's evidence for its objects, as one record.
|
|
178
|
+
const joined = Object.entries(loadState().projects).find(([id, p]) => id === project.projectId && samePath(p.projectDir, dir));
|
|
179
|
+
if (!joined) {
|
|
180
|
+
deps.print(' No evidence recorded: this laptop has not joined this team project. Run: cspeach team join <folder>');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const [projectId, p] = joined;
|
|
184
|
+
const keys = new Set(readBaseline(dir).map((o) => objectKey(o.type, o.name)));
|
|
185
|
+
const ev = evidenceRows(result.objects, keys);
|
|
186
|
+
const res = writeRecord({ ...p, projectId }, 'evidence', ev.rows, { name: whoAmI(deps) }, {
|
|
187
|
+
dir: path.join(dir, 'writers', p.writerId, 'records'),
|
|
188
|
+
source: { file: objectsName, sha256: createHash('sha256').update(objectsText).digest('hex') },
|
|
189
|
+
...(deps.now ? { now: deps.now() } : {}), ...(deps.rand ? { rand: deps.rand } : {}),
|
|
190
|
+
});
|
|
191
|
+
deps.print(` ${EVIDENCE_LINE(res.written)}`);
|
|
192
|
+
if (ev.outside)
|
|
193
|
+
deps.print(` ${OUTSIDE_LINE(ev.outside)}`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|