@cspeach/cli 1.1.30 → 1.1.32
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/README.md +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -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-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -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 +100 -12
- 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/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -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 +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- package/dist/register/xlsx.js +13 -3
- 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/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -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 +53 -53
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/dispatch-skill.js +8 -4
- 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/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- 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/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-10-08 (sessions/folders, Task 13) — `cspeach files sort`, the
|
|
3
|
+
* suggesting part. Given the old project files, the sessions and the known
|
|
4
|
+
* project folders, it suggests one folder per file with a confidence
|
|
5
|
+
* (spec §3.6):
|
|
6
|
+
*
|
|
7
|
+
* high — a session wrote into folder F within ±2 h of the file's
|
|
8
|
+
* createdAt, or the file is linked to a file placed in F;
|
|
9
|
+
* medium — its input names an attached file found in exactly one project
|
|
10
|
+
* folder's `.cspeach`;
|
|
11
|
+
* low — no sign; no folder suggested.
|
|
12
|
+
*
|
|
13
|
+
* Linked files form one chain (same id = versions, promotedFrom, a plan's
|
|
14
|
+
* from.specGap). A chain goes to ONE folder; a chain whose files point to
|
|
15
|
+
* different folders is flagged and gets no folder — it is never split.
|
|
16
|
+
*
|
|
17
|
+
* No writes. The only file system use is the read-only `.cspeach` lookup
|
|
18
|
+
* for medium evidence (replaceable through `holds`).
|
|
19
|
+
*/
|
|
20
|
+
import { readdirSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { folderAboveCspeach } from '../session/backfill.js';
|
|
23
|
+
import { normDir } from '../session/project-match.js';
|
|
24
|
+
import { cspeachRoot, defaultCspeachRoot } from '../config/paths.js';
|
|
25
|
+
const WINDOW_MS = 2 * 60 * 60 * 1000;
|
|
26
|
+
const WRITE_TOOLS = new Set(['file_write', 'file_edit', 'write_file', 'edit_file']);
|
|
27
|
+
const RANK = { low: 0, medium: 1, high: 2 };
|
|
28
|
+
/** Does `<dir>/.cspeach` hold a file called `name` (a few levels deep)? */
|
|
29
|
+
export function cspeachHolds(dir, name) {
|
|
30
|
+
const want = name.toLowerCase();
|
|
31
|
+
const walk = (d, depth) => {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
for (const e of entries)
|
|
40
|
+
if (e.isFile() && e.name.toLowerCase() === want)
|
|
41
|
+
return true;
|
|
42
|
+
if (depth <= 0)
|
|
43
|
+
return false;
|
|
44
|
+
for (const e of entries)
|
|
45
|
+
if (e.isDirectory() && walk(path.join(d, e.name), depth - 1))
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
return walk(path.join(dir, '.cspeach'), 3);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Where a plan's relative pin (`.cspeach/…`, written against the folder that
|
|
53
|
+
* holds the plan's `.cspeach`) can be, on disk. Only places inside the plan's
|
|
54
|
+
* own folder are returned, so an edited pin can never reach outside it.
|
|
55
|
+
*/
|
|
56
|
+
export function linkedFileCandidates(planFile, rel) {
|
|
57
|
+
if (typeof rel !== 'string' || rel === '' || /\u0000/.test(rel) || path.isAbsolute(rel))
|
|
58
|
+
return [];
|
|
59
|
+
const planDir = path.dirname(path.resolve(planFile));
|
|
60
|
+
const inside = (p) => {
|
|
61
|
+
const r = path.relative(planDir, p);
|
|
62
|
+
return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
|
|
63
|
+
};
|
|
64
|
+
const tail = rel.replace(/\\/g, '/').replace(/^(\.\/)*\.cspeach\//i, '');
|
|
65
|
+
const out = [path.resolve(path.dirname(planDir), rel), path.resolve(planDir, tail)];
|
|
66
|
+
return out.filter((p, i) => inside(p) && out.findIndex((q) => normDir(q) === normDir(p)) === i);
|
|
67
|
+
}
|
|
68
|
+
/** Groups of indexes into `files` that must travel together. */
|
|
69
|
+
export function linkChains(files) {
|
|
70
|
+
const parent = files.map((_, i) => i);
|
|
71
|
+
const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
|
|
72
|
+
const join = (a, b) => { const ra = find(a); const rb = find(b); if (ra !== rb)
|
|
73
|
+
parent[rb] = ra; };
|
|
74
|
+
const byId = new Map();
|
|
75
|
+
const byPath = new Map();
|
|
76
|
+
files.forEach((f, i) => {
|
|
77
|
+
byPath.set(normDir(f.path), i);
|
|
78
|
+
const id = f.env?.id;
|
|
79
|
+
if (typeof id !== 'string')
|
|
80
|
+
return;
|
|
81
|
+
const seen = byId.get(id);
|
|
82
|
+
if (seen === undefined)
|
|
83
|
+
byId.set(id, i);
|
|
84
|
+
else
|
|
85
|
+
join(seen, i);
|
|
86
|
+
});
|
|
87
|
+
files.forEach((f, i) => {
|
|
88
|
+
const env = f.env;
|
|
89
|
+
if (!env)
|
|
90
|
+
return;
|
|
91
|
+
const from = env.promotedFrom?.id;
|
|
92
|
+
if (typeof from === 'string' && byId.has(from))
|
|
93
|
+
join(byId.get(from), i);
|
|
94
|
+
const gap = env.artefactType === 'plan' ? env.content?.from?.specGap?.path : undefined;
|
|
95
|
+
if (typeof gap === 'string') {
|
|
96
|
+
for (const c of linkedFileCandidates(f.path, gap)) {
|
|
97
|
+
const hit = byPath.get(normDir(c));
|
|
98
|
+
if (hit !== undefined) {
|
|
99
|
+
join(hit, i);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const groups = new Map();
|
|
106
|
+
files.forEach((_, i) => { const r = find(i); groups.set(r, [...(groups.get(r) ?? []), i]); });
|
|
107
|
+
return [...groups.values()];
|
|
108
|
+
}
|
|
109
|
+
const within = (p, root) => {
|
|
110
|
+
const k = normDir(p);
|
|
111
|
+
const r = normDir(root);
|
|
112
|
+
return k === r || k.startsWith(r.endsWith(path.sep) ? r : r + path.sep);
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Ruling 24: a folder that is, or whose `.cspeach` would be, inside CSPeach's
|
|
116
|
+
* own folder (the CLI home, moved or default) is never a target.
|
|
117
|
+
*/
|
|
118
|
+
export function insideCliHome(dir) {
|
|
119
|
+
const roots = [cspeachRoot(), defaultCspeachRoot()];
|
|
120
|
+
return roots.some((r) => within(dir, r) || within(path.join(dir, '.cspeach'), r));
|
|
121
|
+
}
|
|
122
|
+
const createdMs = (f) => {
|
|
123
|
+
const t = Date.parse(String(f.env?.createdAt ?? ''));
|
|
124
|
+
return Number.isNaN(t) ? f.mtimeMs : t;
|
|
125
|
+
};
|
|
126
|
+
/** A folder that is just the file's own old location is no evidence. */
|
|
127
|
+
const ownFolder = (file, dir) => normDir(path.join(dir, '.cspeach')) === normDir(path.dirname(file)) || normDir(dir) === normDir(path.dirname(file));
|
|
128
|
+
/**
|
|
129
|
+
* high: the folder a session wrote into most within ±2 h of the file's
|
|
130
|
+
* creation. A write counts for the session's own folder (`project_dir`) when
|
|
131
|
+
* it is under it, else for the folder above a `.cspeach` in its path. A write
|
|
132
|
+
* anywhere else (a sub-folder, a temp folder) is no evidence.
|
|
133
|
+
*/
|
|
134
|
+
function sessionFolder(f, sessions) {
|
|
135
|
+
const created = Date.parse(String(f.env?.createdAt ?? ''));
|
|
136
|
+
if (Number.isNaN(created))
|
|
137
|
+
return null;
|
|
138
|
+
const counts = new Map();
|
|
139
|
+
for (const s of sessions) {
|
|
140
|
+
const calls = s?.toolCalls;
|
|
141
|
+
if (!Array.isArray(calls))
|
|
142
|
+
continue;
|
|
143
|
+
const pd = s?.project_dir;
|
|
144
|
+
const projectDir = typeof pd === 'string' && path.isAbsolute(pd) ? pd : null;
|
|
145
|
+
for (const c of calls) {
|
|
146
|
+
const tool = c?.tool ?? c?.name;
|
|
147
|
+
const p = c?.args?.path;
|
|
148
|
+
if (!WRITE_TOOLS.has(tool) || typeof p !== 'string' || !path.isAbsolute(p))
|
|
149
|
+
continue;
|
|
150
|
+
const at = Date.parse(String(c.completed_at ?? c.started_at ?? ''));
|
|
151
|
+
if (Number.isNaN(at) || Math.abs(at - created) > WINDOW_MS)
|
|
152
|
+
continue;
|
|
153
|
+
const dir = projectDir && within(p, projectDir) ? projectDir : folderAboveCspeach(p);
|
|
154
|
+
if (!dir || ownFolder(f.path, dir))
|
|
155
|
+
continue;
|
|
156
|
+
const k = normDir(dir);
|
|
157
|
+
const hit = counts.get(k);
|
|
158
|
+
if (hit)
|
|
159
|
+
hit.n++;
|
|
160
|
+
else
|
|
161
|
+
counts.set(k, { dir, n: 1 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let best = null;
|
|
165
|
+
for (const v of counts.values())
|
|
166
|
+
if (!best || v.n > best.n)
|
|
167
|
+
best = v;
|
|
168
|
+
return best?.dir ?? null;
|
|
169
|
+
}
|
|
170
|
+
/** File names the envelope's input attached (`<attached file="…">`, `@name.ext`). */
|
|
171
|
+
function attachedNames(input) {
|
|
172
|
+
if (typeof input !== 'string')
|
|
173
|
+
return [];
|
|
174
|
+
const names = new Set();
|
|
175
|
+
for (const m of input.matchAll(/<attached file="([^"]+)">/g))
|
|
176
|
+
names.add(path.basename(m[1].replace(/\\/g, '/')));
|
|
177
|
+
for (const m of input.matchAll(/(?:^|\s)@([^\s"'<>]+\.[A-Za-z0-9]{2,5})\b/g))
|
|
178
|
+
names.add(path.basename(m[1].replace(/\\/g, '/')));
|
|
179
|
+
return [...names];
|
|
180
|
+
}
|
|
181
|
+
/** medium: the one project folder whose `.cspeach` holds an attached file. */
|
|
182
|
+
function attachedFolder(f, projectDirs, holds) {
|
|
183
|
+
for (const name of attachedNames(f.env?.source?.input)) {
|
|
184
|
+
const hits = projectDirs.filter((d) => !ownFolder(f.path, d) && holds(d, name));
|
|
185
|
+
if (hits.length === 1)
|
|
186
|
+
return { dir: hits[0], name };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
export function buildSortPlan(input) {
|
|
191
|
+
const holds = input.holds ?? cspeachHolds;
|
|
192
|
+
const projectDirs = input.projectDirs
|
|
193
|
+
.filter((d) => !insideCliHome(d))
|
|
194
|
+
.filter((d, i, a) => a.findIndex((x) => normDir(x) === normDir(d)) === i);
|
|
195
|
+
const files = input.files;
|
|
196
|
+
const evidence = files.map((f) => {
|
|
197
|
+
if (!f.env)
|
|
198
|
+
return null;
|
|
199
|
+
const s = sessionFolder(f, input.sessions);
|
|
200
|
+
if (s)
|
|
201
|
+
return { level: 'high', dir: s, why: 'a session wrote there near this time' };
|
|
202
|
+
const a = attachedFolder(f, projectDirs, holds);
|
|
203
|
+
if (a)
|
|
204
|
+
return { level: 'medium', dir: a.dir, why: `its attached ${a.name} is there` };
|
|
205
|
+
return null;
|
|
206
|
+
});
|
|
207
|
+
const chains = linkChains(files)
|
|
208
|
+
.map((g) => g.sort((a, b) => createdMs(files[a]) - createdMs(files[b])))
|
|
209
|
+
.sort((a, b) => createdMs(files[a[0]]) - createdMs(files[b[0]]));
|
|
210
|
+
const rows = [];
|
|
211
|
+
for (const group of chains) {
|
|
212
|
+
const first = files[group[0]];
|
|
213
|
+
const chain = typeof first.env?.id === 'string' ? first.env.id.slice(0, 8) : path.basename(first.path).replace(/\.cspeach\.json$/, '');
|
|
214
|
+
const all = group.map((i) => evidence[i]).filter((e) => !!e);
|
|
215
|
+
const found = all.filter((e) => !insideCliHome(e.dir));
|
|
216
|
+
const best = found.reduce((m, e) => Math.max(m, RANK[e.level]), 0);
|
|
217
|
+
const top = found.filter((e) => RANK[e.level] === best);
|
|
218
|
+
const dirs = top.filter((e, i) => top.findIndex((x) => normDir(x.dir) === normDir(e.dir)) === i).map((e) => e.dir);
|
|
219
|
+
if (dirs.length > 1) {
|
|
220
|
+
for (const i of group) {
|
|
221
|
+
rows.push({ file: files[i].path, chain, target: null, confidence: 'low',
|
|
222
|
+
why: `linked files point to different folders (${dirs.join(', ')}); pick one for all of them` });
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (dirs.length === 0) {
|
|
227
|
+
for (const i of group) {
|
|
228
|
+
rows.push({ file: files[i].path, chain, target: null, confidence: 'low',
|
|
229
|
+
why: !files[i].env ? 'the file could not be read'
|
|
230
|
+
: all.length > 0 ? "would be inside CSPeach's own folder" : 'no sign of where it belongs' });
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const target = dirs[0];
|
|
235
|
+
const anchor = group.find((i) => evidence[i] && !insideCliHome(evidence[i].dir) && RANK[evidence[i].level] === best);
|
|
236
|
+
const level = best === RANK.high ? 'high' : 'medium';
|
|
237
|
+
for (const i of group) {
|
|
238
|
+
const own = evidence[i];
|
|
239
|
+
if (own && normDir(own.dir) === normDir(target)) {
|
|
240
|
+
rows.push({ file: files[i].path, chain, target, confidence: own.level, why: own.why });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
rows.push({ file: files[i].path, chain, target, confidence: level, why: `linked to ${path.basename(files[anchor].path)}` });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return rows;
|
|
248
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retire the old `workspace_folder` setting (spec 2026-10-08).
|
|
3
|
+
*
|
|
4
|
+
* Files now save in the folder you start CSPeach in, and the resolver
|
|
5
|
+
* ignores `workspace_folder`. People who set it get one plain offer at
|
|
6
|
+
* start-up to remove it from config.toml. Either answer sets
|
|
7
|
+
* `workspace_folder_retired = true`, so the offer never comes back.
|
|
8
|
+
*
|
|
9
|
+
* Also: the old `.abapforge` → `.cspeach` merge offer runs only in a solo
|
|
10
|
+
* folder. A team folder must not move `<team>/.abapforge` into the shared
|
|
11
|
+
* `<team>/.cspeach`, and a home start must not target the CLI home.
|
|
12
|
+
*/
|
|
13
|
+
import { loadConfig, saveConfig } from '../config/loader.js';
|
|
14
|
+
import { unsetConfigKey } from '../commands/config-set.js';
|
|
15
|
+
import { resolveProjectFolderSync } from './project-folder.js';
|
|
16
|
+
/** One-time offer to remove `workspace_folder`. Never throws. */
|
|
17
|
+
export async function maybeOfferWorkspaceRetire(deps) {
|
|
18
|
+
try {
|
|
19
|
+
const cfg = await loadConfig();
|
|
20
|
+
const old = cfg.workspace_folder?.trim();
|
|
21
|
+
if (!old || cfg.workspace_folder_retired === true)
|
|
22
|
+
return;
|
|
23
|
+
deps.log(`Files now save in the folder you start CSPeach in. Your old setting pointed to ${old}; the files there stay.`);
|
|
24
|
+
let yes = false;
|
|
25
|
+
try {
|
|
26
|
+
yes = await deps.confirm('Remove the old setting?');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* cancel = no (Ruling 20): keep the key, still save the flag */
|
|
30
|
+
}
|
|
31
|
+
if (yes)
|
|
32
|
+
unsetConfigKey(cfg, 'workspace_folder');
|
|
33
|
+
cfg.workspace_folder_retired = true;
|
|
34
|
+
await saveConfig(cfg);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* best-effort: start-up must never fail over this offer */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The `.abapforge` merge offer is for solo folders only. */
|
|
41
|
+
export function shouldOfferAbapforgeMigration(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
return resolveProjectFolderSync(cwd).kind === 'solo';
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|