@nathapp/nax 0.47.0 → 0.48.0
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/nax.js +155 -14
- package/package.json +1 -1
- package/src/cli/generate.ts +13 -2
- package/src/cli/plan.ts +117 -9
- package/src/config/runtime-types.ts +12 -0
- package/src/context/generator.ts +96 -1
package/dist/nax.js
CHANGED
|
@@ -22210,7 +22210,7 @@ var package_default;
|
|
|
22210
22210
|
var init_package = __esm(() => {
|
|
22211
22211
|
package_default = {
|
|
22212
22212
|
name: "@nathapp/nax",
|
|
22213
|
-
version: "0.
|
|
22213
|
+
version: "0.48.0",
|
|
22214
22214
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22215
22215
|
type: "module",
|
|
22216
22216
|
bin: {
|
|
@@ -22283,8 +22283,8 @@ var init_version = __esm(() => {
|
|
|
22283
22283
|
NAX_VERSION = package_default.version;
|
|
22284
22284
|
NAX_COMMIT = (() => {
|
|
22285
22285
|
try {
|
|
22286
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22287
|
-
return "
|
|
22286
|
+
if (/^[0-9a-f]{6,10}$/.test("3188738"))
|
|
22287
|
+
return "3188738";
|
|
22288
22288
|
} catch {}
|
|
22289
22289
|
try {
|
|
22290
22290
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -66438,7 +66438,7 @@ init_test_strategy();
|
|
|
66438
66438
|
|
|
66439
66439
|
// src/context/generator.ts
|
|
66440
66440
|
init_path_security2();
|
|
66441
|
-
import { existsSync as existsSync10 } from "fs";
|
|
66441
|
+
import { existsSync as existsSync10, readFileSync } from "fs";
|
|
66442
66442
|
import { join as join11 } from "path";
|
|
66443
66443
|
|
|
66444
66444
|
// src/context/injector.ts
|
|
@@ -66852,6 +66852,73 @@ async function discoverPackages(repoRoot) {
|
|
|
66852
66852
|
}
|
|
66853
66853
|
return packages;
|
|
66854
66854
|
}
|
|
66855
|
+
async function discoverWorkspacePackages(repoRoot) {
|
|
66856
|
+
const existing = await discoverPackages(repoRoot);
|
|
66857
|
+
if (existing.length > 0) {
|
|
66858
|
+
return existing.map((p) => p.replace(`${repoRoot}/`, ""));
|
|
66859
|
+
}
|
|
66860
|
+
const seen = new Set;
|
|
66861
|
+
const results = [];
|
|
66862
|
+
async function resolveGlobs(patterns) {
|
|
66863
|
+
for (const pattern of patterns) {
|
|
66864
|
+
if (pattern.startsWith("!"))
|
|
66865
|
+
continue;
|
|
66866
|
+
const base = pattern.replace(/\/+$/, "");
|
|
66867
|
+
const pkgPattern = base.endsWith("*") ? `${base}/package.json` : `${base}/*/package.json`;
|
|
66868
|
+
const g = new Bun.Glob(pkgPattern);
|
|
66869
|
+
for await (const match of g.scan(repoRoot)) {
|
|
66870
|
+
const rel = match.replace(/\/package\.json$/, "");
|
|
66871
|
+
if (!seen.has(rel)) {
|
|
66872
|
+
seen.add(rel);
|
|
66873
|
+
results.push(rel);
|
|
66874
|
+
}
|
|
66875
|
+
}
|
|
66876
|
+
}
|
|
66877
|
+
}
|
|
66878
|
+
const turboPath = join11(repoRoot, "turbo.json");
|
|
66879
|
+
if (existsSync10(turboPath)) {
|
|
66880
|
+
try {
|
|
66881
|
+
const turbo = JSON.parse(readFileSync(turboPath, "utf-8"));
|
|
66882
|
+
if (Array.isArray(turbo.packages)) {
|
|
66883
|
+
await resolveGlobs(turbo.packages);
|
|
66884
|
+
}
|
|
66885
|
+
} catch {}
|
|
66886
|
+
}
|
|
66887
|
+
const pkgPath = join11(repoRoot, "package.json");
|
|
66888
|
+
if (existsSync10(pkgPath)) {
|
|
66889
|
+
try {
|
|
66890
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
66891
|
+
const ws = pkg.workspaces;
|
|
66892
|
+
const patterns = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
66893
|
+
if (patterns.length > 0)
|
|
66894
|
+
await resolveGlobs(patterns);
|
|
66895
|
+
} catch {}
|
|
66896
|
+
}
|
|
66897
|
+
const pnpmPath = join11(repoRoot, "pnpm-workspace.yaml");
|
|
66898
|
+
if (existsSync10(pnpmPath)) {
|
|
66899
|
+
try {
|
|
66900
|
+
const raw = readFileSync(pnpmPath, "utf-8");
|
|
66901
|
+
const lines = raw.split(`
|
|
66902
|
+
`);
|
|
66903
|
+
let inPackages = false;
|
|
66904
|
+
const patterns = [];
|
|
66905
|
+
for (const line of lines) {
|
|
66906
|
+
if (/^packages\s*:/.test(line)) {
|
|
66907
|
+
inPackages = true;
|
|
66908
|
+
continue;
|
|
66909
|
+
}
|
|
66910
|
+
if (inPackages && /^\s+-\s+/.test(line)) {
|
|
66911
|
+
patterns.push(line.replace(/^\s+-\s+['"]?/, "").replace(/['"]?\s*$/, ""));
|
|
66912
|
+
} else if (inPackages && !/^\s/.test(line)) {
|
|
66913
|
+
break;
|
|
66914
|
+
}
|
|
66915
|
+
}
|
|
66916
|
+
if (patterns.length > 0)
|
|
66917
|
+
await resolveGlobs(patterns);
|
|
66918
|
+
} catch {}
|
|
66919
|
+
}
|
|
66920
|
+
return results.sort();
|
|
66921
|
+
}
|
|
66855
66922
|
async function generateForPackage(packageDir, config2, dryRun = false) {
|
|
66856
66923
|
const contextPath = join11(packageDir, "nax", "context.md");
|
|
66857
66924
|
if (!existsSync10(contextPath)) {
|
|
@@ -67059,7 +67126,9 @@ var _deps2 = {
|
|
|
67059
67126
|
},
|
|
67060
67127
|
mkdirp: (path) => Bun.spawn(["mkdir", "-p", path]).exited.then(() => {}),
|
|
67061
67128
|
existsSync: (path) => existsSync11(path),
|
|
67062
|
-
|
|
67129
|
+
discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages(repoRoot),
|
|
67130
|
+
readPackageJsonAt: (path) => Bun.file(path).json().catch(() => null),
|
|
67131
|
+
createInteractionBridge: () => createCliInteractionBridge()
|
|
67063
67132
|
};
|
|
67064
67133
|
async function planCommand(workdir, config2, options) {
|
|
67065
67134
|
const naxDir = join12(workdir, "nax");
|
|
@@ -67072,11 +67141,15 @@ async function planCommand(workdir, config2, options) {
|
|
|
67072
67141
|
logger?.info("plan", "Scanning codebase...");
|
|
67073
67142
|
const [scan, discoveredPackages, pkg] = await Promise.all([
|
|
67074
67143
|
_deps2.scanCodebase(workdir),
|
|
67075
|
-
_deps2.
|
|
67144
|
+
_deps2.discoverWorkspacePackages(workdir),
|
|
67076
67145
|
_deps2.readPackageJson(workdir)
|
|
67077
67146
|
]);
|
|
67078
67147
|
const codebaseContext = buildCodebaseContext2(scan);
|
|
67079
|
-
const relativePackages = discoveredPackages.map((p) => p.replace(`${workdir}/`, ""));
|
|
67148
|
+
const relativePackages = discoveredPackages.map((p) => p.startsWith("/") ? p.replace(`${workdir}/`, "") : p);
|
|
67149
|
+
const packageDetails = relativePackages.length > 0 ? await Promise.all(relativePackages.map(async (rel) => {
|
|
67150
|
+
const pkgJson = await _deps2.readPackageJsonAt(join12(workdir, rel, "package.json"));
|
|
67151
|
+
return buildPackageSummary(rel, pkgJson);
|
|
67152
|
+
})) : [];
|
|
67080
67153
|
const projectName = detectProjectName(workdir, pkg);
|
|
67081
67154
|
const branchName = options.branch ?? `feat/${options.feature}`;
|
|
67082
67155
|
const outputDir = join12(naxDir, "features", options.feature);
|
|
@@ -67086,7 +67159,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
67086
67159
|
const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
|
|
67087
67160
|
let rawResponse;
|
|
67088
67161
|
if (options.auto) {
|
|
67089
|
-
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages);
|
|
67162
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails);
|
|
67090
67163
|
const cliAdapter = _deps2.getAgent(agentName);
|
|
67091
67164
|
if (!cliAdapter)
|
|
67092
67165
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
@@ -67098,11 +67171,11 @@ async function planCommand(workdir, config2, options) {
|
|
|
67098
67171
|
}
|
|
67099
67172
|
} catch {}
|
|
67100
67173
|
} else {
|
|
67101
|
-
const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages);
|
|
67174
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails);
|
|
67102
67175
|
const adapter = _deps2.getAgent(agentName, config2);
|
|
67103
67176
|
if (!adapter)
|
|
67104
67177
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
67105
|
-
const interactionBridge =
|
|
67178
|
+
const interactionBridge = _deps2.createInteractionBridge();
|
|
67106
67179
|
const pidRegistry = new PidRegistry(workdir);
|
|
67107
67180
|
const resolvedPerm = resolvePermissions(config2, "plan");
|
|
67108
67181
|
const resolvedModel = config2?.plan?.model ?? "balanced";
|
|
@@ -67180,6 +67253,66 @@ function detectProjectName(workdir, pkg) {
|
|
|
67180
67253
|
}
|
|
67181
67254
|
return "unknown";
|
|
67182
67255
|
}
|
|
67256
|
+
var FRAMEWORK_PATTERNS = [
|
|
67257
|
+
[/\bnext\b/, "Next.js"],
|
|
67258
|
+
[/\bnuxt\b/, "Nuxt"],
|
|
67259
|
+
[/\bremix\b/, "Remix"],
|
|
67260
|
+
[/\bexpress\b/, "Express"],
|
|
67261
|
+
[/\bfastify\b/, "Fastify"],
|
|
67262
|
+
[/\bhono\b/, "Hono"],
|
|
67263
|
+
[/\bnestjs|@nestjs\b/, "NestJS"],
|
|
67264
|
+
[/\breact\b/, "React"],
|
|
67265
|
+
[/\bvue\b/, "Vue"],
|
|
67266
|
+
[/\bsvelte\b/, "Svelte"],
|
|
67267
|
+
[/\bastro\b/, "Astro"],
|
|
67268
|
+
[/\belectron\b/, "Electron"]
|
|
67269
|
+
];
|
|
67270
|
+
var TEST_RUNNER_PATTERNS = [
|
|
67271
|
+
[/\bvitest\b/, "vitest"],
|
|
67272
|
+
[/\bjest\b/, "jest"],
|
|
67273
|
+
[/\bmocha\b/, "mocha"],
|
|
67274
|
+
[/\bava\b/, "ava"]
|
|
67275
|
+
];
|
|
67276
|
+
var KEY_DEP_PATTERNS = [
|
|
67277
|
+
[/\bprisma\b/, "prisma"],
|
|
67278
|
+
[/\bdrizzle-orm\b/, "drizzle"],
|
|
67279
|
+
[/\btypeorm\b/, "typeorm"],
|
|
67280
|
+
[/\bmongoose\b/, "mongoose"],
|
|
67281
|
+
[/\bsqlite\b|better-sqlite/, "sqlite"],
|
|
67282
|
+
[/\bstripe\b/, "stripe"],
|
|
67283
|
+
[/\bgraphql\b/, "graphql"],
|
|
67284
|
+
[/\btrpc\b/, "tRPC"],
|
|
67285
|
+
[/\bzod\b/, "zod"],
|
|
67286
|
+
[/\btailwind\b/, "tailwind"]
|
|
67287
|
+
];
|
|
67288
|
+
function buildPackageSummary(rel, pkg) {
|
|
67289
|
+
const name = typeof pkg?.name === "string" ? pkg.name : rel;
|
|
67290
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
67291
|
+
const depNames = Object.keys(allDeps).join(" ");
|
|
67292
|
+
const scripts = pkg?.scripts ?? {};
|
|
67293
|
+
const testScript = scripts.test ?? "";
|
|
67294
|
+
const runtime = testScript.includes("bun ") ? "bun" : testScript.includes("node ") ? "node" : "unknown";
|
|
67295
|
+
const framework = FRAMEWORK_PATTERNS.find(([re]) => re.test(depNames))?.[1] ?? "";
|
|
67296
|
+
const testRunner = TEST_RUNNER_PATTERNS.find(([re]) => re.test(depNames))?.[1] ?? (testScript.includes("bun test") ? "bun:test" : "");
|
|
67297
|
+
const keyDeps = KEY_DEP_PATTERNS.filter(([re]) => re.test(depNames)).map(([, label]) => label);
|
|
67298
|
+
return { path: rel, name, runtime, framework, testRunner, keyDeps };
|
|
67299
|
+
}
|
|
67300
|
+
function buildPackageDetailsSection(details) {
|
|
67301
|
+
if (details.length === 0)
|
|
67302
|
+
return "";
|
|
67303
|
+
const rows = details.map((d) => {
|
|
67304
|
+
const stack = [d.framework, d.testRunner, ...d.keyDeps].filter(Boolean).join(", ") || "\u2014";
|
|
67305
|
+
return `| \`${d.path}\` | ${d.name} | ${stack} |`;
|
|
67306
|
+
});
|
|
67307
|
+
return `
|
|
67308
|
+
## Package Tech Stacks
|
|
67309
|
+
|
|
67310
|
+
| Path | Package | Stack |
|
|
67311
|
+
|:-----|:--------|:------|
|
|
67312
|
+
${rows.join(`
|
|
67313
|
+
`)}
|
|
67314
|
+
`;
|
|
67315
|
+
}
|
|
67183
67316
|
function buildCodebaseContext2(scan) {
|
|
67184
67317
|
const sections = [];
|
|
67185
67318
|
sections.push(`## Codebase Structure
|
|
@@ -67206,15 +67339,16 @@ function buildCodebaseContext2(scan) {
|
|
|
67206
67339
|
return sections.join(`
|
|
67207
67340
|
`);
|
|
67208
67341
|
}
|
|
67209
|
-
function buildPlanningPrompt(specContent, codebaseContext, outputFilePath, packages) {
|
|
67342
|
+
function buildPlanningPrompt(specContent, codebaseContext, outputFilePath, packages, packageDetails) {
|
|
67210
67343
|
const isMonorepo = packages && packages.length > 0;
|
|
67344
|
+
const packageDetailsSection = packageDetails && packageDetails.length > 0 ? buildPackageDetailsSection(packageDetails) : "";
|
|
67211
67345
|
const monorepoHint = isMonorepo ? `
|
|
67212
67346
|
## Monorepo Context
|
|
67213
67347
|
|
|
67214
67348
|
This is a monorepo. Detected packages:
|
|
67215
67349
|
${packages.map((p) => `- ${p}`).join(`
|
|
67216
67350
|
`)}
|
|
67217
|
-
|
|
67351
|
+
${packageDetailsSection}
|
|
67218
67352
|
For each user story, set the "workdir" field to the relevant package path (e.g. "packages/api"). Stories that span the root should omit "workdir".` : "";
|
|
67219
67353
|
const workdirField = isMonorepo ? `
|
|
67220
67354
|
"workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
|
|
@@ -68709,8 +68843,15 @@ async function generateCommand(options) {
|
|
|
68709
68843
|
const suffix = dryRun ? " (dry run)" : "";
|
|
68710
68844
|
console.log(source_default.green(`\u2713 ${agent} \u2192 ${result.outputFile} (${result.content.length} bytes${suffix})`));
|
|
68711
68845
|
} else {
|
|
68712
|
-
|
|
68713
|
-
const
|
|
68846
|
+
const configAgents = config2?.generate?.agents;
|
|
68847
|
+
const agentFilter = configAgents && configAgents.length > 0 ? configAgents : null;
|
|
68848
|
+
if (agentFilter) {
|
|
68849
|
+
console.log(source_default.blue(`\u2192 Generating configs for: ${agentFilter.join(", ")} (from config)...`));
|
|
68850
|
+
} else {
|
|
68851
|
+
console.log(source_default.blue("\u2192 Generating configs for all agents..."));
|
|
68852
|
+
}
|
|
68853
|
+
const allResults = await generateAll(genOptions, config2);
|
|
68854
|
+
const results = agentFilter ? allResults.filter((r) => agentFilter.includes(r.agent)) : allResults;
|
|
68714
68855
|
let errorCount = 0;
|
|
68715
68856
|
for (const result of results) {
|
|
68716
68857
|
if (result.error) {
|
package/package.json
CHANGED
package/src/cli/generate.ts
CHANGED
|
@@ -142,6 +142,7 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
|
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
144
|
if (options.agent) {
|
|
145
|
+
// CLI --agent flag: single specific agent (overrides config)
|
|
145
146
|
const agent = options.agent as AgentType;
|
|
146
147
|
console.log(chalk.blue(`→ Generating config for ${agent}...`));
|
|
147
148
|
|
|
@@ -155,9 +156,19 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
|
|
|
155
156
|
const suffix = dryRun ? " (dry run)" : "";
|
|
156
157
|
console.log(chalk.green(`✓ ${agent} → ${result.outputFile} (${result.content.length} bytes${suffix})`));
|
|
157
158
|
} else {
|
|
158
|
-
|
|
159
|
+
// No --agent flag: use config.generate.agents filter, or generate all
|
|
160
|
+
const configAgents = config?.generate?.agents;
|
|
161
|
+
const agentFilter = configAgents && configAgents.length > 0 ? configAgents : null;
|
|
162
|
+
|
|
163
|
+
if (agentFilter) {
|
|
164
|
+
console.log(chalk.blue(`→ Generating configs for: ${agentFilter.join(", ")} (from config)...`));
|
|
165
|
+
} else {
|
|
166
|
+
console.log(chalk.blue("→ Generating configs for all agents..."));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const allResults = await generateAll(genOptions, config);
|
|
170
|
+
const results = agentFilter ? allResults.filter((r) => agentFilter.includes(r.agent as AgentType)) : allResults;
|
|
159
171
|
|
|
160
|
-
const results = await generateAll(genOptions, config);
|
|
161
172
|
let errorCount = 0;
|
|
162
173
|
|
|
163
174
|
for (const result of results) {
|
package/src/cli/plan.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { CodebaseScan } from "../analyze/types";
|
|
|
17
17
|
import type { NaxConfig } from "../config";
|
|
18
18
|
import { resolvePermissions } from "../config/permissions";
|
|
19
19
|
import { COMPLEXITY_GUIDE, GROUPING_RULES, TEST_STRATEGY_GUIDE } from "../config/test-strategy";
|
|
20
|
-
import {
|
|
20
|
+
import { discoverWorkspacePackages } from "../context/generator";
|
|
21
21
|
import { PidRegistry } from "../execution/pid-registry";
|
|
22
22
|
import { getLogger } from "../logger";
|
|
23
23
|
import { validatePlanOutput } from "../prd/schema";
|
|
@@ -42,7 +42,15 @@ export const _deps = {
|
|
|
42
42
|
},
|
|
43
43
|
mkdirp: (path: string): Promise<void> => Bun.spawn(["mkdir", "-p", path]).exited.then(() => {}),
|
|
44
44
|
existsSync: (path: string): boolean => existsSync(path),
|
|
45
|
-
|
|
45
|
+
discoverWorkspacePackages: (repoRoot: string): Promise<string[]> => discoverWorkspacePackages(repoRoot),
|
|
46
|
+
readPackageJsonAt: (path: string): Promise<Record<string, unknown> | null> =>
|
|
47
|
+
Bun.file(path)
|
|
48
|
+
.json()
|
|
49
|
+
.catch(() => null),
|
|
50
|
+
createInteractionBridge: (): {
|
|
51
|
+
detectQuestion: (text: string) => Promise<boolean>;
|
|
52
|
+
onQuestionDetected: (text: string) => Promise<string>;
|
|
53
|
+
} => createCliInteractionBridge(),
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -89,13 +97,25 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
89
97
|
logger?.info("plan", "Scanning codebase...");
|
|
90
98
|
const [scan, discoveredPackages, pkg] = await Promise.all([
|
|
91
99
|
_deps.scanCodebase(workdir),
|
|
92
|
-
_deps.
|
|
100
|
+
_deps.discoverWorkspacePackages(workdir),
|
|
93
101
|
_deps.readPackageJson(workdir),
|
|
94
102
|
]);
|
|
95
103
|
const codebaseContext = buildCodebaseContext(scan);
|
|
96
104
|
|
|
97
|
-
//
|
|
98
|
-
|
|
105
|
+
// Normalize to repo-relative paths (discoverWorkspacePackages returns relative,
|
|
106
|
+
// but mocks/legacy callers may return absolute — strip workdir prefix if present)
|
|
107
|
+
const relativePackages = discoveredPackages.map((p) => (p.startsWith("/") ? p.replace(`${workdir}/`, "") : p));
|
|
108
|
+
|
|
109
|
+
// Scan per-package tech stacks for richer monorepo planning context
|
|
110
|
+
const packageDetails =
|
|
111
|
+
relativePackages.length > 0
|
|
112
|
+
? await Promise.all(
|
|
113
|
+
relativePackages.map(async (rel) => {
|
|
114
|
+
const pkgJson = await _deps.readPackageJsonAt(join(workdir, rel, "package.json"));
|
|
115
|
+
return buildPackageSummary(rel, pkgJson);
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
: [];
|
|
99
119
|
|
|
100
120
|
// Auto-detect project name
|
|
101
121
|
const projectName = detectProjectName(workdir, pkg);
|
|
@@ -115,7 +135,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
115
135
|
let rawResponse: string;
|
|
116
136
|
if (options.auto) {
|
|
117
137
|
// One-shot: use CLI adapter directly — simple completion doesn't need ACP session overhead
|
|
118
|
-
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages);
|
|
138
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails);
|
|
119
139
|
const cliAdapter = _deps.getAgent(agentName);
|
|
120
140
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
121
141
|
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir, config });
|
|
@@ -130,10 +150,10 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
130
150
|
}
|
|
131
151
|
} else {
|
|
132
152
|
// Interactive: agent writes PRD JSON directly to outputPath (avoids output truncation)
|
|
133
|
-
const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages);
|
|
153
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails);
|
|
134
154
|
const adapter = _deps.getAgent(agentName, config);
|
|
135
155
|
if (!adapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
136
|
-
const interactionBridge =
|
|
156
|
+
const interactionBridge = _deps.createInteractionBridge();
|
|
137
157
|
const pidRegistry = new PidRegistry(workdir);
|
|
138
158
|
const resolvedPerm = resolvePermissions(config, "plan");
|
|
139
159
|
const resolvedModel = config?.plan?.model ?? "balanced";
|
|
@@ -246,6 +266,91 @@ function detectProjectName(workdir: string, pkg: Record<string, unknown> | null)
|
|
|
246
266
|
return "unknown";
|
|
247
267
|
}
|
|
248
268
|
|
|
269
|
+
/** Compact per-package summary for the planning prompt. */
|
|
270
|
+
interface PackageSummary {
|
|
271
|
+
path: string;
|
|
272
|
+
name: string;
|
|
273
|
+
runtime: string;
|
|
274
|
+
framework: string;
|
|
275
|
+
testRunner: string;
|
|
276
|
+
keyDeps: string[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const FRAMEWORK_PATTERNS: [RegExp, string][] = [
|
|
280
|
+
[/\bnext\b/, "Next.js"],
|
|
281
|
+
[/\bnuxt\b/, "Nuxt"],
|
|
282
|
+
[/\bremix\b/, "Remix"],
|
|
283
|
+
[/\bexpress\b/, "Express"],
|
|
284
|
+
[/\bfastify\b/, "Fastify"],
|
|
285
|
+
[/\bhono\b/, "Hono"],
|
|
286
|
+
[/\bnestjs|@nestjs\b/, "NestJS"],
|
|
287
|
+
[/\breact\b/, "React"],
|
|
288
|
+
[/\bvue\b/, "Vue"],
|
|
289
|
+
[/\bsvelte\b/, "Svelte"],
|
|
290
|
+
[/\bastro\b/, "Astro"],
|
|
291
|
+
[/\belectron\b/, "Electron"],
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const TEST_RUNNER_PATTERNS: [RegExp, string][] = [
|
|
295
|
+
[/\bvitest\b/, "vitest"],
|
|
296
|
+
[/\bjest\b/, "jest"],
|
|
297
|
+
[/\bmocha\b/, "mocha"],
|
|
298
|
+
[/\bava\b/, "ava"],
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const KEY_DEP_PATTERNS: [RegExp, string][] = [
|
|
302
|
+
[/\bprisma\b/, "prisma"],
|
|
303
|
+
[/\bdrizzle-orm\b/, "drizzle"],
|
|
304
|
+
[/\btypeorm\b/, "typeorm"],
|
|
305
|
+
[/\bmongoose\b/, "mongoose"],
|
|
306
|
+
[/\bsqlite\b|better-sqlite/, "sqlite"],
|
|
307
|
+
[/\bstripe\b/, "stripe"],
|
|
308
|
+
[/\bgraphql\b/, "graphql"],
|
|
309
|
+
[/\btrpc\b/, "tRPC"],
|
|
310
|
+
[/\bzod\b/, "zod"],
|
|
311
|
+
[/\btailwind\b/, "tailwind"],
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build a compact summary of a package's tech stack from its package.json.
|
|
316
|
+
*/
|
|
317
|
+
function buildPackageSummary(rel: string, pkg: Record<string, unknown> | null): PackageSummary {
|
|
318
|
+
const name = typeof pkg?.name === "string" ? pkg.name : rel;
|
|
319
|
+
const allDeps = { ...(pkg?.dependencies as object | undefined), ...(pkg?.devDependencies as object | undefined) };
|
|
320
|
+
const depNames = Object.keys(allDeps).join(" ");
|
|
321
|
+
const scripts = (pkg?.scripts ?? {}) as Record<string, string>;
|
|
322
|
+
|
|
323
|
+
// Detect runtime from scripts or lock files
|
|
324
|
+
const testScript = scripts.test ?? "";
|
|
325
|
+
const runtime = testScript.includes("bun ") ? "bun" : testScript.includes("node ") ? "node" : "unknown";
|
|
326
|
+
|
|
327
|
+
// Detect framework
|
|
328
|
+
const framework = FRAMEWORK_PATTERNS.find(([re]) => re.test(depNames))?.[1] ?? "";
|
|
329
|
+
|
|
330
|
+
// Detect test runner
|
|
331
|
+
const testRunner =
|
|
332
|
+
TEST_RUNNER_PATTERNS.find(([re]) => re.test(depNames))?.[1] ?? (testScript.includes("bun test") ? "bun:test" : "");
|
|
333
|
+
|
|
334
|
+
// Key deps
|
|
335
|
+
const keyDeps = KEY_DEP_PATTERNS.filter(([re]) => re.test(depNames)).map(([, label]) => label);
|
|
336
|
+
|
|
337
|
+
return { path: rel, name, runtime, framework, testRunner, keyDeps };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Render per-package summaries as a compact markdown table for the prompt.
|
|
342
|
+
*/
|
|
343
|
+
function buildPackageDetailsSection(details: PackageSummary[]): string {
|
|
344
|
+
if (details.length === 0) return "";
|
|
345
|
+
|
|
346
|
+
const rows = details.map((d) => {
|
|
347
|
+
const stack = [d.framework, d.testRunner, ...d.keyDeps].filter(Boolean).join(", ") || "—";
|
|
348
|
+
return `| \`${d.path}\` | ${d.name} | ${stack} |`;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return `\n## Package Tech Stacks\n\n| Path | Package | Stack |\n|:-----|:--------|:------|\n${rows.join("\n")}\n`;
|
|
352
|
+
}
|
|
353
|
+
|
|
249
354
|
/**
|
|
250
355
|
* Build codebase context markdown from scan results.
|
|
251
356
|
*/
|
|
@@ -293,10 +398,13 @@ function buildPlanningPrompt(
|
|
|
293
398
|
codebaseContext: string,
|
|
294
399
|
outputFilePath?: string,
|
|
295
400
|
packages?: string[],
|
|
401
|
+
packageDetails?: PackageSummary[],
|
|
296
402
|
): string {
|
|
297
403
|
const isMonorepo = packages && packages.length > 0;
|
|
404
|
+
const packageDetailsSection =
|
|
405
|
+
packageDetails && packageDetails.length > 0 ? buildPackageDetailsSection(packageDetails) : "";
|
|
298
406
|
const monorepoHint = isMonorepo
|
|
299
|
-
? `\n## Monorepo Context\n\nThis is a monorepo. Detected packages:\n${packages.map((p) => `- ${p}`).join("\n")}\n\nFor each user story, set the "workdir" field to the relevant package path (e.g. "packages/api"). Stories that span the root should omit "workdir".`
|
|
407
|
+
? `\n## Monorepo Context\n\nThis is a monorepo. Detected packages:\n${packages.map((p) => `- ${p}`).join("\n")}\n${packageDetailsSection}\nFor each user story, set the "workdir" field to the relevant package path (e.g. "packages/api"). Stories that span the root should omit "workdir".`
|
|
300
408
|
: "";
|
|
301
409
|
|
|
302
410
|
const workdirField = isMonorepo
|
|
@@ -476,6 +476,18 @@ export interface NaxConfig {
|
|
|
476
476
|
decompose?: DecomposeConfig;
|
|
477
477
|
/** Agent protocol settings (ACP-003) */
|
|
478
478
|
agent?: AgentConfig;
|
|
479
|
+
/** Generate settings */
|
|
480
|
+
generate?: GenerateConfig;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Generate command configuration */
|
|
484
|
+
export interface GenerateConfig {
|
|
485
|
+
/**
|
|
486
|
+
* Agents to generate config files for (default: all).
|
|
487
|
+
* Restricts `nax generate` to only the listed agents.
|
|
488
|
+
* @example ["claude", "opencode"]
|
|
489
|
+
*/
|
|
490
|
+
agents?: Array<"claude" | "codex" | "opencode" | "cursor" | "windsurf" | "aider" | "gemini">;
|
|
479
491
|
}
|
|
480
492
|
|
|
481
493
|
/** Agent protocol configuration (ACP-003) */
|
package/src/context/generator.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Replaces the old constitution generator.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import type { NaxConfig } from "../config";
|
|
11
11
|
import { validateFilePath } from "../config/path-security";
|
|
@@ -166,6 +166,101 @@ export async function discoverPackages(repoRoot: string): Promise<string[]> {
|
|
|
166
166
|
return packages;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Discover packages from workspace manifests (turbo.json, package.json workspaces,
|
|
171
|
+
* pnpm-workspace.yaml). Used as fallback when no nax/context.md files exist yet.
|
|
172
|
+
*
|
|
173
|
+
* Returns relative paths (e.g. "packages/api") sorted alphabetically.
|
|
174
|
+
*/
|
|
175
|
+
export async function discoverWorkspacePackages(repoRoot: string): Promise<string[]> {
|
|
176
|
+
// 1. Prefer packages that already have nax/context.md
|
|
177
|
+
const existing = await discoverPackages(repoRoot);
|
|
178
|
+
if (existing.length > 0) {
|
|
179
|
+
return existing.map((p) => p.replace(`${repoRoot}/`, ""));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
const results: string[] = [];
|
|
184
|
+
|
|
185
|
+
async function resolveGlobs(patterns: string[]): Promise<void> {
|
|
186
|
+
for (const pattern of patterns) {
|
|
187
|
+
if (pattern.startsWith("!")) continue; // skip negations
|
|
188
|
+
// Convert workspace pattern to package.json glob so Bun can scan files
|
|
189
|
+
// "packages/*" → "packages/*/package.json"
|
|
190
|
+
// "packages/**" → "packages/**/package.json"
|
|
191
|
+
const base = pattern.replace(/\/+$/, ""); // strip trailing slashes
|
|
192
|
+
const pkgPattern = base.endsWith("*") ? `${base}/package.json` : `${base}/*/package.json`;
|
|
193
|
+
|
|
194
|
+
const g = new Bun.Glob(pkgPattern);
|
|
195
|
+
for await (const match of g.scan(repoRoot)) {
|
|
196
|
+
const rel = match.replace(/\/package\.json$/, "");
|
|
197
|
+
if (!seen.has(rel)) {
|
|
198
|
+
seen.add(rel);
|
|
199
|
+
results.push(rel);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 2. turbo v2+: turbo.json top-level "packages" array
|
|
206
|
+
const turboPath = join(repoRoot, "turbo.json");
|
|
207
|
+
if (existsSync(turboPath)) {
|
|
208
|
+
try {
|
|
209
|
+
const turbo = JSON.parse(readFileSync(turboPath, "utf-8")) as Record<string, unknown>;
|
|
210
|
+
if (Array.isArray(turbo.packages)) {
|
|
211
|
+
await resolveGlobs(turbo.packages as string[]);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// malformed turbo.json — skip
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. root package.json "workspaces" (npm/yarn/bun/turbo v1)
|
|
219
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
220
|
+
if (existsSync(pkgPath)) {
|
|
221
|
+
try {
|
|
222
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
223
|
+
const ws = pkg.workspaces;
|
|
224
|
+
const patterns: string[] = Array.isArray(ws)
|
|
225
|
+
? (ws as string[])
|
|
226
|
+
: Array.isArray((ws as Record<string, unknown>)?.packages)
|
|
227
|
+
? ((ws as Record<string, unknown>).packages as string[])
|
|
228
|
+
: [];
|
|
229
|
+
if (patterns.length > 0) await resolveGlobs(patterns);
|
|
230
|
+
} catch {
|
|
231
|
+
// malformed package.json — skip
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 4. pnpm-workspace.yaml
|
|
236
|
+
const pnpmPath = join(repoRoot, "pnpm-workspace.yaml");
|
|
237
|
+
if (existsSync(pnpmPath)) {
|
|
238
|
+
try {
|
|
239
|
+
const raw = readFileSync(pnpmPath, "utf-8");
|
|
240
|
+
// Simple YAML parse for "packages:\n - 'packages/*'" without full YAML dep
|
|
241
|
+
const lines = raw.split("\n");
|
|
242
|
+
let inPackages = false;
|
|
243
|
+
const patterns: string[] = [];
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (/^packages\s*:/.test(line)) {
|
|
246
|
+
inPackages = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inPackages && /^\s+-\s+/.test(line)) {
|
|
250
|
+
patterns.push(line.replace(/^\s+-\s+['"]?/, "").replace(/['"]?\s*$/, ""));
|
|
251
|
+
} else if (inPackages && !/^\s/.test(line)) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (patterns.length > 0) await resolveGlobs(patterns);
|
|
256
|
+
} catch {
|
|
257
|
+
// malformed yaml — skip
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return results.sort();
|
|
262
|
+
}
|
|
263
|
+
|
|
169
264
|
/**
|
|
170
265
|
* Generate the claude CLAUDE.md for a specific package.
|
|
171
266
|
*
|