@nathapp/nax 0.46.3 → 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.
@@ -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";
@@ -129,5 +129,185 @@ async function generateAll(options: GenerateOptions, config: NaxConfig): Promise
129
129
  return results;
130
130
  }
131
131
 
132
+ /** Result from generateForPackage */
133
+ export interface PackageGenerationResult {
134
+ packageDir: string;
135
+ outputFile: string;
136
+ content: string;
137
+ written: boolean;
138
+ error?: string;
139
+ }
140
+
141
+ /**
142
+ * Discover packages that have nax/context.md files.
143
+ *
144
+ * Scans up to 2 levels deep (one-level and two-level patterns).
145
+ *
146
+ * @param repoRoot - Absolute repo root to scan
147
+ * @returns Array of package directory paths (absolute)
148
+ */
149
+ export async function discoverPackages(repoRoot: string): Promise<string[]> {
150
+ const packages: string[] = [];
151
+ const seen = new Set<string>();
152
+
153
+ for (const pattern of ["*/nax/context.md", "*/*/nax/context.md"]) {
154
+ const glob = new Bun.Glob(pattern);
155
+ for await (const match of glob.scan(repoRoot)) {
156
+ // match is e.g. "packages/api/nax/context.md" — strip trailing /nax/context.md
157
+ const pkgRelative = match.replace(/\/nax\/context\.md$/, "");
158
+ const pkgAbsolute = join(repoRoot, pkgRelative);
159
+ if (!seen.has(pkgAbsolute)) {
160
+ seen.add(pkgAbsolute);
161
+ packages.push(pkgAbsolute);
162
+ }
163
+ }
164
+ }
165
+
166
+ return packages;
167
+ }
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
+
264
+ /**
265
+ * Generate the claude CLAUDE.md for a specific package.
266
+ *
267
+ * Reads `<packageDir>/nax/context.md` and writes `<packageDir>/CLAUDE.md`.
268
+ * Per-package CLAUDE.md contains only package-specific content — Claude Code's
269
+ * native directory hierarchy merges root CLAUDE.md + package CLAUDE.md at runtime.
270
+ */
271
+ export async function generateForPackage(
272
+ packageDir: string,
273
+ config: NaxConfig,
274
+ dryRun = false,
275
+ ): Promise<PackageGenerationResult> {
276
+ const contextPath = join(packageDir, "nax", "context.md");
277
+
278
+ if (!existsSync(contextPath)) {
279
+ return {
280
+ packageDir,
281
+ outputFile: "CLAUDE.md",
282
+ content: "",
283
+ written: false,
284
+ error: `context.md not found: ${contextPath}`,
285
+ };
286
+ }
287
+
288
+ try {
289
+ const options: GenerateOptions = {
290
+ contextPath,
291
+ outputDir: packageDir,
292
+ workdir: packageDir,
293
+ dryRun,
294
+ autoInject: true,
295
+ };
296
+
297
+ const result = await generateFor("claude", options, config);
298
+
299
+ return {
300
+ packageDir,
301
+ outputFile: result.outputFile,
302
+ content: result.content,
303
+ written: result.written,
304
+ error: result.error,
305
+ };
306
+ } catch (err) {
307
+ const error = err instanceof Error ? err.message : String(err);
308
+ return { packageDir, outputFile: "CLAUDE.md", content: "", written: false, error };
309
+ }
310
+ }
311
+
132
312
  export { generateFor, generateAll };
133
313
  export type { AgentType };
@@ -127,14 +127,33 @@ export async function buildStoryContext(prd: PRD, story: UserStory, _config: Nax
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Load package-level context.md content if it exists.
132
+ *
133
+ * Reads <packageWorkdir>/nax/context.md and returns its content, or null
134
+ * if the file does not exist.
135
+ *
136
+ * @internal
137
+ */
138
+ async function loadPackageContextMd(packageWorkdir: string): Promise<string | null> {
139
+ const contextPath = `${packageWorkdir}/nax/context.md`;
140
+ const file = Bun.file(contextPath);
141
+ if (!(await file.exists())) return null;
142
+ return file.text();
143
+ }
144
+
130
145
  /**
131
146
  * Build story context returning both markdown and element-level data.
132
147
  * Used by `nax prompts` CLI for accurate frontmatter token counts.
148
+ *
149
+ * When `packageWorkdir` is provided (absolute path of story.workdir),
150
+ * appends the package-level nax/context.md after the root context.
133
151
  */
134
152
  export async function buildStoryContextFull(
135
153
  prd: PRD,
136
154
  story: UserStory,
137
155
  config: NaxConfig,
156
+ packageWorkdir?: string,
138
157
  ): Promise<{ markdown: string; builtContext: BuiltContext } | undefined> {
139
158
  try {
140
159
  const storyContext: StoryContext = {
@@ -152,11 +171,23 @@ export async function buildStoryContextFull(
152
171
 
153
172
  const built = await buildContext(storyContext, budget);
154
173
 
155
- if (built.elements.length === 0) {
174
+ // MW-003: append package-level context.md if workdir is set
175
+ let packageSection = "";
176
+ if (packageWorkdir) {
177
+ const pkgContent = await loadPackageContextMd(packageWorkdir);
178
+ if (pkgContent) {
179
+ packageSection = `\n---\n\n${pkgContent.trim()}`;
180
+ }
181
+ }
182
+
183
+ if (built.elements.length === 0 && !packageSection) {
156
184
  return undefined;
157
185
  }
158
186
 
159
- return { markdown: formatContextAsMarkdown(built), builtContext: built };
187
+ const baseMarkdown = built.elements.length > 0 ? formatContextAsMarkdown(built) : "";
188
+ const markdown = packageSection ? `${baseMarkdown}${packageSection}` : baseMarkdown;
189
+
190
+ return { markdown, builtContext: built };
160
191
  } catch (error) {
161
192
  const logger = getSafeLogger();
162
193
  logger?.warn("context", "Context builder failed", {
@@ -20,6 +20,7 @@
20
20
  * ```
21
21
  */
22
22
 
23
+ import { join } from "node:path";
23
24
  import type { ContextElement } from "../../context/types";
24
25
  import { buildStoryContextFull } from "../../execution/helpers";
25
26
  import { getLogger } from "../../logger";
@@ -33,8 +34,11 @@ export const contextStage: PipelineStage = {
33
34
  async execute(ctx: PipelineContext): Promise<StageResult> {
34
35
  const logger = getLogger();
35
36
 
37
+ // MW-003: resolve package workdir for per-package context.md loading
38
+ const packageWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : undefined;
39
+
36
40
  // Build context from PRD with element-level tracking
37
- const result = await buildStoryContextFull(ctx.prd, ctx.story, ctx.config);
41
+ const result = await buildStoryContextFull(ctx.prd, ctx.story, ctx.config, packageWorkdir);
38
42
 
39
43
  // SOFT FAILURE: Empty context is acceptable — agent can work without PRD context
40
44
  // This happens when no relevant stories/context is found, which is normal
@@ -30,6 +30,8 @@
30
30
  * ```
31
31
  */
32
32
 
33
+ import { existsSync } from "node:fs";
34
+ import { join } from "node:path";
33
35
  import { getAgent, validateAgentForTier } from "../../agents";
34
36
  import { resolveModel } from "../../config";
35
37
  import { resolvePermissions } from "../../config/permissions";
@@ -40,6 +42,22 @@ import { runThreeSessionTdd } from "../../tdd";
40
42
  import { autoCommitIfDirty, detectMergeConflict } from "../../utils/git";
41
43
  import type { PipelineContext, PipelineStage, StageResult } from "../types";
42
44
 
45
+ /**
46
+ * Resolve the effective working directory for a story.
47
+ * When story.workdir is set, returns join(repoRoot, story.workdir).
48
+ * Otherwise returns the repo root unchanged.
49
+ *
50
+ * MW-001 runtime check: throws if the resolved workdir does not exist on disk.
51
+ */
52
+ export function resolveStoryWorkdir(repoRoot: string, storyWorkdir?: string): string {
53
+ if (!storyWorkdir) return repoRoot;
54
+ const resolved = join(repoRoot, storyWorkdir);
55
+ if (!existsSync(resolved)) {
56
+ throw new Error(`[execution] story.workdir "${storyWorkdir}" does not exist at "${resolved}"`);
57
+ }
58
+ return resolved;
59
+ }
60
+
43
61
  /**
44
62
  * Detect if agent output contains ambiguity signals
45
63
  * Checks for keywords that indicate the agent is unsure about the implementation
@@ -128,11 +146,13 @@ export const executionStage: PipelineStage = {
128
146
  lite: isLiteMode,
129
147
  });
130
148
 
149
+ const effectiveWorkdir = _executionDeps.resolveStoryWorkdir(ctx.workdir, ctx.story.workdir);
150
+
131
151
  const tddResult = await runThreeSessionTdd({
132
152
  agent,
133
153
  story: ctx.story,
134
154
  config: ctx.config,
135
- workdir: ctx.workdir,
155
+ workdir: effectiveWorkdir,
136
156
  modelTier: ctx.routing.modelTier,
137
157
  featureName: ctx.prd.feature,
138
158
  contextMarkdown: ctx.contextMarkdown,
@@ -212,9 +232,11 @@ export const executionStage: PipelineStage = {
212
232
  });
213
233
  }
214
234
 
235
+ const storyWorkdir = _executionDeps.resolveStoryWorkdir(ctx.workdir, ctx.story.workdir);
236
+
215
237
  const result = await agent.run({
216
238
  prompt: ctx.prompt,
217
- workdir: ctx.workdir,
239
+ workdir: storyWorkdir,
218
240
  modelTier: ctx.routing.modelTier,
219
241
  modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
220
242
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
@@ -258,7 +280,7 @@ export const executionStage: PipelineStage = {
258
280
  ctx.agentResult = result;
259
281
 
260
282
  // BUG-058: Auto-commit if agent left uncommitted changes (single-session/test-after)
261
- await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
283
+ await autoCommitIfDirty(storyWorkdir, "execution", "single-session", ctx.story.id);
262
284
 
263
285
  // merge-conflict trigger: detect CONFLICT markers in agent output
264
286
  const combinedOutput = (result.output ?? "") + (result.stderr ?? "");
@@ -327,4 +349,5 @@ export const _executionDeps = {
327
349
  checkMergeConflict,
328
350
  isAmbiguousOutput,
329
351
  checkStoryAmbiguity,
352
+ resolveStoryWorkdir,
330
353
  };
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  // RE-ARCH: rewrite
14
+ import { join } from "node:path";
14
15
  import { checkSecurityReview, isTriggerEnabled } from "../../interaction/triggers";
15
16
  import { getLogger } from "../../logger";
16
17
  import { reviewOrchestrator } from "../../review/orchestrator";
@@ -25,12 +26,16 @@ export const reviewStage: PipelineStage = {
25
26
 
26
27
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
27
28
 
29
+ // MW-010: scope review to package directory when story.workdir is set
30
+ const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
31
+
28
32
  const result = await reviewOrchestrator.review(
29
33
  ctx.config.review,
30
- ctx.workdir,
34
+ effectiveWorkdir,
31
35
  ctx.config.execution,
32
36
  ctx.plugins,
33
37
  ctx.storyGitRef,
38
+ ctx.story.workdir, // MW-010: scope changed-file checks to package
34
39
  );
35
40
 
36
41
  ctx.reviewResult = result.builtIn;
@@ -9,6 +9,8 @@
9
9
  * - `escalate`: Tests failed (retry with escalation)
10
10
  */
11
11
 
12
+ import { join } from "node:path";
13
+ import { loadConfigForWorkdir } from "../../config/loader";
12
14
  import type { SmartTestRunnerConfig } from "../../config/types";
13
15
  import { getLogger } from "../../logger";
14
16
  import { logTestOutput } from "../../utils/log-test-output";
@@ -53,15 +55,20 @@ export const verifyStage: PipelineStage = {
53
55
  async execute(ctx: PipelineContext): Promise<StageResult> {
54
56
  const logger = getLogger();
55
57
 
58
+ // MW-009: resolve effective config for per-package test commands
59
+ const effectiveConfig = ctx.story.workdir
60
+ ? await _verifyDeps.loadConfigForWorkdir(join(ctx.workdir, "nax", "config.json"), ctx.story.workdir)
61
+ : ctx.config;
62
+
56
63
  // Skip verification if tests are not required
57
- if (!ctx.config.quality.requireTests) {
64
+ if (!effectiveConfig.quality.requireTests) {
58
65
  logger.debug("verify", "Skipping verification (quality.requireTests = false)", { storyId: ctx.story.id });
59
66
  return { action: "continue" };
60
67
  }
61
68
 
62
69
  // Skip verification if no test command is configured
63
- const testCommand = ctx.config.review?.commands?.test ?? ctx.config.quality.commands.test;
64
- const testScopedTemplate = ctx.config.quality.commands.testScoped;
70
+ const testCommand = effectiveConfig.review?.commands?.test ?? effectiveConfig.quality.commands.test;
71
+ const testScopedTemplate = effectiveConfig.quality.commands.testScoped;
65
72
  if (!testCommand) {
66
73
  logger.debug("verify", "Skipping verification (no test command configured)", { storyId: ctx.story.id });
67
74
  return { action: "continue" };
@@ -69,6 +76,9 @@ export const verifyStage: PipelineStage = {
69
76
 
70
77
  logger.info("verify", "Running verification", { storyId: ctx.story.id });
71
78
 
79
+ // MW-006: resolve effective workdir for test execution
80
+ const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
81
+
72
82
  // Determine effective test command (smart runner or full suite)
73
83
  let effectiveCommand = testCommand;
74
84
  let isFullSuite = true;
@@ -76,10 +86,15 @@ export const verifyStage: PipelineStage = {
76
86
  const regressionMode = ctx.config.execution.regressionGate?.mode ?? "deferred";
77
87
 
78
88
  if (smartRunnerConfig.enabled) {
79
- const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(ctx.workdir, ctx.storyGitRef);
89
+ // MW-006: pass packagePrefix so git diff is scoped to the package in monorepos
90
+ const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(
91
+ effectiveWorkdir,
92
+ ctx.storyGitRef,
93
+ ctx.story.workdir,
94
+ );
80
95
 
81
96
  // Pass 1: path convention mapping
82
- const pass1Files = await _smartRunnerDeps.mapSourceToTests(sourceFiles, ctx.workdir);
97
+ const pass1Files = await _smartRunnerDeps.mapSourceToTests(sourceFiles, effectiveWorkdir);
83
98
  if (pass1Files.length > 0) {
84
99
  logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
85
100
  storyId: ctx.story.id,
@@ -90,7 +105,7 @@ export const verifyStage: PipelineStage = {
90
105
  // Pass 2: import-grep fallback
91
106
  const pass2Files = await _smartRunnerDeps.importGrepFallback(
92
107
  sourceFiles,
93
- ctx.workdir,
108
+ effectiveWorkdir,
94
109
  smartRunnerConfig.testFilePatterns,
95
110
  );
96
111
  if (pass2Files.length > 0) {
@@ -126,7 +141,7 @@ export const verifyStage: PipelineStage = {
126
141
 
127
142
  // Use unified regression gate (includes 2s wait for agent process cleanup)
128
143
  const result = await _verifyDeps.regression({
129
- workdir: ctx.workdir,
144
+ workdir: effectiveWorkdir,
130
145
  command: effectiveCommand,
131
146
  timeoutSeconds: ctx.config.execution.verificationTimeoutSeconds,
132
147
  acceptOnTimeout: ctx.config.execution.regressionGate?.acceptOnTimeout ?? true,
@@ -199,4 +214,5 @@ export const verifyStage: PipelineStage = {
199
214
  */
200
215
  export const _verifyDeps = {
201
216
  regression,
217
+ loadConfigForWorkdir,
202
218
  };
package/src/prd/schema.ts CHANGED
@@ -155,6 +155,22 @@ function validateStory(raw: unknown, index: number, allIds: Set<string>): UserSt
155
155
  const rawTags = s.tags;
156
156
  const tags: string[] = Array.isArray(rawTags) ? (rawTags as string[]) : [];
157
157
 
158
+ // workdir — optional, relative path only, no traversal
159
+ const rawWorkdir = s.workdir;
160
+ let workdir: string | undefined;
161
+ if (rawWorkdir !== undefined && rawWorkdir !== null) {
162
+ if (typeof rawWorkdir !== "string") {
163
+ throw new Error(`[schema] story[${index}].workdir must be a string`);
164
+ }
165
+ if (rawWorkdir.startsWith("/")) {
166
+ throw new Error(`[schema] story[${index}].workdir must be relative (no leading /): "${rawWorkdir}"`);
167
+ }
168
+ if (rawWorkdir.includes("..")) {
169
+ throw new Error(`[schema] story[${index}].workdir must not contain '..': "${rawWorkdir}"`);
170
+ }
171
+ workdir = rawWorkdir;
172
+ }
173
+
158
174
  return {
159
175
  id,
160
176
  title: title.trim(),
@@ -172,6 +188,7 @@ function validateStory(raw: unknown, index: number, allIds: Set<string>): UserSt
172
188
  testStrategy,
173
189
  reasoning: "validated from LLM output",
174
190
  },
191
+ ...(workdir !== undefined ? { workdir } : {}),
175
192
  };
176
193
  }
177
194
 
package/src/prd/types.ts CHANGED
@@ -127,6 +127,12 @@ export interface UserStory {
127
127
  failureCategory?: FailureCategory;
128
128
  /** Worktree path for parallel execution (set when --parallel is used) */
129
129
  worktreePath?: string;
130
+ /**
131
+ * Working directory for this story, relative to repo root.
132
+ * Overrides the global workdir for pipeline execution.
133
+ * @example "packages/api"
134
+ */
135
+ workdir?: string;
130
136
  }
131
137
 
132
138
  // ============================================================================
@@ -37,127 +37,65 @@ export async function checkDependenciesInstalled(workdir: string): Promise<Check
37
37
  };
38
38
  }
39
39
 
40
- /** Check if test command works. Skips silently if command is null/false. */
40
+ /** Check if test command is configured. Downgraded to warning since the verify stage will catch actual failures. */
41
41
  export async function checkTestCommand(config: NaxConfig): Promise<Check> {
42
42
  const testCommand = config.execution.testCommand || (config.quality?.commands?.test as string | undefined);
43
43
 
44
44
  if (!testCommand || testCommand === null) {
45
45
  return {
46
46
  name: "test-command-works",
47
- tier: "blocker",
47
+ tier: "warning",
48
48
  passed: true,
49
- message: "Test command not configured (skipped)",
49
+ message: "Test command not configured (will use default: bun test)",
50
50
  };
51
51
  }
52
52
 
53
- const parts = testCommand.split(" ");
54
- const [cmd, ...args] = parts;
55
-
56
- try {
57
- const proc = Bun.spawn([cmd, ...args, "--help"], {
58
- stdout: "pipe",
59
- stderr: "pipe",
60
- });
61
-
62
- const exitCode = await proc.exited;
63
- const passed = exitCode === 0;
64
-
65
- return {
66
- name: "test-command-works",
67
- tier: "blocker",
68
- passed,
69
- message: passed ? "Test command is available" : `Test command failed: ${testCommand}`,
70
- };
71
- } catch {
72
- return {
73
- name: "test-command-works",
74
- tier: "blocker",
75
- passed: false,
76
- message: `Test command failed: ${testCommand}`,
77
- };
78
- }
53
+ return {
54
+ name: "test-command-works",
55
+ tier: "warning",
56
+ passed: true,
57
+ message: `Test command configured: ${testCommand}`,
58
+ };
79
59
  }
80
60
 
81
- /** Check if lint command works. Skips silently if command is null/false. */
61
+ /** Check if lint command is configured. Downgraded to warning since the verify stage will catch actual failures. */
82
62
  export async function checkLintCommand(config: NaxConfig): Promise<Check> {
83
63
  const lintCommand = config.execution.lintCommand;
84
64
 
85
65
  if (!lintCommand || lintCommand === null) {
86
66
  return {
87
67
  name: "lint-command-works",
88
- tier: "blocker",
68
+ tier: "warning",
89
69
  passed: true,
90
70
  message: "Lint command not configured (skipped)",
91
71
  };
92
72
  }
93
73
 
94
- const parts = lintCommand.split(" ");
95
- const [cmd, ...args] = parts;
96
-
97
- try {
98
- const proc = Bun.spawn([cmd, ...args, "--help"], {
99
- stdout: "pipe",
100
- stderr: "pipe",
101
- });
102
-
103
- const exitCode = await proc.exited;
104
- const passed = exitCode === 0;
105
-
106
- return {
107
- name: "lint-command-works",
108
- tier: "blocker",
109
- passed,
110
- message: passed ? "Lint command is available" : `Lint command failed: ${lintCommand}`,
111
- };
112
- } catch {
113
- return {
114
- name: "lint-command-works",
115
- tier: "blocker",
116
- passed: false,
117
- message: `Lint command failed: ${lintCommand}`,
118
- };
119
- }
74
+ return {
75
+ name: "lint-command-works",
76
+ tier: "warning",
77
+ passed: true,
78
+ message: `Lint command configured: ${lintCommand}`,
79
+ };
120
80
  }
121
81
 
122
- /** Check if typecheck command works. Skips silently if command is null/false. */
82
+ /** Check if typecheck command is configured. Downgraded to warning since the verify stage will catch actual failures. */
123
83
  export async function checkTypecheckCommand(config: NaxConfig): Promise<Check> {
124
84
  const typecheckCommand = config.execution.typecheckCommand;
125
85
 
126
86
  if (!typecheckCommand || typecheckCommand === null) {
127
87
  return {
128
88
  name: "typecheck-command-works",
129
- tier: "blocker",
89
+ tier: "warning",
130
90
  passed: true,
131
91
  message: "Typecheck command not configured (skipped)",
132
92
  };
133
93
  }
134
94
 
135
- const parts = typecheckCommand.split(" ");
136
- const [cmd, ...args] = parts;
137
-
138
- try {
139
- const proc = Bun.spawn([cmd, ...args, "--help"], {
140
- stdout: "pipe",
141
- stderr: "pipe",
142
- });
143
-
144
- const exitCode = await proc.exited;
145
- const passed = exitCode === 0;
146
-
147
- return {
148
- name: "typecheck-command-works",
149
- tier: "blocker",
150
- passed,
151
- message: passed
152
- ? `Typecheck command is available: ${typecheckCommand}`
153
- : `Typecheck command failed: ${typecheckCommand}`,
154
- };
155
- } catch {
156
- return {
157
- name: "typecheck-command-works",
158
- tier: "blocker",
159
- passed: false,
160
- message: `Typecheck command failed: ${typecheckCommand}`,
161
- };
162
- }
95
+ return {
96
+ name: "typecheck-command-works",
97
+ tier: "warning",
98
+ passed: true,
99
+ message: `Typecheck command configured: ${typecheckCommand}`,
100
+ };
163
101
  }