@foundation0/api 1.1.10 → 1.1.12
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/mcp/manual.md +41 -23
- package/mcp/server.test.ts +137 -0
- package/mcp/server.ts +66 -26
- package/package.json +1 -1
- package/projects.ts +43 -21
package/mcp/manual.md
CHANGED
|
@@ -44,23 +44,27 @@ Tool call (prefixed or unprefixed names both work here):
|
|
|
44
44
|
{ "name": "mcp.describeTool", "arguments": { "args": ["projects.listProjects"] } }
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
### C) `mcp.search` for "search docs/spec" requests
|
|
48
|
-
|
|
49
|
-
Tool call:
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"name": "mcp.search",
|
|
54
|
-
"arguments": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"repoName": "<repo-name>",
|
|
59
|
-
"ignoreCase": true,
|
|
60
|
-
"maxCount": 50
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
47
|
+
### C) `mcp.search` for "search docs/spec" requests
|
|
48
|
+
|
|
49
|
+
Tool call:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"name": "mcp.search",
|
|
54
|
+
"arguments": {
|
|
55
|
+
"section": "spec",
|
|
56
|
+
"pattern": "authentication",
|
|
57
|
+
"paths": ["."],
|
|
58
|
+
"repoName": "<repo-name>",
|
|
59
|
+
"ignoreCase": true,
|
|
60
|
+
"maxCount": 50
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
- If only one project exists in the workspace, `projectName` is optional and will be inferred.
|
|
67
|
+
- If multiple projects exist, pass `projectName` explicitly.
|
|
64
68
|
|
|
65
69
|
### D) `mcp.workspace` to debug repoName/workspaceRoot/cwd issues
|
|
66
70
|
|
|
@@ -88,14 +92,28 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
|
|
|
88
92
|
|
|
89
93
|
## 4) Common calls (examples)
|
|
90
94
|
|
|
91
|
-
### A) List projects
|
|
95
|
+
### A) List projects
|
|
92
96
|
|
|
93
97
|
```json
|
|
94
|
-
{
|
|
95
|
-
"name": "projects.listProjects",
|
|
96
|
-
"arguments": { "repoName": "<repo-name>" }
|
|
97
|
-
}
|
|
98
|
-
```
|
|
98
|
+
{
|
|
99
|
+
"name": "projects.listProjects",
|
|
100
|
+
"arguments": { "repoName": "<repo-name>" }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### E) Search specs without `projectName` (single-project workspaces)
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"name": "projects.searchSpecs",
|
|
109
|
+
"arguments": {
|
|
110
|
+
"pattern": "TASK-003",
|
|
111
|
+
"paths": ["07_roadmap/phases.md"],
|
|
112
|
+
"repoName": "<repo-name>",
|
|
113
|
+
"source": "auto"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
99
117
|
|
|
100
118
|
### B) List agents
|
|
101
119
|
|
package/mcp/server.test.ts
CHANGED
|
@@ -272,6 +272,143 @@ describe("createExampleMcpServer request handling", () => {
|
|
|
272
272
|
}
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
it("runs projects.searchSpecs without projectName when only one project exists", async () => {
|
|
276
|
+
const originalCwd = process.cwd();
|
|
277
|
+
const tempDir = await fs.mkdtemp(
|
|
278
|
+
path.join(os.tmpdir(), "f0-mcp-server-searchspecs-no-project-"),
|
|
279
|
+
);
|
|
280
|
+
try {
|
|
281
|
+
await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
|
|
282
|
+
await fs.writeFile(
|
|
283
|
+
path.join(tempDir, ".git", "config"),
|
|
284
|
+
[
|
|
285
|
+
'[remote "origin"]',
|
|
286
|
+
"\turl = https://example.com/F0/adl.git",
|
|
287
|
+
"",
|
|
288
|
+
].join("\n"),
|
|
289
|
+
"utf8",
|
|
290
|
+
);
|
|
291
|
+
await fs.mkdir(path.join(tempDir, "spec", "07_roadmap"), {
|
|
292
|
+
recursive: true,
|
|
293
|
+
});
|
|
294
|
+
await fs.writeFile(
|
|
295
|
+
path.join(tempDir, "spec", "07_roadmap", "phases.md"),
|
|
296
|
+
"# Phases\n\n- TASK-003 deliver search fallback\n",
|
|
297
|
+
"utf8",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
process.chdir(tempDir);
|
|
301
|
+
const instance = createExampleMcpServer();
|
|
302
|
+
const handler = getToolHandler(instance);
|
|
303
|
+
|
|
304
|
+
const result = await handler(
|
|
305
|
+
{
|
|
306
|
+
method: "tools/call",
|
|
307
|
+
params: {
|
|
308
|
+
name: "projects.searchSpecs",
|
|
309
|
+
arguments: {
|
|
310
|
+
pattern: "TASK-003",
|
|
311
|
+
paths: ["07_roadmap/phases.md"],
|
|
312
|
+
source: "auto",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(result.isError).toBe(false);
|
|
320
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
321
|
+
expect(payload.ok).toBe(true);
|
|
322
|
+
expect(payload.result.section).toBe("spec");
|
|
323
|
+
expect(payload.result.source).toBe("local");
|
|
324
|
+
expect(payload.result.filesMatched).toBe(1);
|
|
325
|
+
expect(payload.result.output).toContain("TASK-003");
|
|
326
|
+
} finally {
|
|
327
|
+
process.chdir(originalCwd);
|
|
328
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('accepts owner/repo projectName selectors when /projects exists', async () => {
|
|
333
|
+
const originalCwd = process.cwd();
|
|
334
|
+
const tempDir = await fs.mkdtemp(
|
|
335
|
+
path.join(os.tmpdir(), "f0-mcp-server-projectname-leaf-"),
|
|
336
|
+
);
|
|
337
|
+
try {
|
|
338
|
+
await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
|
|
339
|
+
await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
|
|
340
|
+
recursive: true,
|
|
341
|
+
});
|
|
342
|
+
await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
|
|
343
|
+
await fs.writeFile(path.join(tempDir, ".git", "config"), "", "utf8");
|
|
344
|
+
|
|
345
|
+
process.chdir(tempDir);
|
|
346
|
+
const instance = createExampleMcpServer();
|
|
347
|
+
const handler = getToolHandler(instance);
|
|
348
|
+
|
|
349
|
+
const result = await handler(
|
|
350
|
+
{
|
|
351
|
+
method: "tools/call",
|
|
352
|
+
params: {
|
|
353
|
+
name: "projects.resolveProjectRoot",
|
|
354
|
+
arguments: {
|
|
355
|
+
projectName: "F0/adl",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{},
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
expect(result.isError).toBe(false);
|
|
363
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
364
|
+
expect(payload.ok).toBe(true);
|
|
365
|
+
expect(payload.result).toBe(path.join(path.resolve(tempDir), "projects", "adl"));
|
|
366
|
+
} finally {
|
|
367
|
+
process.chdir(originalCwd);
|
|
368
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("parses owner/repo repoName into repo segment even without full-name metadata", async () => {
|
|
373
|
+
const originalCwd = process.cwd();
|
|
374
|
+
const tempDir = await fs.mkdtemp(
|
|
375
|
+
path.join(os.tmpdir(), "f0-mcp-server-reponame-segment-fallback-"),
|
|
376
|
+
);
|
|
377
|
+
try {
|
|
378
|
+
await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
|
|
379
|
+
await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
|
|
380
|
+
recursive: true,
|
|
381
|
+
});
|
|
382
|
+
await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
|
|
383
|
+
await fs.writeFile(path.join(tempDir, ".git", "config"), "", "utf8");
|
|
384
|
+
|
|
385
|
+
process.chdir(tempDir);
|
|
386
|
+
const instance = createExampleMcpServer();
|
|
387
|
+
const handler = getToolHandler(instance);
|
|
388
|
+
|
|
389
|
+
const result = await handler(
|
|
390
|
+
{
|
|
391
|
+
method: "tools/call",
|
|
392
|
+
params: {
|
|
393
|
+
name: "projects.listProjects",
|
|
394
|
+
arguments: {
|
|
395
|
+
repoName: "F0/adl",
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{},
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(result.isError).toBe(false);
|
|
403
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
404
|
+
expect(payload.ok).toBe(true);
|
|
405
|
+
expect(payload.result).toContain("adl");
|
|
406
|
+
} finally {
|
|
407
|
+
process.chdir(originalCwd);
|
|
408
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
275
412
|
it("auto-detects Git workspace from process.cwd() when repoName is omitted", async () => {
|
|
276
413
|
const tempDir = await fs.mkdtemp(
|
|
277
414
|
path.join(os.tmpdir(), "f0-mcp-server-autoworkspace-"),
|
package/mcp/server.ts
CHANGED
|
@@ -393,6 +393,11 @@ const resolveRepoSelectorOptions = (
|
|
|
393
393
|
next.repoName = matched;
|
|
394
394
|
return next;
|
|
395
395
|
}
|
|
396
|
+
if (repoSegment !== "." && repoSegment !== "..") {
|
|
397
|
+
next.processRoot = ctx.defaultProcessRoot;
|
|
398
|
+
next.repoName = repoSegment;
|
|
399
|
+
return next;
|
|
400
|
+
}
|
|
396
401
|
}
|
|
397
402
|
}
|
|
398
403
|
|
|
@@ -597,9 +602,23 @@ const coercePayloadForTool = (
|
|
|
597
602
|
)
|
|
598
603
|
return;
|
|
599
604
|
const name = popStringOption(options, "projectName", "project");
|
|
600
|
-
|
|
601
|
-
if (
|
|
602
|
-
|
|
605
|
+
const fallback = (() => {
|
|
606
|
+
if (name) return null;
|
|
607
|
+
const repoName =
|
|
608
|
+
typeof options.repoName === "string" ? options.repoName.trim() : "";
|
|
609
|
+
if (!repoName) return null;
|
|
610
|
+
const leaf = repoName
|
|
611
|
+
.split(/[\\/]/)
|
|
612
|
+
.map((part) => part.trim())
|
|
613
|
+
.filter((part) => part.length > 0)
|
|
614
|
+
.at(-1);
|
|
615
|
+
if (!leaf || leaf === "." || leaf === "..") return null;
|
|
616
|
+
return leaf;
|
|
617
|
+
})();
|
|
618
|
+
const resolvedName = name ?? fallback;
|
|
619
|
+
if (resolvedName) {
|
|
620
|
+
if (args.length === 0) args.push(resolvedName);
|
|
621
|
+
else args[0] = resolvedName;
|
|
603
622
|
}
|
|
604
623
|
};
|
|
605
624
|
|
|
@@ -659,7 +678,12 @@ const coercePayloadForTool = (
|
|
|
659
678
|
rgArgs.push(pattern, ...(paths.length > 0 ? paths : ["."]));
|
|
660
679
|
|
|
661
680
|
if (args.length === 0) {
|
|
662
|
-
//
|
|
681
|
+
// If projectName is omitted, preserve the rg args via options.args so
|
|
682
|
+
// projects.searchDocs/searchSpecs can run in seed-options mode.
|
|
683
|
+
// (projectName will be inferred when only one project exists).
|
|
684
|
+
if (!Array.isArray(options.args) || options.args.length === 0) {
|
|
685
|
+
options.args = rgArgs;
|
|
686
|
+
}
|
|
663
687
|
return;
|
|
664
688
|
}
|
|
665
689
|
|
|
@@ -668,13 +692,17 @@ const coercePayloadForTool = (
|
|
|
668
692
|
}
|
|
669
693
|
};
|
|
670
694
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
695
|
+
switch (toolName) {
|
|
696
|
+
case "projects.listProjects": {
|
|
697
|
+
// No positional args. processRoot is injected from the selected repoName.
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case "projects.resolveProjectRoot":
|
|
701
|
+
case "projects.listProjectDocs": {
|
|
702
|
+
coerceProjectName();
|
|
703
|
+
ensureArg0(null);
|
|
674
704
|
break;
|
|
675
705
|
}
|
|
676
|
-
case "projects.resolveProjectRoot":
|
|
677
|
-
case "projects.listProjectDocs":
|
|
678
706
|
case "projects.fetchGitTasks":
|
|
679
707
|
case "projects.createGitIssue": {
|
|
680
708
|
coerceProjectName();
|
|
@@ -690,7 +718,11 @@ const coercePayloadForTool = (
|
|
|
690
718
|
"docPath",
|
|
691
719
|
);
|
|
692
720
|
if (requestPath) {
|
|
693
|
-
|
|
721
|
+
if (args.length === 0) {
|
|
722
|
+
args.push(null, requestPath);
|
|
723
|
+
} else if (args.length === 1) {
|
|
724
|
+
args.push(requestPath);
|
|
725
|
+
}
|
|
694
726
|
}
|
|
695
727
|
break;
|
|
696
728
|
}
|
|
@@ -1095,7 +1127,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1095
1127
|
properties: {
|
|
1096
1128
|
projectName: {
|
|
1097
1129
|
type: "string",
|
|
1098
|
-
description:
|
|
1130
|
+
description:
|
|
1131
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1099
1132
|
},
|
|
1100
1133
|
repoName: {
|
|
1101
1134
|
type: "string",
|
|
@@ -1113,7 +1146,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1113
1146
|
additionalProperties: true,
|
|
1114
1147
|
},
|
|
1115
1148
|
},
|
|
1116
|
-
required: [
|
|
1149
|
+
required: [],
|
|
1117
1150
|
},
|
|
1118
1151
|
"projects.listProjectDocs": {
|
|
1119
1152
|
type: "object",
|
|
@@ -1121,7 +1154,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1121
1154
|
properties: {
|
|
1122
1155
|
projectName: {
|
|
1123
1156
|
type: "string",
|
|
1124
|
-
description:
|
|
1157
|
+
description:
|
|
1158
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1125
1159
|
},
|
|
1126
1160
|
repoName: {
|
|
1127
1161
|
type: "string",
|
|
@@ -1139,7 +1173,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1139
1173
|
additionalProperties: true,
|
|
1140
1174
|
},
|
|
1141
1175
|
},
|
|
1142
|
-
required: [
|
|
1176
|
+
required: [],
|
|
1143
1177
|
},
|
|
1144
1178
|
"projects.readProjectDoc": {
|
|
1145
1179
|
type: "object",
|
|
@@ -1147,7 +1181,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1147
1181
|
properties: {
|
|
1148
1182
|
projectName: {
|
|
1149
1183
|
type: "string",
|
|
1150
|
-
description:
|
|
1184
|
+
description:
|
|
1185
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1151
1186
|
},
|
|
1152
1187
|
requestPath: {
|
|
1153
1188
|
type: "string",
|
|
@@ -1170,7 +1205,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1170
1205
|
additionalProperties: true,
|
|
1171
1206
|
},
|
|
1172
1207
|
},
|
|
1173
|
-
required: ["
|
|
1208
|
+
required: ["requestPath"],
|
|
1174
1209
|
},
|
|
1175
1210
|
"projects.searchDocs": {
|
|
1176
1211
|
type: "object",
|
|
@@ -1178,7 +1213,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1178
1213
|
properties: {
|
|
1179
1214
|
projectName: {
|
|
1180
1215
|
type: "string",
|
|
1181
|
-
description:
|
|
1216
|
+
description:
|
|
1217
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1182
1218
|
},
|
|
1183
1219
|
pattern: {
|
|
1184
1220
|
type: "string",
|
|
@@ -1239,7 +1275,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1239
1275
|
},
|
|
1240
1276
|
args: {
|
|
1241
1277
|
type: "array",
|
|
1242
|
-
description:
|
|
1278
|
+
description:
|
|
1279
|
+
"Legacy rg-like args ([projectName], PATTERN, [PATH...]). projectName may be omitted when only one project exists.",
|
|
1243
1280
|
items: {},
|
|
1244
1281
|
},
|
|
1245
1282
|
options: {
|
|
@@ -1248,7 +1285,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1248
1285
|
additionalProperties: true,
|
|
1249
1286
|
},
|
|
1250
1287
|
},
|
|
1251
|
-
required: [
|
|
1288
|
+
required: [],
|
|
1252
1289
|
},
|
|
1253
1290
|
"projects.searchSpecs": {
|
|
1254
1291
|
type: "object",
|
|
@@ -1256,7 +1293,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1256
1293
|
properties: {
|
|
1257
1294
|
projectName: {
|
|
1258
1295
|
type: "string",
|
|
1259
|
-
description:
|
|
1296
|
+
description:
|
|
1297
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1260
1298
|
},
|
|
1261
1299
|
pattern: {
|
|
1262
1300
|
type: "string",
|
|
@@ -1317,7 +1355,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1317
1355
|
},
|
|
1318
1356
|
args: {
|
|
1319
1357
|
type: "array",
|
|
1320
|
-
description:
|
|
1358
|
+
description:
|
|
1359
|
+
"Legacy rg-like args ([projectName], PATTERN, [PATH...]). projectName may be omitted when only one project exists.",
|
|
1321
1360
|
items: {},
|
|
1322
1361
|
},
|
|
1323
1362
|
options: {
|
|
@@ -1326,7 +1365,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1326
1365
|
additionalProperties: true,
|
|
1327
1366
|
},
|
|
1328
1367
|
},
|
|
1329
|
-
required: [
|
|
1368
|
+
required: [],
|
|
1330
1369
|
},
|
|
1331
1370
|
"projects.parseProjectTargetSpec": {
|
|
1332
1371
|
type: "object",
|
|
@@ -1601,7 +1640,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1601
1640
|
properties: {
|
|
1602
1641
|
projectName: {
|
|
1603
1642
|
type: "string",
|
|
1604
|
-
description:
|
|
1643
|
+
description:
|
|
1644
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1605
1645
|
},
|
|
1606
1646
|
section: {
|
|
1607
1647
|
type: "string",
|
|
@@ -2360,9 +2400,9 @@ export const createExampleMcpServer = (
|
|
|
2360
2400
|
const hint = (() => {
|
|
2361
2401
|
if (!hasProjectsDir) {
|
|
2362
2402
|
return [
|
|
2363
|
-
"Git workspace does not contain /projects.",
|
|
2364
|
-
"
|
|
2365
|
-
|
|
2403
|
+
"Git workspace does not contain /projects (single-repo layout).",
|
|
2404
|
+
"Search tools can omit projectName; it will be inferred when only one project exists.",
|
|
2405
|
+
'Use repoName like "adl" or "F0/adl" when needed.',
|
|
2366
2406
|
].join(" ");
|
|
2367
2407
|
}
|
|
2368
2408
|
if (hasProjectsDir && projects.length === 0) {
|
package/package.json
CHANGED
package/projects.ts
CHANGED
|
@@ -304,31 +304,46 @@ export function usage(): string {
|
|
|
304
304
|
`The active file created is [file].active.<ext>.\n`
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
export function resolveProjectRoot(
|
|
308
|
-
|
|
307
|
+
export function resolveProjectRoot(projectNameInput: string, processRoot: string = process.cwd()): string {
|
|
308
|
+
const resolvedProcessRoot = path.resolve(processRoot)
|
|
309
|
+
|
|
310
|
+
const rawProjectName = typeof projectNameInput === 'string' ? projectNameInput.trim() : ''
|
|
311
|
+
const inferredProjectName = rawProjectName.length > 0 ? rawProjectName : null
|
|
312
|
+
const projectName = inferredProjectName ?? (() => {
|
|
313
|
+
const projects = listProjects(resolvedProcessRoot)
|
|
314
|
+
if (projects.length === 1) {
|
|
315
|
+
return projects[0]
|
|
316
|
+
}
|
|
317
|
+
if (projects.length > 1) {
|
|
318
|
+
throw new Error(`project-name is required. Available projects: ${projects.join(', ')}`)
|
|
319
|
+
}
|
|
309
320
|
throw new Error('project-name is required.')
|
|
310
|
-
}
|
|
321
|
+
})()
|
|
311
322
|
|
|
312
|
-
const normalized = projectName.
|
|
323
|
+
const normalized = projectName.replace(/[\\/]+/g, path.sep)
|
|
313
324
|
if (normalized === '.' || normalized === '..' || normalized.includes(path.sep + '..') || normalized.includes('..')) {
|
|
314
325
|
throw new Error('project-name may not include path traversal.')
|
|
315
326
|
}
|
|
316
327
|
|
|
317
|
-
const
|
|
318
|
-
const projectRoot = path.join(
|
|
328
|
+
const projectsRoot = resolveProjectsRoot(resolvedProcessRoot)
|
|
329
|
+
const projectRoot = path.join(projectsRoot, normalized)
|
|
319
330
|
if (!existsDir(projectRoot)) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
if (normalized.includes(path.sep)) {
|
|
332
|
+
const leaf = normalized.split(path.sep).filter(Boolean).at(-1)
|
|
333
|
+
if (leaf && leaf !== normalized) {
|
|
334
|
+
const leafRoot = path.join(projectsRoot, leaf)
|
|
335
|
+
if (existsDir(leafRoot)) {
|
|
336
|
+
return leafRoot
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
329
340
|
|
|
330
|
-
if (
|
|
331
|
-
|
|
341
|
+
if (!existsDir(projectsRoot)) {
|
|
342
|
+
const hasSingleRepoDocs = existsDir(path.join(resolvedProcessRoot, 'docs'))
|
|
343
|
+
const hasSingleRepoSpec = existsDir(path.join(resolvedProcessRoot, 'spec'))
|
|
344
|
+
if (hasSingleRepoDocs || hasSingleRepoSpec) {
|
|
345
|
+
return resolvedProcessRoot
|
|
346
|
+
}
|
|
332
347
|
}
|
|
333
348
|
|
|
334
349
|
throw new Error(`Project folder not found: ${projectRoot}`)
|
|
@@ -411,11 +426,18 @@ async function searchProjectSection(
|
|
|
411
426
|
const optionProjectName = parseStringOption(options.projectName) ?? ''
|
|
412
427
|
const sourcePreference = parseSearchSourceOption(options.source, options.remote)
|
|
413
428
|
|
|
414
|
-
if (!positionalProjectName && !optionProjectName) {
|
|
415
|
-
throw new Error('Missing project name. Pass it as first arg or options.projectName.')
|
|
416
|
-
}
|
|
417
|
-
|
|
418
429
|
let projectName = positionalProjectName || optionProjectName
|
|
430
|
+
|
|
431
|
+
if (!projectName) {
|
|
432
|
+
const candidates = listProjects(processRoot)
|
|
433
|
+
if (candidates.length === 1) {
|
|
434
|
+
projectName = candidates[0]
|
|
435
|
+
} else if (candidates.length > 1) {
|
|
436
|
+
throw new Error(`Missing project name. Available projects: ${candidates.join(', ')}`)
|
|
437
|
+
} else {
|
|
438
|
+
throw new Error('Missing project name. Pass it as first arg or options.projectName.')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
419
441
|
let projectRoot: string | null = null
|
|
420
442
|
|
|
421
443
|
if (positionalProjectName && optionProjectName && positionalProjectName !== optionProjectName) {
|