@nathapp/nax 0.65.2 → 0.65.3

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.
Files changed (2) hide show
  1. package/dist/nax.js +130 -26
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -32506,7 +32506,8 @@ function acceptanceTestFilename(language) {
32506
32506
  }
32507
32507
  }
32508
32508
  function resolveAcceptanceTestFile(language, testPathConfig) {
32509
- return testPathConfig ?? acceptanceTestFilename(language);
32509
+ const candidate = testPathConfig ?? acceptanceTestFilename(language);
32510
+ return sanitizeTestFileName(candidate, "acceptance.testPath");
32510
32511
  }
32511
32512
  function resolveAcceptanceFeatureTestPath(featureDir, testPathConfig, language) {
32512
32513
  return path3.join(featureDir, resolveAcceptanceTestFile(language, testPathConfig));
@@ -32560,7 +32561,21 @@ function suggestedTestFilename(language) {
32560
32561
  }
32561
32562
  }
32562
32563
  function resolveSuggestedTestFile(language, testPathConfig) {
32563
- return testPathConfig ?? suggestedTestFilename(language);
32564
+ const candidate = testPathConfig ?? suggestedTestFilename(language);
32565
+ return sanitizeTestFileName(candidate, "acceptance.suggestedTestPath");
32566
+ }
32567
+ function sanitizeTestFileName(value, fieldName) {
32568
+ const filename = value.trim();
32569
+ if (filename.length === 0) {
32570
+ throw new Error(`${fieldName} must be non-empty`);
32571
+ }
32572
+ if (filename.includes("/") || filename.includes("\\")) {
32573
+ throw new Error(`${fieldName} must be a filename, not a path: ${filename}`);
32574
+ }
32575
+ if (filename.includes("..")) {
32576
+ throw new Error(`${fieldName} cannot contain '..': ${filename}`);
32577
+ }
32578
+ return filename;
32564
32579
  }
32565
32580
  function resolveSuggestedPackageFeatureTestPath(packageDir, featureName, testPathConfig, language) {
32566
32581
  return path3.join(packageDir, ".nax", "features", featureName, resolveSuggestedTestFile(language, testPathConfig));
@@ -33915,6 +33930,20 @@ function killProcessGroup(pid, signal) {
33915
33930
 
33916
33931
  // src/quality/runner.ts
33917
33932
  var {spawn: spawn2 } = globalThis.Bun;
33933
+ function createDrainDeadline(deadlineMs) {
33934
+ let timeoutId;
33935
+ const promise2 = new Promise((resolve11) => {
33936
+ timeoutId = setTimeout(() => resolve11(""), deadlineMs);
33937
+ });
33938
+ return {
33939
+ promise: promise2,
33940
+ cancel: () => {
33941
+ if (timeoutId !== undefined) {
33942
+ clearTimeout(timeoutId);
33943
+ }
33944
+ }
33945
+ };
33946
+ }
33918
33947
  async function runQualityCommand(opts) {
33919
33948
  const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
33920
33949
  const startTime = Date.now();
@@ -33947,11 +33976,22 @@ async function runQualityCommand(opts) {
33947
33976
  }
33948
33977
  }, SIGKILL_GRACE_PERIOD_MS);
33949
33978
  }, timeoutMs);
33950
- const [exitCode, stdout, stderr] = await Promise.all([
33951
- proc.exited,
33952
- new Response(proc.stdout).text(),
33953
- new Response(proc.stderr).text()
33954
- ]);
33979
+ const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
33980
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
33981
+ const exitCode = await proc.exited;
33982
+ const [stdout, stderr] = timedOut ? await (async () => {
33983
+ const stdoutDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
33984
+ const stderrDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
33985
+ try {
33986
+ return await Promise.all([
33987
+ Promise.race([stdoutPromise, stdoutDrain.promise]),
33988
+ Promise.race([stderrPromise, stderrDrain.promise])
33989
+ ]);
33990
+ } finally {
33991
+ stdoutDrain.cancel();
33992
+ stderrDrain.cancel();
33993
+ }
33994
+ })() : await Promise.all([stdoutPromise, stderrPromise]);
33955
33995
  clearTimeout(killTimer);
33956
33996
  if (sigkillTimer !== undefined) {
33957
33997
  clearTimeout(sigkillTimer);
@@ -34003,7 +34043,7 @@ async function runQualityCommand(opts) {
34003
34043
  };
34004
34044
  }
34005
34045
  }
34006
- var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, _qualityRunnerDeps;
34046
+ var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, STREAM_DRAIN_TIMEOUT_MS = 2000, _qualityRunnerDeps;
34007
34047
  var init_runner = __esm(() => {
34008
34048
  init_logger2();
34009
34049
  _qualityRunnerDeps = {
@@ -34368,6 +34408,8 @@ function buildSmartTestCommand(testFiles, baseCommand) {
34368
34408
  if (testFiles.length === 0) {
34369
34409
  return baseCommand;
34370
34410
  }
34411
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
34412
+ const quotedTestFiles = testFiles.map(shellQuote);
34371
34413
  const parts = baseCommand.trim().split(/\s+/);
34372
34414
  let lastPathIndex = -1;
34373
34415
  for (let i = parts.length - 1;i >= 0; i--) {
@@ -34377,11 +34419,11 @@ function buildSmartTestCommand(testFiles, baseCommand) {
34377
34419
  }
34378
34420
  }
34379
34421
  if (lastPathIndex === -1) {
34380
- return `${baseCommand} ${testFiles.join(" ")}`;
34422
+ return `${baseCommand} ${quotedTestFiles.join(" ")}`;
34381
34423
  }
34382
34424
  const beforePath = parts.slice(0, lastPathIndex);
34383
34425
  const afterPath = parts.slice(lastPathIndex + 1);
34384
- const newParts = [...beforePath, ...testFiles, ...afterPath];
34426
+ const newParts = [...beforePath, ...quotedTestFiles, ...afterPath];
34385
34427
  return newParts.join(" ");
34386
34428
  }
34387
34429
  async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileRegex = [], naxIgnoreIndex, repoRoot) {
@@ -34468,7 +34510,8 @@ function coerceSmartRunner(val) {
34468
34510
  }
34469
34511
  function buildScopedCommand(testFiles, baseCommand, testScopedTemplate) {
34470
34512
  if (testScopedTemplate) {
34471
- return testScopedTemplate.replace("{{files}}", testFiles.join(" "));
34513
+ const quotedFiles = testFiles.map((file3) => `'${file3.replaceAll("'", "'\\''")}'`);
34514
+ return testScopedTemplate.replace("{{files}}", quotedFiles.join(" "));
34472
34515
  }
34473
34516
  return _scopedDeps.buildSmartTestCommand(testFiles, baseCommand);
34474
34517
  }
@@ -35262,6 +35305,23 @@ var init_operations = __esm(() => {
35262
35305
  init_auto_approve();
35263
35306
  });
35264
35307
 
35308
+ // src/utils/feature-name.ts
35309
+ function validateFeatureName(feature) {
35310
+ if (!feature || feature.trim() === "") {
35311
+ throw new Error("Feature name must be non-empty");
35312
+ }
35313
+ if (feature.includes("/") || feature.includes("\\")) {
35314
+ throw new Error(`Feature name must be a single path segment: ${feature}`);
35315
+ }
35316
+ if (feature.includes("..")) {
35317
+ throw new Error(`Feature name cannot contain '..': ${feature}`);
35318
+ }
35319
+ const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
35320
+ if (!validPattern.test(feature)) {
35321
+ throw new Error(`Feature name contains invalid characters: ${feature}`);
35322
+ }
35323
+ }
35324
+
35265
35325
  // src/cli/plan-helpers.ts
35266
35326
  import { createInterface } from "readline";
35267
35327
  function createCliInteractionBridge() {
@@ -39424,7 +39484,7 @@ class SessionManager {
39424
39484
  _pidRegistry;
39425
39485
  _watchdogControllerRegistry;
39426
39486
  _onStreamActivity;
39427
- _watchdogCancelledCalls = new Set;
39487
+ _watchdogCancelledCallsBySession = new Map;
39428
39488
  _agentStreamUnsubscribe;
39429
39489
  constructor(opts) {
39430
39490
  this._getAdapter = opts?.getAdapter ?? (() => {
@@ -39454,22 +39514,26 @@ class SessionManager {
39454
39514
  this._agentStreamUnsubscribe = opts.agentStreamEvents.onAgentStream((event) => {
39455
39515
  if (event.kind === "agent.call_ended") {
39456
39516
  this._watchdogControllerRegistry?.delete(event.callId);
39457
- this._watchdogCancelledCalls.delete(event.callId);
39458
39517
  }
39459
39518
  });
39460
39519
  }
39461
39520
  }
39462
- _buildOnActiveCall() {
39521
+ _buildOnActiveCall(sessionName) {
39463
39522
  const registry2 = this._watchdogControllerRegistry;
39464
39523
  if (!registry2)
39465
39524
  return;
39466
39525
  return (callId, cancel) => {
39467
39526
  registry2.set(callId, async () => {
39468
- this._watchdogCancelledCalls.add(callId);
39527
+ const cancelledCalls = this._watchdogCancelledCallsBySession.get(sessionName) ?? new Set;
39528
+ cancelledCalls.add(callId);
39529
+ this._watchdogCancelledCallsBySession.set(sessionName, cancelledCalls);
39469
39530
  await cancel();
39470
39531
  });
39471
39532
  };
39472
39533
  }
39534
+ _clearWatchdogCancelledCalls(sessionName) {
39535
+ this._watchdogCancelledCallsBySession.delete(sessionName);
39536
+ }
39473
39537
  _persistDescriptor(descriptor) {
39474
39538
  if (!descriptor.scratchDir)
39475
39539
  return;
@@ -39692,7 +39756,7 @@ class SessionManager {
39692
39756
  onSessionEstablished: opts.onSessionEstablished,
39693
39757
  signal: opts.signal,
39694
39758
  resume,
39695
- onActiveCall: this._buildOnActiveCall(),
39759
+ onActiveCall: this._buildOnActiveCall(name),
39696
39760
  onStreamActivity: this._onStreamActivity
39697
39761
  });
39698
39762
  this._liveHandles.set(name, handle);
@@ -39779,8 +39843,7 @@ class SessionManager {
39779
39843
  return { ...result, protocolIds: result.protocolIds ?? handle.protocolIds };
39780
39844
  } catch (err) {
39781
39845
  if (err instanceof SessionTurnError && err.cancelled) {
39782
- const wasWatchdog = this._watchdogCancelledCalls.size > 0;
39783
- this._watchdogCancelledCalls.clear();
39846
+ const wasWatchdog = (this._watchdogCancelledCallsBySession.get(handle.id)?.size ?? 0) > 0;
39784
39847
  if (wasWatchdog) {
39785
39848
  throw new SessionFailureError("idle watchdog cancelled session \u2014 no stream activity", {
39786
39849
  category: "availability",
@@ -39799,6 +39862,7 @@ class SessionManager {
39799
39862
  }
39800
39863
  throw err;
39801
39864
  } finally {
39865
+ this._clearWatchdogCancelledCalls(handle.id);
39802
39866
  this._busySessions.delete(handle.id);
39803
39867
  }
39804
39868
  }
@@ -40478,8 +40542,10 @@ import { basename as basename5, join as join28 } from "path";
40478
40542
  function createRuntime(config2, workdir, opts) {
40479
40543
  const runId = crypto.randomUUID();
40480
40544
  const controller = new AbortController;
40545
+ let parentAbortHandler;
40481
40546
  if (opts?.parentSignal) {
40482
- opts.parentSignal.addEventListener("abort", () => controller.abort(opts.parentSignal?.reason), { once: true });
40547
+ parentAbortHandler = () => controller.abort(opts.parentSignal?.reason);
40548
+ opts.parentSignal.addEventListener("abort", parentAbortHandler, { once: true });
40483
40549
  }
40484
40550
  const configLoader = createConfigLoader(config2);
40485
40551
  const dispatchEvents = new DispatchEventBus;
@@ -40580,6 +40646,9 @@ function createRuntime(config2, workdir, opts) {
40580
40646
  offReviewAudit();
40581
40647
  offAgentStreamLogging();
40582
40648
  offWatchdog();
40649
+ if (opts?.parentSignal && parentAbortHandler) {
40650
+ opts.parentSignal.removeEventListener("abort", parentAbortHandler);
40651
+ }
40583
40652
  const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
40584
40653
  for (const r of results) {
40585
40654
  if (r.status === "rejected") {
@@ -42299,6 +42368,11 @@ Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, co
42299
42368
  }
42300
42369
  let featureDir;
42301
42370
  if (feature) {
42371
+ try {
42372
+ validateFeatureName(feature);
42373
+ } catch (error48) {
42374
+ throw new NaxError(error48.message, "FEATURE_INVALID", { feature });
42375
+ }
42302
42376
  const featuresDir = join32(naxDir, "features");
42303
42377
  featureDir = join32(featuresDir, feature);
42304
42378
  if (!existsSync16(featureDir)) {
@@ -54459,6 +54533,20 @@ var init_command_argv = __esm(() => {
54459
54533
 
54460
54534
  // src/hooks/runner.ts
54461
54535
  import { join as join67 } from "path";
54536
+ function createDrainDeadline2(deadlineMs) {
54537
+ let timeoutId;
54538
+ const promise2 = new Promise((resolve16) => {
54539
+ timeoutId = setTimeout(() => resolve16(""), deadlineMs);
54540
+ });
54541
+ return {
54542
+ promise: promise2,
54543
+ cancel: () => {
54544
+ if (timeoutId !== undefined) {
54545
+ clearTimeout(timeoutId);
54546
+ }
54547
+ }
54548
+ };
54549
+ }
54462
54550
  async function loadHooksConfig(projectDir, globalDir) {
54463
54551
  let globalHooks = { hooks: {} };
54464
54552
  let projectHooks = { hooks: {} };
@@ -54561,15 +54649,30 @@ async function executeHook(hookDef, ctx, workdir) {
54561
54649
  stderr: "pipe",
54562
54650
  env: buildAllowedEnv({ env: env2 })
54563
54651
  });
54652
+ let timedOut = false;
54564
54653
  const timeoutId = setTimeout(() => {
54654
+ timedOut = true;
54565
54655
  killProcessGroup(proc.pid, "SIGTERM");
54566
54656
  }, timeout);
54657
+ const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
54658
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
54567
54659
  const exitCode = await proc.exited;
54568
54660
  clearTimeout(timeoutId);
54569
- const stdout = await new Response(proc.stdout).text();
54570
- const stderr = await new Response(proc.stderr).text();
54661
+ const [stdout, stderr] = timedOut ? await (async () => {
54662
+ const stdoutDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
54663
+ const stderrDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
54664
+ try {
54665
+ return await Promise.all([
54666
+ Promise.race([stdoutPromise, stdoutDrain.promise]),
54667
+ Promise.race([stderrPromise, stderrDrain.promise])
54668
+ ]);
54669
+ } finally {
54670
+ stdoutDrain.cancel();
54671
+ stderrDrain.cancel();
54672
+ }
54673
+ })() : await Promise.all([stdoutPromise, stderrPromise]);
54571
54674
  const output = (stdout + stderr).trim();
54572
- if (exitCode !== 0 && output === "") {
54675
+ if (timedOut) {
54573
54676
  return {
54574
54677
  success: false,
54575
54678
  output: `Hook timed out after ${timeout}ms`
@@ -54607,7 +54710,7 @@ async function fireHook(config2, event, ctx, workdir) {
54607
54710
  }
54608
54711
  }
54609
54712
  }
54610
- var DEFAULT_TIMEOUT = 5000;
54713
+ var DEFAULT_TIMEOUT = 5000, STREAM_DRAIN_TIMEOUT_MS2 = 2000;
54611
54714
  var init_runner5 = __esm(() => {
54612
54715
  init_env();
54613
54716
  init_logger2();
@@ -54625,7 +54728,7 @@ var package_default;
54625
54728
  var init_package = __esm(() => {
54626
54729
  package_default = {
54627
54730
  name: "@nathapp/nax",
54628
- version: "0.65.2",
54731
+ version: "0.65.3",
54629
54732
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
54630
54733
  type: "module",
54631
54734
  bin: {
@@ -54711,8 +54814,8 @@ var init_version = __esm(() => {
54711
54814
  NAX_VERSION = package_default.version;
54712
54815
  NAX_COMMIT = (() => {
54713
54816
  try {
54714
- if (/^[0-9a-f]{6,10}$/.test("99828ef9"))
54715
- return "99828ef9";
54817
+ if (/^[0-9a-f]{6,10}$/.test("9ff2ea7d"))
54818
+ return "9ff2ea7d";
54716
54819
  } catch {}
54717
54820
  try {
54718
54821
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -91422,6 +91525,7 @@ async function planCommand(workdir, config2, options) {
91422
91525
  if (!existsSync15(naxDir)) {
91423
91526
  throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
91424
91527
  }
91528
+ validateFeatureName(options.feature);
91425
91529
  const logger = getLogger();
91426
91530
  logger?.info("plan", "Reading spec", { from: options.from });
91427
91531
  const specContent = await _planDeps.readFile(options.from);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.65.2",
3
+ "version": "0.65.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {