openspecui 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,15 @@ import { Http2ServerRequest } from "http2";
4
4
  import { Readable } from "stream";
5
5
  import crypto from "crypto";
6
6
  import { EventEmitter } from "events";
7
- import { mkdir, readFile, readdir, rename, stat, writeFile } from "fs/promises";
7
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
8
8
  import { join } from "path";
9
- import { watch } from "fs";
10
- import { existsSync, readFileSync as readFileSync$1, statSync as statSync$1 } from "node:fs";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { readFile as readFile$1, readdir, stat } from "node:fs/promises";
11
+ import { dirname as dirname$1, join as join$1, resolve as resolve$1 } from "node:path";
12
+ import { existsSync, readFileSync, statSync, watch } from "node:fs";
13
+ import { watch as watch$1 } from "fs";
14
+ import { spawn } from "child_process";
11
15
  import { createServer as createServer$1 } from "node:net";
12
- import { dirname as dirname$1, join as join$1 } from "node:path";
13
16
  import { fileURLToPath } from "node:url";
14
17
 
15
18
  //#region rolldown:runtime
@@ -376,7 +379,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
376
379
  });
377
380
  if (!chunk) {
378
381
  if (i === 1) {
379
- await new Promise((resolve$1) => setTimeout(resolve$1));
382
+ await new Promise((resolve$2) => setTimeout(resolve$2));
380
383
  maxReadCount = 3;
381
384
  continue;
382
385
  }
@@ -6653,8 +6656,8 @@ var Unpromise = class Unpromise$1 {
6653
6656
  status: "fulfilled",
6654
6657
  value
6655
6658
  };
6656
- subscribers === null || subscribers === void 0 || subscribers.forEach(({ resolve: resolve$1 }) => {
6657
- resolve$1(value);
6659
+ subscribers === null || subscribers === void 0 || subscribers.forEach(({ resolve: resolve$2 }) => {
6660
+ resolve$2(value);
6658
6661
  });
6659
6662
  });
6660
6663
  if ("catch" in thenReturn) thenReturn.catch((reason) => {
@@ -6801,14 +6804,14 @@ function resolveSelfTuple(promise) {
6801
6804
  /** VENDORED (Future) PROMISE UTILITIES */
6802
6805
  /** Reference implementation of https://github.com/tc39/proposal-promise-with-resolvers */
6803
6806
  function withResolvers() {
6804
- let resolve$1;
6807
+ let resolve$2;
6805
6808
  let reject;
6806
6809
  return {
6807
6810
  promise: new Promise((_resolve, _reject) => {
6808
- resolve$1 = _resolve;
6811
+ resolve$2 = _resolve;
6809
6812
  reject = _reject;
6810
6813
  }),
6811
- resolve: resolve$1,
6814
+ resolve: resolve$2,
6812
6815
  reject
6813
6816
  };
6814
6817
  }
@@ -6864,8 +6867,8 @@ function timerResource(ms) {
6864
6867
  let timer = null;
6865
6868
  return makeResource({ start() {
6866
6869
  if (timer) throw new Error("Timer already started");
6867
- return new Promise((resolve$1) => {
6868
- timer = setTimeout(() => resolve$1(disposablePromiseTimerResult), ms);
6870
+ return new Promise((resolve$2) => {
6871
+ timer = setTimeout(() => resolve$2(disposablePromiseTimerResult), ms);
6869
6872
  });
6870
6873
  } }, () => {
6871
6874
  if (timer) clearTimeout(timer);
@@ -7082,14 +7085,14 @@ function _takeWithGrace() {
7082
7085
  return _takeWithGrace.apply(this, arguments);
7083
7086
  }
7084
7087
  function createDeferred() {
7085
- let resolve$1;
7088
+ let resolve$2;
7086
7089
  let reject;
7087
7090
  return {
7088
7091
  promise: new Promise((res, rej) => {
7089
- resolve$1 = res;
7092
+ resolve$2 = res;
7090
7093
  reject = rej;
7091
7094
  }),
7092
- resolve: resolve$1,
7095
+ resolve: resolve$2,
7093
7096
  reject
7094
7097
  };
7095
7098
  }
@@ -8703,8 +8706,8 @@ function getWSConnectionHandler(opts) {
8703
8706
  try {
8704
8707
  var _usingCtx = (0, import_usingCtx.default)();
8705
8708
  const iterator = _usingCtx.a(iteratorResource(iterable));
8706
- const abortPromise = new Promise((resolve$1) => {
8707
- abortController$1.signal.onabort = () => resolve$1("abort");
8709
+ const abortPromise = new Promise((resolve$2) => {
8710
+ abortController$1.signal.onabort = () => resolve$2("abort");
8708
8711
  });
8709
8712
  let next;
8710
8713
  let result$1;
@@ -12716,6 +12719,390 @@ var Validator = class {
12716
12719
  }
12717
12720
  };
12718
12721
 
12722
+ //#endregion
12723
+ //#region ../core/src/reactive-fs/reactive-state.ts
12724
+ /**
12725
+ * 全局的 AsyncLocalStorage,用于在异步调用链中传递 ReactiveContext
12726
+ * 这是实现依赖收集的核心机制
12727
+ */
12728
+ const contextStorage = new AsyncLocalStorage();
12729
+ /**
12730
+ * 响应式状态类,类似 Signal.State
12731
+ *
12732
+ * 核心机制:
12733
+ * - get() 时自动注册到当前 ReactiveContext 的依赖列表
12734
+ * - set() 时如果值变化,通知所有依赖的 Context
12735
+ */
12736
+ var ReactiveState = class {
12737
+ currentValue;
12738
+ equals;
12739
+ /** 所有依赖此状态的 Context */
12740
+ subscribers = /* @__PURE__ */ new Set();
12741
+ constructor(initialValue, options) {
12742
+ this.currentValue = initialValue;
12743
+ this.equals = options?.equals ?? ((a, b) => a === b);
12744
+ }
12745
+ /**
12746
+ * 获取当前值
12747
+ * 如果在 ReactiveContext 中调用,会自动注册依赖
12748
+ */
12749
+ get() {
12750
+ const context = contextStorage.getStore();
12751
+ if (context) {
12752
+ context.track(this);
12753
+ this.subscribers.add(context);
12754
+ }
12755
+ return this.currentValue;
12756
+ }
12757
+ /**
12758
+ * 设置新值
12759
+ * 如果值变化,通知所有订阅者
12760
+ * @returns 是否发生了变化
12761
+ */
12762
+ set(newValue) {
12763
+ if (this.equals(this.currentValue, newValue)) return false;
12764
+ this.currentValue = newValue;
12765
+ for (const context of this.subscribers) context.notifyChange();
12766
+ return true;
12767
+ }
12768
+ /**
12769
+ * 取消订阅
12770
+ * 当 Context 销毁时调用
12771
+ */
12772
+ unsubscribe(context) {
12773
+ this.subscribers.delete(context);
12774
+ }
12775
+ /**
12776
+ * 获取当前订阅者数量(用于调试)
12777
+ */
12778
+ get subscriberCount() {
12779
+ return this.subscribers.size;
12780
+ }
12781
+ };
12782
+
12783
+ //#endregion
12784
+ //#region ../core/src/reactive-fs/reactive-context.ts
12785
+ function createPromiseWithResolvers() {
12786
+ let resolve$2;
12787
+ let reject;
12788
+ return {
12789
+ promise: new Promise((res, rej) => {
12790
+ resolve$2 = res;
12791
+ reject = rej;
12792
+ }),
12793
+ resolve: resolve$2,
12794
+ reject
12795
+ };
12796
+ }
12797
+ /**
12798
+ * 响应式上下文,管理依赖收集和变更通知
12799
+ *
12800
+ * 核心机制:
12801
+ * - 在 stream() 中执行任务时,通过 AsyncLocalStorage 传递 this
12802
+ * - 任务中的所有 ReactiveState.get() 调用都会自动注册依赖
12803
+ * - 当任何依赖变更时,重新执行任务并 yield 新结果
12804
+ */
12805
+ var ReactiveContext = class {
12806
+ /** 当前追踪的依赖 */
12807
+ dependencies = /* @__PURE__ */ new Set();
12808
+ /** 等待变更的 Promise */
12809
+ changePromise;
12810
+ /** 是否已销毁 */
12811
+ destroyed = false;
12812
+ /**
12813
+ * 追踪依赖
12814
+ * 由 ReactiveState.get() 调用
12815
+ */
12816
+ track(state) {
12817
+ if (!this.destroyed) this.dependencies.add(state);
12818
+ }
12819
+ /**
12820
+ * 通知变更
12821
+ * 由 ReactiveState.set() 调用
12822
+ */
12823
+ notifyChange() {
12824
+ if (!this.destroyed && this.changePromise) this.changePromise.resolve();
12825
+ }
12826
+ /**
12827
+ * 运行响应式任务流
12828
+ * 每次依赖变更时重新执行任务并 yield 结果
12829
+ *
12830
+ * @param task 要执行的异步任务
12831
+ * @param signal 用于取消的 AbortSignal
12832
+ */
12833
+ async *stream(task, signal) {
12834
+ try {
12835
+ while (!signal?.aborted && !this.destroyed) {
12836
+ this.clearDependencies();
12837
+ this.changePromise = createPromiseWithResolvers();
12838
+ yield await contextStorage.run(this, task);
12839
+ if (this.dependencies.size === 0) break;
12840
+ await Promise.race([this.changePromise.promise, signal ? this.waitForAbort(signal) : new Promise(() => {})]);
12841
+ if (signal?.aborted) break;
12842
+ }
12843
+ } finally {
12844
+ this.destroy();
12845
+ }
12846
+ }
12847
+ /**
12848
+ * 执行一次任务(非响应式)
12849
+ * 用于初始数据获取
12850
+ */
12851
+ async runOnce(task) {
12852
+ return contextStorage.run(this, task);
12853
+ }
12854
+ /**
12855
+ * 清理依赖
12856
+ */
12857
+ clearDependencies() {
12858
+ for (const state of this.dependencies) state.unsubscribe(this);
12859
+ this.dependencies.clear();
12860
+ }
12861
+ /**
12862
+ * 销毁上下文
12863
+ * @param reason 可选的销毁原因,如果提供则 reject changePromise
12864
+ */
12865
+ destroy(reason) {
12866
+ this.destroyed = true;
12867
+ this.clearDependencies();
12868
+ if (reason && this.changePromise) this.changePromise.reject(reason);
12869
+ this.changePromise = void 0;
12870
+ }
12871
+ /**
12872
+ * 等待 AbortSignal
12873
+ */
12874
+ waitForAbort(signal) {
12875
+ return new Promise((_, reject) => {
12876
+ if (signal.aborted) {
12877
+ reject(new DOMException("Aborted", "AbortError"));
12878
+ return;
12879
+ }
12880
+ signal.addEventListener("abort", () => {
12881
+ reject(new DOMException("Aborted", "AbortError"));
12882
+ });
12883
+ });
12884
+ }
12885
+ };
12886
+
12887
+ //#endregion
12888
+ //#region ../core/src/reactive-fs/watcher-pool.ts
12889
+ /** 全局监听器池,共享同一路径的监听器 */
12890
+ const watcherPool = /* @__PURE__ */ new Map();
12891
+ /** 防抖定时器 */
12892
+ const debounceTimers = /* @__PURE__ */ new Map();
12893
+ /** 默认防抖时间 (ms) */
12894
+ const DEBOUNCE_MS = 100;
12895
+ /**
12896
+ * 获取或创建文件/目录监听器
12897
+ *
12898
+ * 特性:
12899
+ * - 同一路径共享监听器
12900
+ * - 引用计数管理生命周期
12901
+ * - 内置防抖机制
12902
+ *
12903
+ * @param path 要监听的路径
12904
+ * @param onChange 变更回调
12905
+ * @param options 监听选项
12906
+ * @returns 释放函数,调用后取消订阅
12907
+ */
12908
+ function acquireWatcher(path$1, onChange, options = {}) {
12909
+ const normalizedPath = resolve$1(path$1);
12910
+ const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
12911
+ let entry = watcherPool.get(normalizedPath);
12912
+ if (!entry) {
12913
+ const watcher = watch(normalizedPath, {
12914
+ recursive: options.recursive ?? false,
12915
+ persistent: false
12916
+ }, () => {
12917
+ const existingTimer = debounceTimers.get(normalizedPath);
12918
+ if (existingTimer) clearTimeout(existingTimer);
12919
+ const timer = setTimeout(() => {
12920
+ debounceTimers.delete(normalizedPath);
12921
+ const currentEntry = watcherPool.get(normalizedPath);
12922
+ if (currentEntry) for (const cb of currentEntry.callbacks) try {
12923
+ cb();
12924
+ } catch (err) {
12925
+ console.error(`Watcher callback error for ${normalizedPath}:`, err);
12926
+ }
12927
+ }, debounceMs);
12928
+ debounceTimers.set(normalizedPath, timer);
12929
+ });
12930
+ watcher.on("error", (err) => {
12931
+ console.error(`Watcher error for ${normalizedPath}:`, err);
12932
+ });
12933
+ entry = {
12934
+ watcher,
12935
+ refCount: 0,
12936
+ callbacks: /* @__PURE__ */ new Set()
12937
+ };
12938
+ watcherPool.set(normalizedPath, entry);
12939
+ }
12940
+ entry.refCount++;
12941
+ entry.callbacks.add(onChange);
12942
+ return () => {
12943
+ const currentEntry = watcherPool.get(normalizedPath);
12944
+ if (!currentEntry) return;
12945
+ currentEntry.callbacks.delete(onChange);
12946
+ currentEntry.refCount--;
12947
+ if (currentEntry.refCount === 0) {
12948
+ currentEntry.watcher.close();
12949
+ watcherPool.delete(normalizedPath);
12950
+ const timer = debounceTimers.get(normalizedPath);
12951
+ if (timer) {
12952
+ clearTimeout(timer);
12953
+ debounceTimers.delete(normalizedPath);
12954
+ }
12955
+ }
12956
+ };
12957
+ }
12958
+
12959
+ //#endregion
12960
+ //#region ../core/src/reactive-fs/reactive-fs.ts
12961
+ /** 状态缓存:路径 -> ReactiveState */
12962
+ const stateCache = /* @__PURE__ */ new Map();
12963
+ /** 监听器释放函数缓存 */
12964
+ const releaseCache = /* @__PURE__ */ new Map();
12965
+ /**
12966
+ * 响应式读取文件内容
12967
+ *
12968
+ * 特性:
12969
+ * - 自动注册文件监听
12970
+ * - 文件变更时自动更新状态
12971
+ * - 在 ReactiveContext 中调用时自动追踪依赖
12972
+ *
12973
+ * @param filepath 文件路径
12974
+ * @returns 文件内容,文件不存在时返回 null
12975
+ */
12976
+ async function reactiveReadFile(filepath) {
12977
+ const normalizedPath = resolve$1(filepath);
12978
+ const key = `file:${normalizedPath}`;
12979
+ const getValue = async () => {
12980
+ try {
12981
+ return await readFile$1(normalizedPath, "utf-8");
12982
+ } catch {
12983
+ return null;
12984
+ }
12985
+ };
12986
+ let state = stateCache.get(key);
12987
+ if (!state) {
12988
+ state = new ReactiveState(await getValue());
12989
+ stateCache.set(key, state);
12990
+ const release = acquireWatcher(dirname$1(normalizedPath), async () => {
12991
+ const newValue = await getValue();
12992
+ state.set(newValue);
12993
+ });
12994
+ releaseCache.set(key, release);
12995
+ }
12996
+ return state.get();
12997
+ }
12998
+ /**
12999
+ * 响应式读取目录内容
13000
+ *
13001
+ * 特性:
13002
+ * - 自动注册目录监听
13003
+ * - 目录变更时自动更新状态
13004
+ * - 在 ReactiveContext 中调用时自动追踪依赖
13005
+ *
13006
+ * @param dirpath 目录路径
13007
+ * @param options 选项
13008
+ * @returns 目录项名称数组
13009
+ */
13010
+ async function reactiveReadDir(dirpath, options = {}) {
13011
+ const normalizedPath = resolve$1(dirpath);
13012
+ const key = `dir:${normalizedPath}:${JSON.stringify(options)}`;
13013
+ const getValue = async () => {
13014
+ try {
13015
+ return (await readdir(normalizedPath, { withFileTypes: true })).filter((entry) => {
13016
+ if (!options.includeHidden && entry.name.startsWith(".")) return false;
13017
+ if (options.exclude?.includes(entry.name)) return false;
13018
+ if (options.directoriesOnly && !entry.isDirectory()) return false;
13019
+ if (options.filesOnly && !entry.isFile()) return false;
13020
+ return true;
13021
+ }).map((entry) => entry.name);
13022
+ } catch {
13023
+ return [];
13024
+ }
13025
+ };
13026
+ let state = stateCache.get(key);
13027
+ if (!state) {
13028
+ state = new ReactiveState(await getValue(), { equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
13029
+ stateCache.set(key, state);
13030
+ const release = acquireWatcher(normalizedPath, async () => {
13031
+ const newValue = await getValue();
13032
+ state.set(newValue);
13033
+ }, { recursive: true });
13034
+ releaseCache.set(key, release);
13035
+ }
13036
+ return state.get();
13037
+ }
13038
+ /**
13039
+ * 响应式检查路径是否存在
13040
+ *
13041
+ * @param path 路径
13042
+ * @returns 是否存在
13043
+ */
13044
+ async function reactiveExists(path$1) {
13045
+ const normalizedPath = resolve$1(path$1);
13046
+ const key = `exists:${normalizedPath}`;
13047
+ const getValue = async () => {
13048
+ try {
13049
+ await stat(normalizedPath);
13050
+ return true;
13051
+ } catch {
13052
+ return false;
13053
+ }
13054
+ };
13055
+ let state = stateCache.get(key);
13056
+ if (!state) {
13057
+ state = new ReactiveState(await getValue());
13058
+ stateCache.set(key, state);
13059
+ const release = acquireWatcher(dirname$1(normalizedPath), async () => {
13060
+ const newValue = await getValue();
13061
+ state.set(newValue);
13062
+ });
13063
+ releaseCache.set(key, release);
13064
+ }
13065
+ return state.get();
13066
+ }
13067
+ /**
13068
+ * 响应式获取文件/目录的 stat 信息
13069
+ *
13070
+ * @param path 路径
13071
+ * @returns stat 信息,不存在时返回 null
13072
+ */
13073
+ async function reactiveStat(path$1) {
13074
+ const normalizedPath = resolve$1(path$1);
13075
+ const key = `stat:${normalizedPath}`;
13076
+ const getValue = async () => {
13077
+ try {
13078
+ const s = await stat(normalizedPath);
13079
+ return {
13080
+ isDirectory: s.isDirectory(),
13081
+ isFile: s.isFile(),
13082
+ mtime: s.mtime.getTime(),
13083
+ birthtime: s.birthtime.getTime()
13084
+ };
13085
+ } catch {
13086
+ return null;
13087
+ }
13088
+ };
13089
+ let state = stateCache.get(key);
13090
+ if (!state) {
13091
+ state = new ReactiveState(await getValue(), { equals: (a, b) => {
13092
+ if (a === null && b === null) return true;
13093
+ if (a === null || b === null) return false;
13094
+ return a.isDirectory === b.isDirectory && a.isFile === b.isFile && a.mtime === b.mtime && a.birthtime === b.birthtime;
13095
+ } });
13096
+ stateCache.set(key, state);
13097
+ const release = acquireWatcher(dirname$1(normalizedPath), async () => {
13098
+ const newValue = await getValue();
13099
+ state.set(newValue);
13100
+ });
13101
+ releaseCache.set(key, release);
13102
+ }
13103
+ return state.get();
13104
+ }
13105
+
12719
13106
  //#endregion
12720
13107
  //#region ../core/src/adapter.ts
12721
13108
  /**
@@ -12741,30 +13128,19 @@ var OpenSpecAdapter = class {
12741
13128
  return join(this.changesDir, "archive");
12742
13129
  }
12743
13130
  async isInitialized() {
12744
- try {
12745
- return (await stat(this.openspecDir)).isDirectory();
12746
- } catch {
12747
- return false;
12748
- }
13131
+ return (await reactiveStat(this.openspecDir))?.isDirectory ?? false;
12749
13132
  }
12750
- /** File time info derived from filesystem */
13133
+ /** File time info derived from filesystem (reactive) */
12751
13134
  async getFileTimeInfo(filePath) {
12752
- try {
12753
- const fileStat = await stat(filePath);
12754
- return {
12755
- createdAt: fileStat.birthtime.getTime(),
12756
- updatedAt: fileStat.mtime.getTime()
12757
- };
12758
- } catch {
12759
- return null;
12760
- }
13135
+ const statInfo = await reactiveStat(filePath);
13136
+ if (!statInfo) return null;
13137
+ return {
13138
+ createdAt: statInfo.birthtime,
13139
+ updatedAt: statInfo.mtime
13140
+ };
12761
13141
  }
12762
13142
  async listSpecs() {
12763
- try {
12764
- return (await readdir(this.specsDir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
12765
- } catch {
12766
- return [];
12767
- }
13143
+ return reactiveReadDir(this.specsDir, { directoriesOnly: true });
12768
13144
  }
12769
13145
  /**
12770
13146
  * List specs with metadata (id, name, and time info)
@@ -12787,11 +13163,10 @@ var OpenSpecAdapter = class {
12787
13163
  }))).filter((r) => r !== null).sort((a, b) => b.updatedAt - a.updatedAt);
12788
13164
  }
12789
13165
  async listChanges() {
12790
- try {
12791
- return (await readdir(this.changesDir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "archive").map((e) => e.name);
12792
- } catch {
12793
- return [];
12794
- }
13166
+ return reactiveReadDir(this.changesDir, {
13167
+ directoriesOnly: true,
13168
+ exclude: ["archive"]
13169
+ });
12795
13170
  }
12796
13171
  /**
12797
13172
  * List changes with metadata (id, name, progress, and time info)
@@ -12815,11 +13190,7 @@ var OpenSpecAdapter = class {
12815
13190
  }))).filter((r) => r !== null).sort((a, b) => b.updatedAt - a.updatedAt);
12816
13191
  }
12817
13192
  async listArchivedChanges() {
12818
- try {
12819
- return (await readdir(this.archiveDir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
12820
- } catch {
12821
- return [];
12822
- }
13193
+ return reactiveReadDir(this.archiveDir, { directoriesOnly: true });
12823
13194
  }
12824
13195
  /**
12825
13196
  * List archived changes with metadata and time info
@@ -12842,24 +13213,16 @@ var OpenSpecAdapter = class {
12842
13213
  }))).filter((r) => r !== null).sort((a, b) => b.updatedAt - a.updatedAt);
12843
13214
  }
12844
13215
  /**
12845
- * Read project.md content
13216
+ * Read project.md content (reactive)
12846
13217
  */
12847
13218
  async readProjectMd() {
12848
- try {
12849
- return await readFile(join(this.openspecDir, "project.md"), "utf-8");
12850
- } catch {
12851
- return null;
12852
- }
13219
+ return reactiveReadFile(join(this.openspecDir, "project.md"));
12853
13220
  }
12854
13221
  /**
12855
- * Read AGENTS.md content
13222
+ * Read AGENTS.md content (reactive)
12856
13223
  */
12857
13224
  async readAgentsMd() {
12858
- try {
12859
- return await readFile(join(this.openspecDir, "AGENTS.md"), "utf-8");
12860
- } catch {
12861
- return null;
12862
- }
13225
+ return reactiveReadFile(join(this.openspecDir, "AGENTS.md"));
12863
13226
  }
12864
13227
  /**
12865
13228
  * Write project.md content
@@ -12883,11 +13246,7 @@ var OpenSpecAdapter = class {
12883
13246
  }
12884
13247
  }
12885
13248
  async readSpecRaw(specId) {
12886
- try {
12887
- return await readFile(join(this.specsDir, specId, "spec.md"), "utf-8");
12888
- } catch {
12889
- return null;
12890
- }
13249
+ return reactiveReadFile(join(this.specsDir, specId, "spec.md"));
12891
13250
  }
12892
13251
  async readChange(changeId) {
12893
13252
  try {
@@ -12899,17 +13258,14 @@ var OpenSpecAdapter = class {
12899
13258
  }
12900
13259
  }
12901
13260
  async readChangeRaw(changeId) {
12902
- try {
12903
- const proposalPath = join(this.changesDir, changeId, "proposal.md");
12904
- const tasksPath = join(this.changesDir, changeId, "tasks.md");
12905
- const [proposal, tasks] = await Promise.all([readFile(proposalPath, "utf-8"), readFile(tasksPath, "utf-8").catch(() => "")]);
12906
- return {
12907
- proposal,
12908
- tasks
12909
- };
12910
- } catch {
12911
- return null;
12912
- }
13261
+ const proposalPath = join(this.changesDir, changeId, "proposal.md");
13262
+ const tasksPath = join(this.changesDir, changeId, "tasks.md");
13263
+ const [proposal, tasks] = await Promise.all([reactiveReadFile(proposalPath), reactiveReadFile(tasksPath)]);
13264
+ if (!proposal) return null;
13265
+ return {
13266
+ proposal,
13267
+ tasks: tasks ?? ""
13268
+ };
12913
13269
  }
12914
13270
  /**
12915
13271
  * Read an archived change
@@ -12924,20 +13280,17 @@ var OpenSpecAdapter = class {
12924
13280
  }
12925
13281
  }
12926
13282
  /**
12927
- * Read raw archived change files
13283
+ * Read raw archived change files (reactive)
12928
13284
  */
12929
13285
  async readArchivedChangeRaw(changeId) {
12930
- try {
12931
- const proposalPath = join(this.archiveDir, changeId, "proposal.md");
12932
- const tasksPath = join(this.archiveDir, changeId, "tasks.md");
12933
- const [proposal, tasks] = await Promise.all([readFile(proposalPath, "utf-8"), readFile(tasksPath, "utf-8").catch(() => "")]);
12934
- return {
12935
- proposal,
12936
- tasks
12937
- };
12938
- } catch {
12939
- return null;
12940
- }
13286
+ const proposalPath = join(this.archiveDir, changeId, "proposal.md");
13287
+ const tasksPath = join(this.archiveDir, changeId, "tasks.md");
13288
+ const [proposal, tasks] = await Promise.all([reactiveReadFile(proposalPath), reactiveReadFile(tasksPath)]);
13289
+ if (!proposal) return null;
13290
+ return {
13291
+ proposal,
13292
+ tasks: tasks ?? ""
13293
+ };
12941
13294
  }
12942
13295
  async writeSpec(specId, content) {
12943
13296
  const specDir = join(this.specsDir, specId);
@@ -13254,7 +13607,7 @@ var OpenSpecWatcher = class extends EventEmitter {
13254
13607
  */
13255
13608
  watchDir(dir, callback) {
13256
13609
  try {
13257
- const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
13610
+ const watcher = watch$1(dir, { recursive: true }, (eventType, filename) => {
13258
13611
  if (filename) callback(filename, eventType);
13259
13612
  });
13260
13613
  watcher.on("error", (error) => {
@@ -13279,6 +13632,397 @@ var OpenSpecWatcher = class extends EventEmitter {
13279
13632
  }
13280
13633
  };
13281
13634
 
13635
+ //#endregion
13636
+ //#region ../core/src/config.ts
13637
+ /**
13638
+ * OpenSpecUI 配置 Schema
13639
+ *
13640
+ * 存储在 openspec/.openspecui.json 中,利用文件监听实现响应式更新
13641
+ */
13642
+ const OpenSpecUIConfigSchema = objectType({
13643
+ cli: objectType({ command: stringType().default("npx @fission-ai/openspec") }).default({}),
13644
+ ui: objectType({ theme: enumType([
13645
+ "light",
13646
+ "dark",
13647
+ "system"
13648
+ ]).default("system") }).default({})
13649
+ });
13650
+ /** 默认配置 */
13651
+ const DEFAULT_CONFIG = {
13652
+ cli: { command: "npx @fission-ai/openspec" },
13653
+ ui: { theme: "system" }
13654
+ };
13655
+ /**
13656
+ * 配置管理器
13657
+ *
13658
+ * 负责读写 openspec/.openspecui.json 配置文件。
13659
+ * 读取操作使用 reactiveReadFile,支持响应式更新。
13660
+ */
13661
+ var ConfigManager = class {
13662
+ configPath;
13663
+ constructor(projectDir) {
13664
+ this.configPath = join(projectDir, "openspec", ".openspecui.json");
13665
+ }
13666
+ /**
13667
+ * 读取配置(响应式)
13668
+ *
13669
+ * 如果配置文件不存在,返回默认配置。
13670
+ * 如果配置文件格式错误,返回默认配置并打印警告。
13671
+ */
13672
+ async readConfig() {
13673
+ const content = await reactiveReadFile(this.configPath);
13674
+ if (!content) return DEFAULT_CONFIG;
13675
+ try {
13676
+ const parsed = JSON.parse(content);
13677
+ const result = OpenSpecUIConfigSchema.safeParse(parsed);
13678
+ if (result.success) return result.data;
13679
+ console.warn("Invalid config format, using defaults:", result.error.message);
13680
+ return DEFAULT_CONFIG;
13681
+ } catch (err) {
13682
+ console.warn("Failed to parse config, using defaults:", err);
13683
+ return DEFAULT_CONFIG;
13684
+ }
13685
+ }
13686
+ /**
13687
+ * 写入配置
13688
+ *
13689
+ * 会触发文件监听,自动更新订阅者。
13690
+ */
13691
+ async writeConfig(config) {
13692
+ const current = await this.readConfig();
13693
+ const merged = {
13694
+ ...current,
13695
+ ...config,
13696
+ cli: {
13697
+ ...current.cli,
13698
+ ...config.cli
13699
+ },
13700
+ ui: {
13701
+ ...current.ui,
13702
+ ...config.ui
13703
+ }
13704
+ };
13705
+ await writeFile(this.configPath, JSON.stringify(merged, null, 2), "utf-8");
13706
+ }
13707
+ /**
13708
+ * 获取 CLI 命令
13709
+ */
13710
+ async getCliCommand() {
13711
+ return (await this.readConfig()).cli.command;
13712
+ }
13713
+ /**
13714
+ * 设置 CLI 命令
13715
+ */
13716
+ async setCliCommand(command) {
13717
+ await this.writeConfig({ cli: { command } });
13718
+ }
13719
+ };
13720
+
13721
+ //#endregion
13722
+ //#region ../core/src/cli-executor.ts
13723
+ /**
13724
+ * CLI 执行器
13725
+ *
13726
+ * 负责调用外部 openspec CLI 命令。
13727
+ * 命令前缀从 ConfigManager 获取,支持:
13728
+ * - npx @fission-ai/openspec (默认)
13729
+ * - bunx openspec
13730
+ * - openspec (本地安装)
13731
+ * - 自定义命令 (如 xspec)
13732
+ */
13733
+ var CliExecutor = class {
13734
+ constructor(configManager, projectDir) {
13735
+ this.configManager = configManager;
13736
+ this.projectDir = projectDir;
13737
+ }
13738
+ /**
13739
+ * 创建干净的环境变量,移除 pnpm 特有的配置
13740
+ * 避免 pnpm 环境变量污染 npx/npm 执行
13741
+ */
13742
+ getCleanEnv() {
13743
+ const env = { ...process.env };
13744
+ for (const key of Object.keys(env)) if (key.startsWith("npm_config_") || key.startsWith("npm_package_") || key === "npm_execpath" || key === "npm_lifecycle_event" || key === "npm_lifecycle_script") delete env[key];
13745
+ return env;
13746
+ }
13747
+ /**
13748
+ * 执行 CLI 命令
13749
+ *
13750
+ * @param args CLI 参数,如 ['init'] 或 ['archive', 'change-id']
13751
+ * @returns 执行结果
13752
+ */
13753
+ async execute(args) {
13754
+ const parts = (await this.configManager.getCliCommand()).split(/\s+/);
13755
+ const cmd = parts[0];
13756
+ const cmdArgs = [...parts.slice(1), ...args];
13757
+ return new Promise((resolve$2) => {
13758
+ const child = spawn(cmd, cmdArgs, {
13759
+ cwd: this.projectDir,
13760
+ shell: true,
13761
+ env: this.getCleanEnv()
13762
+ });
13763
+ let stdout = "";
13764
+ let stderr = "";
13765
+ child.stdout?.on("data", (data) => {
13766
+ stdout += data.toString();
13767
+ });
13768
+ child.stderr?.on("data", (data) => {
13769
+ stderr += data.toString();
13770
+ });
13771
+ child.on("close", (exitCode) => {
13772
+ resolve$2({
13773
+ success: exitCode === 0,
13774
+ stdout,
13775
+ stderr,
13776
+ exitCode
13777
+ });
13778
+ });
13779
+ child.on("error", (err) => {
13780
+ resolve$2({
13781
+ success: false,
13782
+ stdout,
13783
+ stderr: stderr + "\n" + err.message,
13784
+ exitCode: null
13785
+ });
13786
+ });
13787
+ });
13788
+ }
13789
+ /**
13790
+ * 执行 openspec init(非交互式)
13791
+ *
13792
+ * @param tools 工具列表,如 ['claude', 'cursor'] 或 'all' 或 'none'
13793
+ */
13794
+ async init(tools = "all") {
13795
+ const toolsArg = Array.isArray(tools) ? tools.join(",") : tools;
13796
+ return this.execute(["init", `--tools=${toolsArg}`]);
13797
+ }
13798
+ /**
13799
+ * 执行 openspec archive <changeId>(非交互式)
13800
+ *
13801
+ * @param changeId 要归档的 change ID
13802
+ * @param options 选项
13803
+ */
13804
+ async archive(changeId, options = {}) {
13805
+ const args = [
13806
+ "archive",
13807
+ "-y",
13808
+ changeId
13809
+ ];
13810
+ if (options.skipSpecs) args.push("--skip-specs");
13811
+ if (options.noValidate) args.push("--no-validate");
13812
+ return this.execute(args);
13813
+ }
13814
+ /**
13815
+ * 执行 openspec validate [type] [id]
13816
+ */
13817
+ async validate(type, id) {
13818
+ const args = ["validate"];
13819
+ if (type) args.push(type);
13820
+ if (id) args.push(id);
13821
+ return this.execute(args);
13822
+ }
13823
+ /**
13824
+ * 检查 CLI 是否可用
13825
+ */
13826
+ async checkAvailability() {
13827
+ try {
13828
+ const result = await this.execute(["--version"]);
13829
+ if (result.success) return {
13830
+ available: true,
13831
+ version: result.stdout.trim()
13832
+ };
13833
+ return {
13834
+ available: false,
13835
+ error: result.stderr || "Unknown error"
13836
+ };
13837
+ } catch (err) {
13838
+ return {
13839
+ available: false,
13840
+ error: err instanceof Error ? err.message : "Unknown error"
13841
+ };
13842
+ }
13843
+ }
13844
+ };
13845
+
13846
+ //#endregion
13847
+ //#region ../core/src/tool-config.ts
13848
+ /**
13849
+ * 工具配置检测模块
13850
+ *
13851
+ * 基于 @fission-ai/openspec 的 configurators 实现
13852
+ * 用于检测项目中已配置的 AI 工具
13853
+ *
13854
+ * 重要:使用响应式文件系统 (reactiveExists) 实现,
13855
+ * 当配置文件变化时会自动触发更新。
13856
+ *
13857
+ * @see references/openspec/src/core/configurators/slash/
13858
+ */
13859
+ /**
13860
+ * 所有支持的工具配置
13861
+ *
13862
+ * 检测路径使用 proposal 命令文件,因为这是 openspec init 创建的第一个文件
13863
+ * 如果该文件存在,说明工具已配置
13864
+ */
13865
+ const TOOL_CONFIGS = [
13866
+ {
13867
+ toolId: "claude",
13868
+ detectionPath: ".claude/commands/openspec/proposal.md"
13869
+ },
13870
+ {
13871
+ toolId: "cursor",
13872
+ detectionPath: ".cursor/commands/openspec-proposal.md"
13873
+ },
13874
+ {
13875
+ toolId: "windsurf",
13876
+ detectionPath: ".windsurf/workflows/openspec-proposal.md"
13877
+ },
13878
+ {
13879
+ toolId: "cline",
13880
+ detectionPath: ".clinerules/workflows/openspec-proposal.md"
13881
+ },
13882
+ {
13883
+ toolId: "github-copilot",
13884
+ detectionPath: ".github/prompts/openspec-proposal.prompt.md"
13885
+ },
13886
+ {
13887
+ toolId: "amazon-q",
13888
+ detectionPath: ".amazonq/prompts/openspec-proposal.md"
13889
+ },
13890
+ {
13891
+ toolId: "codex",
13892
+ detectionPath: ".codex/prompts/openspec-proposal.md"
13893
+ },
13894
+ {
13895
+ toolId: "gemini",
13896
+ detectionPath: ".gemini/commands/openspec/proposal.toml"
13897
+ },
13898
+ {
13899
+ toolId: "auggie",
13900
+ detectionPath: ".augment/commands/openspec-proposal.md"
13901
+ },
13902
+ {
13903
+ toolId: "codebuddy",
13904
+ detectionPath: ".codebuddy/commands/openspec/proposal.md"
13905
+ },
13906
+ {
13907
+ toolId: "qoder",
13908
+ detectionPath: ".qoder/commands/openspec/proposal.md"
13909
+ },
13910
+ {
13911
+ toolId: "roocode",
13912
+ detectionPath: ".roo/commands/openspec-proposal.md"
13913
+ },
13914
+ {
13915
+ toolId: "kilocode",
13916
+ detectionPath: ".kilocode/workflows/openspec-proposal.md"
13917
+ },
13918
+ {
13919
+ toolId: "opencode",
13920
+ detectionPath: ".opencode/command/openspec-proposal.md"
13921
+ },
13922
+ {
13923
+ toolId: "factory",
13924
+ detectionPath: ".factory/commands/openspec-proposal.md"
13925
+ },
13926
+ {
13927
+ toolId: "crush",
13928
+ detectionPath: ".crush/commands/openspec/proposal.md"
13929
+ },
13930
+ {
13931
+ toolId: "costrict",
13932
+ detectionPath: ".cospec/openspec/commands/openspec-proposal.md"
13933
+ },
13934
+ {
13935
+ toolId: "qwen",
13936
+ detectionPath: ".qwen/commands/openspec-proposal.toml"
13937
+ },
13938
+ {
13939
+ toolId: "iflow",
13940
+ detectionPath: ".iflow/commands/openspec-proposal.md"
13941
+ },
13942
+ {
13943
+ toolId: "antigravity",
13944
+ detectionPath: ".agent/workflows/openspec-proposal.md"
13945
+ }
13946
+ ];
13947
+ /**
13948
+ * 获取所有可用的工具 ID 列表
13949
+ */
13950
+ function getAvailableToolIds() {
13951
+ return TOOL_CONFIGS.map((config) => config.toolId);
13952
+ }
13953
+ /**
13954
+ * 检测项目中已配置的工具(响应式)
13955
+ *
13956
+ * 使用 reactiveExists 检测文件,当文件变化时会自动触发更新。
13957
+ * 必须在 ReactiveContext 中调用才能获得响应式能力。
13958
+ *
13959
+ * @param projectDir 项目根目录
13960
+ * @returns 已配置的工具 ID 列表
13961
+ */
13962
+ async function getConfiguredTools(projectDir) {
13963
+ const configured = [];
13964
+ for (const config of TOOL_CONFIGS) if (await reactiveExists(join(projectDir, config.detectionPath))) configured.push(config.toolId);
13965
+ return configured;
13966
+ }
13967
+
13968
+ //#endregion
13969
+ //#region ../server/src/reactive-subscription.ts
13970
+ /**
13971
+ * 创建响应式订阅
13972
+ *
13973
+ * 自动追踪 task 中的文件依赖,当依赖变更时自动重新执行并推送新数据。
13974
+ *
13975
+ * @param task 要执行的异步任务,内部的文件读取会被自动追踪
13976
+ * @returns tRPC observable
13977
+ *
13978
+ * @example
13979
+ * ```typescript
13980
+ * // 在 router 中使用
13981
+ * subscribe: publicProcedure.subscription(({ ctx }) => {
13982
+ * return createReactiveSubscription(() => ctx.adapter.listSpecsWithMeta())
13983
+ * })
13984
+ * ```
13985
+ */
13986
+ function createReactiveSubscription(task) {
13987
+ return observable((emit) => {
13988
+ const context = new ReactiveContext();
13989
+ const controller = new AbortController();
13990
+ (async () => {
13991
+ try {
13992
+ for await (const data of context.stream(task, controller.signal)) emit.next(data);
13993
+ } catch (err) {
13994
+ if (!controller.signal.aborted) emit.error(err);
13995
+ }
13996
+ })();
13997
+ return () => {
13998
+ controller.abort();
13999
+ };
14000
+ });
14001
+ }
14002
+ /**
14003
+ * 创建带输入参数的响应式订阅
14004
+ *
14005
+ * @param task 接收输入参数的异步任务
14006
+ * @returns 返回一个函数,接收输入参数并返回 tRPC observable
14007
+ *
14008
+ * @example
14009
+ * ```typescript
14010
+ * // 在 router 中使用
14011
+ * subscribeOne: publicProcedure
14012
+ * .input(z.object({ id: z.string() }))
14013
+ * .subscription(({ ctx, input }) => {
14014
+ * return createReactiveSubscriptionWithInput(
14015
+ * (id: string) => ctx.adapter.readSpec(id)
14016
+ * )(input.id)
14017
+ * })
14018
+ * ```
14019
+ */
14020
+ function createReactiveSubscriptionWithInput(task) {
14021
+ return (input) => {
14022
+ return createReactiveSubscription(() => task(input));
14023
+ };
14024
+ }
14025
+
13282
14026
  //#endregion
13283
14027
  //#region ../server/src/router.ts
13284
14028
  const t = initTRPC.context().create();
@@ -13293,6 +14037,12 @@ const dashboardRouter = router({
13293
14037
  }),
13294
14038
  isInitialized: publicProcedure.query(async ({ ctx }) => {
13295
14039
  return ctx.adapter.isInitialized();
14040
+ }),
14041
+ subscribe: publicProcedure.subscription(({ ctx }) => {
14042
+ return createReactiveSubscription(() => ctx.adapter.getDashboardData());
14043
+ }),
14044
+ subscribeInitialized: publicProcedure.subscription(({ ctx }) => {
14045
+ return createReactiveSubscription(() => ctx.adapter.isInitialized());
13296
14046
  })
13297
14047
  });
13298
14048
  /**
@@ -13320,6 +14070,15 @@ const specRouter = router({
13320
14070
  }),
13321
14071
  validate: publicProcedure.input(objectType({ id: stringType() })).query(async ({ ctx, input }) => {
13322
14072
  return ctx.adapter.validateSpec(input.id);
14073
+ }),
14074
+ subscribe: publicProcedure.subscription(({ ctx }) => {
14075
+ return createReactiveSubscription(() => ctx.adapter.listSpecsWithMeta());
14076
+ }),
14077
+ subscribeOne: publicProcedure.input(objectType({ id: stringType() })).subscription(({ ctx, input }) => {
14078
+ return createReactiveSubscriptionWithInput((id) => ctx.adapter.readSpec(id))(input.id);
14079
+ }),
14080
+ subscribeRaw: publicProcedure.input(objectType({ id: stringType() })).subscription(({ ctx, input }) => {
14081
+ return createReactiveSubscriptionWithInput((id) => ctx.adapter.readSpecRaw(id))(input.id);
13323
14082
  })
13324
14083
  });
13325
14084
  /**
@@ -13362,6 +14121,15 @@ const changeRouter = router({
13362
14121
  })).mutation(async ({ ctx, input }) => {
13363
14122
  if (!await ctx.adapter.toggleTask(input.changeId, input.taskIndex, input.completed)) throw new Error(`Failed to toggle task ${input.taskIndex} in change ${input.changeId}`);
13364
14123
  return { success: true };
14124
+ }),
14125
+ subscribe: publicProcedure.subscription(({ ctx }) => {
14126
+ return createReactiveSubscription(() => ctx.adapter.listChangesWithMeta());
14127
+ }),
14128
+ subscribeOne: publicProcedure.input(objectType({ id: stringType() })).subscription(({ ctx, input }) => {
14129
+ return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChange(id))(input.id);
14130
+ }),
14131
+ subscribeRaw: publicProcedure.input(objectType({ id: stringType() })).subscription(({ ctx, input }) => {
14132
+ return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChangeRaw(id))(input.id);
13365
14133
  })
13366
14134
  });
13367
14135
  /**
@@ -13459,6 +14227,12 @@ const projectRouter = router({
13459
14227
  saveAgentsMd: publicProcedure.input(objectType({ content: stringType() })).mutation(async ({ ctx, input }) => {
13460
14228
  await ctx.adapter.writeAgentsMd(input.content);
13461
14229
  return { success: true };
14230
+ }),
14231
+ subscribeProjectMd: publicProcedure.subscription(({ ctx }) => {
14232
+ return createReactiveSubscription(() => ctx.adapter.readProjectMd());
14233
+ }),
14234
+ subscribeAgentsMd: publicProcedure.subscription(({ ctx }) => {
14235
+ return createReactiveSubscription(() => ctx.adapter.readAgentsMd());
13462
14236
  })
13463
14237
  });
13464
14238
  /**
@@ -13476,6 +14250,12 @@ const archiveRouter = router({
13476
14250
  }),
13477
14251
  getRaw: publicProcedure.input(objectType({ id: stringType() })).query(async ({ ctx, input }) => {
13478
14252
  return ctx.adapter.readArchivedChangeRaw(input.id);
14253
+ }),
14254
+ subscribe: publicProcedure.subscription(({ ctx }) => {
14255
+ return createReactiveSubscription(() => ctx.adapter.listArchivedChangesWithMeta());
14256
+ }),
14257
+ subscribeOne: publicProcedure.input(objectType({ id: stringType() })).subscription(({ ctx, input }) => {
14258
+ return createReactiveSubscriptionWithInput((id) => ctx.adapter.readArchivedChange(id))(input.id);
13479
14259
  })
13480
14260
  });
13481
14261
  objectType({
@@ -13549,6 +14329,75 @@ const realtimeRouter = router({
13549
14329
  })
13550
14330
  });
13551
14331
  /**
14332
+ * Config router - configuration management
14333
+ */
14334
+ const configRouter = router({
14335
+ get: publicProcedure.query(async ({ ctx }) => {
14336
+ return ctx.configManager.readConfig();
14337
+ }),
14338
+ update: publicProcedure.input(objectType({
14339
+ cli: objectType({ command: stringType() }).optional(),
14340
+ ui: objectType({ theme: enumType([
14341
+ "light",
14342
+ "dark",
14343
+ "system"
14344
+ ]) }).optional()
14345
+ })).mutation(async ({ ctx, input }) => {
14346
+ await ctx.configManager.writeConfig(input);
14347
+ return { success: true };
14348
+ }),
14349
+ setCliCommand: publicProcedure.input(objectType({ command: stringType() })).mutation(async ({ ctx, input }) => {
14350
+ await ctx.configManager.setCliCommand(input.command);
14351
+ return { success: true };
14352
+ }),
14353
+ subscribe: publicProcedure.subscription(({ ctx }) => {
14354
+ return createReactiveSubscription(() => ctx.configManager.readConfig());
14355
+ })
14356
+ });
14357
+ /**
14358
+ * CLI router - execute external openspec CLI commands
14359
+ */
14360
+ const cliRouter = router({
14361
+ checkAvailability: publicProcedure.query(async ({ ctx }) => {
14362
+ return ctx.cliExecutor.checkAvailability();
14363
+ }),
14364
+ getAvailableTools: publicProcedure.query(() => {
14365
+ return getAvailableToolIds();
14366
+ }),
14367
+ getConfiguredTools: publicProcedure.query(async ({ ctx }) => {
14368
+ return getConfiguredTools(ctx.projectDir);
14369
+ }),
14370
+ subscribeConfiguredTools: publicProcedure.subscription(({ ctx }) => {
14371
+ return createReactiveSubscription(() => getConfiguredTools(ctx.projectDir));
14372
+ }),
14373
+ init: publicProcedure.input(objectType({ tools: unionType([
14374
+ arrayType(stringType()),
14375
+ literalType("all"),
14376
+ literalType("none")
14377
+ ]).optional() }).optional()).mutation(async ({ ctx, input }) => {
14378
+ return ctx.cliExecutor.init(input?.tools ?? "all");
14379
+ }),
14380
+ archive: publicProcedure.input(objectType({
14381
+ changeId: stringType(),
14382
+ skipSpecs: booleanType().optional(),
14383
+ noValidate: booleanType().optional()
14384
+ })).mutation(async ({ ctx, input }) => {
14385
+ return ctx.cliExecutor.archive(input.changeId, {
14386
+ skipSpecs: input.skipSpecs,
14387
+ noValidate: input.noValidate
14388
+ });
14389
+ }),
14390
+ validate: publicProcedure.input(objectType({
14391
+ type: enumType(["spec", "change"]).optional(),
14392
+ id: stringType().optional()
14393
+ })).mutation(async ({ ctx, input }) => {
14394
+ return ctx.cliExecutor.validate(input.type, input.id);
14395
+ }),
14396
+ execute: publicProcedure.input(objectType({ args: arrayType(stringType()) })).mutation(async ({ ctx, input }) => {
14397
+ return ctx.cliExecutor.execute(input.args);
14398
+ })
14399
+ });
14400
+ /**
13552
14401
  * Main app router
13553
14402
  */
13554
14403
  const appRouter = router({
@@ -13559,7 +14408,9 @@ const appRouter = router({
13559
14408
  project: projectRouter,
13560
14409
  ai: aiRouter,
13561
14410
  init: initRouter,
13562
- realtime: realtimeRouter
14411
+ realtime: realtimeRouter,
14412
+ config: configRouter,
14413
+ cli: cliRouter
13563
14414
  });
13564
14415
 
13565
14416
  //#endregion
@@ -13582,6 +14433,8 @@ const appRouter = router({
13582
14433
  function createServer$2(config) {
13583
14434
  const adapter = new OpenSpecAdapter(config.projectDir);
13584
14435
  const providerManager = new ProviderManager(config.providers);
14436
+ const configManager = new ConfigManager(config.projectDir);
14437
+ const cliExecutor = new CliExecutor(configManager, config.projectDir);
13585
14438
  const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
13586
14439
  const app = new Hono();
13587
14440
  const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
@@ -13604,19 +14457,27 @@ function createServer$2(config) {
13604
14457
  createContext: () => ({
13605
14458
  adapter,
13606
14459
  providerManager,
13607
- watcher
14460
+ configManager,
14461
+ cliExecutor,
14462
+ watcher,
14463
+ projectDir: config.projectDir
13608
14464
  })
13609
14465
  });
13610
14466
  });
13611
14467
  const createContext = () => ({
13612
14468
  adapter,
13613
14469
  providerManager,
13614
- watcher
14470
+ configManager,
14471
+ cliExecutor,
14472
+ watcher,
14473
+ projectDir: config.projectDir
13615
14474
  });
13616
14475
  return {
13617
14476
  app,
13618
14477
  adapter,
13619
14478
  providerManager,
14479
+ configManager,
14480
+ cliExecutor,
13620
14481
  watcher,
13621
14482
  createContext,
13622
14483
  port: config.port ?? 3100
@@ -13658,19 +14519,19 @@ const __dirname = dirname$1(fileURLToPath(import.meta.url));
13658
14519
  * Tests both 127.0.0.1 and 0.0.0.0 to ensure the port is truly available.
13659
14520
  */
13660
14521
  function isPortAvailable(port) {
13661
- return new Promise((resolve$1) => {
14522
+ return new Promise((resolve$2) => {
13662
14523
  const server1 = createServer$1();
13663
14524
  server1.once("error", () => {
13664
- resolve$1(false);
14525
+ resolve$2(false);
13665
14526
  });
13666
14527
  server1.once("listening", () => {
13667
14528
  server1.close(() => {
13668
14529
  const server2 = createServer$1();
13669
14530
  server2.once("error", () => {
13670
- resolve$1(false);
14531
+ resolve$2(false);
13671
14532
  });
13672
14533
  server2.once("listening", () => {
13673
- server2.close(() => resolve$1(true));
14534
+ server2.close(() => resolve$2(true));
13674
14535
  });
13675
14536
  server2.listen(port, "0.0.0.0");
13676
14537
  });
@@ -13744,8 +14605,8 @@ async function startServer(options = {}) {
13744
14605
  const path$1 = c.req.path === "/" ? "/index.html" : c.req.path;
13745
14606
  if (path$1.startsWith("/trpc")) return next();
13746
14607
  const filePath = join$1(webDir, path$1);
13747
- if (existsSync(filePath) && statSync$1(filePath).isFile()) {
13748
- const content = readFileSync$1(filePath);
14608
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
14609
+ const content = readFileSync(filePath);
13749
14610
  const contentType = {
13750
14611
  html: "text/html",
13751
14612
  js: "application/javascript",
@@ -13766,7 +14627,7 @@ async function startServer(options = {}) {
13766
14627
  if (!path$1.includes(".")) {
13767
14628
  const indexPath = join$1(webDir, "index.html");
13768
14629
  if (existsSync(indexPath)) {
13769
- const content = readFileSync$1(indexPath, "utf-8");
14630
+ const content = readFileSync(indexPath, "utf-8");
13770
14631
  return c.html(content);
13771
14632
  }
13772
14633
  }