@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
|
@@ -7,55 +7,23 @@
|
|
|
7
7
|
* the user — the path-copy circus. The workspace folder is one configurable
|
|
8
8
|
* directory that the CLI saves into and resolves bare @<name> tokens against.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2. `<git-root>/.cspeach/` — when the cwd is inside a git repository.
|
|
14
|
-
* Each customer / project gets its own workspace; multi-project
|
|
15
|
-
* consultants don't have artefacts piling up in one folder.
|
|
16
|
-
* 3. `<cwd>/.cspeach/` — fallback when cwd isn't inside a git repo.
|
|
17
|
-
*
|
|
18
|
-
* Earlier (pre-2026-05-08) default was `~/CSPeach/`. A first-run migration
|
|
19
|
-
* prompt (see src/projects/migration.ts) offers to move existing artefacts
|
|
20
|
-
* into the new project-rooted workspace, or pin workspace_folder back to
|
|
21
|
-
* `~/CSPeach/` for users who prefer the old behaviour.
|
|
10
|
+
* Resolution (2026-10-08): see projects/project-folder.ts — the folder you
|
|
11
|
+
* start in; team folders save to the seat. No git walk; workspace_folder is
|
|
12
|
+
* ignored.
|
|
22
13
|
*
|
|
23
14
|
* The folder is created on first use — no `cspeach init` ceremony.
|
|
24
15
|
*/
|
|
25
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
26
17
|
import { dirname, join, isAbsolute } from 'node:path';
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
18
|
+
import { legacyDirsFor, resolveProjectFolderSync } from './project-folder.js';
|
|
19
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
20
|
+
import { normDir } from '../session/project-match.js';
|
|
30
21
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* containing `.git`, or null when the search hits the filesystem root
|
|
34
|
-
* without finding one.
|
|
35
|
-
*
|
|
36
|
-
* Bounded by parent === dir, which terminates at "/" on POSIX and at
|
|
37
|
-
* each drive root on Windows.
|
|
38
|
-
*/
|
|
39
|
-
export function findProjectRoot(startDir) {
|
|
40
|
-
let dir = startDir;
|
|
41
|
-
for (let i = 0; i < 200; i++) {
|
|
42
|
-
if (existsSync(join(dir, '.git')))
|
|
43
|
-
return dir;
|
|
44
|
-
const parent = dirname(dir);
|
|
45
|
-
if (parent === dir)
|
|
46
|
-
return null;
|
|
47
|
-
dir = parent;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve the project-rooted default workspace path for a given cwd.
|
|
53
|
-
* Pure: does NOT touch the filesystem (other than the .git existence
|
|
54
|
-
* check via findProjectRoot). Caller decides whether to mkdir.
|
|
22
|
+
* The workspace path for a given cwd (see projects/project-folder.ts).
|
|
23
|
+
* Pure: does NOT create anything. Caller decides whether to mkdir.
|
|
55
24
|
*/
|
|
56
25
|
export function defaultWorkspaceFor(cwd) {
|
|
57
|
-
|
|
58
|
-
return join(root ?? cwd, '.cspeach');
|
|
26
|
+
return resolveProjectFolderSync(cwd).workDir;
|
|
59
27
|
}
|
|
60
28
|
/**
|
|
61
29
|
* If the workspace lives directly under a project root that has a
|
|
@@ -91,17 +59,13 @@ function maybeAddToGitignore(workspaceDir) {
|
|
|
91
59
|
}
|
|
92
60
|
}
|
|
93
61
|
/**
|
|
94
|
-
* Resolve the workspace folder PATH without creating it.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* save-command.ts, which must not leave filesystem side effects before the
|
|
99
|
-
* user says yes to the save.
|
|
62
|
+
* Resolve the workspace folder PATH without creating it. Strictly read-only:
|
|
63
|
+
* no mkdir, no .gitignore append. For callers that only need to LOOK at the
|
|
64
|
+
* workspace — e.g. the pre-confirm prior-plan scan in save-command.ts, which
|
|
65
|
+
* must not leave filesystem side effects before the user says yes to the save.
|
|
100
66
|
*/
|
|
101
67
|
export async function resolveWorkspacePath() {
|
|
102
|
-
|
|
103
|
-
const configured = cfg.workspace_folder?.trim();
|
|
104
|
-
return resolveTarget(configured);
|
|
68
|
+
return defaultWorkspaceFor(process.cwd());
|
|
105
69
|
}
|
|
106
70
|
/**
|
|
107
71
|
* App lane routing I3 — the same read-only resolution as resolveWorkspacePath,
|
|
@@ -109,64 +73,35 @@ export async function resolveWorkspacePath() {
|
|
|
109
73
|
* real session the two are the same folder; tests pass a temp cwd.
|
|
110
74
|
*/
|
|
111
75
|
export async function resolveWorkspacePathFor(cwd) {
|
|
112
|
-
|
|
113
|
-
const configured = cfg.workspace_folder?.trim();
|
|
114
|
-
return configured && configured.length > 0 ? expandHome(configured) : defaultWorkspaceFor(cwd);
|
|
115
|
-
}
|
|
116
|
-
function resolveTarget(configured) {
|
|
117
|
-
return configured && configured.length > 0
|
|
118
|
-
? expandHome(configured)
|
|
119
|
-
: defaultWorkspaceFor(process.cwd());
|
|
76
|
+
return defaultWorkspaceFor(cwd);
|
|
120
77
|
}
|
|
121
78
|
/**
|
|
122
|
-
* Resolve the workspace folder path
|
|
123
|
-
* Returns the absolute path. Idempotent.
|
|
79
|
+
* Resolve the workspace folder path for `cwd` (default: process.cwd()),
|
|
80
|
+
* creating it if missing. Returns the absolute path. Idempotent.
|
|
124
81
|
*/
|
|
125
|
-
export async function ensureWorkspace() {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// would be overreach.
|
|
136
|
-
if (!configured)
|
|
137
|
-
maybeAddToGitignore(target);
|
|
138
|
-
}
|
|
139
|
-
return target;
|
|
82
|
+
export async function ensureWorkspace(cwd = process.cwd()) {
|
|
83
|
+
const pf = resolveProjectFolderSync(cwd);
|
|
84
|
+
if (!existsSync(pf.workDir)) {
|
|
85
|
+
mkdirSync(pf.workDir, { recursive: true });
|
|
86
|
+
// Only for the folder you start in. The .gitignore beside a team folder
|
|
87
|
+
// or ~/CSPeach is not ours to edit.
|
|
88
|
+
if (pf.kind === 'solo')
|
|
89
|
+
maybeAddToGitignore(pf.workDir);
|
|
90
|
+
}
|
|
91
|
+
return pf.workDir;
|
|
140
92
|
}
|
|
141
93
|
/**
|
|
142
94
|
* Synchronous variant for callers that cannot await — most importantly
|
|
143
|
-
* Node's readline `completer` interface, which is sync.
|
|
144
|
-
* config directly via fs.readFileSync. Falls back to project-rooted
|
|
145
|
-
* default on any error (missing config, parse failure, etc.) — the
|
|
146
|
-
* completer should never throw.
|
|
147
|
-
*
|
|
148
|
-
* Earlier revision used `require('@iarna/toml')` here, which threw in
|
|
149
|
-
* ESM context (cspeach-cli has `"type": "module"`) and silently broke
|
|
150
|
-
* Tab completion. Top-level import is correct for ESM modules.
|
|
95
|
+
* Node's readline `completer` interface, which is sync. Never throws.
|
|
151
96
|
*/
|
|
152
|
-
export function ensureWorkspaceSync() {
|
|
153
|
-
|
|
97
|
+
export function ensureWorkspaceSync(cwd = process.cwd()) {
|
|
98
|
+
const target = defaultWorkspaceFor(cwd);
|
|
154
99
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const raw = readFileSync(cfgPath, 'utf-8');
|
|
158
|
-
const parsed = toml.parse(raw);
|
|
159
|
-
const configured = parsed.workspace_folder?.trim();
|
|
160
|
-
if (configured && configured.length > 0) {
|
|
161
|
-
target = expandHome(configured);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
100
|
+
if (!existsSync(target))
|
|
101
|
+
mkdirSync(target, { recursive: true });
|
|
164
102
|
}
|
|
165
103
|
catch {
|
|
166
|
-
//
|
|
167
|
-
}
|
|
168
|
-
if (!existsSync(target)) {
|
|
169
|
-
mkdirSync(target, { recursive: true });
|
|
104
|
+
// The completer must never throw.
|
|
170
105
|
}
|
|
171
106
|
return target;
|
|
172
107
|
}
|
|
@@ -205,16 +140,6 @@ export function listProjectFilesSync(workspace) {
|
|
|
205
140
|
return [];
|
|
206
141
|
}
|
|
207
142
|
}
|
|
208
|
-
/**
|
|
209
|
-
* Expand a leading `~/` to the user's home directory. Other paths are
|
|
210
|
-
* returned unchanged.
|
|
211
|
-
*/
|
|
212
|
-
function expandHome(p) {
|
|
213
|
-
if (p.startsWith('~/') || p === '~') {
|
|
214
|
-
return join(os.homedir(), p.slice(p === '~' ? 1 : 2));
|
|
215
|
-
}
|
|
216
|
-
return p;
|
|
217
|
-
}
|
|
218
143
|
/** File suffixes recognised as text-input candidates for skills. */
|
|
219
144
|
const TEXT_SUFFIXES = ['.txt', '.md'];
|
|
220
145
|
/**
|
|
@@ -251,18 +176,63 @@ export function isAttachableFilename(name) {
|
|
|
251
176
|
* yield an `unknown` summary rather than throwing — callers see the
|
|
252
177
|
* file name with no metadata so the user can investigate.
|
|
253
178
|
*/
|
|
254
|
-
export async function listProjectFiles() {
|
|
255
|
-
const ws = await ensureWorkspace();
|
|
256
|
-
const
|
|
179
|
+
export async function listProjectFiles(cwd = process.cwd()) {
|
|
180
|
+
const ws = await ensureWorkspace(cwd);
|
|
181
|
+
const out = summarizeDir(readdirSync(ws), ws, false);
|
|
182
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
183
|
+
// 2026-10-08 (sessions/folders, Task 11) — for one release the old save
|
|
184
|
+
// locations stay readable: their files follow the new folder's, marked
|
|
185
|
+
// `earlier`. Read-only: nothing is created or written there. A file with
|
|
186
|
+
// the same name in the new folder wins.
|
|
187
|
+
const seen = new Set(out.map((f) => f.filename.toLowerCase()));
|
|
188
|
+
let legacy;
|
|
189
|
+
try {
|
|
190
|
+
legacy = legacyDirsFor(cwd);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
legacy = [];
|
|
194
|
+
}
|
|
195
|
+
for (const dir of legacy) {
|
|
196
|
+
let names;
|
|
197
|
+
try {
|
|
198
|
+
names = readdirSync(dir);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const rows = summarizeDir(names, dir, isCliHome(dir)).filter((f) => !seen.has(f.filename.toLowerCase()));
|
|
204
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
205
|
+
for (const f of rows) {
|
|
206
|
+
seen.add(f.filename.toLowerCase());
|
|
207
|
+
out.push({ ...f, earlier: true });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Whether `dir` is the CLI home. In the CLI home only `*.cspeach.json` files
|
|
214
|
+
* directly in it are ever listed — never config.toml, auth or sessions.
|
|
215
|
+
*/
|
|
216
|
+
function isCliHome(dir) {
|
|
217
|
+
return normDir(dir) === normDir(cspeachRoot());
|
|
218
|
+
}
|
|
219
|
+
/** The supported files among `names` in `dir`, parsed for the summary. */
|
|
220
|
+
function summarizeDir(names, dir, envelopesOnly) {
|
|
257
221
|
const out = [];
|
|
258
|
-
for (const name of
|
|
222
|
+
for (const name of names) {
|
|
259
223
|
const isEnvelope = name.endsWith('.cspeach.json');
|
|
260
|
-
const isText = isTextFilename(name);
|
|
261
|
-
const isDoc = isDocFilename(name);
|
|
224
|
+
const isText = !envelopesOnly && isTextFilename(name);
|
|
225
|
+
const isDoc = !envelopesOnly && isDocFilename(name);
|
|
262
226
|
if (!isEnvelope && !isText && !isDoc)
|
|
263
227
|
continue;
|
|
264
|
-
const path = join(
|
|
265
|
-
|
|
228
|
+
const path = join(dir, name);
|
|
229
|
+
let stat;
|
|
230
|
+
try {
|
|
231
|
+
stat = statSync(path);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
266
236
|
if (!stat.isFile())
|
|
267
237
|
continue;
|
|
268
238
|
if (isEnvelope) {
|
|
@@ -306,9 +276,95 @@ export async function listProjectFiles() {
|
|
|
306
276
|
});
|
|
307
277
|
}
|
|
308
278
|
}
|
|
309
|
-
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
310
279
|
return out;
|
|
311
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* The @ completer's view of the old save locations (sync, names only): the
|
|
283
|
+
* same files listProjectFiles marks `earlier`; names in `skip` (the new
|
|
284
|
+
* folder's) are left out. Never throws.
|
|
285
|
+
*/
|
|
286
|
+
export function listLegacyFilesSync(cwd, skip) {
|
|
287
|
+
const out = [];
|
|
288
|
+
const seen = new Set([...skip].map((n) => n.toLowerCase()));
|
|
289
|
+
let dirs;
|
|
290
|
+
try {
|
|
291
|
+
dirs = legacyDirsFor(cwd);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
for (const dir of dirs) {
|
|
297
|
+
const home = isCliHome(dir);
|
|
298
|
+
for (const f of listProjectFilesSync(dir)) {
|
|
299
|
+
if (home && f.kind !== 'envelope')
|
|
300
|
+
continue;
|
|
301
|
+
if (seen.has(f.filename.toLowerCase()))
|
|
302
|
+
continue;
|
|
303
|
+
seen.add(f.filename.toLowerCase());
|
|
304
|
+
out.push(f);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
/** Number of `*.cspeach.json` files directly in `dir` (0 when unreadable). */
|
|
310
|
+
function countEnvelopes(dir) {
|
|
311
|
+
try {
|
|
312
|
+
return readdirSync(dir).filter((n) => n.endsWith('.cspeach.json')).length;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The start-up hint for files left in old save locations, or null when
|
|
320
|
+
* there are none. Names the first old folder; counts envelopes in all.
|
|
321
|
+
*/
|
|
322
|
+
export function legacyHintLine(cwd) {
|
|
323
|
+
let dirs;
|
|
324
|
+
try {
|
|
325
|
+
dirs = legacyDirsFor(cwd);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const n = dirs.reduce((sum, d) => sum + countEnvelopes(d), 0);
|
|
331
|
+
if (n === 0)
|
|
332
|
+
return null;
|
|
333
|
+
const lead = n === 1
|
|
334
|
+
? `1 project file from earlier builds is in ${dirs[0]}.`
|
|
335
|
+
: `${n} project files from earlier builds are in ${dirs[0]}.`;
|
|
336
|
+
return `${lead} Run: cspeach files sort`;
|
|
337
|
+
}
|
|
338
|
+
function localDay(d) {
|
|
339
|
+
const pad = (x) => String(x).padStart(2, '0');
|
|
340
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* At most once per folder per day (spec §3.6): true when the hint for `cwd`
|
|
344
|
+
* was not shown today, and records today in <CSPEACH_HOME>/hints.json.
|
|
345
|
+
* Best-effort: a missing or broken file counts as "not shown"; never throws.
|
|
346
|
+
*/
|
|
347
|
+
export function claimLegacyHintToday(cwd, now = new Date()) {
|
|
348
|
+
const file = join(cspeachRoot(), 'hints.json');
|
|
349
|
+
const today = localDay(now);
|
|
350
|
+
let map = {};
|
|
351
|
+
try {
|
|
352
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
353
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
354
|
+
map = parsed;
|
|
355
|
+
}
|
|
356
|
+
catch { /* missing or broken — start fresh */ }
|
|
357
|
+
const k = normDir(cwd);
|
|
358
|
+
if (map[k] === today)
|
|
359
|
+
return false;
|
|
360
|
+
map[k] = today;
|
|
361
|
+
try {
|
|
362
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
363
|
+
writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`, 'utf8');
|
|
364
|
+
}
|
|
365
|
+
catch { /* best-effort */ }
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
312
368
|
/**
|
|
313
369
|
* Inspect a `design` envelope and decide whether the design describes a
|
|
314
370
|
* RAP stack (Behavior Definition / Behavior Pool / Service Binding) or a
|
|
@@ -389,6 +445,12 @@ export function resolveAtToken(args) {
|
|
|
389
445
|
if (exact.length === 1) {
|
|
390
446
|
return { kind: 'path', path: exact[0].path };
|
|
391
447
|
}
|
|
448
|
+
// Final fix wave, item 6 — one match in this folder beats matches that are
|
|
449
|
+
// only in an earlier location.
|
|
450
|
+
const here = matches.filter((f) => !f.earlier);
|
|
451
|
+
if (here.length === 1) {
|
|
452
|
+
return { kind: 'path', path: here[0].path };
|
|
453
|
+
}
|
|
392
454
|
return { kind: 'ambiguous', matches };
|
|
393
455
|
}
|
|
394
456
|
/**
|
|
@@ -477,8 +539,8 @@ export function resolveBareTokenInCwd(token, cwd) {
|
|
|
477
539
|
* a plain `@spec.txt` instead of an absolute path.
|
|
478
540
|
*/
|
|
479
541
|
export async function resolveAtTokenAsync(token, cwd) {
|
|
480
|
-
const workspace = await ensureWorkspace();
|
|
481
|
-
const files = await listProjectFiles();
|
|
542
|
+
const workspace = await ensureWorkspace(cwd);
|
|
543
|
+
const files = await listProjectFiles(cwd);
|
|
482
544
|
const result = resolveAtToken({ token, cwd, workspace, files });
|
|
483
545
|
if (result.kind === 'notFound' && isBareToken(token)) {
|
|
484
546
|
const cwdResult = resolveBareTokenInCwd(token, cwd);
|
|
@@ -505,7 +567,7 @@ export function formatProjectFileList(files, workspace) {
|
|
|
505
567
|
const meta = f.kind === 'envelope'
|
|
506
568
|
? `${f.artefactType.padEnd(9)} v${f.version ?? '?'}`
|
|
507
569
|
: `${f.kind.padEnd(9)} ${formatBytes(f.sizeBytes).padStart(7)}`;
|
|
508
|
-
lines.push(` ${f.filename} — ${meta} ${ageStr}`);
|
|
570
|
+
lines.push(` ${f.filename} — ${meta} ${ageStr}${f.earlier ? ' (earlier location)' : ''}`);
|
|
509
571
|
}
|
|
510
572
|
lines.push('');
|
|
511
573
|
lines.push(`${files.length} file(s). Use @<partial-name> to load — e.g. @${shortHint(files[0].filename)}`);
|
|
@@ -254,6 +254,14 @@ const quoteNeverClosed = (line) => `A quotation mark in line ${line} is never cl
|
|
|
254
254
|
* binary on one line — none of which may reach a person through CSPeach.
|
|
255
255
|
*/
|
|
256
256
|
export function shown(text) {
|
|
257
|
+
return shownUpTo(text, 60);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The same cleaning as `shown()`, with a longer cap: for text a person wrote on
|
|
261
|
+
* purpose (a decision's reason), which is read whole, not glanced at in a list.
|
|
262
|
+
* Kept to `max` characters, then "…".
|
|
263
|
+
*/
|
|
264
|
+
export function shownUpTo(text, max) {
|
|
257
265
|
const flat = String(text)
|
|
258
266
|
// An OSC sequence runs to BEL or ESC-backslash and can retitle the window.
|
|
259
267
|
.replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)?/g, ' ')
|
|
@@ -268,7 +276,7 @@ export function shown(text) {
|
|
|
268
276
|
.replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
|
|
269
277
|
.replace(/\s+/g, ' ')
|
|
270
278
|
.trim();
|
|
271
|
-
return flat.length >
|
|
279
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat;
|
|
272
280
|
}
|
|
273
281
|
/** Five names and the count of the rest: a list nobody can read is a list nobody acts on. */
|
|
274
282
|
export const upToFive = (items) => (items.length > 5 ? `${items.slice(0, 5).join(', ')} and ${items.length - 5} more` : items.join(', '));
|