@foundation0/api 1.1.11 → 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 +97 -0
- package/mcp/server.ts +61 -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,103 @@ 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
|
+
|
|
275
372
|
it("parses owner/repo repoName into repo segment even without full-name metadata", async () => {
|
|
276
373
|
const originalCwd = process.cwd();
|
|
277
374
|
const tempDir = await fs.mkdtemp(
|
package/mcp/server.ts
CHANGED
|
@@ -602,9 +602,23 @@ const coercePayloadForTool = (
|
|
|
602
602
|
)
|
|
603
603
|
return;
|
|
604
604
|
const name = popStringOption(options, "projectName", "project");
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
|
|
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;
|
|
608
622
|
}
|
|
609
623
|
};
|
|
610
624
|
|
|
@@ -664,7 +678,12 @@ const coercePayloadForTool = (
|
|
|
664
678
|
rgArgs.push(pattern, ...(paths.length > 0 ? paths : ["."]));
|
|
665
679
|
|
|
666
680
|
if (args.length === 0) {
|
|
667
|
-
//
|
|
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
|
+
}
|
|
668
687
|
return;
|
|
669
688
|
}
|
|
670
689
|
|
|
@@ -673,13 +692,17 @@ const coercePayloadForTool = (
|
|
|
673
692
|
}
|
|
674
693
|
};
|
|
675
694
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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);
|
|
679
704
|
break;
|
|
680
705
|
}
|
|
681
|
-
case "projects.resolveProjectRoot":
|
|
682
|
-
case "projects.listProjectDocs":
|
|
683
706
|
case "projects.fetchGitTasks":
|
|
684
707
|
case "projects.createGitIssue": {
|
|
685
708
|
coerceProjectName();
|
|
@@ -695,7 +718,11 @@ const coercePayloadForTool = (
|
|
|
695
718
|
"docPath",
|
|
696
719
|
);
|
|
697
720
|
if (requestPath) {
|
|
698
|
-
|
|
721
|
+
if (args.length === 0) {
|
|
722
|
+
args.push(null, requestPath);
|
|
723
|
+
} else if (args.length === 1) {
|
|
724
|
+
args.push(requestPath);
|
|
725
|
+
}
|
|
699
726
|
}
|
|
700
727
|
break;
|
|
701
728
|
}
|
|
@@ -1100,7 +1127,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1100
1127
|
properties: {
|
|
1101
1128
|
projectName: {
|
|
1102
1129
|
type: "string",
|
|
1103
|
-
description:
|
|
1130
|
+
description:
|
|
1131
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1104
1132
|
},
|
|
1105
1133
|
repoName: {
|
|
1106
1134
|
type: "string",
|
|
@@ -1118,7 +1146,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1118
1146
|
additionalProperties: true,
|
|
1119
1147
|
},
|
|
1120
1148
|
},
|
|
1121
|
-
required: [
|
|
1149
|
+
required: [],
|
|
1122
1150
|
},
|
|
1123
1151
|
"projects.listProjectDocs": {
|
|
1124
1152
|
type: "object",
|
|
@@ -1126,7 +1154,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1126
1154
|
properties: {
|
|
1127
1155
|
projectName: {
|
|
1128
1156
|
type: "string",
|
|
1129
|
-
description:
|
|
1157
|
+
description:
|
|
1158
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1130
1159
|
},
|
|
1131
1160
|
repoName: {
|
|
1132
1161
|
type: "string",
|
|
@@ -1144,7 +1173,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1144
1173
|
additionalProperties: true,
|
|
1145
1174
|
},
|
|
1146
1175
|
},
|
|
1147
|
-
required: [
|
|
1176
|
+
required: [],
|
|
1148
1177
|
},
|
|
1149
1178
|
"projects.readProjectDoc": {
|
|
1150
1179
|
type: "object",
|
|
@@ -1152,7 +1181,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1152
1181
|
properties: {
|
|
1153
1182
|
projectName: {
|
|
1154
1183
|
type: "string",
|
|
1155
|
-
description:
|
|
1184
|
+
description:
|
|
1185
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1156
1186
|
},
|
|
1157
1187
|
requestPath: {
|
|
1158
1188
|
type: "string",
|
|
@@ -1175,7 +1205,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1175
1205
|
additionalProperties: true,
|
|
1176
1206
|
},
|
|
1177
1207
|
},
|
|
1178
|
-
required: ["
|
|
1208
|
+
required: ["requestPath"],
|
|
1179
1209
|
},
|
|
1180
1210
|
"projects.searchDocs": {
|
|
1181
1211
|
type: "object",
|
|
@@ -1183,7 +1213,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1183
1213
|
properties: {
|
|
1184
1214
|
projectName: {
|
|
1185
1215
|
type: "string",
|
|
1186
|
-
description:
|
|
1216
|
+
description:
|
|
1217
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1187
1218
|
},
|
|
1188
1219
|
pattern: {
|
|
1189
1220
|
type: "string",
|
|
@@ -1244,7 +1275,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1244
1275
|
},
|
|
1245
1276
|
args: {
|
|
1246
1277
|
type: "array",
|
|
1247
|
-
description:
|
|
1278
|
+
description:
|
|
1279
|
+
"Legacy rg-like args ([projectName], PATTERN, [PATH...]). projectName may be omitted when only one project exists.",
|
|
1248
1280
|
items: {},
|
|
1249
1281
|
},
|
|
1250
1282
|
options: {
|
|
@@ -1253,7 +1285,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1253
1285
|
additionalProperties: true,
|
|
1254
1286
|
},
|
|
1255
1287
|
},
|
|
1256
|
-
required: [
|
|
1288
|
+
required: [],
|
|
1257
1289
|
},
|
|
1258
1290
|
"projects.searchSpecs": {
|
|
1259
1291
|
type: "object",
|
|
@@ -1261,7 +1293,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1261
1293
|
properties: {
|
|
1262
1294
|
projectName: {
|
|
1263
1295
|
type: "string",
|
|
1264
|
-
description:
|
|
1296
|
+
description:
|
|
1297
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1265
1298
|
},
|
|
1266
1299
|
pattern: {
|
|
1267
1300
|
type: "string",
|
|
@@ -1322,7 +1355,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1322
1355
|
},
|
|
1323
1356
|
args: {
|
|
1324
1357
|
type: "array",
|
|
1325
|
-
description:
|
|
1358
|
+
description:
|
|
1359
|
+
"Legacy rg-like args ([projectName], PATTERN, [PATH...]). projectName may be omitted when only one project exists.",
|
|
1326
1360
|
items: {},
|
|
1327
1361
|
},
|
|
1328
1362
|
options: {
|
|
@@ -1331,7 +1365,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1331
1365
|
additionalProperties: true,
|
|
1332
1366
|
},
|
|
1333
1367
|
},
|
|
1334
|
-
required: [
|
|
1368
|
+
required: [],
|
|
1335
1369
|
},
|
|
1336
1370
|
"projects.parseProjectTargetSpec": {
|
|
1337
1371
|
type: "object",
|
|
@@ -1606,7 +1640,8 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1606
1640
|
properties: {
|
|
1607
1641
|
projectName: {
|
|
1608
1642
|
type: "string",
|
|
1609
|
-
description:
|
|
1643
|
+
description:
|
|
1644
|
+
'Project name. In a monorepo: folder under /projects (e.g. "adl"). Optional when only one project exists.',
|
|
1610
1645
|
},
|
|
1611
1646
|
section: {
|
|
1612
1647
|
type: "string",
|
|
@@ -2365,9 +2400,9 @@ export const createExampleMcpServer = (
|
|
|
2365
2400
|
const hint = (() => {
|
|
2366
2401
|
if (!hasProjectsDir) {
|
|
2367
2402
|
return [
|
|
2368
|
-
"Git workspace does not contain /projects.",
|
|
2369
|
-
"
|
|
2370
|
-
|
|
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.',
|
|
2371
2406
|
].join(" ");
|
|
2372
2407
|
}
|
|
2373
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) {
|