@cspeach/cli 1.1.30 → 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 +121 -8
- package/dist/cli.js +36 -14
- package/dist/commands/config-set.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 +375 -41
- 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 +20 -1
- package/dist/projects/clean-core-confirm.js +156 -0
- 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 +83 -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/discover.js +901 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +25 -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 +24 -6
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +3 -0
- package/dist/register/status-html.js +3 -1
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +27 -7
- 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 +2 -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 +157 -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 +1 -1
- 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/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 +77 -4
- 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/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -7,56 +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 {
|
|
30
|
-
import { configFile } from '../config/paths.js';
|
|
18
|
+
import { legacyDirsFor, resolveProjectFolderSync } from './project-folder.js';
|
|
19
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
20
|
+
import { normDir } from '../session/project-match.js';
|
|
31
21
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* containing `.git`, or null when the search hits the filesystem root
|
|
35
|
-
* without finding one.
|
|
36
|
-
*
|
|
37
|
-
* Bounded by parent === dir, which terminates at "/" on POSIX and at
|
|
38
|
-
* each drive root on Windows.
|
|
39
|
-
*/
|
|
40
|
-
export function findProjectRoot(startDir) {
|
|
41
|
-
let dir = startDir;
|
|
42
|
-
for (let i = 0; i < 200; i++) {
|
|
43
|
-
if (existsSync(join(dir, '.git')))
|
|
44
|
-
return dir;
|
|
45
|
-
const parent = dirname(dir);
|
|
46
|
-
if (parent === dir)
|
|
47
|
-
return null;
|
|
48
|
-
dir = parent;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Resolve the project-rooted default workspace path for a given cwd.
|
|
54
|
-
* Pure: does NOT touch the filesystem (other than the .git existence
|
|
55
|
-
* 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.
|
|
56
24
|
*/
|
|
57
25
|
export function defaultWorkspaceFor(cwd) {
|
|
58
|
-
|
|
59
|
-
return join(root ?? cwd, '.cspeach');
|
|
26
|
+
return resolveProjectFolderSync(cwd).workDir;
|
|
60
27
|
}
|
|
61
28
|
/**
|
|
62
29
|
* If the workspace lives directly under a project root that has a
|
|
@@ -92,17 +59,13 @@ function maybeAddToGitignore(workspaceDir) {
|
|
|
92
59
|
}
|
|
93
60
|
}
|
|
94
61
|
/**
|
|
95
|
-
* Resolve the workspace folder PATH without creating it.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* save-command.ts, which must not leave filesystem side effects before the
|
|
100
|
-
* 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.
|
|
101
66
|
*/
|
|
102
67
|
export async function resolveWorkspacePath() {
|
|
103
|
-
|
|
104
|
-
const configured = cfg.workspace_folder?.trim();
|
|
105
|
-
return resolveTarget(configured);
|
|
68
|
+
return defaultWorkspaceFor(process.cwd());
|
|
106
69
|
}
|
|
107
70
|
/**
|
|
108
71
|
* App lane routing I3 — the same read-only resolution as resolveWorkspacePath,
|
|
@@ -110,64 +73,35 @@ export async function resolveWorkspacePath() {
|
|
|
110
73
|
* real session the two are the same folder; tests pass a temp cwd.
|
|
111
74
|
*/
|
|
112
75
|
export async function resolveWorkspacePathFor(cwd) {
|
|
113
|
-
|
|
114
|
-
const configured = cfg.workspace_folder?.trim();
|
|
115
|
-
return configured && configured.length > 0 ? expandHome(configured) : defaultWorkspaceFor(cwd);
|
|
116
|
-
}
|
|
117
|
-
function resolveTarget(configured) {
|
|
118
|
-
return configured && configured.length > 0
|
|
119
|
-
? expandHome(configured)
|
|
120
|
-
: defaultWorkspaceFor(process.cwd());
|
|
76
|
+
return defaultWorkspaceFor(cwd);
|
|
121
77
|
}
|
|
122
78
|
/**
|
|
123
|
-
* Resolve the workspace folder path
|
|
124
|
-
* 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.
|
|
125
81
|
*/
|
|
126
|
-
export async function ensureWorkspace() {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// would be overreach.
|
|
137
|
-
if (!configured)
|
|
138
|
-
maybeAddToGitignore(target);
|
|
139
|
-
}
|
|
140
|
-
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;
|
|
141
92
|
}
|
|
142
93
|
/**
|
|
143
94
|
* Synchronous variant for callers that cannot await — most importantly
|
|
144
|
-
* Node's readline `completer` interface, which is sync.
|
|
145
|
-
* config directly via fs.readFileSync. Falls back to project-rooted
|
|
146
|
-
* default on any error (missing config, parse failure, etc.) — the
|
|
147
|
-
* completer should never throw.
|
|
148
|
-
*
|
|
149
|
-
* Earlier revision used `require('@iarna/toml')` here, which threw in
|
|
150
|
-
* ESM context (cspeach-cli has `"type": "module"`) and silently broke
|
|
151
|
-
* Tab completion. Top-level import is correct for ESM modules.
|
|
95
|
+
* Node's readline `completer` interface, which is sync. Never throws.
|
|
152
96
|
*/
|
|
153
|
-
export function ensureWorkspaceSync() {
|
|
154
|
-
|
|
97
|
+
export function ensureWorkspaceSync(cwd = process.cwd()) {
|
|
98
|
+
const target = defaultWorkspaceFor(cwd);
|
|
155
99
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const raw = readFileSync(cfgPath, 'utf-8');
|
|
159
|
-
const parsed = toml.parse(raw);
|
|
160
|
-
const configured = parsed.workspace_folder?.trim();
|
|
161
|
-
if (configured && configured.length > 0) {
|
|
162
|
-
target = expandHome(configured);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
100
|
+
if (!existsSync(target))
|
|
101
|
+
mkdirSync(target, { recursive: true });
|
|
165
102
|
}
|
|
166
103
|
catch {
|
|
167
|
-
//
|
|
168
|
-
}
|
|
169
|
-
if (!existsSync(target)) {
|
|
170
|
-
mkdirSync(target, { recursive: true });
|
|
104
|
+
// The completer must never throw.
|
|
171
105
|
}
|
|
172
106
|
return target;
|
|
173
107
|
}
|
|
@@ -206,16 +140,6 @@ export function listProjectFilesSync(workspace) {
|
|
|
206
140
|
return [];
|
|
207
141
|
}
|
|
208
142
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Expand a leading `~/` to the user's home directory. Other paths are
|
|
211
|
-
* returned unchanged.
|
|
212
|
-
*/
|
|
213
|
-
function expandHome(p) {
|
|
214
|
-
if (p.startsWith('~/') || p === '~') {
|
|
215
|
-
return join(os.homedir(), p.slice(p === '~' ? 1 : 2));
|
|
216
|
-
}
|
|
217
|
-
return p;
|
|
218
|
-
}
|
|
219
143
|
/** File suffixes recognised as text-input candidates for skills. */
|
|
220
144
|
const TEXT_SUFFIXES = ['.txt', '.md'];
|
|
221
145
|
/**
|
|
@@ -252,18 +176,63 @@ export function isAttachableFilename(name) {
|
|
|
252
176
|
* yield an `unknown` summary rather than throwing — callers see the
|
|
253
177
|
* file name with no metadata so the user can investigate.
|
|
254
178
|
*/
|
|
255
|
-
export async function listProjectFiles() {
|
|
256
|
-
const ws = await ensureWorkspace();
|
|
257
|
-
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) {
|
|
258
221
|
const out = [];
|
|
259
|
-
for (const name of
|
|
222
|
+
for (const name of names) {
|
|
260
223
|
const isEnvelope = name.endsWith('.cspeach.json');
|
|
261
|
-
const isText = isTextFilename(name);
|
|
262
|
-
const isDoc = isDocFilename(name);
|
|
224
|
+
const isText = !envelopesOnly && isTextFilename(name);
|
|
225
|
+
const isDoc = !envelopesOnly && isDocFilename(name);
|
|
263
226
|
if (!isEnvelope && !isText && !isDoc)
|
|
264
227
|
continue;
|
|
265
|
-
const path = join(
|
|
266
|
-
|
|
228
|
+
const path = join(dir, name);
|
|
229
|
+
let stat;
|
|
230
|
+
try {
|
|
231
|
+
stat = statSync(path);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
267
236
|
if (!stat.isFile())
|
|
268
237
|
continue;
|
|
269
238
|
if (isEnvelope) {
|
|
@@ -307,9 +276,95 @@ export async function listProjectFiles() {
|
|
|
307
276
|
});
|
|
308
277
|
}
|
|
309
278
|
}
|
|
310
|
-
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
311
279
|
return out;
|
|
312
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
|
+
}
|
|
313
368
|
/**
|
|
314
369
|
* Inspect a `design` envelope and decide whether the design describes a
|
|
315
370
|
* RAP stack (Behavior Definition / Behavior Pool / Service Binding) or a
|
|
@@ -390,6 +445,12 @@ export function resolveAtToken(args) {
|
|
|
390
445
|
if (exact.length === 1) {
|
|
391
446
|
return { kind: 'path', path: exact[0].path };
|
|
392
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
|
+
}
|
|
393
454
|
return { kind: 'ambiguous', matches };
|
|
394
455
|
}
|
|
395
456
|
/**
|
|
@@ -478,8 +539,8 @@ export function resolveBareTokenInCwd(token, cwd) {
|
|
|
478
539
|
* a plain `@spec.txt` instead of an absolute path.
|
|
479
540
|
*/
|
|
480
541
|
export async function resolveAtTokenAsync(token, cwd) {
|
|
481
|
-
const workspace = await ensureWorkspace();
|
|
482
|
-
const files = await listProjectFiles();
|
|
542
|
+
const workspace = await ensureWorkspace(cwd);
|
|
543
|
+
const files = await listProjectFiles(cwd);
|
|
483
544
|
const result = resolveAtToken({ token, cwd, workspace, files });
|
|
484
545
|
if (result.kind === 'notFound' && isBareToken(token)) {
|
|
485
546
|
const cwdResult = resolveBareTokenInCwd(token, cwd);
|
|
@@ -506,7 +567,7 @@ export function formatProjectFileList(files, workspace) {
|
|
|
506
567
|
const meta = f.kind === 'envelope'
|
|
507
568
|
? `${f.artefactType.padEnd(9)} v${f.version ?? '?'}`
|
|
508
569
|
: `${f.kind.padEnd(9)} ${formatBytes(f.sizeBytes).padStart(7)}`;
|
|
509
|
-
lines.push(` ${f.filename} — ${meta} ${ageStr}`);
|
|
570
|
+
lines.push(` ${f.filename} — ${meta} ${ageStr}${f.earlier ? ' (earlier location)' : ''}`);
|
|
510
571
|
}
|
|
511
572
|
lines.push('');
|
|
512
573
|
lines.push(`${files.length} file(s). Use @<partial-name> to load — e.g. @${shortHint(files[0].filename)}`);
|