substrate-ai 0.6.0 → 0.6.1
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/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
2
|
+
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-IDOmPys1.js";
|
|
3
3
|
import { createLogger } from "../logger-D2fS2ccL.js";
|
|
4
4
|
import { AdapterRegistry } from "../adapter-registry-D2zdMwVu.js";
|
|
5
5
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
|
|
@@ -17,9 +17,11 @@ import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
|
17
17
|
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
18
18
|
import yaml from "js-yaml";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
|
+
import * as path$2 from "node:path";
|
|
20
21
|
import * as path$1 from "node:path";
|
|
21
22
|
import { isAbsolute, join as join$1 } from "node:path";
|
|
22
23
|
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
24
|
+
import * as fs from "node:fs/promises";
|
|
23
25
|
import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
|
|
24
26
|
import { createInterface } from "node:readline";
|
|
25
27
|
import { homedir } from "os";
|
|
@@ -257,6 +259,417 @@ function registerAdaptersCommand(program, version, registry) {
|
|
|
257
259
|
});
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/modules/project-profile/detect.ts
|
|
264
|
+
/**
|
|
265
|
+
* Ordered array of build system markers. Detection checks them in priority
|
|
266
|
+
* order — the first matching marker wins at the single-project level.
|
|
267
|
+
*/
|
|
268
|
+
const STACK_MARKERS = [
|
|
269
|
+
{
|
|
270
|
+
file: "go.mod",
|
|
271
|
+
language: "go",
|
|
272
|
+
buildTool: "go",
|
|
273
|
+
buildCommand: "go build ./...",
|
|
274
|
+
testCommand: "go test ./..."
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
file: "build.gradle.kts",
|
|
278
|
+
language: "kotlin",
|
|
279
|
+
buildTool: "gradle",
|
|
280
|
+
buildCommand: "./gradlew build",
|
|
281
|
+
testCommand: "./gradlew test"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
file: "build.gradle",
|
|
285
|
+
language: "java",
|
|
286
|
+
buildTool: "gradle",
|
|
287
|
+
buildCommand: "./gradlew build",
|
|
288
|
+
testCommand: "./gradlew test"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
file: "pom.xml",
|
|
292
|
+
language: "java",
|
|
293
|
+
buildTool: "maven",
|
|
294
|
+
buildCommand: "mvn compile",
|
|
295
|
+
testCommand: "mvn test"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
file: "Cargo.toml",
|
|
299
|
+
language: "rust",
|
|
300
|
+
buildTool: "cargo",
|
|
301
|
+
buildCommand: "cargo build",
|
|
302
|
+
testCommand: "cargo test"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
file: "pyproject.toml",
|
|
306
|
+
language: "python",
|
|
307
|
+
buildTool: "pip",
|
|
308
|
+
buildCommand: "pip install -e .",
|
|
309
|
+
testCommand: "pytest"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
file: "package.json",
|
|
313
|
+
language: "typescript",
|
|
314
|
+
buildTool: "npm",
|
|
315
|
+
buildCommand: "npm run build",
|
|
316
|
+
testCommand: "npm test"
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
async function fileExists(filePath) {
|
|
320
|
+
try {
|
|
321
|
+
await fs.access(filePath);
|
|
322
|
+
return true;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Derives the Node.js build tool from lock file markers.
|
|
329
|
+
* Mirrors the existing package manager detection logic (dispatcher-impl.ts).
|
|
330
|
+
*/
|
|
331
|
+
async function detectNodeBuildTool(dir) {
|
|
332
|
+
if (await fileExists(path$2.join(dir, "pnpm-lock.yaml"))) return {
|
|
333
|
+
buildTool: "pnpm",
|
|
334
|
+
buildCommand: "pnpm run build",
|
|
335
|
+
testCommand: "pnpm test"
|
|
336
|
+
};
|
|
337
|
+
if (await fileExists(path$2.join(dir, "yarn.lock"))) return {
|
|
338
|
+
buildTool: "yarn",
|
|
339
|
+
buildCommand: "yarn build",
|
|
340
|
+
testCommand: "yarn test"
|
|
341
|
+
};
|
|
342
|
+
if (await fileExists(path$2.join(dir, "bun.lockb"))) return {
|
|
343
|
+
buildTool: "bun",
|
|
344
|
+
buildCommand: "bun run build",
|
|
345
|
+
testCommand: "bun test"
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
buildTool: "npm",
|
|
349
|
+
buildCommand: "npm run build",
|
|
350
|
+
testCommand: "npm test"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Detects the language and build tool for a single project directory.
|
|
355
|
+
*
|
|
356
|
+
* Iterates `STACK_MARKERS` in priority order, calling `fs.access()` for each
|
|
357
|
+
* marker file. Returns the first match, or falls back to TypeScript/npm if no
|
|
358
|
+
* marker file is found.
|
|
359
|
+
*
|
|
360
|
+
* @param dir - Absolute path to the directory to inspect.
|
|
361
|
+
* @returns A `PackageEntry` describing the detected stack.
|
|
362
|
+
*/
|
|
363
|
+
async function detectSingleProjectStack(dir) {
|
|
364
|
+
for (const marker of STACK_MARKERS) {
|
|
365
|
+
const markerPath = path$2.join(dir, marker.file);
|
|
366
|
+
if (!await fileExists(markerPath)) continue;
|
|
367
|
+
if (marker.file === "package.json") {
|
|
368
|
+
const nodeInfo = await detectNodeBuildTool(dir);
|
|
369
|
+
return {
|
|
370
|
+
path: dir,
|
|
371
|
+
language: "typescript",
|
|
372
|
+
buildTool: nodeInfo.buildTool,
|
|
373
|
+
buildCommand: nodeInfo.buildCommand,
|
|
374
|
+
testCommand: nodeInfo.testCommand
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (marker.file === "pyproject.toml") {
|
|
378
|
+
const hasPoetry = await fileExists(path$2.join(dir, "poetry.lock"));
|
|
379
|
+
return {
|
|
380
|
+
path: dir,
|
|
381
|
+
language: "python",
|
|
382
|
+
buildTool: hasPoetry ? "poetry" : "pip",
|
|
383
|
+
buildCommand: hasPoetry ? "poetry build" : "pip install -e .",
|
|
384
|
+
testCommand: "pytest"
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
path: dir,
|
|
389
|
+
language: marker.language,
|
|
390
|
+
buildTool: marker.buildTool,
|
|
391
|
+
buildCommand: marker.buildCommand,
|
|
392
|
+
testCommand: marker.testCommand
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
path: dir,
|
|
397
|
+
language: "typescript",
|
|
398
|
+
buildTool: "npm",
|
|
399
|
+
buildCommand: "npm run build",
|
|
400
|
+
testCommand: "npm test"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Detects if the project root is a Turborepo monorepo.
|
|
405
|
+
*
|
|
406
|
+
* Checks for `turbo.json` at the root, then enumerates package directories
|
|
407
|
+
* under `apps/` and `packages/`, calling `detectSingleProjectStack()` for each.
|
|
408
|
+
*
|
|
409
|
+
* @param rootDir - Absolute path to the project root.
|
|
410
|
+
* @returns A `ProjectProfile` if Turborepo is detected, otherwise `null`.
|
|
411
|
+
*/
|
|
412
|
+
async function detectMonorepoProfile(rootDir) {
|
|
413
|
+
const turboJsonPath = path$2.join(rootDir, "turbo.json");
|
|
414
|
+
if (!await fileExists(turboJsonPath)) return null;
|
|
415
|
+
const packageDirs = [];
|
|
416
|
+
for (const subdir of ["apps", "packages"]) {
|
|
417
|
+
const fullSubdir = path$2.join(rootDir, subdir);
|
|
418
|
+
try {
|
|
419
|
+
const entries = await fs.readdir(fullSubdir, { withFileTypes: true });
|
|
420
|
+
for (const entry of entries) if (entry.isDirectory()) packageDirs.push(path$2.join(subdir, entry.name));
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
const packages = [];
|
|
424
|
+
for (const relPath of packageDirs) {
|
|
425
|
+
const absPath = path$2.join(rootDir, relPath);
|
|
426
|
+
const stackEntry = await detectSingleProjectStack(absPath);
|
|
427
|
+
packages.push({
|
|
428
|
+
...stackEntry,
|
|
429
|
+
path: relPath
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return { project: {
|
|
433
|
+
type: "monorepo",
|
|
434
|
+
tool: "turborepo",
|
|
435
|
+
buildCommand: "turbo build",
|
|
436
|
+
testCommand: "turbo test",
|
|
437
|
+
packages
|
|
438
|
+
} };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Auto-detects the project profile for the given root directory.
|
|
442
|
+
*
|
|
443
|
+
* First attempts Turborepo monorepo detection. If `turbo.json` is not found,
|
|
444
|
+
* falls back to single-project stack detection.
|
|
445
|
+
*
|
|
446
|
+
* The result is NOT written to disk — detection is purely in-memory.
|
|
447
|
+
*
|
|
448
|
+
* @param rootDir - Absolute path to the project root.
|
|
449
|
+
* @returns A fully populated `ProjectProfile`, or `null` if no recognizable
|
|
450
|
+
* stack markers are found (enabling callers to implement AC7-style
|
|
451
|
+
* graceful no-detection behaviour).
|
|
452
|
+
*/
|
|
453
|
+
async function detectProjectProfile(rootDir) {
|
|
454
|
+
const monorepoProfile = await detectMonorepoProfile(rootDir);
|
|
455
|
+
if (monorepoProfile !== null) return monorepoProfile;
|
|
456
|
+
let anyMarkerFound = false;
|
|
457
|
+
for (const marker of STACK_MARKERS) if (await fileExists(path$2.join(rootDir, marker.file))) {
|
|
458
|
+
anyMarkerFound = true;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
if (!anyMarkerFound) return null;
|
|
462
|
+
const stackEntry = await detectSingleProjectStack(rootDir);
|
|
463
|
+
return { project: {
|
|
464
|
+
type: "single",
|
|
465
|
+
tool: null,
|
|
466
|
+
language: stackEntry.language,
|
|
467
|
+
buildTool: stackEntry.buildTool,
|
|
468
|
+
framework: stackEntry.framework,
|
|
469
|
+
buildCommand: stackEntry.buildCommand ?? "npm run build",
|
|
470
|
+
testCommand: stackEntry.testCommand ?? "npm test",
|
|
471
|
+
packages: []
|
|
472
|
+
} };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/modules/project-profile/writer.ts
|
|
477
|
+
const PROFILE_HEADER = "# Substrate Project Profile\n# Generated by `substrate init`\n# Edit to override auto-detected settings.\n\n";
|
|
478
|
+
/**
|
|
479
|
+
* Serializes the given `ProjectProfile` to YAML and writes it to `profilePath`.
|
|
480
|
+
*
|
|
481
|
+
* The generated file begins with a human-readable comment header so users can
|
|
482
|
+
* understand the file's purpose at a glance. The YAML content is produced by
|
|
483
|
+
* `js-yaml`'s `dump()` function for consistent formatting.
|
|
484
|
+
*
|
|
485
|
+
* @param profilePath - Absolute path to write the profile YAML file.
|
|
486
|
+
* @param profile - The `ProjectProfile` object to serialize.
|
|
487
|
+
*/
|
|
488
|
+
async function writeProjectProfile(profilePath, profile) {
|
|
489
|
+
const yamlContent = yaml.dump(profile);
|
|
490
|
+
await writeFile(profilePath, PROFILE_HEADER + yamlContent, "utf-8");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/cli/templates/build-dev-notes.ts
|
|
495
|
+
const DEV_WORKFLOW_START_MARKER = "<!-- dev-workflow:start -->";
|
|
496
|
+
const DEV_WORKFLOW_END_MARKER = "<!-- dev-workflow:end -->";
|
|
497
|
+
function detectPackageManager(buildCommand) {
|
|
498
|
+
if (buildCommand.includes("pnpm")) return "pnpm";
|
|
499
|
+
if (buildCommand.includes("yarn")) return "yarn";
|
|
500
|
+
if (buildCommand.includes("bun")) return "bun";
|
|
501
|
+
return "npm";
|
|
502
|
+
}
|
|
503
|
+
function getBuildCmd(pm) {
|
|
504
|
+
switch (pm) {
|
|
505
|
+
case "pnpm": return "pnpm run build";
|
|
506
|
+
case "yarn": return "yarn build";
|
|
507
|
+
case "bun": return "bun run build";
|
|
508
|
+
default: return "npm run build";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function getTestCmd(pm) {
|
|
512
|
+
switch (pm) {
|
|
513
|
+
case "pnpm": return "pnpm test";
|
|
514
|
+
case "yarn": return "yarn test";
|
|
515
|
+
case "bun": return "bun test";
|
|
516
|
+
default: return "npm test";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function stackDefaultTestCommand(pkg) {
|
|
520
|
+
if (pkg.testCommand) return pkg.testCommand;
|
|
521
|
+
switch (pkg.language) {
|
|
522
|
+
case "go": return "go test ./...";
|
|
523
|
+
case "rust": return "cargo test";
|
|
524
|
+
case "java":
|
|
525
|
+
case "kotlin":
|
|
526
|
+
if (pkg.buildTool === "maven") return "mvn test";
|
|
527
|
+
return "./gradlew test";
|
|
528
|
+
case "python": return "pytest";
|
|
529
|
+
default: {
|
|
530
|
+
const buildToolPm = pkg.buildTool;
|
|
531
|
+
if (buildToolPm === "pnpm") return "pnpm test";
|
|
532
|
+
if (buildToolPm === "yarn") return "yarn test";
|
|
533
|
+
if (buildToolPm === "bun") return "bun test";
|
|
534
|
+
return "npm test";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function buildNodeSection(buildCommand) {
|
|
539
|
+
const pm = detectPackageManager(buildCommand);
|
|
540
|
+
const buildCmd = getBuildCmd(pm);
|
|
541
|
+
const testCmd = getTestCmd(pm);
|
|
542
|
+
return [
|
|
543
|
+
"## Dev Workflow",
|
|
544
|
+
"",
|
|
545
|
+
"**Build:** `" + buildCmd + "`",
|
|
546
|
+
"**Test:** `" + testCmd + "`",
|
|
547
|
+
"",
|
|
548
|
+
"### Testing Notes",
|
|
549
|
+
"- Run targeted tests during development to avoid slow feedback loops",
|
|
550
|
+
"- Run the full suite before merging"
|
|
551
|
+
].join("\n");
|
|
552
|
+
}
|
|
553
|
+
function buildGoSection() {
|
|
554
|
+
return [
|
|
555
|
+
"## Dev Workflow",
|
|
556
|
+
"",
|
|
557
|
+
"**Build:** `go build ./...`",
|
|
558
|
+
"**Test:** `go test ./...`",
|
|
559
|
+
"",
|
|
560
|
+
"### Testing Notes",
|
|
561
|
+
"- Run targeted tests: `go test ./pkg/... -v -run TestFunctionName`",
|
|
562
|
+
"- Run with short flag to skip long-running tests: `go test ./... -short`",
|
|
563
|
+
"- Verbose output: `go test ./... -v`"
|
|
564
|
+
].join("\n");
|
|
565
|
+
}
|
|
566
|
+
function buildGradleSection() {
|
|
567
|
+
return [
|
|
568
|
+
"## Dev Workflow",
|
|
569
|
+
"",
|
|
570
|
+
"**Build:** `./gradlew build`",
|
|
571
|
+
"**Test:** `./gradlew test`",
|
|
572
|
+
"",
|
|
573
|
+
"### Testing Notes",
|
|
574
|
+
"- Run a specific test class: `./gradlew test --tests \"com.example.ClassName\"`",
|
|
575
|
+
"- Run a specific method: `./gradlew test --tests \"com.example.ClassName.methodName\"`"
|
|
576
|
+
].join("\n");
|
|
577
|
+
}
|
|
578
|
+
function buildMavenSection() {
|
|
579
|
+
return [
|
|
580
|
+
"## Dev Workflow",
|
|
581
|
+
"",
|
|
582
|
+
"**Build:** `mvn compile`",
|
|
583
|
+
"**Test:** `mvn test`",
|
|
584
|
+
"",
|
|
585
|
+
"### Testing Notes",
|
|
586
|
+
"- Run a specific test class: `mvn test -Dtest=ClassName`",
|
|
587
|
+
"- Run a specific method: `mvn test -Dtest=\"ClassName#methodName\"`"
|
|
588
|
+
].join("\n");
|
|
589
|
+
}
|
|
590
|
+
function buildCargoSection() {
|
|
591
|
+
return [
|
|
592
|
+
"## Dev Workflow",
|
|
593
|
+
"",
|
|
594
|
+
"**Build:** `cargo build`",
|
|
595
|
+
"**Test:** `cargo test`",
|
|
596
|
+
"",
|
|
597
|
+
"### Testing Notes",
|
|
598
|
+
"- Show test output: `cargo test -- --nocapture`",
|
|
599
|
+
"- Run a specific test: `cargo test test_function_name`",
|
|
600
|
+
"- Run tests in a module: `cargo test --lib test_module`"
|
|
601
|
+
].join("\n");
|
|
602
|
+
}
|
|
603
|
+
function buildPythonSection(buildCommand) {
|
|
604
|
+
let installCmd;
|
|
605
|
+
if (buildCommand.includes("poetry")) installCmd = "poetry install";
|
|
606
|
+
else installCmd = "pip install -e .";
|
|
607
|
+
return [
|
|
608
|
+
"## Dev Workflow",
|
|
609
|
+
"",
|
|
610
|
+
"**Install:** `" + installCmd + "`",
|
|
611
|
+
"**Test:** `pytest -v`",
|
|
612
|
+
"",
|
|
613
|
+
"### Testing Notes",
|
|
614
|
+
"- Run targeted tests: `pytest -k \"test_name\" -v`",
|
|
615
|
+
"- Run a specific file and test: `pytest tests/test_foo.py::test_bar -v`"
|
|
616
|
+
].join("\n");
|
|
617
|
+
}
|
|
618
|
+
function buildMonorepoSection(profile) {
|
|
619
|
+
const { project } = profile;
|
|
620
|
+
const lines = [
|
|
621
|
+
"## Dev Workflow",
|
|
622
|
+
"",
|
|
623
|
+
`**Root build:** \`${project.buildCommand}\``,
|
|
624
|
+
`**Root test:** \`${project.testCommand}\``
|
|
625
|
+
];
|
|
626
|
+
const packages = project.packages ?? [];
|
|
627
|
+
if (packages.length > 0) {
|
|
628
|
+
lines.push("");
|
|
629
|
+
lines.push("### Package Structure");
|
|
630
|
+
lines.push("");
|
|
631
|
+
lines.push("| Package | Language | Framework | Test Command |");
|
|
632
|
+
lines.push("|---------|----------|-----------|--------------|");
|
|
633
|
+
for (const pkg of packages) {
|
|
634
|
+
const lang = pkg.language;
|
|
635
|
+
const framework = pkg.framework ?? "—";
|
|
636
|
+
const testCmd = stackDefaultTestCommand(pkg);
|
|
637
|
+
lines.push(`| ${pkg.path} | ${lang} | ${framework} | ${testCmd} |`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return lines.join("\n");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Generates a stack-aware "Dev Workflow" section for inclusion in CLAUDE.md.
|
|
644
|
+
*
|
|
645
|
+
* Returns an empty string when `profile` is null (backward-compatible — the
|
|
646
|
+
* caller should skip prepending the dev workflow block in that case).
|
|
647
|
+
*
|
|
648
|
+
* When a profile is present, returns a string wrapped in
|
|
649
|
+
* `<!-- dev-workflow:start -->` / `<!-- dev-workflow:end -->` markers.
|
|
650
|
+
*/
|
|
651
|
+
function buildStackAwareDevNotes(profile) {
|
|
652
|
+
if (!profile) return "";
|
|
653
|
+
const { project } = profile;
|
|
654
|
+
let body;
|
|
655
|
+
if (project.type === "monorepo") body = buildMonorepoSection(profile);
|
|
656
|
+
else {
|
|
657
|
+
const buildTool = project.buildTool;
|
|
658
|
+
const language = project.language;
|
|
659
|
+
if (buildTool === "go" || language === "go") body = buildGoSection();
|
|
660
|
+
else if (buildTool === "gradle") body = buildGradleSection();
|
|
661
|
+
else if (buildTool === "maven") body = buildMavenSection();
|
|
662
|
+
else if (buildTool === "cargo" || language === "rust") body = buildCargoSection();
|
|
663
|
+
else if (language === "python") body = buildPythonSection(project.buildCommand);
|
|
664
|
+
else body = buildNodeSection(project.buildCommand);
|
|
665
|
+
}
|
|
666
|
+
return [
|
|
667
|
+
DEV_WORKFLOW_START_MARKER,
|
|
668
|
+
body,
|
|
669
|
+
DEV_WORKFLOW_END_MARKER
|
|
670
|
+
].join("\n");
|
|
671
|
+
}
|
|
672
|
+
|
|
260
673
|
//#endregion
|
|
261
674
|
//#region src/cli/commands/init.ts
|
|
262
675
|
const logger$18 = createLogger("init");
|
|
@@ -314,7 +727,7 @@ async function scaffoldBmadFramework(projectRoot, force, outputFormat) {
|
|
|
314
727
|
}
|
|
315
728
|
const CLAUDE_MD_START_MARKER = "<!-- substrate:start -->";
|
|
316
729
|
const CLAUDE_MD_END_MARKER = "<!-- substrate:end -->";
|
|
317
|
-
async function scaffoldClaudeMd(projectRoot) {
|
|
730
|
+
async function scaffoldClaudeMd(projectRoot, profile) {
|
|
318
731
|
const claudeMdPath = join(projectRoot, "CLAUDE.md");
|
|
319
732
|
const pkgRoot = findPackageRoot(__dirname);
|
|
320
733
|
const templateName = "claude-md-substrate-section.md";
|
|
@@ -328,6 +741,7 @@ async function scaffoldClaudeMd(projectRoot) {
|
|
|
328
741
|
return;
|
|
329
742
|
}
|
|
330
743
|
if (!sectionContent.endsWith("\n")) sectionContent += "\n";
|
|
744
|
+
const devNotesSection = buildStackAwareDevNotes(profile ?? null);
|
|
331
745
|
let existingContent = "";
|
|
332
746
|
let claudeMdExists = false;
|
|
333
747
|
try {
|
|
@@ -335,11 +749,23 @@ async function scaffoldClaudeMd(projectRoot) {
|
|
|
335
749
|
claudeMdExists = true;
|
|
336
750
|
} catch {}
|
|
337
751
|
let newContent;
|
|
338
|
-
if (!claudeMdExists) newContent = sectionContent;
|
|
339
|
-
else
|
|
752
|
+
if (!claudeMdExists) if (devNotesSection) newContent = devNotesSection + "\n\n" + sectionContent;
|
|
753
|
+
else newContent = sectionContent;
|
|
340
754
|
else {
|
|
341
|
-
|
|
342
|
-
|
|
755
|
+
let updatedExisting;
|
|
756
|
+
if (existingContent.includes(CLAUDE_MD_START_MARKER)) updatedExisting = existingContent.replace(new RegExp(`${CLAUDE_MD_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${CLAUDE_MD_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), sectionContent.trimEnd());
|
|
757
|
+
else {
|
|
758
|
+
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
759
|
+
updatedExisting = existingContent + separator + sectionContent;
|
|
760
|
+
}
|
|
761
|
+
if (devNotesSection) if (updatedExisting.includes(DEV_WORKFLOW_START_MARKER)) newContent = updatedExisting.replace(new RegExp(`${DEV_WORKFLOW_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${DEV_WORKFLOW_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), devNotesSection);
|
|
762
|
+
else if (updatedExisting.includes(CLAUDE_MD_START_MARKER)) newContent = updatedExisting.replace(CLAUDE_MD_START_MARKER, devNotesSection + "\n\n" + CLAUDE_MD_START_MARKER);
|
|
763
|
+
else {
|
|
764
|
+
const sep = updatedExisting.endsWith("\n") ? "\n" : "\n\n";
|
|
765
|
+
newContent = devNotesSection + sep + updatedExisting;
|
|
766
|
+
}
|
|
767
|
+
else if (updatedExisting.includes(DEV_WORKFLOW_START_MARKER)) newContent = updatedExisting.replace(new RegExp(`${DEV_WORKFLOW_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${DEV_WORKFLOW_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`), "");
|
|
768
|
+
else newContent = updatedExisting;
|
|
343
769
|
}
|
|
344
770
|
await writeFile(claudeMdPath, newContent, "utf8");
|
|
345
771
|
logger$18.info({ claudeMdPath }, "Wrote substrate section to CLAUDE.md");
|
|
@@ -543,6 +969,51 @@ function buildProviderConfig(adapterId, cliPath, subscriptionRouting) {
|
|
|
543
969
|
subscription_routing: subscriptionRouting
|
|
544
970
|
};
|
|
545
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Formats a detected project profile as a human-readable string.
|
|
974
|
+
* For single projects: shows stack, build, and test commands.
|
|
975
|
+
* For monorepos: shows the tool, root commands, and per-package breakdown.
|
|
976
|
+
*/
|
|
977
|
+
function formatProjectProfile(profile) {
|
|
978
|
+
const lines = ["", " Detected project profile:"];
|
|
979
|
+
const { project } = profile;
|
|
980
|
+
if (project.type === "monorepo") {
|
|
981
|
+
lines.push(` Type: monorepo (${project.tool ?? "unknown"})`);
|
|
982
|
+
lines.push(` Build: ${project.buildCommand}`);
|
|
983
|
+
lines.push(` Test: ${project.testCommand}`);
|
|
984
|
+
if (project.packages && project.packages.length > 0) {
|
|
985
|
+
lines.push(" Packages:");
|
|
986
|
+
for (const pkg of project.packages) lines.push(` ${pkg.path} ${pkg.language}`);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
const lang = project.language ?? "unknown";
|
|
990
|
+
const stackStr = project.framework ? `${lang} (${project.framework})` : lang;
|
|
991
|
+
lines.push(` Stack: ${stackStr}`);
|
|
992
|
+
lines.push(` Build: ${project.buildCommand}`);
|
|
993
|
+
lines.push(` Test: ${project.testCommand}`);
|
|
994
|
+
}
|
|
995
|
+
return lines.join("\n");
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Prompts the user to accept or decline the detected project profile.
|
|
999
|
+
* In non-interactive mode, always returns true (auto-accept).
|
|
1000
|
+
*/
|
|
1001
|
+
async function promptProfileConfirmation(nonInteractive) {
|
|
1002
|
+
if (nonInteractive) return true;
|
|
1003
|
+
const readline = await import("readline");
|
|
1004
|
+
const rl = readline.createInterface({
|
|
1005
|
+
input: process.stdin,
|
|
1006
|
+
output: process.stdout
|
|
1007
|
+
});
|
|
1008
|
+
return new Promise((resolve$2) => {
|
|
1009
|
+
rl.question("\n Accept detected project profile? [Y/n]: ", (answer) => {
|
|
1010
|
+
rl.close();
|
|
1011
|
+
const trimmed = answer.trim().toLowerCase();
|
|
1012
|
+
if (trimmed === "" || trimmed === "y" || trimmed === "yes") resolve$2(true);
|
|
1013
|
+
else resolve$2(false);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
546
1017
|
async function promptSubscriptionRouting(providerName, nonInteractive) {
|
|
547
1018
|
if (nonInteractive) return "auto";
|
|
548
1019
|
const readline = await import("readline");
|
|
@@ -559,9 +1030,9 @@ async function promptSubscriptionRouting(providerName, nonInteractive) {
|
|
|
559
1030
|
});
|
|
560
1031
|
});
|
|
561
1032
|
}
|
|
562
|
-
async function directoryExists(path$
|
|
1033
|
+
async function directoryExists(path$3) {
|
|
563
1034
|
try {
|
|
564
|
-
await access(path$
|
|
1035
|
+
await access(path$3);
|
|
565
1036
|
return true;
|
|
566
1037
|
} catch {
|
|
567
1038
|
return false;
|
|
@@ -630,6 +1101,33 @@ async function runInitAction(options) {
|
|
|
630
1101
|
await writeFile(configPath, configHeader + yaml.dump(config), "utf-8");
|
|
631
1102
|
const routingHeader = "# Substrate Routing Policy\n# Defines how tasks are routed to AI providers.\n# Customize rules to match your workflow and available agents.\n\n";
|
|
632
1103
|
await writeFile(routingPolicyPath, routingHeader + yaml.dump(routingPolicy), "utf-8");
|
|
1104
|
+
const projectProfilePath = join(substrateDir, "project-profile.yaml");
|
|
1105
|
+
let detectedProfile = null;
|
|
1106
|
+
let projectProfileWritten = false;
|
|
1107
|
+
try {
|
|
1108
|
+
detectedProfile = await detectProjectProfile(dbRoot);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
logger$18.warn({ err }, "Project profile detection failed; skipping");
|
|
1111
|
+
}
|
|
1112
|
+
if (detectedProfile === null) {
|
|
1113
|
+
if (outputFormat !== "json") process.stdout.write(" No project stack detected. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
|
|
1114
|
+
} else {
|
|
1115
|
+
if (outputFormat !== "json") process.stdout.write(formatProjectProfile(detectedProfile) + "\n");
|
|
1116
|
+
let profileExists = false;
|
|
1117
|
+
try {
|
|
1118
|
+
await access(projectProfilePath);
|
|
1119
|
+
profileExists = true;
|
|
1120
|
+
} catch {}
|
|
1121
|
+
if (profileExists && !force) {
|
|
1122
|
+
if (outputFormat !== "json") process.stdout.write(" .substrate/project-profile.yaml already exists — skipping (use --force to overwrite)\n");
|
|
1123
|
+
} else {
|
|
1124
|
+
const accepted = await promptProfileConfirmation(nonInteractive);
|
|
1125
|
+
if (accepted) {
|
|
1126
|
+
await writeProjectProfile(projectProfilePath, detectedProfile);
|
|
1127
|
+
projectProfileWritten = true;
|
|
1128
|
+
} else if (outputFormat !== "json") process.stdout.write(" Profile not written. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
633
1131
|
await scaffoldBmadFramework(projectRoot, force, outputFormat);
|
|
634
1132
|
const localManifest = join(packPath, "manifest.yaml");
|
|
635
1133
|
let scaffolded = false;
|
|
@@ -671,7 +1169,7 @@ async function runInitAction(options) {
|
|
|
671
1169
|
});
|
|
672
1170
|
await initSchema(dbAdapter);
|
|
673
1171
|
await dbAdapter.close();
|
|
674
|
-
await scaffoldClaudeMd(projectRoot);
|
|
1172
|
+
await scaffoldClaudeMd(projectRoot, detectedProfile);
|
|
675
1173
|
await scaffoldStatuslineScript(projectRoot);
|
|
676
1174
|
await scaffoldClaudeSettings(projectRoot);
|
|
677
1175
|
await scaffoldClaudeCommands(projectRoot, outputFormat);
|
|
@@ -705,7 +1203,9 @@ async function runInitAction(options) {
|
|
|
705
1203
|
scaffolded,
|
|
706
1204
|
configPath,
|
|
707
1205
|
routingPolicyPath,
|
|
708
|
-
doltInitialized
|
|
1206
|
+
doltInitialized,
|
|
1207
|
+
projectProfile: detectedProfile ?? null,
|
|
1208
|
+
projectProfileWritten
|
|
709
1209
|
}, "json", true) + "\n");
|
|
710
1210
|
else {
|
|
711
1211
|
process.stdout.write(`\n Substrate initialized successfully!\n\n`);
|
|
@@ -2969,7 +3469,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
2969
3469
|
await initSchema(expAdapter);
|
|
2970
3470
|
const { runRunAction: runPipeline } = await import(
|
|
2971
3471
|
/* @vite-ignore */
|
|
2972
|
-
"../run-
|
|
3472
|
+
"../run-DTOsG7PJ.js"
|
|
2973
3473
|
);
|
|
2974
3474
|
const runStoryFn = async (opts) => {
|
|
2975
3475
|
const exitCode = await runPipeline({
|
|
@@ -3817,8 +4317,8 @@ function registerMetricsCommand(program, _version = "0.0.0", projectRoot = proce
|
|
|
3817
4317
|
* This function now always returns an empty snapshot.
|
|
3818
4318
|
*/
|
|
3819
4319
|
async function readSqliteSnapshot(dbPath) {
|
|
3820
|
-
const { existsSync: fileExists } = await import("node:fs");
|
|
3821
|
-
if (fileExists(dbPath)) process.stderr.write(`Warning: Legacy SQLite database found at ${dbPath} but SQLite support has been\nremoved in Epic 29. To migrate historical data, downgrade to Substrate v0.4.x,\nrun 'substrate migrate', then upgrade back to this version.\n`);
|
|
4320
|
+
const { existsSync: fileExists$1 } = await import("node:fs");
|
|
4321
|
+
if (fileExists$1(dbPath)) process.stderr.write(`Warning: Legacy SQLite database found at ${dbPath} but SQLite support has been\nremoved in Epic 29. To migrate historical data, downgrade to Substrate v0.4.x,\nrun 'substrate migrate', then upgrade back to this version.\n`);
|
|
3822
4322
|
return { storyMetrics: [] };
|
|
3823
4323
|
}
|
|
3824
4324
|
const BATCH_SIZE = 100;
|