@notis_ai/cli 0.2.15 → 0.2.17

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.
@@ -4564,7 +4564,7 @@ var init_skill_sync = __esm({
4564
4564
  });
4565
4565
 
4566
4566
  // src/cli.js
4567
- import { readFileSync as readFileSync20 } from "node:fs";
4567
+ import { readFileSync as readFileSync21 } from "node:fs";
4568
4568
  import { dirname as dirname18, join as join19 } from "node:path";
4569
4569
  import { fileURLToPath as fileURLToPath10 } from "node:url";
4570
4570
 
@@ -4587,7 +4587,7 @@ var {
4587
4587
 
4588
4588
  // src/command-specs/apps.js
4589
4589
  import { randomUUID as randomUUID9 } from "node:crypto";
4590
- import { mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, rmSync as rmSync9 } from "node:fs";
4590
+ import { mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, readdirSync as readdirSync6, rmSync as rmSync10 } from "node:fs";
4591
4591
  import { tmpdir as tmpdir2 } from "node:os";
4592
4592
  import { basename as basename3, isAbsolute as isAbsolute2, join as join12, relative as relative3, resolve as resolve7, sep } from "node:path";
4593
4593
 
@@ -5849,6 +5849,9 @@ function validateConfiguredRoutes(routes) {
5849
5849
  if (route.default) {
5850
5850
  defaultCount += 1;
5851
5851
  }
5852
+ if (route.resourceDeepLinks !== void 0 && typeof route.resourceDeepLinks !== "boolean") {
5853
+ throw usageError(`Route "${route.slug}" resourceDeepLinks must be a boolean.`);
5854
+ }
5852
5855
  }
5853
5856
  if (defaultCount !== 1) {
5854
5857
  throw usageError(`Expected exactly one default route, received ${defaultCount}.`);
@@ -5940,6 +5943,7 @@ function generateManifest(appConfig, projectDir) {
5940
5943
  icon: route.icon || null,
5941
5944
  parentSlug: route.parentSlug || null,
5942
5945
  default: route.default || false,
5946
+ resourceDeepLinks: route.resourceDeepLinks === true,
5943
5947
  export_name: route.exportName || route.export_name || exportNameFromPath(route.path),
5944
5948
  collection: route.collection || null
5945
5949
  };
@@ -6031,6 +6035,14 @@ function generateManifest(appConfig, projectDir) {
6031
6035
  onboarding: appConfig.onboarding || null
6032
6036
  };
6033
6037
  }
6038
+ function appRowFieldsFromManifest(manifest) {
6039
+ const app = manifest?.app && typeof manifest.app === "object" ? manifest.app : {};
6040
+ const displayName = typeof app.title === "string" && app.title.trim() ? app.title.trim() : typeof app.name === "string" && app.name.trim() ? app.name.trim() : null;
6041
+ return {
6042
+ ...displayName ? { name: displayName } : {},
6043
+ accent: app.accent ?? null
6044
+ };
6045
+ }
6034
6046
  function normalizeAppToolBindings(bindings) {
6035
6047
  return (Array.isArray(bindings) ? bindings : []).map((binding) => {
6036
6048
  const name = typeof binding?.name === "string" ? binding.name.trim() : "";
@@ -7642,7 +7654,7 @@ async function updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, man
7642
7654
  },
7643
7655
  body: JSON.stringify({
7644
7656
  manifest: { ...manifest, version: newVersion },
7645
- accent: manifest?.app?.accent ?? null,
7657
+ ...appRowFieldsFromManifest(manifest),
7646
7658
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
7647
7659
  })
7648
7660
  });
@@ -7709,9 +7721,20 @@ async function directDeploy(projectDir, appId) {
7709
7721
 
7710
7722
  // src/runtime/app-dev-server.js
7711
7723
  import { createServer } from "node:http";
7712
- import { spawn as spawn2 } from "node:child_process";
7713
- import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, watch as fsWatch } from "node:fs";
7724
+ import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
7725
+ import {
7726
+ appendFileSync,
7727
+ existsSync as existsSync7,
7728
+ mkdirSync as mkdirSync5,
7729
+ readFileSync as readFileSync7,
7730
+ renameSync as renameSync4,
7731
+ rmSync as rmSync5,
7732
+ statSync as statSync3,
7733
+ watch as fsWatch
7734
+ } from "node:fs";
7735
+ import { freemem, loadavg, totalmem } from "node:os";
7714
7736
  import { dirname as dirname6, join as join7, resolve as resolve5 } from "node:path";
7737
+ import { setTimeout as delay } from "node:timers/promises";
7715
7738
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7716
7739
 
7717
7740
  // src/runtime/app-dev-sessions.js
@@ -8783,6 +8806,100 @@ function removeAppDevSession(sessionId, filePath) {
8783
8806
  return result;
8784
8807
  }
8785
8808
 
8809
+ // src/runtime/app-dev-process-identity.js
8810
+ import { execFileSync } from "node:child_process";
8811
+ import { readlinkSync, realpathSync as realpathSync2 } from "node:fs";
8812
+ var NOTIS_APP_BUILD_COMMAND_FINGERPRINT = "npm:run-build:watch:v1";
8813
+ var NOTIS_APPS_DEV_HOST_COMMAND_FINGERPRINT = "notis:apps-dev:v1";
8814
+ function normalizePath(value) {
8815
+ if (typeof value !== "string" || !value.trim()) return null;
8816
+ try {
8817
+ return realpathSync2(value.trim());
8818
+ } catch {
8819
+ return null;
8820
+ }
8821
+ }
8822
+ function isExpectedNotisBuildCommand(command) {
8823
+ if (typeof command !== "string") return false;
8824
+ return /(?:^|[/\s])npm(?:\s|$)/.test(command) && /\brun\s+build\b/.test(command) && /(?:^|\s)--watch(?:\s|$)/.test(command);
8825
+ }
8826
+ function isExpectedNotisAppsDevHostCommand(command) {
8827
+ return typeof command === "string" && /(?:notis(?:\.js)?|@notis_ai[/\\]cli)/.test(command) && /\bapps\s+dev\b/.test(command);
8828
+ }
8829
+ function readDarwinProcessCwd(pid, execute) {
8830
+ const output = execute("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
8831
+ encoding: "utf8",
8832
+ stdio: ["ignore", "pipe", "ignore"]
8833
+ });
8834
+ const line = output.split("\n").find((entry) => entry.startsWith("n"));
8835
+ return line ? line.slice(1) : null;
8836
+ }
8837
+ function inspectAppDevWatcherProcess(pid, {
8838
+ platform = process.platform,
8839
+ execute = execFileSync,
8840
+ readLink = readlinkSync
8841
+ } = {}) {
8842
+ if (!Number.isSafeInteger(pid) || pid <= 0 || platform === "win32") return null;
8843
+ try {
8844
+ const ps = (field) => execute("ps", ["-o", `${field}=`, "-p", String(pid)], {
8845
+ encoding: "utf8",
8846
+ stdio: ["ignore", "pipe", "ignore"]
8847
+ }).trim();
8848
+ const processGroupPid = Number.parseInt(ps("pgid"), 10);
8849
+ const startIdentity = ps("lstart").replace(/\s+/g, " ").trim();
8850
+ const command = ps("command");
8851
+ const cwd = platform === "linux" ? readLink(`/proc/${pid}/cwd`) : readDarwinProcessCwd(pid, execute);
8852
+ const projectDir = normalizePath(cwd);
8853
+ if (!Number.isSafeInteger(processGroupPid) || processGroupPid <= 0) return null;
8854
+ if (!startIdentity || !command || !projectDir) return null;
8855
+ return { pid, processGroupPid, startIdentity, command, projectDir };
8856
+ } catch {
8857
+ return null;
8858
+ }
8859
+ }
8860
+ function captureDesktopWatcherOwnership({
8861
+ pid,
8862
+ projectDir,
8863
+ desktopOwnerId,
8864
+ desktopOwnerScope,
8865
+ inspect: inspect2 = inspectAppDevWatcherProcess
8866
+ } = {}) {
8867
+ const owner = typeof desktopOwnerId === "string" ? desktopOwnerId.trim() : "";
8868
+ const ownerScope = typeof desktopOwnerScope === "string" ? desktopOwnerScope.trim() : "";
8869
+ const expectedProjectDir = normalizePath(projectDir);
8870
+ if (!owner || !ownerScope || !expectedProjectDir) return null;
8871
+ const identity = inspect2(pid);
8872
+ if (!identity || identity.processGroupPid !== pid || identity.projectDir !== expectedProjectDir || !isExpectedNotisBuildCommand(identity.command)) {
8873
+ return null;
8874
+ }
8875
+ return {
8876
+ desktopOwnerId: owner,
8877
+ desktopOwnerScope: ownerScope,
8878
+ watcherProcessGroupPid: identity.processGroupPid,
8879
+ watcherStartIdentity: identity.startIdentity,
8880
+ watcherProjectDir: identity.projectDir,
8881
+ watcherCommandFingerprint: NOTIS_APP_BUILD_COMMAND_FINGERPRINT
8882
+ };
8883
+ }
8884
+ function captureDesktopHostOwnership({
8885
+ pid = process.pid,
8886
+ desktopOwnerId,
8887
+ desktopOwnerScope,
8888
+ inspect: inspect2 = inspectAppDevWatcherProcess
8889
+ } = {}) {
8890
+ const owner = typeof desktopOwnerId === "string" ? desktopOwnerId.trim() : "";
8891
+ const ownerScope = typeof desktopOwnerScope === "string" ? desktopOwnerScope.trim() : "";
8892
+ if (!owner || !ownerScope) return null;
8893
+ const identity = inspect2(pid);
8894
+ if (!identity || !isExpectedNotisAppsDevHostCommand(identity.command)) return null;
8895
+ return {
8896
+ desktopOwnerId: owner,
8897
+ desktopOwnerScope: ownerScope,
8898
+ desktopHostStartIdentity: identity.startIdentity,
8899
+ desktopHostCommandFingerprint: NOTIS_APPS_DEV_HOST_COMMAND_FINGERPRINT
8900
+ };
8901
+ }
8902
+
8786
8903
  // src/runtime/app-dev-server.js
8787
8904
  var CONTENT_TYPES = {
8788
8905
  ".js": "application/javascript; charset=utf-8",
@@ -8795,6 +8912,100 @@ var CLI_ROOT2 = resolve5(RUNTIME_DIR, "../..");
8795
8912
  var REPO_ROOT = resolve5(RUNTIME_DIR, "../../../..");
8796
8913
  var HARNESS_TEMPLATE_PATH = join7(CLI_ROOT2, "template", ".harness", "index.html.tmpl");
8797
8914
  var FALLBACK_REACT_VERSION = "19.0.0";
8915
+ var BUILD_PROCESS_STOP_GRACE_MS = 1e3;
8916
+ var DEV_DIAGNOSTIC_INTERVAL_MS = 3e4;
8917
+ var DEV_DIAGNOSTIC_MAX_BYTES = 20 * 1024 * 1024;
8918
+ function processGroupIsRunning(pid, signalProcess = process.kill) {
8919
+ try {
8920
+ signalProcess(-pid, 0);
8921
+ return true;
8922
+ } catch (error) {
8923
+ return error?.code !== "ESRCH";
8924
+ }
8925
+ }
8926
+ function readProcessGroupRssBytes(groupPids) {
8927
+ if (process.platform === "win32" || groupPids.size === 0) return /* @__PURE__ */ new Map();
8928
+ try {
8929
+ const output = execFileSync2("ps", ["-axo", "pgid=,rss="], {
8930
+ encoding: "utf8",
8931
+ maxBuffer: 1024 * 1024,
8932
+ stdio: ["ignore", "pipe", "ignore"]
8933
+ });
8934
+ const rssByGroup = /* @__PURE__ */ new Map();
8935
+ for (const line of output.split("\n")) {
8936
+ const [rawGroupPid, rawRssKiB] = line.trim().split(/\s+/, 2);
8937
+ const groupPid = Number.parseInt(rawGroupPid, 10);
8938
+ if (!groupPids.has(groupPid)) continue;
8939
+ const rssKiB = Number.parseInt(rawRssKiB, 10);
8940
+ if (!Number.isFinite(rssKiB)) continue;
8941
+ rssByGroup.set(groupPid, (rssByGroup.get(groupPid) || 0) + rssKiB * 1024);
8942
+ }
8943
+ return rssByGroup;
8944
+ } catch {
8945
+ return /* @__PURE__ */ new Map();
8946
+ }
8947
+ }
8948
+ async function terminateBuildProcessTree(child, {
8949
+ platform = process.platform,
8950
+ signalProcess = process.kill,
8951
+ spawnProcess = spawn2,
8952
+ graceMs = BUILD_PROCESS_STOP_GRACE_MS
8953
+ } = {}) {
8954
+ const pid = child?.pid;
8955
+ if (!Number.isSafeInteger(pid) || pid <= 0) return;
8956
+ if (platform === "win32") {
8957
+ await new Promise((resolvePromise) => {
8958
+ let settled = false;
8959
+ const finish = () => {
8960
+ if (settled) return;
8961
+ settled = true;
8962
+ resolvePromise();
8963
+ };
8964
+ try {
8965
+ const killer = spawnProcess("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
8966
+ stdio: "ignore",
8967
+ windowsHide: true
8968
+ });
8969
+ killer.once("error", () => {
8970
+ try {
8971
+ child.kill("SIGTERM");
8972
+ } catch {
8973
+ }
8974
+ finish();
8975
+ });
8976
+ killer.once("exit", finish);
8977
+ setTimeout(finish, graceMs).unref?.();
8978
+ } catch {
8979
+ try {
8980
+ child.kill("SIGTERM");
8981
+ } catch {
8982
+ }
8983
+ finish();
8984
+ }
8985
+ });
8986
+ return;
8987
+ }
8988
+ try {
8989
+ signalProcess(-pid, "SIGTERM");
8990
+ } catch (error) {
8991
+ if (error?.code === "ESRCH") return;
8992
+ try {
8993
+ child.kill("SIGTERM");
8994
+ } catch {
8995
+ return;
8996
+ }
8997
+ }
8998
+ const deadline = Date.now() + graceMs;
8999
+ while (processGroupIsRunning(pid, signalProcess) && Date.now() < deadline) {
9000
+ await delay(25);
9001
+ }
9002
+ if (!processGroupIsRunning(pid, signalProcess)) return;
9003
+ try {
9004
+ signalProcess(-pid, "SIGKILL");
9005
+ } catch (error) {
9006
+ if (error?.code !== "ESRCH") throw error;
9007
+ }
9008
+ }
8798
9009
  function extFor(pathname) {
8799
9010
  const idx = pathname.lastIndexOf(".");
8800
9011
  return idx === -1 ? "" : pathname.slice(idx);
@@ -8966,10 +9177,11 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario =
8966
9177
  icon: route.icon || null,
8967
9178
  parentSlug: route.parentSlug || null,
8968
9179
  default: Boolean(route.default),
9180
+ resourceDeepLinks: route.resourceDeepLinks === true,
8969
9181
  collection: route.collection || null
8970
9182
  },
8971
9183
  databases,
8972
- context: { collectionItem: null, screenshotScenario: scenario },
9184
+ context: { collectionItem: null, resourceId: null, screenshotScenario: scenario },
8973
9185
  tools
8974
9186
  };
8975
9187
  }
@@ -9011,6 +9223,10 @@ async function startAppDevServer({
9011
9223
  watch = true,
9012
9224
  sessionsFilePath,
9013
9225
  harness = {},
9226
+ diagnosticsFile = process.env.NOTIS_DEV_DIAGNOSTICS_FILE || null,
9227
+ desktopOwnerId = process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_ID || null,
9228
+ desktopOwnerScope = process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_SCOPE || null,
9229
+ terminateBuildProcess = terminateBuildProcessTree,
9014
9230
  log = (msg) => process.stdout.write(`${msg}
9015
9231
  `),
9016
9232
  logError = (msg) => process.stderr.write(`${msg}
@@ -9020,6 +9236,9 @@ async function startAppDevServer({
9020
9236
  throw new Error("startAppDevServer requires at least one app.");
9021
9237
  }
9022
9238
  const appState = /* @__PURE__ */ new Map();
9239
+ let diagnosticsTimer = null;
9240
+ let diagnosticWriteFailed = false;
9241
+ let serverClosing = false;
9023
9242
  const hostSseClients = /* @__PURE__ */ new Set();
9024
9243
  const createAppState = (app) => {
9025
9244
  let resolveBundleReady;
@@ -9044,16 +9263,65 @@ async function startAppDevServer({
9044
9263
  prepareTimer: null,
9045
9264
  reloadTimer: null,
9046
9265
  buildProcess: null,
9266
+ watcherOwnership: null,
9047
9267
  lastMtimeMs: 0,
9048
9268
  watchPollTimer: null,
9049
9269
  bundleReady: false,
9050
9270
  bundleReadyPromise,
9051
- resolveBundleReady
9271
+ resolveBundleReady,
9272
+ buildProcessStopPromise: null
9052
9273
  };
9053
9274
  };
9054
9275
  for (const app of apps) {
9055
9276
  appState.set(app.slug, createAppState(app));
9056
9277
  }
9278
+ function writeDevDiagnostic(event) {
9279
+ if (!diagnosticsFile) return;
9280
+ const memory = process.memoryUsage();
9281
+ const watcherPids = new Set(
9282
+ [...appState.values()].map((state) => state.buildProcess?.pid).filter((pid) => Number.isSafeInteger(pid) && pid > 0)
9283
+ );
9284
+ const watcherGroupRss = readProcessGroupRssBytes(watcherPids);
9285
+ const record = {
9286
+ at: (/* @__PURE__ */ new Date()).toISOString(),
9287
+ event,
9288
+ host_pid: process.pid,
9289
+ parent_pid: process.ppid,
9290
+ rss_bytes: memory.rss,
9291
+ heap_used_bytes: memory.heapUsed,
9292
+ heap_total_bytes: memory.heapTotal,
9293
+ external_bytes: memory.external,
9294
+ system_free_bytes: freemem(),
9295
+ system_total_bytes: totalmem(),
9296
+ load_average_1m: loadavg()[0],
9297
+ watcher_groups_rss_bytes: [...watcherGroupRss.values()].reduce((total, rss) => total + rss, 0),
9298
+ apps: [...appState.values()].map((state) => ({
9299
+ slug: state.slug,
9300
+ project_dir: state.projectDir,
9301
+ watcher_pid: state.buildProcess?.pid || null,
9302
+ watcher_exit_code: state.buildProcess?.exitCode ?? null,
9303
+ watcher_signal: state.buildProcess?.signalCode ?? null,
9304
+ watcher_group_rss_bytes: watcherGroupRss.get(state.buildProcess?.pid) ?? null,
9305
+ bundle_ready: state.bundleReady
9306
+ }))
9307
+ };
9308
+ try {
9309
+ mkdirSync5(dirname6(diagnosticsFile), { recursive: true, mode: 448 });
9310
+ if (existsSync7(diagnosticsFile) && statSync3(diagnosticsFile).size >= DEV_DIAGNOSTIC_MAX_BYTES) {
9311
+ const previous = `${diagnosticsFile}.previous`;
9312
+ rmSync5(previous, { force: true });
9313
+ renameSync4(diagnosticsFile, previous);
9314
+ }
9315
+ appendFileSync(diagnosticsFile, `${JSON.stringify(record)}
9316
+ `, { mode: 384 });
9317
+ diagnosticWriteFailed = false;
9318
+ } catch (error) {
9319
+ if (!diagnosticWriteFailed) {
9320
+ diagnosticWriteFailed = true;
9321
+ logError(`[notis apps dev] persistent diagnostics failed: ${error instanceof Error ? error.message : String(error)}`);
9322
+ }
9323
+ }
9324
+ }
9057
9325
  function broadcastReload(slug) {
9058
9326
  const state = appState.get(slug);
9059
9327
  if (!state) return;
@@ -9487,21 +9755,49 @@ data: ${JSON.stringify({ slug, at: Date.now() })}
9487
9755
  await prepareArtifactBuild(state.projectDir);
9488
9756
  watchManifestInputs(state);
9489
9757
  pollForBundleAndWatch(state);
9490
- state.buildProcess = spawn2("npm", ["run", "build", "--", "--watch"], {
9758
+ const buildProcess = spawn2("npm", ["run", "build", "--", "--watch"], {
9491
9759
  cwd: state.projectDir,
9760
+ detached: process.platform !== "win32",
9492
9761
  stdio: "inherit",
9493
9762
  env: { ...process.env, NOTIS_DEV: "1" }
9494
9763
  });
9495
- state.buildProcess.on("exit", (code) => {
9764
+ state.buildProcess = buildProcess;
9765
+ for (let attempt = 0; attempt < 5 && !state.watcherOwnership; attempt += 1) {
9766
+ state.watcherOwnership = captureDesktopWatcherOwnership({
9767
+ pid: buildProcess.pid,
9768
+ projectDir: state.projectDir,
9769
+ desktopOwnerId,
9770
+ desktopOwnerScope
9771
+ });
9772
+ if (!state.watcherOwnership && attempt < 4) await delay(10);
9773
+ }
9774
+ buildProcess.on("exit", (code) => {
9496
9775
  if (code !== 0 && code !== null) {
9497
9776
  logError(`[notis apps dev] ${state.slug}: vite build --watch exited with code ${code}`);
9498
9777
  }
9778
+ if (!serverClosing) {
9779
+ if (state.buildProcess === buildProcess) state.buildProcess = null;
9780
+ const stopPromise = terminateBuildProcess(buildProcess).catch((error) => {
9781
+ logError(`[notis apps dev] ${state.slug}: watcher cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
9782
+ });
9783
+ state.buildProcessStopPromise = stopPromise;
9784
+ void stopPromise.finally(() => {
9785
+ if (state.buildProcessStopPromise === stopPromise) {
9786
+ state.buildProcessStopPromise = null;
9787
+ }
9788
+ });
9789
+ }
9499
9790
  });
9500
9791
  } else {
9501
9792
  updateBundleDir(state, resolveBundleDir(state));
9502
9793
  }
9503
9794
  log(`[notis apps dev] ${state.slug}: serving bundle at http://127.0.0.1:${port}/a/${state.slug}/bundle/app.js`);
9504
9795
  }
9796
+ writeDevDiagnostic("started");
9797
+ if (diagnosticsFile) {
9798
+ diagnosticsTimer = setInterval(() => writeDevDiagnostic("sample"), DEV_DIAGNOSTIC_INTERVAL_MS);
9799
+ diagnosticsTimer.unref?.();
9800
+ }
9505
9801
  return {
9506
9802
  port,
9507
9803
  updateApp(slug, updates = {}) {
@@ -9523,7 +9819,19 @@ data: ${JSON.stringify({ slug, at: Date.now() })}
9523
9819
  if (!state) return Promise.reject(new Error(`unknown app: ${slug}`));
9524
9820
  return state.bundleReadyPromise;
9525
9821
  },
9822
+ getWatcherOwnership(slug) {
9823
+ const state = appState.get(slug);
9824
+ if (!state) throw new Error(`unknown app: ${slug}`);
9825
+ return state.watcherOwnership ? { ...state.watcherOwnership } : null;
9826
+ },
9526
9827
  async close() {
9828
+ serverClosing = true;
9829
+ if (diagnosticsTimer) {
9830
+ clearInterval(diagnosticsTimer);
9831
+ diagnosticsTimer = null;
9832
+ }
9833
+ writeDevDiagnostic("stopping");
9834
+ const buildProcessStops = [];
9527
9835
  for (const state of appState.values()) {
9528
9836
  if (state.prepareTimer) clearTimeout(state.prepareTimer);
9529
9837
  if (state.reloadTimer) clearTimeout(state.reloadTimer);
@@ -9548,10 +9856,16 @@ data: ${JSON.stringify({ slug, at: Date.now() })}
9548
9856
  }
9549
9857
  }
9550
9858
  state.sseClients.clear();
9551
- if (state.buildProcess && state.buildProcess.exitCode === null) {
9552
- state.buildProcess.kill("SIGTERM");
9859
+ if (state.buildProcessStopPromise) {
9860
+ buildProcessStops.push(state.buildProcessStopPromise);
9861
+ }
9862
+ if (state.buildProcess) {
9863
+ const buildProcess = state.buildProcess;
9864
+ state.buildProcess = null;
9865
+ buildProcessStops.push(terminateBuildProcess(buildProcess));
9553
9866
  }
9554
9867
  }
9868
+ await Promise.allSettled(buildProcessStops);
9555
9869
  for (const res of hostSseClients) {
9556
9870
  try {
9557
9871
  res.end();
@@ -9560,6 +9874,7 @@ data: ${JSON.stringify({ slug, at: Date.now() })}
9560
9874
  }
9561
9875
  hostSseClients.clear();
9562
9876
  await new Promise((resolvePromise) => server.close(() => resolvePromise()));
9877
+ writeDevDiagnostic("stopped");
9563
9878
  }
9564
9879
  };
9565
9880
  }
@@ -9569,12 +9884,12 @@ import { randomUUID as randomUUID4 } from "node:crypto";
9569
9884
  import {
9570
9885
  existsSync as existsSync8,
9571
9886
  lstatSync as lstatSync3,
9572
- mkdirSync as mkdirSync5,
9887
+ mkdirSync as mkdirSync6,
9573
9888
  readFileSync as readFileSync8,
9574
9889
  readdirSync as readdirSync4,
9575
- realpathSync as realpathSync2,
9576
- renameSync as renameSync4,
9577
- rmSync as rmSync5,
9890
+ realpathSync as realpathSync3,
9891
+ renameSync as renameSync5,
9892
+ rmSync as rmSync6,
9578
9893
  statSync as statSync4,
9579
9894
  writeFileSync as writeFileSync5
9580
9895
  } from "node:fs";
@@ -9604,13 +9919,13 @@ function readLockOwner2(lockPath) {
9604
9919
  }
9605
9920
  }
9606
9921
  function withLock(filePath, callback) {
9607
- mkdirSync5(dirname7(filePath), { recursive: true, mode: 448 });
9922
+ mkdirSync6(dirname7(filePath), { recursive: true, mode: 448 });
9608
9923
  const lockPath = `${filePath}.lock`;
9609
9924
  const owner = `${process.pid}.${randomUUID4()}`;
9610
9925
  const startedAt = Date.now();
9611
9926
  while (true) {
9612
9927
  try {
9613
- mkdirSync5(lockPath, { mode: 448 });
9928
+ mkdirSync6(lockPath, { mode: 448 });
9614
9929
  writeFileSync5(join8(lockPath, "owner"), owner, { mode: 384 });
9615
9930
  break;
9616
9931
  } catch (error) {
@@ -9627,7 +9942,7 @@ function withLock(filePath, callback) {
9627
9942
  process.kill(pid, 0);
9628
9943
  } catch (ownerError) {
9629
9944
  if (ownerError?.code === "ESRCH") {
9630
- rmSync5(lockPath, { recursive: true, force: true });
9945
+ rmSync6(lockPath, { recursive: true, force: true });
9631
9946
  continue;
9632
9947
  }
9633
9948
  }
@@ -9646,7 +9961,7 @@ function withLock(filePath, callback) {
9646
9961
  return callback();
9647
9962
  } finally {
9648
9963
  if (readLockOwner2(lockPath) === owner) {
9649
- rmSync5(lockPath, { recursive: true, force: true });
9964
+ rmSync6(lockPath, { recursive: true, force: true });
9650
9965
  }
9651
9966
  }
9652
9967
  }
@@ -9661,7 +9976,7 @@ function canonicalExistingDirectory(inputPath) {
9661
9976
  if (!stat2.isDirectory()) {
9662
9977
  throw usageError(`App development root is not a directory: ${absolute}`);
9663
9978
  }
9664
- return realpathSync2(absolute);
9979
+ return realpathSync3(absolute);
9665
9980
  }
9666
9981
  function normalizeRegistry(raw) {
9667
9982
  const roots = Array.isArray(raw?.roots) ? raw.roots : [];
@@ -9687,14 +10002,14 @@ function readRaw(filePath) {
9687
10002
  }
9688
10003
  }
9689
10004
  function writeRaw(registry, filePath) {
9690
- mkdirSync5(dirname7(filePath), { recursive: true, mode: 448 });
10005
+ mkdirSync6(dirname7(filePath), { recursive: true, mode: 448 });
9691
10006
  const normalized = {
9692
10007
  version: APP_DEV_ROOTS_VERSION,
9693
10008
  roots: normalizeRegistry(registry)
9694
10009
  };
9695
10010
  const temporary = `${filePath}.${process.pid}.${randomUUID4()}.tmp`;
9696
10011
  writeFileSync5(temporary, JSON.stringify(normalized, null, 2), { mode: 384 });
9697
- renameSync4(temporary, filePath);
10012
+ renameSync5(temporary, filePath);
9698
10013
  return normalized;
9699
10014
  }
9700
10015
  function migrateLegacyProjectsUnlocked(registry, options) {
@@ -9722,12 +10037,12 @@ function migrateLegacyProjectsUnlocked(registry, options) {
9722
10037
  } catch {
9723
10038
  }
9724
10039
  }
9725
- rmSync5(path3, { force: true });
10040
+ rmSync6(path3, { force: true });
9726
10041
  return { version: APP_DEV_ROOTS_VERSION, roots: [...byPath.values()] };
9727
10042
  }
9728
10043
  function realpathIfExists(path3) {
9729
10044
  try {
9730
- return realpathSync2(path3);
10045
+ return realpathSync3(path3);
9731
10046
  } catch {
9732
10047
  return resolve6(path3);
9733
10048
  }
@@ -9808,7 +10123,7 @@ function discoverRegisteredAppProjects(options = {}) {
9808
10123
 
9809
10124
  // src/runtime/agent-browser.js
9810
10125
  import { spawn as spawn3, spawnSync } from "node:child_process";
9811
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
10126
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
9812
10127
  import { dirname as dirname8 } from "node:path";
9813
10128
  var PREPARE_CAPTURE_SCRIPT = "(() => { const s = document.getElementById('harness-status'); if (s) s.style.display = 'none'; const r = document.getElementById('root'); if (r) { r.style.paddingTop = '0'; r.style.minHeight = '0'; } document.body.style.minHeight = '0'; document.documentElement.style.minHeight = '0'; return true; })()";
9814
10129
  var FONTS_STATUS_SCRIPT = "(() => (document.fonts ? document.fonts.status : 'loaded'))()";
@@ -9876,7 +10191,7 @@ async function waitForFonts(sessionName, timeoutMs = 3e3) {
9876
10191
  } catch {
9877
10192
  return;
9878
10193
  }
9879
- await delay(150);
10194
+ await delay2(150);
9880
10195
  }
9881
10196
  }
9882
10197
  async function setViewport(sessionName, width, height, scale = null) {
@@ -9887,7 +10202,7 @@ async function setViewport(sessionName, width, height, scale = null) {
9887
10202
  const result = await runAgentBrowser(args, { timeoutMs: 5e3 });
9888
10203
  return result.exitCode === 0;
9889
10204
  }
9890
- function delay(ms) {
10205
+ function delay2(ms) {
9891
10206
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
9892
10207
  }
9893
10208
  function commandError(phase, result) {
@@ -10010,15 +10325,15 @@ async function runHarnessRoute({
10010
10325
  while (Date.now() < deadline) {
10011
10326
  const lastRead = await readHarness(sessionName, 5e3);
10012
10327
  if (!lastRead.ok) {
10013
- await delay(250);
10328
+ await delay2(250);
10014
10329
  continue;
10015
10330
  }
10016
10331
  const harness2 = lastRead.harness;
10017
10332
  if (harness2?.mounted || Array.isArray(harness2?.errors) && harness2.errors.length > 0) {
10018
- await delay(250);
10333
+ await delay2(250);
10019
10334
  break;
10020
10335
  }
10021
- await delay(250);
10336
+ await delay2(250);
10022
10337
  }
10023
10338
  const finalRead = await readHarness(sessionName, 5e3);
10024
10339
  if (!finalRead.ok) {
@@ -10038,7 +10353,7 @@ async function runHarnessRoute({
10038
10353
  timeoutMs: 1e4
10039
10354
  });
10040
10355
  if (snapshot.exitCode === 0) {
10041
- mkdirSync6(dirname8(snapshotPath), { recursive: true });
10356
+ mkdirSync7(dirname8(snapshotPath), { recursive: true });
10042
10357
  writeFileSync6(snapshotPath, snapshot.stdout);
10043
10358
  savedSnapshotPath = snapshotPath;
10044
10359
  }
@@ -10096,7 +10411,7 @@ async function captureHarnessScreenshot({
10096
10411
  } else {
10097
10412
  lastReadError = read.toolError;
10098
10413
  }
10099
- await delay(250);
10414
+ await delay2(250);
10100
10415
  }
10101
10416
  if (lastErrors.length > 0) {
10102
10417
  return { ok: false, mounted, screenshotPath: null, errors: lastErrors, tool_error: null };
@@ -10134,7 +10449,7 @@ async function captureHarnessScreenshot({
10134
10449
  break;
10135
10450
  }
10136
10451
  framing = { ...fitted, content_height: contentHeight };
10137
- await delay(150);
10452
+ await delay2(150);
10138
10453
  const remeasured = await evalNumber(sessionName, CONTENT_HEIGHT_SCRIPT);
10139
10454
  if (remeasured == null || Math.abs(remeasured - contentHeight) <= 32) {
10140
10455
  framing.content_height = remeasured ?? contentHeight;
@@ -10143,8 +10458,8 @@ async function captureHarnessScreenshot({
10143
10458
  contentHeight = remeasured;
10144
10459
  }
10145
10460
  }
10146
- await delay(500);
10147
- mkdirSync6(dirname8(screenshotPath), { recursive: true });
10461
+ await delay2(500);
10462
+ mkdirSync7(dirname8(screenshotPath), { recursive: true });
10148
10463
  const screenshotArgs = ["--session", sessionName, "screenshot"];
10149
10464
  if (focusSelector) {
10150
10465
  screenshotArgs.push(focusSelector);
@@ -10157,7 +10472,7 @@ async function captureHarnessScreenshot({
10157
10472
  const dimensions = pngDimensions2(screenshotPath);
10158
10473
  if (!focusSelector && framing && (!dimensions || dimensions.width !== width || dimensions.height !== height)) {
10159
10474
  await setViewport(sessionName, width, height);
10160
- await delay(300);
10475
+ await delay2(300);
10161
10476
  const retry = await runAgentBrowser(
10162
10477
  ["--session", sessionName, "screenshot", screenshotPath, "--screenshot-format", "png"],
10163
10478
  { timeoutMs: 15e3 }
@@ -10193,7 +10508,7 @@ async function closeAgentBrowserSession(sessionName) {
10193
10508
 
10194
10509
  // src/runtime/app-dev-host-lock.js
10195
10510
  import { createHash, randomUUID as randomUUID5 } from "node:crypto";
10196
- import { lstatSync as lstatSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
10511
+ import { lstatSync as lstatSync4, mkdirSync as mkdirSync8, readFileSync as readFileSync10, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
10197
10512
  import { homedir as homedir4 } from "node:os";
10198
10513
  import { join as join9 } from "node:path";
10199
10514
  var DEFAULT_LOCK_ROOT = join9(homedir4(), ".notis", "app-dev-host-locks");
@@ -10228,25 +10543,25 @@ function reclaimable(path3, now, staleAfterMs) {
10228
10543
  }
10229
10544
  function tryAcquireAppDevHostLock(options) {
10230
10545
  const lockRoot = options.lockRoot || DEFAULT_LOCK_ROOT;
10231
- mkdirSync7(lockRoot, { recursive: true, mode: 448 });
10546
+ mkdirSync8(lockRoot, { recursive: true, mode: 448 });
10232
10547
  const path3 = join9(lockRoot, lockName(options.identity, options.apiBase, options.projectDir));
10233
10548
  const ownerId = `${process.pid}.${randomUUID5()}`;
10234
10549
  for (let attempt = 0; attempt < 2; attempt += 1) {
10235
10550
  try {
10236
- mkdirSync7(path3, { mode: 448 });
10551
+ mkdirSync8(path3, { mode: 448 });
10237
10552
  try {
10238
10553
  writeFileSync7(join9(path3, OWNER_FILE), JSON.stringify({ pid: process.pid, ownerId }), {
10239
10554
  mode: 384
10240
10555
  });
10241
10556
  } catch (error) {
10242
- rmSync6(path3, { recursive: true, force: true });
10557
+ rmSync7(path3, { recursive: true, force: true });
10243
10558
  throw error;
10244
10559
  }
10245
10560
  return { path: path3, ownerId };
10246
10561
  } catch (error) {
10247
10562
  if (error?.code !== "EEXIST") throw error;
10248
10563
  if (reclaimable(path3, options.now ?? Date.now(), options.staleAfterMs ?? 6e4)) {
10249
- rmSync6(path3, { recursive: true, force: true });
10564
+ rmSync7(path3, { recursive: true, force: true });
10250
10565
  continue;
10251
10566
  }
10252
10567
  return null;
@@ -10257,7 +10572,7 @@ function tryAcquireAppDevHostLock(options) {
10257
10572
  function releaseAppDevHostLock(lock) {
10258
10573
  try {
10259
10574
  if (readOwner(lock.path)?.ownerId === lock.ownerId) {
10260
- rmSync6(lock.path, { recursive: true, force: true });
10575
+ rmSync7(lock.path, { recursive: true, force: true });
10261
10576
  }
10262
10577
  } catch {
10263
10578
  }
@@ -10268,10 +10583,10 @@ import { randomUUID as randomUUID6 } from "node:crypto";
10268
10583
  import {
10269
10584
  existsSync as existsSync9,
10270
10585
  lstatSync as lstatSync5,
10271
- mkdirSync as mkdirSync8,
10586
+ mkdirSync as mkdirSync9,
10272
10587
  readFileSync as readFileSync11,
10273
- renameSync as renameSync5,
10274
- rmSync as rmSync7,
10588
+ renameSync as renameSync6,
10589
+ rmSync as rmSync8,
10275
10590
  writeFileSync as writeFileSync8
10276
10591
  } from "node:fs";
10277
10592
  import { homedir as homedir5 } from "node:os";
@@ -10304,20 +10619,20 @@ function reclaimConsumerLock(lockPath) {
10304
10619
  return Date.now() - stat2.mtimeMs >= CONSUMER_LOCK_STALE_AFTER_MS;
10305
10620
  }
10306
10621
  function withLock2(filePath, callback) {
10307
- mkdirSync8(dirname9(filePath), { recursive: true, mode: 448 });
10622
+ mkdirSync9(dirname9(filePath), { recursive: true, mode: 448 });
10308
10623
  const lockPath = `${filePath}.lock`;
10309
10624
  const owner = `${process.pid}.${randomUUID6()}`;
10310
10625
  const startedAt = Date.now();
10311
10626
  while (true) {
10312
10627
  try {
10313
- mkdirSync8(lockPath, { mode: 448 });
10628
+ mkdirSync9(lockPath, { mode: 448 });
10314
10629
  writeFileSync8(join10(lockPath, "owner"), owner, { mode: 384 });
10315
10630
  break;
10316
10631
  } catch (error) {
10317
10632
  if (error?.code !== "EEXIST") throw error;
10318
10633
  try {
10319
10634
  if (reclaimConsumerLock(lockPath)) {
10320
- rmSync7(lockPath, { recursive: true, force: true });
10635
+ rmSync8(lockPath, { recursive: true, force: true });
10321
10636
  continue;
10322
10637
  }
10323
10638
  } catch (lockError) {
@@ -10335,7 +10650,7 @@ function withLock2(filePath, callback) {
10335
10650
  } finally {
10336
10651
  try {
10337
10652
  if (readFileSync11(join10(lockPath, "owner"), "utf8").trim() === owner) {
10338
- rmSync7(lockPath, { recursive: true, force: true });
10653
+ rmSync8(lockPath, { recursive: true, force: true });
10339
10654
  }
10340
10655
  } catch {
10341
10656
  }
@@ -10357,10 +10672,10 @@ function readAppDevConsumers(filePath = DEFAULT_APP_DEV_CONSUMERS_FILE, now = Da
10357
10672
  }
10358
10673
  }
10359
10674
  function writeAppDevConsumers(filePath, consumers) {
10360
- mkdirSync8(dirname9(filePath), { recursive: true, mode: 448 });
10675
+ mkdirSync9(dirname9(filePath), { recursive: true, mode: 448 });
10361
10676
  const temporary = `${filePath}.${process.pid}.${randomUUID6()}.tmp`;
10362
10677
  writeFileSync8(temporary, JSON.stringify({ version: 1, consumers }, null, 2), { mode: 384 });
10363
- renameSync5(temporary, filePath);
10678
+ renameSync6(temporary, filePath);
10364
10679
  }
10365
10680
  function heartbeatAppDevConsumer(lease, filePath = DEFAULT_APP_DEV_CONSUMERS_FILE) {
10366
10681
  return withLock2(filePath, () => {
@@ -10597,12 +10912,12 @@ import {
10597
10912
  timingSafeEqual
10598
10913
  } from "node:crypto";
10599
10914
  import {
10600
- mkdirSync as mkdirSync9,
10915
+ mkdirSync as mkdirSync10,
10601
10916
  readdirSync as readdirSync5,
10602
10917
  readFileSync as readFileSync12,
10603
- renameSync as renameSync6,
10918
+ renameSync as renameSync7,
10604
10919
  rmdirSync as rmdirSync2,
10605
- rmSync as rmSync8,
10920
+ rmSync as rmSync9,
10606
10921
  statSync as statSync5,
10607
10922
  writeFileSync as writeFileSync9
10608
10923
  } from "node:fs";
@@ -10610,7 +10925,7 @@ import { createServer as createServer3 } from "node:http";
10610
10925
  import { homedir as homedir6 } from "node:os";
10611
10926
  import { basename as basename2, dirname as dirname10, join as join11 } from "node:path";
10612
10927
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10613
- import { execFileSync, spawn as spawn4 } from "node:child_process";
10928
+ import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
10614
10929
  import { createInterface } from "node:readline/promises";
10615
10930
  var DEFAULT_CLI_OAUTH_SCOPES = [
10616
10931
  "notis:read",
@@ -11263,7 +11578,7 @@ function legacyPendingAuthorizationFile(runtime) {
11263
11578
  }
11264
11579
  function savePendingAuthorization(runtime, pending) {
11265
11580
  const file = pendingAuthorizationFile(runtime);
11266
- mkdirSync9(dirname10(file), { recursive: true });
11581
+ mkdirSync10(dirname10(file), { recursive: true });
11267
11582
  writeFileSync9(file, JSON.stringify(pending, null, 2), { mode: 384 });
11268
11583
  }
11269
11584
  function readPendingAuthorization(runtime, { includeExpired = false } = {}) {
@@ -11286,7 +11601,7 @@ function readPendingAuthorization(runtime, { includeExpired = false } = {}) {
11286
11601
  }
11287
11602
  function clearPendingAuthorization(runtime, file = pendingAuthorizationFile(runtime)) {
11288
11603
  try {
11289
- rmSync8(file);
11604
+ rmSync9(file);
11290
11605
  } catch {
11291
11606
  }
11292
11607
  }
@@ -11295,7 +11610,7 @@ function clearPendingAuthorizations(runtime) {
11295
11610
  const legacyFile = legacyPendingAuthorizationFile(runtime);
11296
11611
  try {
11297
11612
  const pending = JSON.parse(readFileSync12(legacyFile, "utf-8"));
11298
- if (pending?.profile === runtime.profileName) rmSync8(legacyFile);
11613
+ if (pending?.profile === runtime.profileName) rmSync9(legacyFile);
11299
11614
  } catch {
11300
11615
  }
11301
11616
  }
@@ -11399,20 +11714,20 @@ function listenerGlobalLockIsStale(lockDir, staleMs) {
11399
11714
  function retireStaleListenerGlobalLock(lockDir) {
11400
11715
  const retiredDir = `${lockDir}.stale-${process.pid}-${base64url(randomBytes(8))}`;
11401
11716
  try {
11402
- renameSync6(lockDir, retiredDir);
11717
+ renameSync7(lockDir, retiredDir);
11403
11718
  } catch {
11404
11719
  return false;
11405
11720
  }
11406
- rmSync8(retiredDir, { recursive: true, force: true });
11721
+ rmSync9(retiredDir, { recursive: true, force: true });
11407
11722
  return true;
11408
11723
  }
11409
11724
  async function acquireListenerStartLock(runtime) {
11410
11725
  const lockDir = listenerStartLockDir(runtime);
11411
- mkdirSync9(dirname10(lockDir), { recursive: true });
11726
+ mkdirSync10(dirname10(lockDir), { recursive: true });
11412
11727
  const deadline = Date.now() + LISTENER_START_LOCK_STALE_MS * 2;
11413
11728
  for (; ; ) {
11414
11729
  try {
11415
- mkdirSync9(lockDir);
11730
+ mkdirSync10(lockDir);
11416
11731
  return lockDir;
11417
11732
  } catch (error) {
11418
11733
  if (error?.code !== "EEXIST") throw error;
@@ -11446,11 +11761,11 @@ async function acquireListenerGlobalLock({
11446
11761
  heartbeatMs = LISTENER_GLOBAL_LOCK_HEARTBEAT_MS
11447
11762
  } = {}) {
11448
11763
  const lockDir = listenerGlobalLockDir();
11449
- mkdirSync9(dirname10(lockDir), { recursive: true });
11764
+ mkdirSync10(dirname10(lockDir), { recursive: true });
11450
11765
  const deadline = Date.now() + waitMs;
11451
11766
  for (; ; ) {
11452
11767
  try {
11453
- mkdirSync9(lockDir);
11768
+ mkdirSync10(lockDir);
11454
11769
  const lock = {
11455
11770
  lockDir,
11456
11771
  ownerToken: base64url(randomBytes(24)),
@@ -11487,23 +11802,23 @@ function releaseListenerGlobalLock(lock) {
11487
11802
  if (owner?.owner_token !== lock.ownerToken) return;
11488
11803
  const releasedDir = `${lock.lockDir}.released-${process.pid}-${lock.ownerToken}`;
11489
11804
  try {
11490
- renameSync6(lock.lockDir, releasedDir);
11805
+ renameSync7(lock.lockDir, releasedDir);
11491
11806
  } catch {
11492
11807
  return;
11493
11808
  }
11494
11809
  const movedOwner = readListenerGlobalLockOwner(releasedDir);
11495
11810
  if (movedOwner?.owner_token !== lock.ownerToken) {
11496
11811
  try {
11497
- renameSync6(releasedDir, lock.lockDir);
11812
+ renameSync7(releasedDir, lock.lockDir);
11498
11813
  } catch {
11499
11814
  }
11500
11815
  return;
11501
11816
  }
11502
- rmSync8(releasedDir, { recursive: true, force: true });
11817
+ rmSync9(releasedDir, { recursive: true, force: true });
11503
11818
  }
11504
11819
  function saveListenerState(runtime, state) {
11505
11820
  const file = listenerStateFile(runtime);
11506
- mkdirSync9(dirname10(file), { recursive: true });
11821
+ mkdirSync10(dirname10(file), { recursive: true });
11507
11822
  writeFileSync9(file, JSON.stringify(state, null, 2), { mode: 384 });
11508
11823
  }
11509
11824
  function readListenerState(runtime) {
@@ -11524,7 +11839,7 @@ function clearListenerState(runtime, { ownerPid = null } = {}) {
11524
11839
  }
11525
11840
  }
11526
11841
  try {
11527
- rmSync8(listenerStateFile(runtime));
11842
+ rmSync9(listenerStateFile(runtime));
11528
11843
  } catch {
11529
11844
  }
11530
11845
  }
@@ -11542,7 +11857,7 @@ function readLiveListener(runtime, { sameApiBase = true } = {}) {
11542
11857
  }
11543
11858
  function listenerProcessIsAlive(pid, {
11544
11859
  platform = process.platform,
11545
- run = execFileSync,
11860
+ run = execFileSync3,
11546
11861
  signal = process.kill.bind(process),
11547
11862
  expectedIdentity = null,
11548
11863
  expectedScriptPath = null
@@ -11607,7 +11922,7 @@ async function startDetachedLoopbackListener(runtime, {
11607
11922
  const identityToken = randomBytes(24).toString("base64url");
11608
11923
  const payloadFile = `${listenerStateFile(runtime)}.payload.${process.pid}.${randomBytes(8).toString("hex")}`;
11609
11924
  try {
11610
- mkdirSync9(dirname10(payloadFile), { recursive: true });
11925
+ mkdirSync10(dirname10(payloadFile), { recursive: true });
11611
11926
  writeFileSync9(payloadFile, JSON.stringify({
11612
11927
  profile: runtime.profileName,
11613
11928
  api_base: runtime.apiBase,
@@ -11634,7 +11949,7 @@ async function startDetachedLoopbackListener(runtime, {
11634
11949
  });
11635
11950
  } catch {
11636
11951
  try {
11637
- rmSync8(payloadFile);
11952
+ rmSync9(payloadFile);
11638
11953
  } catch {
11639
11954
  }
11640
11955
  return null;
@@ -11663,7 +11978,7 @@ async function startDetachedLoopbackListener(runtime, {
11663
11978
  } catch {
11664
11979
  }
11665
11980
  try {
11666
- rmSync8(payloadFile);
11981
+ rmSync9(payloadFile);
11667
11982
  } catch {
11668
11983
  }
11669
11984
  return null;
@@ -12355,11 +12670,11 @@ function lockIsStale() {
12355
12670
  }
12356
12671
  }
12357
12672
  async function acquireRefreshLock(runtime, waitMs = 6e4) {
12358
- mkdirSync9(join11(homedir6(), ".notis"), { recursive: true });
12673
+ mkdirSync10(join11(homedir6(), ".notis"), { recursive: true });
12359
12674
  const deadline = Date.now() + waitMs;
12360
12675
  for (; ; ) {
12361
12676
  try {
12362
- mkdirSync9(OAUTH_LOCK_DIR);
12677
+ mkdirSync10(OAUTH_LOCK_DIR);
12363
12678
  return true;
12364
12679
  } catch (error) {
12365
12680
  if (error?.code !== "EEXIST") throw error;
@@ -13085,6 +13400,67 @@ function projectIsWithinRoot(projectDir, rootDir) {
13085
13400
  const nested = relative3(rootDir, projectDir);
13086
13401
  return nested === "" || nested !== ".." && !nested.startsWith(`..${sep}`) && !isAbsolute2(nested);
13087
13402
  }
13403
+ function parseNotisAppVersion(value) {
13404
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(
13405
+ String(value || "").trim()
13406
+ );
13407
+ if (!match) return null;
13408
+ const prerelease = match[4] ? match[4].split(".") : [];
13409
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
13410
+ return null;
13411
+ }
13412
+ return {
13413
+ major: match[1],
13414
+ minor: match[2],
13415
+ patch: match[3],
13416
+ prerelease
13417
+ };
13418
+ }
13419
+ function compareNumericSemverIdentifiers(left, right) {
13420
+ if (left.length !== right.length) return left.length > right.length ? 1 : -1;
13421
+ return left === right ? 0 : left > right ? 1 : -1;
13422
+ }
13423
+ function compareNotisPrerelease(left, right) {
13424
+ if (left.length === 0 || right.length === 0) {
13425
+ return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
13426
+ }
13427
+ const length = Math.max(left.length, right.length);
13428
+ for (let index = 0; index < length; index += 1) {
13429
+ const leftIdentifier = left[index];
13430
+ const rightIdentifier = right[index];
13431
+ if (leftIdentifier === void 0 || rightIdentifier === void 0) {
13432
+ return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === void 0 ? -1 : 1;
13433
+ }
13434
+ if (leftIdentifier === rightIdentifier) continue;
13435
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
13436
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
13437
+ if (leftNumeric && rightNumeric) {
13438
+ return compareNumericSemverIdentifiers(leftIdentifier, rightIdentifier);
13439
+ }
13440
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
13441
+ return leftIdentifier > rightIdentifier ? 1 : -1;
13442
+ }
13443
+ return 0;
13444
+ }
13445
+ function compareNotisAppVersions(leftValue, rightValue) {
13446
+ const left = parseNotisAppVersion(leftValue);
13447
+ const right = parseNotisAppVersion(rightValue);
13448
+ if (!left || !right) return null;
13449
+ for (const key of ["major", "minor", "patch"]) {
13450
+ const comparison = compareNumericSemverIdentifiers(left[key], right[key]);
13451
+ if (comparison !== 0) return comparison;
13452
+ }
13453
+ return compareNotisPrerelease(left.prerelease, right.prerelease);
13454
+ }
13455
+ function readLocalNotisAppVersion(projectDir) {
13456
+ try {
13457
+ const packageJson = JSON.parse(readFileSync13(join12(projectDir, "package.json"), "utf8"));
13458
+ const version = String(packageJson.notisAppVersion || "").trim();
13459
+ return parseNotisAppVersion(version) ? version : null;
13460
+ } catch {
13461
+ return null;
13462
+ }
13463
+ }
13088
13464
  function discoverAppDevLaunchProjects(rootDir, {
13089
13465
  skipRootRegistration = false,
13090
13466
  registerRoot = registerAppDevRoot,
@@ -13320,7 +13696,7 @@ function pruneStaleScreenshotFiles(outputDir, keepCount) {
13320
13696
  if (!entry.isFile()) continue;
13321
13697
  const match = /^screenshot-(\d+)\.png$/i.exec(entry.name);
13322
13698
  if (match && Number.parseInt(match[1], 10) > keepCount) {
13323
- rmSync9(join12(outputDir, entry.name), { force: true });
13699
+ rmSync10(join12(outputDir, entry.name), { force: true });
13324
13700
  }
13325
13701
  }
13326
13702
  }
@@ -13331,12 +13707,6 @@ function screenshotIndexByRouteSlug(manifest) {
13331
13707
  const routes = Array.isArray(manifest?.routes) ? manifest.routes : [];
13332
13708
  return new Map(routes.map((route, index) => [route.slug, index + 1]));
13333
13709
  }
13334
- function appRowFieldsFromManifest(manifest) {
13335
- const app = manifest?.app && typeof manifest.app === "object" ? manifest.app : {};
13336
- return {
13337
- accent: app.accent ?? null
13338
- };
13339
- }
13340
13710
  function screenshotExitCode(failedCount) {
13341
13711
  return failedCount === 0 ? EXIT_CODES.ok : EXIT_CODES.unexpected;
13342
13712
  }
@@ -13472,7 +13842,7 @@ function renderVerifyReport({ summary, results, noBrowser }) {
13472
13842
  }
13473
13843
  return lines.join("\n");
13474
13844
  }
13475
- function buildManifestForDev(appConfig) {
13845
+ function buildManifestForDev(appConfig, projectDir) {
13476
13846
  const routes = Array.isArray(appConfig.routes) ? appConfig.routes : [];
13477
13847
  return {
13478
13848
  version: 1,
@@ -13480,7 +13850,8 @@ function buildManifestForDev(appConfig) {
13480
13850
  app: {
13481
13851
  name: appConfig.name,
13482
13852
  description: appConfig.description || null,
13483
- icon: appConfig.icon || null
13853
+ icon: appConfig.icon || null,
13854
+ release_version: readLocalNotisAppVersion(projectDir)
13484
13855
  },
13485
13856
  routes: routes.map((route) => ({
13486
13857
  path: route.path,
@@ -13489,6 +13860,7 @@ function buildManifestForDev(appConfig) {
13489
13860
  icon: route.icon || null,
13490
13861
  parentSlug: route.parentSlug || null,
13491
13862
  default: route.default || false,
13863
+ resourceDeepLinks: route.resourceDeepLinks === true,
13492
13864
  export_name: route.exportName || route.export_name,
13493
13865
  collection: route.collection || null
13494
13866
  })),
@@ -13606,7 +13978,7 @@ async function ensureDevInstall({
13606
13978
  if (!devSlug) {
13607
13979
  throw usageError(`notis.config.ts devSlug or name in ${projectDir} must slugify to a non-empty value.`);
13608
13980
  }
13609
- const manifest = buildManifestForDev(appConfig);
13981
+ const manifest = buildManifestForDev(appConfig, projectDir);
13610
13982
  const skills = resolveConfiguredAppSkills(appConfig, projectDir);
13611
13983
  const profileKey = linkedStateProfileKey(ctx.runtime);
13612
13984
  let linkedState = readLinkedState(projectDir, profileKey);
@@ -13675,6 +14047,9 @@ async function ensureDevInstall({
13675
14047
  "version"
13676
14048
  ].includes(key))
13677
14049
  ) : linkedState;
14050
+ const localReleaseVersion = manifest.app.release_version || null;
14051
+ const installedReleaseVersion = linkedApp?.manifest?.app?.release_version || "0.0.0";
14052
+ const mountEligible = !linkedApp || compareNotisAppVersions(localReleaseVersion, installedReleaseVersion) === 1;
13678
14053
  const ensureArguments = buildEnsureDevInstallArguments({
13679
14054
  appConfig,
13680
14055
  manifest,
@@ -13735,6 +14110,9 @@ async function ensureDevInstall({
13735
14110
  linkedAppId: runtimeLinkedState?.app_id || null,
13736
14111
  targetAppId: runtimeLinkedState?.app_id || null,
13737
14112
  targetAppSlug: linkedApp?.slug || null,
14113
+ localReleaseVersion,
14114
+ installedReleaseVersion: linkedApp ? installedReleaseVersion : null,
14115
+ mountEligible,
13738
14116
  databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
13739
14117
  liveData: ensureResult.payload.live_data || null
13740
14118
  };
@@ -13755,6 +14133,13 @@ function databaseMaterializationWarnings(apps) {
13755
14133
  function liveDataWarnings(apps) {
13756
14134
  return apps.filter((app) => app.liveData?.warning).map((app) => `${app.name}: ${app.liveData.warning}`);
13757
14135
  }
14136
+ function versionPrecedenceWarnings(apps) {
14137
+ return apps.filter((app) => app.targetAppId && app.mountEligible === false).map((app) => {
14138
+ const localVersion = app.localReleaseVersion || "missing or invalid";
14139
+ const pullCommand = `npx --package @notis_ai/cli@latest -- notis apps pull ${app.targetAppId} ${JSON.stringify(app.projectDir)} --force`;
14140
+ return `${app.name}: local version ${localVersion} is not strictly newer than installed version ${app.installedReleaseVersion}; Workspace keeps serving the online bundle. Preserve any local edits, pull latest with \`${pullCommand}\`, then bump package.json notisAppVersion before development.`;
14141
+ });
14142
+ }
13758
14143
  async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
13759
14144
  const result = await runTool({
13760
14145
  runtime,
@@ -14078,7 +14463,8 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14078
14463
  updateApp: () => {
14079
14464
  },
14080
14465
  waitForBundle: async () => {
14081
- }
14466
+ },
14467
+ getWatcherOwnership: () => null
14082
14468
  } : await startAppDevServer({
14083
14469
  apps: canonicalCandidates.map((app) => ({
14084
14470
  slug: app.devSlug,
@@ -14093,7 +14479,60 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14093
14479
  port,
14094
14480
  sessionsFilePath
14095
14481
  });
14096
- let heartbeatTimer = setInterval(() => {
14482
+ const desktopHostOwnership = captureDesktopHostOwnership({
14483
+ desktopOwnerId: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_ID,
14484
+ desktopOwnerScope: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_SCOPE
14485
+ });
14486
+ let heartbeatTimer = null;
14487
+ let consumerTimer = null;
14488
+ let shuttingDown = false;
14489
+ const shutdown = async (signal) => {
14490
+ if (shuttingDown) return;
14491
+ shuttingDown = true;
14492
+ process.stdout.write(`
14493
+ [notis apps dev] stopping (${signal})...
14494
+ `);
14495
+ if (heartbeatTimer) {
14496
+ clearInterval(heartbeatTimer);
14497
+ heartbeatTimer = null;
14498
+ }
14499
+ if (consumerTimer) {
14500
+ clearInterval(consumerTimer);
14501
+ consumerTimer = null;
14502
+ }
14503
+ if (manualConsumerTimer) {
14504
+ clearInterval(manualConsumerTimer);
14505
+ manualConsumerTimer = null;
14506
+ }
14507
+ if (manualConsumerInstanceId) {
14508
+ try {
14509
+ removeAppDevConsumer(manualConsumerInstanceId);
14510
+ } catch {
14511
+ }
14512
+ }
14513
+ try {
14514
+ await devServer.close();
14515
+ } catch {
14516
+ }
14517
+ try {
14518
+ removeAppDevSession(sessionId, sessionsFilePath);
14519
+ } catch {
14520
+ }
14521
+ if (sourceHostLock) {
14522
+ releaseAppDevHostLock(sourceHostLock);
14523
+ sourceHostLock = null;
14524
+ }
14525
+ process.exit(EXIT_CODES.ok);
14526
+ };
14527
+ const handleSigint = () => {
14528
+ void shutdown("SIGINT");
14529
+ };
14530
+ const handleSigterm = () => {
14531
+ void shutdown("SIGTERM");
14532
+ };
14533
+ process.on("SIGINT", handleSigint);
14534
+ process.on("SIGTERM", handleSigterm);
14535
+ heartbeatTimer = setInterval(() => {
14097
14536
  try {
14098
14537
  heartbeatAppDevSession(sessionId, (/* @__PURE__ */ new Date()).toISOString(), sessionsFilePath);
14099
14538
  } catch (error) {
@@ -14146,6 +14585,8 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14146
14585
  sessionId,
14147
14586
  hostPid: process.pid,
14148
14587
  sourceHost: !sharedBundleBaseUrls,
14588
+ ...desktopHostOwnership || {},
14589
+ ...devServer.getWatcherOwnership(app.devSlug) || {},
14149
14590
  bundleReady: devServer.isBundleReady(app.devSlug),
14150
14591
  ...!sharedBundleBaseUrls ? {
14151
14592
  discoveredProjects: discoveredAppDirs,
@@ -14194,6 +14635,8 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14194
14635
  }
14195
14636
  }
14196
14637
  if (apps.length === 0) {
14638
+ process.off("SIGINT", handleSigint);
14639
+ process.off("SIGTERM", handleSigterm);
14197
14640
  clearInterval(heartbeatTimer);
14198
14641
  heartbeatTimer = null;
14199
14642
  try {
@@ -14221,9 +14664,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14221
14664
  ...canonicalSelection.warnings,
14222
14665
  ...registrationWarnings,
14223
14666
  ...databaseMaterializationWarnings(apps),
14224
- ...liveDataWarnings(apps)
14667
+ ...liveDataWarnings(apps),
14668
+ ...versionPrecedenceWarnings(apps)
14225
14669
  ];
14226
- let consumerTimer = null;
14227
14670
  ctx.output.emitSuccess({
14228
14671
  command: ctx.spec.command_path.join(" "),
14229
14672
  data: {
@@ -14244,7 +14687,10 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14244
14687
  created: app.created,
14245
14688
  linked_app_id: app.linkedAppId,
14246
14689
  database_materialization: app.databaseMaterialization,
14247
- live_data: app.liveData
14690
+ live_data: app.liveData,
14691
+ local_release_version: app.localReleaseVersion,
14692
+ installed_release_version: app.installedReleaseVersion,
14693
+ mount_eligible: app.mountEligible
14248
14694
  }))
14249
14695
  },
14250
14696
  warnings,
@@ -14254,58 +14700,13 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14254
14700
  `Watching ${readAppDevRoots().roots.length} persistent development root(s).`,
14255
14701
  ...useInstalledDatabases ? [`Databases: ${apps.filter((app) => app.liveData?.enabled).length}/${apps.length} app(s) reading the installed app's live rows`] : [],
14256
14702
  "",
14257
- ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
14703
+ ...apps.map((app) => app.mountEligible ? ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}` : ` ${app.name.padEnd(24)} online v${app.installedReleaseVersion} (local ${app.localReleaseVersion || "version missing"})`),
14258
14704
  "",
14259
- sharedBundleBaseUrls ? `Mounted ${apps.length} shared source app${apps.length === 1 ? "" : "s"} for this authenticated environment.` : `Serving one shared loopback host for ${apps.length} app${apps.length === 1 ? "" : "s"}.`,
14705
+ sharedBundleBaseUrls ? `Attached to the shared source host: ${apps.filter((app) => app.mountEligible).length}/${apps.length} app${apps.length === 1 ? "" : "s"} eligible to substitute.` : `Serving one shared loopback host for ${apps.length} app${apps.length === 1 ? "" : "s"}; ${apps.filter((app) => app.mountEligible).length} eligible to substitute.`,
14260
14706
  "",
14261
14707
  "Press Ctrl-C to stop."
14262
14708
  ].join("\n")
14263
14709
  });
14264
- let shuttingDown = false;
14265
- const shutdown = async (signal) => {
14266
- if (shuttingDown) return;
14267
- shuttingDown = true;
14268
- process.stdout.write(`
14269
- [notis apps dev] stopping (${signal})...
14270
- `);
14271
- if (heartbeatTimer) {
14272
- clearInterval(heartbeatTimer);
14273
- heartbeatTimer = null;
14274
- }
14275
- if (consumerTimer) {
14276
- clearInterval(consumerTimer);
14277
- consumerTimer = null;
14278
- }
14279
- if (manualConsumerTimer) {
14280
- clearInterval(manualConsumerTimer);
14281
- manualConsumerTimer = null;
14282
- }
14283
- if (manualConsumerInstanceId) {
14284
- try {
14285
- removeAppDevConsumer(manualConsumerInstanceId);
14286
- } catch {
14287
- }
14288
- }
14289
- try {
14290
- removeAppDevSession(sessionId, sessionsFilePath);
14291
- } catch {
14292
- }
14293
- try {
14294
- await devServer.close();
14295
- } catch {
14296
- }
14297
- if (sourceHostLock) {
14298
- releaseAppDevHostLock(sourceHostLock);
14299
- sourceHostLock = null;
14300
- }
14301
- process.exit(EXIT_CODES.ok);
14302
- };
14303
- process.on("SIGINT", () => {
14304
- void shutdown("SIGINT");
14305
- });
14306
- process.on("SIGTERM", () => {
14307
- void shutdown("SIGTERM");
14308
- });
14309
14710
  if (consumerMode === "machine" || consumerMode === "environment") {
14310
14711
  consumerTimer = setInterval(() => {
14311
14712
  if (!hasAppDevConsumer(readAppDevConsumers(), {
@@ -14666,7 +15067,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14666
15067
  `)
14667
15068
  });
14668
15069
  browserTouched = true;
14669
- mkdirSync10(outputDir, { recursive: true });
15070
+ mkdirSync11(outputDir, { recursive: true });
14670
15071
  const results = [];
14671
15072
  for (const capture of captures) {
14672
15073
  const { route, scenario, focus, theme, fileName } = capture;
@@ -14764,7 +15165,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
14764
15165
  }
14765
15166
  }
14766
15167
  if (rawOutputDir) {
14767
- rmSync9(rawOutputDir, { recursive: true, force: true });
15168
+ rmSync10(rawOutputDir, { recursive: true, force: true });
14768
15169
  }
14769
15170
  }
14770
15171
  }
@@ -14803,13 +15204,19 @@ async function appsPullHandler(ctx) {
14803
15204
  const appId = ctx.args.appId;
14804
15205
  const result = await runToolCommand({
14805
15206
  runtime: ctx.runtime,
14806
- toolName: GET_APP_TOOL,
14807
- arguments_: { app_id: appId }
15207
+ // Pull is source retrieval plus local link state. LIST_APPS is deliberately
15208
+ // non-materializing; GET_APP hydrates missing declared databases and would
15209
+ // turn a read-only pull into a remote mutation before build/verification.
15210
+ toolName: LIST_APPS_TOOL
14808
15211
  });
14809
15212
  if (ctx.runtime.credentialKind === "oauth" && !await ensureFreshOAuthCredential(ctx.runtime)) {
14810
15213
  throw usageError("Pulling app source requires a current OAuth grant. Run `notis login` and retry.");
14811
15214
  }
14812
- const app = result.payload?.app || {};
15215
+ const apps = Array.isArray(result.payload?.apps) ? result.payload.apps : [];
15216
+ const app = apps.find((candidate) => (candidate?.app_id || candidate?.id) === appId);
15217
+ if (!app) {
15218
+ throw usageError(`App ${appId} is not accessible to the active profile.`);
15219
+ }
14813
15220
  const defaultDir = slugify(app.slug) || slugify(app.name) || slugify(appId);
14814
15221
  const targetDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : defaultAppProjectDir(defaultDir);
14815
15222
  const version = ctx.options.sourceVersion || "latest";
@@ -14830,7 +15237,7 @@ async function appsPullHandler(ctx) {
14830
15237
  project_dir: pulled.projectDir,
14831
15238
  version: pulled.version
14832
15239
  },
14833
- humanSummary: `Pulled ${versionLabel} to ${pulled.projectDir}. Run \`cd ${pulled.projectDir} && npm install && notis apps dev\` to start editing.`
15240
+ humanSummary: `Pulled ${versionLabel} to ${pulled.projectDir}. Increment package.json notisAppVersion above the pulled release, then run \`cd ${pulled.projectDir} && npm install && notis apps dev\` to substitute the online bundle.`
14834
15241
  });
14835
15242
  }
14836
15243
  function updateLinkedDeployState(projectDir, linkedState, appId, version, profileKey = null) {
@@ -15212,7 +15619,7 @@ var appsCommandSpecs = [
15212
15619
  {
15213
15620
  command_path: ["apps", "dev"],
15214
15621
  summary: "Register a development root and connect its apps to the shared local development host.",
15215
- when_to_use: "Run this once for any folder that should be watched permanently. The folder itself, direct child apps, and apps/* are discovered automatically by every signed-in Notis Desktop instance.",
15622
+ when_to_use: "Run this once for any folder that should be watched permanently. The folder itself, direct child apps, and apps/* are discovered automatically by every signed-in Notis Desktop instance. A linked app substitutes its online bundle only when local notisAppVersion is strictly greater than the installed release.",
15216
15623
  args_schema: {
15217
15624
  arguments: [
15218
15625
  { token: "[dir]", key: "dir", description: "Project directory or monorepo root (default: current dir)." }
@@ -15374,7 +15781,7 @@ var appsCommandSpecs = [
15374
15781
  {
15375
15782
  command_path: ["apps", "pull"],
15376
15783
  summary: "Download a Notis app source snapshot into a local project folder.",
15377
- when_to_use: "Edit an installed app locally. Pulls the persisted source, links the directory to the app/version, then continue with npm install, notis apps dev, notis apps build, and notis apps deploy.",
15784
+ when_to_use: "Edit an installed app locally. Preserve any local edits, pull and link the latest persisted source, then increment package.json notisAppVersion above that release before notis apps dev; continue with build and deploy.",
15378
15785
  args_schema: {
15379
15786
  arguments: [
15380
15787
  { token: "<app-id>", description: "Remote app ID to pull." },
@@ -15482,7 +15889,7 @@ var appsCommandSpecs = [
15482
15889
  ];
15483
15890
 
15484
15891
  // src/command-specs/tools.js
15485
- import { accessSync, constants as fsConstants2, createReadStream as createReadStream2, existsSync as existsSync11, readFileSync as readFileSync13, statSync as statSync6 } from "node:fs";
15892
+ import { accessSync, constants as fsConstants2, createReadStream as createReadStream2, existsSync as existsSync11, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
15486
15893
  import { createHash as createHash3 } from "node:crypto";
15487
15894
  import { basename as basename4 } from "node:path";
15488
15895
  async function resolveJsonInput(value, label) {
@@ -15499,7 +15906,7 @@ async function resolveJsonInput(value, label) {
15499
15906
  if (!existsSync11(filePath)) {
15500
15907
  throw usageError(`File not found: ${filePath}`);
15501
15908
  }
15502
- return parseJson(readFileSync13(filePath, "utf-8"), label);
15909
+ return parseJson(readFileSync14(filePath, "utf-8"), label);
15503
15910
  }
15504
15911
  return parseJson(value, label);
15505
15912
  }
@@ -16373,7 +16780,7 @@ var metaCommandSpecs = [
16373
16780
  ];
16374
16781
 
16375
16782
  // src/command-specs/onboarding.js
16376
- import { readFileSync as readFileSync16 } from "node:fs";
16783
+ import { readFileSync as readFileSync17 } from "node:fs";
16377
16784
  import { dirname as dirname15, join as join15 } from "node:path";
16378
16785
  import { fileURLToPath as fileURLToPath8 } from "node:url";
16379
16786
 
@@ -16385,10 +16792,10 @@ import { basename as basename5 } from "node:path";
16385
16792
  import {
16386
16793
  chmodSync,
16387
16794
  existsSync as existsSync12,
16388
- mkdirSync as mkdirSync11,
16389
- readFileSync as readFileSync14,
16795
+ mkdirSync as mkdirSync12,
16796
+ readFileSync as readFileSync15,
16390
16797
  readdirSync as readdirSync7,
16391
- renameSync as renameSync7,
16798
+ renameSync as renameSync8,
16392
16799
  writeFileSync as writeFileSync10
16393
16800
  } from "node:fs";
16394
16801
  import { createHash as createHash4 } from "node:crypto";
@@ -16405,13 +16812,13 @@ var MANAGED_HOOK_MARKER = "--notis-managed-agent-hook";
16405
16812
  var LEGACY_MANAGED_HOOK_MARKER = "NOTIS_MANAGED_AGENT_HOOK=1";
16406
16813
  var AGENT_IDS = Object.freeze(["codex", "claude-code"]);
16407
16814
  function atomicWrite(filePath, contents, mode = 384) {
16408
- mkdirSync11(dirname13(filePath), { recursive: true, mode: 448 });
16815
+ mkdirSync12(dirname13(filePath), { recursive: true, mode: 448 });
16409
16816
  const temporaryPath = `${filePath}.notis-${process.pid}-${Date.now()}.tmp`;
16410
16817
  writeFileSync10(temporaryPath, contents, { mode });
16411
- renameSync7(temporaryPath, filePath);
16818
+ renameSync8(temporaryPath, filePath);
16412
16819
  }
16413
16820
  function readText(filePath) {
16414
- return existsSync12(filePath) ? readFileSync14(filePath, "utf-8") : "";
16821
+ return existsSync12(filePath) ? readFileSync15(filePath, "utf-8") : "";
16415
16822
  }
16416
16823
  function activeCodexInstructionsPath(home) {
16417
16824
  const overridePath = join13(home, ".codex", "AGENTS.override.md");
@@ -16454,7 +16861,7 @@ function upsertInstructionBlock(filePath, block) {
16454
16861
  }
16455
16862
  function readJsonObject(filePath) {
16456
16863
  if (!existsSync12(filePath)) return {};
16457
- const raw = readFileSync14(filePath, "utf-8");
16864
+ const raw = readFileSync15(filePath, "utf-8");
16458
16865
  try {
16459
16866
  const parsed = JSON.parse(raw);
16460
16867
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -16506,7 +16913,7 @@ function installManagedHookRuntime({
16506
16913
  nodePath = process.execPath,
16507
16914
  platform = process.platform
16508
16915
  } = {}) {
16509
- const bundle = readFileSync14(bundlePath);
16916
+ const bundle = readFileSync15(bundlePath);
16510
16917
  const digest = createHash4("sha256").update(bundle).digest("hex");
16511
16918
  const runtimePath = join13(
16512
16919
  home,
@@ -16516,7 +16923,7 @@ function installManagedHookRuntime({
16516
16923
  digest,
16517
16924
  "notis-agent-hook.mjs"
16518
16925
  );
16519
- const existingDigest = existsSync12(runtimePath) ? createHash4("sha256").update(readFileSync14(runtimePath)).digest("hex") : null;
16926
+ const existingDigest = existsSync12(runtimePath) ? createHash4("sha256").update(readFileSync15(runtimePath)).digest("hex") : null;
16520
16927
  if (existingDigest !== digest) atomicWrite(runtimePath, bundle, 320);
16521
16928
  chmodSync(runtimePath, 320);
16522
16929
  const launcherPath = join13(
@@ -16635,7 +17042,7 @@ function installAgentSetup({
16635
17042
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(String(profileName || ""))) {
16636
17043
  throw new Error("A valid authenticated Notis profile is required for agent setup");
16637
17044
  }
16638
- const instructions = readFileSync14(INSTRUCTIONS_PATH, "utf-8").trim();
17045
+ const instructions = readFileSync15(INSTRUCTIONS_PATH, "utf-8").trim();
16639
17046
  const results = [];
16640
17047
  const shouldRepairManagedHooks = memoryHooks === true || memoryHooks === null && agents.some((agentId) => fileHasManagedNotisHook(agentPaths(agentId, home).hooks));
16641
17048
  const hookRuntime = shouldRepairManagedHooks ? installManagedHookRuntime({ home, platform }) : null;
@@ -16680,9 +17087,9 @@ function installAgentSetup({
16680
17087
  import { createHash as createHash5 } from "node:crypto";
16681
17088
  import {
16682
17089
  existsSync as existsSync13,
16683
- mkdirSync as mkdirSync12,
16684
- readFileSync as readFileSync15,
16685
- renameSync as renameSync8,
17090
+ mkdirSync as mkdirSync13,
17091
+ readFileSync as readFileSync16,
17092
+ renameSync as renameSync9,
16686
17093
  writeFileSync as writeFileSync11
16687
17094
  } from "node:fs";
16688
17095
  import { homedir as homedir8 } from "node:os";
@@ -16700,7 +17107,7 @@ function readState(sessionId, home) {
16700
17107
  const filePath = statePath(sessionId, home);
16701
17108
  if (!filePath || !existsSync13(filePath)) return { seen: [], pending: null };
16702
17109
  try {
16703
- const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
17110
+ const parsed = JSON.parse(readFileSync16(filePath, "utf-8"));
16704
17111
  return parsed && typeof parsed === "object" ? {
16705
17112
  seen: Array.isArray(parsed.seen) ? parsed.seen.filter((item) => typeof item === "string") : [],
16706
17113
  pending: parsed.pending && typeof parsed.pending === "object" ? parsed.pending : null,
@@ -16730,11 +17137,11 @@ function stateForInput(sessionId, input, home) {
16730
17137
  function writeState(sessionId, state, home) {
16731
17138
  const filePath = statePath(sessionId, home);
16732
17139
  if (!filePath) return;
16733
- mkdirSync12(dirname14(filePath), { recursive: true, mode: 448 });
17140
+ mkdirSync13(dirname14(filePath), { recursive: true, mode: 448 });
16734
17141
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
16735
17142
  writeFileSync11(temporaryPath, `${JSON.stringify(state)}
16736
17143
  `, { mode: 384 });
16737
- renameSync8(temporaryPath, filePath);
17144
+ renameSync9(temporaryPath, filePath);
16738
17145
  }
16739
17146
  function rememberPendingTurn(input, home = homedir8()) {
16740
17147
  const sessionId = input?.session_id;
@@ -17276,7 +17683,7 @@ async function fetchBrief(apiBase, timeoutMs) {
17276
17683
  } catch {
17277
17684
  }
17278
17685
  try {
17279
- return { markdown: readFileSync16(BUNDLED_BRIEF_PATH, "utf-8"), source: "bundled" };
17686
+ return { markdown: readFileSync17(BUNDLED_BRIEF_PATH, "utf-8"), source: "bundled" };
17280
17687
  } catch {
17281
17688
  return { markdown: null, source: null };
17282
17689
  }
@@ -17444,7 +17851,7 @@ var onboardingCommandSpecs = [
17444
17851
 
17445
17852
  // src/command-specs/diagnostics.js
17446
17853
  import { createHash as createHash7, randomUUID as randomUUID10 } from "node:crypto";
17447
- import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
17854
+ import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
17448
17855
  var ENTITLEMENT_FIELDS = [
17449
17856
  "created_at",
17450
17857
  "included_credit_seat_count",
@@ -17850,7 +18257,7 @@ ORDER BY i.created_at ASC;`;
17850
18257
  function traceFileDiagnostics(path3) {
17851
18258
  if (!path3) return null;
17852
18259
  if (!existsSync14(path3)) throw usageError(`Trace file not found: ${path3}`);
17853
- const trace = JSON.parse(readFileSync17(path3, "utf-8"));
18260
+ const trace = JSON.parse(readFileSync18(path3, "utf-8"));
17854
18261
  const text = JSON.stringify(trace).toLowerCase();
17855
18262
  const modelCounts = {};
17856
18263
  const generationIds = /* @__PURE__ */ new Set();
@@ -18044,13 +18451,13 @@ var diagnosticCommandSpecs = [
18044
18451
  import { createHash as createHash8, randomUUID as randomUUID11 } from "node:crypto";
18045
18452
  import {
18046
18453
  mkdtempSync as mkdtempSync4,
18047
- readFileSync as readFileSync18,
18048
- rmSync as rmSync10,
18454
+ readFileSync as readFileSync19,
18455
+ rmSync as rmSync11,
18049
18456
  writeFileSync as writeFileSync12
18050
18457
  } from "node:fs";
18051
18458
  import { tmpdir as tmpdir3 } from "node:os";
18052
18459
  import { basename as basename6, join as join16 } from "node:path";
18053
- import { setTimeout as delay2 } from "node:timers/promises";
18460
+ import { setTimeout as delay3 } from "node:timers/promises";
18054
18461
  function discoveryNames(payload) {
18055
18462
  return [...new Set(
18056
18463
  (payload.results || []).flatMap((result) => [
@@ -18089,7 +18496,7 @@ async function fetchDiscoveryWithRetry(ctx, useCase, knownFields) {
18089
18496
  message: `Tool discovery transiently failed; retrying (${attempt}/${maxAttempts - 1})`,
18090
18497
  requestId: error?.details?.request_id || null
18091
18498
  });
18092
- await delay2(250 * 2 ** (attempt - 1));
18499
+ await delay3(250 * 2 ** (attempt - 1));
18093
18500
  }
18094
18501
  }
18095
18502
  throw usageError("Dropbox tool discovery exhausted its retries.");
@@ -18145,7 +18552,7 @@ unicode=\xE9t\xE9 Z\xFCrich \u6771\u4EAC
18145
18552
  );
18146
18553
  }
18147
18554
  const localHash = await hashFileSha256(fixturePath);
18148
- const localBytes = readFileSync18(fixturePath);
18555
+ const localBytes = readFileSync19(fixturePath);
18149
18556
  const destination = remotePath(
18150
18557
  ctx.options.remoteFolder,
18151
18558
  `notis-file-upload-${marker}-${basename6(fixturePath)}`
@@ -18358,7 +18765,7 @@ unicode=\xE9t\xE9 Z\xFCrich \u6771\u4EAC
18358
18765
  result.cleanup.deleted = false;
18359
18766
  }
18360
18767
  }
18361
- rmSync10(tempDir, { recursive: true, force: true });
18768
+ rmSync11(tempDir, { recursive: true, force: true });
18362
18769
  }
18363
18770
  }
18364
18771
  var smokeCommandSpecs = [
@@ -18666,7 +19073,7 @@ var profileCommandSpecs = [
18666
19073
 
18667
19074
  // src/runtime/git.js
18668
19075
  import { spawnSync as spawnSync2 } from "node:child_process";
18669
- import { lstatSync as lstatSync6, readFileSync as readFileSync19 } from "node:fs";
19076
+ import { lstatSync as lstatSync6, readFileSync as readFileSync20 } from "node:fs";
18670
19077
  import { basename as basename7, relative as relative4, resolve as resolve8, sep as sep2 } from "node:path";
18671
19078
  var GIT_TIMEOUT_MS = 12e4;
18672
19079
  var MAX_SECRET_SCAN_BYTES = 1e6;
@@ -18735,7 +19142,7 @@ function sensitiveAutoCommitFiles(repository) {
18735
19142
  try {
18736
19143
  const stat2 = lstatSync6(absolute);
18737
19144
  if (!stat2.isFile() || stat2.size > MAX_SECRET_SCAN_BYTES) continue;
18738
- const content = readFileSync19(absolute, "utf8");
19145
+ const content = readFileSync20(absolute, "utf8");
18739
19146
  if (SENSITIVE_CONTENT.some((pattern) => pattern.test(content))) sensitive.push(path3);
18740
19147
  } catch {
18741
19148
  }
@@ -19121,10 +19528,10 @@ import {
19121
19528
  cpSync as cpSync2,
19122
19529
  existsSync as existsSync15,
19123
19530
  lstatSync as lstatSync7,
19124
- mkdirSync as mkdirSync13,
19125
- readlinkSync,
19126
- renameSync as renameSync9,
19127
- rmSync as rmSync11,
19531
+ mkdirSync as mkdirSync14,
19532
+ readlinkSync as readlinkSync2,
19533
+ renameSync as renameSync10,
19534
+ rmSync as rmSync12,
19128
19535
  symlinkSync
19129
19536
  } from "node:fs";
19130
19537
  import { homedir as homedir9 } from "node:os";
@@ -19168,35 +19575,35 @@ function getBaseSkillPaths({ home = homedir9(), userId = null } = {}) {
19168
19575
  function sameLinkTarget(linkPath, expectedTarget) {
19169
19576
  try {
19170
19577
  if (!lstatSync7(linkPath).isSymbolicLink()) return false;
19171
- return resolve9(dirname16(linkPath), readlinkSync(linkPath)) === resolve9(expectedTarget);
19578
+ return resolve9(dirname16(linkPath), readlinkSync2(linkPath)) === resolve9(expectedTarget);
19172
19579
  } catch {
19173
19580
  return false;
19174
19581
  }
19175
19582
  }
19176
19583
  function backupConflict(linkPath, backupRoot, targetLabel, skillName, now) {
19177
19584
  const backupPath = join17(backupRoot, String(now), targetLabel, skillName);
19178
- mkdirSync13(dirname16(backupPath), { recursive: true, mode: 448 });
19179
- renameSync9(linkPath, backupPath);
19585
+ mkdirSync14(dirname16(backupPath), { recursive: true, mode: 448 });
19586
+ renameSync10(linkPath, backupPath);
19180
19587
  return backupPath;
19181
19588
  }
19182
19589
  function replaceDirectory(source, destination) {
19183
- mkdirSync13(dirname16(destination), { recursive: true, mode: 448 });
19590
+ mkdirSync14(dirname16(destination), { recursive: true, mode: 448 });
19184
19591
  const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
19185
19592
  const previous = `${destination}.previous-${process.pid}-${Date.now()}`;
19186
- rmSync11(temporary, { recursive: true, force: true });
19593
+ rmSync12(temporary, { recursive: true, force: true });
19187
19594
  cpSync2(source, temporary, { recursive: true });
19188
19595
  let movedPrevious = false;
19189
19596
  try {
19190
19597
  if (existsSync15(destination)) {
19191
- renameSync9(destination, previous);
19598
+ renameSync10(destination, previous);
19192
19599
  movedPrevious = true;
19193
19600
  }
19194
- renameSync9(temporary, destination);
19195
- if (movedPrevious) rmSync11(previous, { recursive: true, force: true });
19601
+ renameSync10(temporary, destination);
19602
+ if (movedPrevious) rmSync12(previous, { recursive: true, force: true });
19196
19603
  } catch (error) {
19197
- rmSync11(temporary, { recursive: true, force: true });
19604
+ rmSync12(temporary, { recursive: true, force: true });
19198
19605
  if (movedPrevious && !existsSync15(destination) && existsSync15(previous)) {
19199
- renameSync9(previous, destination);
19606
+ renameSync10(previous, destination);
19200
19607
  }
19201
19608
  throw error;
19202
19609
  }
@@ -19224,7 +19631,7 @@ function reconcileBaseSkills({
19224
19631
  result.installed += 1;
19225
19632
  }
19226
19633
  for (const targetRoot of paths.targetRoots) {
19227
- mkdirSync13(targetRoot, { recursive: true, mode: 448 });
19634
+ mkdirSync14(targetRoot, { recursive: true, mode: 448 });
19228
19635
  const targetLabel = relative5(home, targetRoot).replaceAll("/", "_") || "home";
19229
19636
  for (const name of BASE_SKILL_NAMES) {
19230
19637
  const skillTarget = join17(paths.baseRoot, name);
@@ -19294,7 +19701,7 @@ function processIsAlive2(pid) {
19294
19701
  return error?.code === "EPERM";
19295
19702
  }
19296
19703
  }
19297
- function delay3(milliseconds) {
19704
+ function delay4(milliseconds) {
19298
19705
  return new Promise((resolve10) => setTimeout(resolve10, milliseconds));
19299
19706
  }
19300
19707
  async function quarantineStaleLock(lockDirectory, snapshot) {
@@ -19315,6 +19722,20 @@ async function writeLockOwnerAtomically(lockDirectory, owner) {
19315
19722
  await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 384 });
19316
19723
  await rename(temporaryOwnerPath, ownerPath);
19317
19724
  }
19725
+ async function releaseOwnedLock(lockDirectory, ownerId) {
19726
+ const owner = await lockSnapshot(lockDirectory);
19727
+ if (owner?.id !== ownerId) return;
19728
+ const quarantineRoot = join18(dirname17(lockDirectory), ".stale-operation-locks");
19729
+ const releasedDirectory = join18(quarantineRoot, `released.${ownerId}`);
19730
+ await mkdir(quarantineRoot, { recursive: true, mode: 448 });
19731
+ try {
19732
+ await rename(lockDirectory, releasedDirectory);
19733
+ } catch (error) {
19734
+ if (error?.code === "ENOENT") return;
19735
+ throw error;
19736
+ }
19737
+ await rm(releasedDirectory, { recursive: true, force: true });
19738
+ }
19318
19739
  async function withSkillSyncLock(callback, {
19319
19740
  home = homedir10(),
19320
19741
  timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
@@ -19345,7 +19766,7 @@ async function withSkillSyncLock(callback, {
19345
19766
  }
19346
19767
  const observed = await lockSnapshot(lockDirectory);
19347
19768
  if (observed && now() - observed.at > staleMs && !processIsAlive2(observed.pid)) {
19348
- await delay3(Math.max(pollMs, 10));
19769
+ await delay4(Math.max(pollMs, 10));
19349
19770
  const current = await lockSnapshot(lockDirectory);
19350
19771
  if (sameLockSnapshot(observed, current) && !processIsAlive2(current?.pid)) {
19351
19772
  if (await quarantineStaleLock(lockDirectory, current)) continue;
@@ -19354,7 +19775,7 @@ async function withSkillSyncLock(callback, {
19354
19775
  if (now() >= deadline) {
19355
19776
  throw new Error("Timed out waiting for another Notis skill sync to finish.");
19356
19777
  }
19357
- await delay3(pollMs);
19778
+ await delay4(pollMs);
19358
19779
  }
19359
19780
  const heartbeatMs = Math.max(10, Math.min(3e4, Math.floor(staleMs / 3)));
19360
19781
  let heartbeatStopped = false;
@@ -19384,10 +19805,7 @@ async function withSkillSyncLock(callback, {
19384
19805
  heartbeatStopped = true;
19385
19806
  clearInterval(heartbeat);
19386
19807
  await heartbeatInFlight;
19387
- const owner = await lockSnapshot(lockDirectory);
19388
- if (owner?.id === ownerId) {
19389
- await rm(lockDirectory, { recursive: true, force: true });
19390
- }
19808
+ await releaseOwnedLock(lockDirectory, ownerId);
19391
19809
  }
19392
19810
  }
19393
19811
  async function reconcileAllSkills({
@@ -19573,7 +19991,7 @@ async function reportCliCommand({
19573
19991
  function readCliVersion() {
19574
19992
  try {
19575
19993
  const manifestPath = join19(dirname18(fileURLToPath10(import.meta.url)), "..", "package.json");
19576
- const version = JSON.parse(readFileSync20(manifestPath, "utf-8")).version;
19994
+ const version = JSON.parse(readFileSync21(manifestPath, "utf-8")).version;
19577
19995
  if (typeof version === "string" && version.trim()) {
19578
19996
  return version.trim();
19579
19997
  }