@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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal .xlsx writer (spec §6.1): an .xlsx is a zip of a few XML parts.
|
|
3
|
+
*
|
|
4
|
+
* Text cells are inline strings (`t="inlineStr"`), which Excel never evaluates
|
|
5
|
+
* as formulas, so `=SUM(` in a reason stays text; a text cell that starts with
|
|
6
|
+
* = + - or @ also carries the quote-prefix style, so it stays text after an edit. Every cell goes through XML
|
|
7
|
+
* escaping, and characters XML 1.0 cannot hold are dropped. One bold header
|
|
8
|
+
* style, column widths, a frozen header row and an autofilter. Entry times are
|
|
9
|
+
* pinned, so the same sheets give the same bytes.
|
|
10
|
+
*
|
|
11
|
+
* Quote characters inside regex literals are written \x22 / \x27 so the
|
|
12
|
+
* terminal words guard (team-words.test.ts) keeps its place in this file.
|
|
13
|
+
*/
|
|
14
|
+
import AdmZip from 'adm-zip';
|
|
15
|
+
import { writeWhole } from './write-whole.js';
|
|
16
|
+
const PINNED = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
|
|
17
|
+
const MAX_CELL = 32767;
|
|
18
|
+
/** XML 1.0 forbids C0 controls other than tab, LF and CR, and U+FFFE / U+FFFF. */
|
|
19
|
+
const NOT_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
|
|
20
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
21
|
+
const NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
|
22
|
+
const REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
23
|
+
const HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
24
|
+
/** Cells Excel could read as a formula: =, +, - or @ first. */
|
|
25
|
+
const FORMULA_LEAD = /^[=+\-@]/;
|
|
26
|
+
/** The cell style with `quotePrefix` (the third xf in STYLES). */
|
|
27
|
+
const QUOTED = 2;
|
|
28
|
+
export function xmlText(v) {
|
|
29
|
+
// Strip lone surrogates again after the cut: the cut can split a pair.
|
|
30
|
+
return v.replace(NOT_XML, '').replace(LONE_SURROGATE, '').slice(0, MAX_CELL).replace(LONE_SURROGATE, '')
|
|
31
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\x22/g, '"');
|
|
32
|
+
}
|
|
33
|
+
export function colName(i) {
|
|
34
|
+
let s = '';
|
|
35
|
+
for (let n = i + 1; n > 0; n = Math.floor((n - 1) / 26))
|
|
36
|
+
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
function cellXml(v, ref, style) {
|
|
40
|
+
if (v === null || v === undefined || v === '')
|
|
41
|
+
return '';
|
|
42
|
+
const s = style ? ` s="${style}"` : '';
|
|
43
|
+
if (typeof v === 'number')
|
|
44
|
+
return Number.isFinite(v) ? `<c r="${ref}"${s}><v>${v}</v></c>` : '';
|
|
45
|
+
const text = String(v);
|
|
46
|
+
// Text that starts like a formula gets Excel's own quote-prefix style: shown as
|
|
47
|
+
// typed, and kept text even when someone edits the cell in Excel and presses Enter.
|
|
48
|
+
const t = !style && FORMULA_LEAD.test(text) ? ` s="${QUOTED}"` : s;
|
|
49
|
+
return `<c r="${ref}"${t} t="inlineStr"><is><t xml:space="preserve">${xmlText(text)}</t></is></c>`;
|
|
50
|
+
}
|
|
51
|
+
function sheetXml(sheet) {
|
|
52
|
+
const width = Math.max(1, sheet.header.length, ...sheet.rows.map((r) => r.length));
|
|
53
|
+
const row = (cells, r, style) => `<row r="${r}">${cells.map((v, c) => cellXml(v, `${colName(c)}${r}`, style)).join('')}</row>`;
|
|
54
|
+
const cols = (sheet.widths ?? []).map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join('');
|
|
55
|
+
const filtered = sheet.filter !== false && sheet.rows.length > 0;
|
|
56
|
+
return `${HEAD}<worksheet xmlns="${NS}">`
|
|
57
|
+
+ '<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>'
|
|
58
|
+
+ (cols ? `<cols>${cols}</cols>` : '')
|
|
59
|
+
+ `<sheetData>${row(sheet.header, 1, 1)}${sheet.rows.map((r, i) => row(r, i + 2, 0)).join('')}</sheetData>`
|
|
60
|
+
+ (filtered ? `<autoFilter ref="A1:${colName(width - 1)}${sheet.rows.length + 1}"/>` : '')
|
|
61
|
+
+ '</worksheet>';
|
|
62
|
+
}
|
|
63
|
+
const STYLES = `${HEAD}<styleSheet xmlns="${NS}">`
|
|
64
|
+
+ '<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>'
|
|
65
|
+
+ '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
|
|
66
|
+
+ '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
|
|
67
|
+
+ '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
|
|
68
|
+
+ '<cellXfs count="3"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>'
|
|
69
|
+
+ '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" quotePrefix="1"/></cellXfs>'
|
|
70
|
+
+ '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>';
|
|
71
|
+
export function buildXlsx(sheets) {
|
|
72
|
+
const taken = new Set();
|
|
73
|
+
const named = sheets.map((s) => ({ ...s, name: safeSheetName(s.name, taken) }));
|
|
74
|
+
const zip = new AdmZip();
|
|
75
|
+
const add = (p, text) => zip.addFile(p, Buffer.from(text, 'utf-8'));
|
|
76
|
+
add('[Content_Types].xml', `${HEAD}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`
|
|
77
|
+
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
78
|
+
+ '<Default Extension="xml" ContentType="application/xml"/>'
|
|
79
|
+
+ '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
|
|
80
|
+
+ '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
|
|
81
|
+
+ named.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('')
|
|
82
|
+
+ '</Types>');
|
|
83
|
+
add('_rels/.rels', `${HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
|
|
84
|
+
+ `<Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/></Relationships>`);
|
|
85
|
+
const defined = named.map((s, i) => {
|
|
86
|
+
if (s.filter === false || !s.rows.length)
|
|
87
|
+
return '';
|
|
88
|
+
const width = Math.max(1, s.header.length, ...s.rows.map((r) => r.length));
|
|
89
|
+
return `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">'${xmlText(s.name.replace(/\x27/g, "''"))}'!$A$1:$${colName(width - 1)}$${s.rows.length + 1}</definedName>`;
|
|
90
|
+
}).join('');
|
|
91
|
+
add('xl/workbook.xml', `${HEAD}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>`
|
|
92
|
+
+ named.map((s, i) => `<sheet name="${xmlText(s.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join('')
|
|
93
|
+
+ `</sheets>${defined ? `<definedNames>${defined}</definedNames>` : ''}</workbook>`);
|
|
94
|
+
add('xl/_rels/workbook.xml.rels', `${HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
|
|
95
|
+
+ named.map((_, i) => `<Relationship Id="rId${i + 1}" Type="${REL_NS}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`).join('')
|
|
96
|
+
+ `<Relationship Id="rId${named.length + 1}" Type="${REL_NS}/styles" Target="styles.xml"/></Relationships>`);
|
|
97
|
+
add('xl/styles.xml', STYLES);
|
|
98
|
+
named.forEach((s, i) => add(`xl/worksheets/sheet${i + 1}.xml`, sheetXml(s)));
|
|
99
|
+
for (const e of zip.getEntries())
|
|
100
|
+
e.header.time = PINNED;
|
|
101
|
+
return zip.toBuffer();
|
|
102
|
+
}
|
|
103
|
+
export function writeXlsxFile(file, sheets) {
|
|
104
|
+
writeWhole(file, buildXlsx(sheets));
|
|
105
|
+
}
|
|
106
|
+
/** Excel's rules: no []:*?/\ , no leading or trailing apostrophe, 1–31 characters, unique ignoring case. */
|
|
107
|
+
export function safeSheetName(name, taken) {
|
|
108
|
+
const cleaned = String(name ?? '').replace(/[[\]:*?/\\]/g, ' ').replace(/\s+/g, ' ').trim().replace(/^\x27+|\x27+$/g, '').trim();
|
|
109
|
+
// Cut, then clean the ends again: a cut can leave an apostrophe (or a space) last.
|
|
110
|
+
const base = (cleaned || 'Sheet').slice(0, 31).replace(/^[\s\x27]+|[\s\x27]+$/g, '') || 'Sheet';
|
|
111
|
+
let candidate = base;
|
|
112
|
+
for (let n = 2; taken.has(candidate.toLowerCase()); n += 1) {
|
|
113
|
+
const tail = ` (${n})`;
|
|
114
|
+
candidate = `${base.slice(0, 31 - tail.length).trim()}${tail}`;
|
|
115
|
+
}
|
|
116
|
+
taken.add(candidate.toLowerCase());
|
|
117
|
+
return candidate;
|
|
118
|
+
}
|
|
119
|
+
const RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
120
|
+
/** A name Windows (and every sync tool) accepts as a file name stem. */
|
|
121
|
+
export function safeFileStem(name) {
|
|
122
|
+
const cleaned = String(name ?? '').replace(/[<>:\x22/\\|?*\u0000-\u001F]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
123
|
+
.replace(/[. ]+$/, '').slice(0, 80).trim();
|
|
124
|
+
if (!cleaned)
|
|
125
|
+
return 'team project';
|
|
126
|
+
return RESERVED.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
127
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The where-used index, turned into calling objects (spec §5.3).
|
|
3
|
+
*
|
|
4
|
+
* WBCROSSGT and CROSS name the INCLUDE that uses an object. A caller is the
|
|
5
|
+
* object that include belongs to: a class (its pool includes follow a fixed
|
|
6
|
+
* naming scheme), an interface pool, a function group (SAPL<fg>, or an L<fg>…
|
|
7
|
+
* include through its D010INC master), or a report (its master, or itself).
|
|
8
|
+
* Anything that resolves to nothing is counted as unmapped — never guessed.
|
|
9
|
+
*
|
|
10
|
+
* Namespaced names (`/ACME/CL_THING======CP`, `/ACME/SAPLZFG`) follow the
|
|
11
|
+
* same rules; the namespace stays part of the object name.
|
|
12
|
+
*/
|
|
13
|
+
/** Class-pool includes: name padded with '=' then CP/CU/CI/CO/CT/CS, CMnnn, CCDEF/CCIMP/CCMAC/CCAU. */
|
|
14
|
+
const CLASS_INCLUDE = /^([^=]{1,30})=+(C[A-Z]{1,2}|CM[0-9A-Z]{3}|CCDEF|CCIMP|CCMAC|CCAU)$/;
|
|
15
|
+
/** Interface-pool includes. */
|
|
16
|
+
const INTF_INCLUDE = /^([^=]{1,30})=+I[PU]$/;
|
|
17
|
+
/**
|
|
18
|
+
* The same pool includes read at fixed width: the object name is the first 30
|
|
19
|
+
* characters, padded with '='. A name of exactly 30 characters has no '=' at
|
|
20
|
+
* all, which the patterns above cannot see.
|
|
21
|
+
*/
|
|
22
|
+
const CLASS_SUFFIX = /^(C[A-Z]{1,2}|CM[0-9A-Z]{3}|CCDEF|CCIMP|CCMAC|CCAU)$/;
|
|
23
|
+
const INTF_SUFFIX = /^I[PU]$/;
|
|
24
|
+
/** Function-group main program, optionally namespaced: SAPLZFG, /ACME/SAPLZFG. */
|
|
25
|
+
const FUGR_MAIN = /^(\/[A-Z0-9_]+\/)?SAPL(.+)$/;
|
|
26
|
+
/** Pattern-only mapping: what an include or main program name says by itself. */
|
|
27
|
+
function byPattern(include) {
|
|
28
|
+
let m = CLASS_INCLUDE.exec(include);
|
|
29
|
+
if (m)
|
|
30
|
+
return { type: 'CLAS', name: m[1] };
|
|
31
|
+
m = INTF_INCLUDE.exec(include);
|
|
32
|
+
if (m)
|
|
33
|
+
return { type: 'INTF', name: m[1] };
|
|
34
|
+
if (include.length > 30) {
|
|
35
|
+
const name = include.slice(0, 30).replace(/=+$/, '');
|
|
36
|
+
const suffix = include.slice(30);
|
|
37
|
+
if (name && !name.includes('=')) {
|
|
38
|
+
if (CLASS_SUFFIX.test(suffix))
|
|
39
|
+
return { type: 'CLAS', name };
|
|
40
|
+
if (INTF_SUFFIX.test(suffix))
|
|
41
|
+
return { type: 'INTF', name };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
m = FUGR_MAIN.exec(include);
|
|
45
|
+
if (m)
|
|
46
|
+
return { type: 'FUGR', name: `${m[1] ?? ''}${m[2]}` };
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The object an include belongs to. `masters` maps include → main program
|
|
51
|
+
* (from D010INC); a report known from TADIR is entered as mapping to itself.
|
|
52
|
+
*/
|
|
53
|
+
export function objectOfInclude(include, masters) {
|
|
54
|
+
const inc = (include ?? '').trim().toUpperCase();
|
|
55
|
+
if (!inc)
|
|
56
|
+
return null;
|
|
57
|
+
const direct = byPattern(inc);
|
|
58
|
+
if (direct)
|
|
59
|
+
return direct;
|
|
60
|
+
const master = masters.get(inc);
|
|
61
|
+
if (!master)
|
|
62
|
+
return null;
|
|
63
|
+
return byPattern(master) ?? { type: 'PROG', name: master };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Callers per referenced object name. Index names can carry a component
|
|
67
|
+
* (`ZCL_X\ME:RUN`); the part before the backslash is the object. `selfOf(name)`
|
|
68
|
+
* gives that object's own key (`TYPE|NAME`) so a self-reference is dropped.
|
|
69
|
+
* `includesUnmapped` counts distinct includes CSPeach could not map.
|
|
70
|
+
*/
|
|
71
|
+
export function callersFromIndex(rows, masters, selfOf) {
|
|
72
|
+
const out = new Map();
|
|
73
|
+
const unmapped = new Map();
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const name = (row.name ?? '').trim().toUpperCase().split('\\')[0];
|
|
76
|
+
if (!name)
|
|
77
|
+
continue;
|
|
78
|
+
const include = (row.include ?? '').trim().toUpperCase();
|
|
79
|
+
let entry = out.get(name);
|
|
80
|
+
if (!entry) {
|
|
81
|
+
entry = { objects: new Set(), includesUnmapped: 0 };
|
|
82
|
+
out.set(name, entry);
|
|
83
|
+
}
|
|
84
|
+
const ref = objectOfInclude(include, masters);
|
|
85
|
+
if (!ref) {
|
|
86
|
+
let seen = unmapped.get(name);
|
|
87
|
+
if (!seen) {
|
|
88
|
+
seen = new Set();
|
|
89
|
+
unmapped.set(name, seen);
|
|
90
|
+
}
|
|
91
|
+
if (include)
|
|
92
|
+
seen.add(include);
|
|
93
|
+
entry.includesUnmapped = seen.size;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const key = `${ref.type}|${ref.name}`;
|
|
97
|
+
if (key === selfOf(name))
|
|
98
|
+
continue;
|
|
99
|
+
entry.objects.add(key);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** The REPOSRC program name that carries an object's change date, or null for other types. */
|
|
104
|
+
export function reposrcNameFor(type, name) {
|
|
105
|
+
const t = type.toUpperCase();
|
|
106
|
+
const n = name.toUpperCase();
|
|
107
|
+
switch (t) {
|
|
108
|
+
case 'PROG': return n;
|
|
109
|
+
case 'CLAS': return `${n.padEnd(30, '=')}CP`;
|
|
110
|
+
case 'INTF': return `${n.padEnd(30, '=')}IP`;
|
|
111
|
+
case 'FUGR': {
|
|
112
|
+
const m = /^(\/[A-Z0-9_]+\/)(.+)$/.exec(n);
|
|
113
|
+
return m ? `${m[1]}SAPL${m[2]}` : `SAPL${n}`;
|
|
114
|
+
}
|
|
115
|
+
default: return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -12,6 +12,7 @@ import { formatAttention } from './notice-shape.js';
|
|
|
12
12
|
import { truncateToWidth, uiWidth, frameWidth } from './ui-width.js';
|
|
13
13
|
import { sanitizeControlChars } from './sanitize.js';
|
|
14
14
|
import { holdBrand } from './brand-settled.js';
|
|
15
|
+
import { formatTimeAgo } from '../session/time-ago.js';
|
|
15
16
|
export { brandSettled } from './brand-settled.js';
|
|
16
17
|
/** Folder budget on the panels status line (controller ruling R2). */
|
|
17
18
|
const STATUS_PATH_MAX = 20;
|
|
@@ -131,6 +132,16 @@ export function panelsResumeLine(last, priorTurns) {
|
|
|
131
132
|
const fact = last ?? `${priorTurns} prior turn${priorTurns === 1 ? '' : 's'}`;
|
|
132
133
|
return ' ' + paint('dim', truncateToWidth(sanitizeControlChars(`resumed · ${fact}`), Math.max(20, uiWidth() - 3)));
|
|
133
134
|
}
|
|
135
|
+
/** 2026-10-08 (sessions/folders) — the dim start line pointing at this folder's
|
|
136
|
+
* latest session. Quotes the user's prompt, so control characters are neutralised. */
|
|
137
|
+
export function lastSessionHereLine(s, now = new Date()) {
|
|
138
|
+
const when = formatTimeAgo(s.lastTurnAt, now);
|
|
139
|
+
const what = [
|
|
140
|
+
s.skill ? `/${s.skill}` : null,
|
|
141
|
+
s.promptSnippet ? `"${truncateToWidth(sanitizeControlChars(s.promptSnippet), 40)}"` : null,
|
|
142
|
+
].filter(Boolean).join(', ');
|
|
143
|
+
return ' ' + paint('dim', `Last session here: ${when}${what ? ` (${what})` : ''} — cspeach --continue picks it up.`);
|
|
144
|
+
}
|
|
134
145
|
/** A "!" line for the start screen (no Next: line → exactly one line). */
|
|
135
146
|
export function panelsInterruptedNote(n) {
|
|
136
147
|
return formatAttention({ text: `${n} unfinished session${n === 1 ? '' : 's'}`, fact: 'cspeach --resume' }).trimEnd();
|
|
@@ -160,6 +160,8 @@ const RULES = {
|
|
|
160
160
|
sap_link: (a) => ({ verb: 'SAP link', object: str(a['action']) ?? '' }),
|
|
161
161
|
cap_link_smoke: (a) => ({ verb: 'S/4 link check', object: str(a['s4']?.['entitySet']) ?? '' }),
|
|
162
162
|
app_lane_resolve: (a) => ({ verb: 'Chose lane', object: shortLeft(str(a['specPath']) ?? '') }),
|
|
163
|
+
team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
|
|
164
|
+
team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
|
|
163
165
|
sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
|
|
164
166
|
sap_snapshot_list: (a) => ({ verb: 'Listed snapshots', object: shortRight(str(a['name']) ?? '') }),
|
|
165
167
|
sap_snapshot_read: sapObject('Read snapshot'),
|
|
@@ -265,7 +267,7 @@ const PRESENT = {
|
|
|
265
267
|
Preview: 'Preview', 'Searched samples': 'Searching samples', 'Read sample': 'Reading sample',
|
|
266
268
|
'Audited metadata': 'Auditing metadata', 'Extended model': 'Extending model',
|
|
267
269
|
'Checked capability': 'Checking capability',
|
|
268
|
-
'Read texts of': 'Reading texts of', 'Wrote texts': 'Writing texts',
|
|
270
|
+
'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Read estate inventory': 'Reading estate inventory', 'Wrote texts': 'Writing texts',
|
|
269
271
|
};
|
|
270
272
|
export function toolActivityLabel(name, args) {
|
|
271
273
|
const l = toolLabel(name, args);
|
|
@@ -402,6 +402,18 @@ function approvalDeclined(content) {
|
|
|
402
402
|
return undefined;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
+
/** 1.1.31 — true when request_approval approved by the user's auto_approve setting, not by a person. */
|
|
406
|
+
function approvalAutoApproved(content) {
|
|
407
|
+
if (!content || !content.trim().startsWith('{'))
|
|
408
|
+
return false;
|
|
409
|
+
try {
|
|
410
|
+
const r = JSON.parse(content);
|
|
411
|
+
return r.approved === true && r.auto_approved === true;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
405
417
|
function panelsRow(p) {
|
|
406
418
|
const width = Math.max(20, getEffectiveBodyWidth());
|
|
407
419
|
const label = toolLabel(p.name, p.args, p.isError ? undefined : p.resultContent);
|
|
@@ -410,7 +422,9 @@ function panelsRow(p) {
|
|
|
410
422
|
const declined = !p.isError && p.name === 'request_approval' ? approvalDeclined(p.resultContent) : undefined;
|
|
411
423
|
const icon = p.isError ? paint('bad', '✗') : declined ? paint('warn', '·') : paint('ok', '✓');
|
|
412
424
|
const timing = p.durationMs >= 1000 ? formatDuration(p.durationMs) : '';
|
|
413
|
-
|
|
425
|
+
// 1.1.31: a setting said yes, not a person — the row says so.
|
|
426
|
+
const auto = !p.isError && !declined && p.name === 'request_approval' && approvalAutoApproved(p.resultContent);
|
|
427
|
+
const tail = [declined ?? (auto ? 'auto-approved' : label.detail), timing].filter(Boolean).join(' · ');
|
|
414
428
|
const verb = label.verb.padEnd(VERB_COL);
|
|
415
429
|
// Budget: "✓ " + verb + " " + object + " " + tail must fit the width.
|
|
416
430
|
const budget = width - 2 - verb.length - 1 - (tail ? tail.length + 2 : 0);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* The cache TTL is short enough that newly-saved files appear within a
|
|
18
18
|
* few seconds of their save without manual refresh.
|
|
19
19
|
*/
|
|
20
|
-
import { ensureWorkspaceSync, listProjectFilesSync } from '../projects/workspace.js';
|
|
20
|
+
import { ensureWorkspaceSync, listLegacyFilesSync, listProjectFilesSync } from '../projects/workspace.js';
|
|
21
21
|
const CACHE_TTL_MS = 3000;
|
|
22
22
|
let cache = null;
|
|
23
23
|
function getCachedFiles() {
|
|
@@ -25,7 +25,11 @@ function getCachedFiles() {
|
|
|
25
25
|
if (!cache || now - cache.at > CACHE_TTL_MS) {
|
|
26
26
|
try {
|
|
27
27
|
const ws = ensureWorkspaceSync();
|
|
28
|
-
|
|
28
|
+
const files = listProjectFilesSync(ws);
|
|
29
|
+
// Task 11 — old save locations stay readable for one release; their
|
|
30
|
+
// files follow the new folder's, same names skipped.
|
|
31
|
+
const legacy = listLegacyFilesSync(process.cwd(), files.map((f) => f.filename));
|
|
32
|
+
cache = { files: [...files, ...legacy], at: now };
|
|
29
33
|
}
|
|
30
34
|
catch {
|
|
31
35
|
cache = { files: [], at: now };
|
package/dist/repl/at-picker.js
CHANGED
|
@@ -48,7 +48,7 @@ export function listWorkspaceFileChoices(opts) {
|
|
|
48
48
|
const seen = new Set();
|
|
49
49
|
// Workspace half (takes precedence on name collisions).
|
|
50
50
|
try {
|
|
51
|
-
const ws = opts?.workspace ?? ensureWorkspaceSync();
|
|
51
|
+
const ws = opts?.workspace ?? ensureWorkspaceSync(opts?.cwd);
|
|
52
52
|
for (const f of listProjectFilesSync(ws)) {
|
|
53
53
|
if (seen.has(f.filename))
|
|
54
54
|
continue;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { enterJourney } from '../journey/chain.js';
|
|
2
|
+
import { openStatusPage } from '../util/open-in-browser.js';
|
|
3
|
+
export async function runJourneyEntry(trigger, a) {
|
|
4
|
+
const hooks = {
|
|
5
|
+
session: a.session,
|
|
6
|
+
cwd: a.cwd,
|
|
7
|
+
headless: a.headless,
|
|
8
|
+
emit: a.emit,
|
|
9
|
+
...(a.queueNext && !a.headless ? { queueNext: a.queueNext } : {}),
|
|
10
|
+
...(!a.standalone && a.adt ? { adt: a.adt } : {}),
|
|
11
|
+
sapAlias: a.sapAlias,
|
|
12
|
+
...(a.standalone ? { noSap: true } : {}),
|
|
13
|
+
signal: a.signal,
|
|
14
|
+
// Task 6 — the status page opens in the browser after Yes on its card.
|
|
15
|
+
openFile: (file) => openStatusPage(file),
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
await enterJourney(trigger, hooks);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
22
|
+
throw err;
|
|
23
|
+
a.emit('The clean-core steps stopped here. Say carry on to continue.');
|
|
24
|
+
}
|
|
25
|
+
}
|