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.
- package/dist/cli.mjs +17 -16
- package/dist/index.mjs +1 -1
- package/dist/{open-CmiqsyCQ.mjs → open-Cw_l4kh9.mjs} +11 -11
- package/dist/{src-D7s-EgJ2.mjs → src-BS3LYQ6T.mjs} +959 -98
- package/package.json +3 -3
- package/web/assets/index-DU-ty3pA.css +1 -0
- package/web/assets/index-noSlrtA-.js +337 -0
- package/web/index.html +2 -2
- package/web/assets/index-DIHAzyX3.js +0 -332
- package/web/assets/index-DuIo-wPB.css +0 -1
|
@@ -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,
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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$
|
|
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$
|
|
6657
|
-
resolve$
|
|
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$
|
|
6807
|
+
let resolve$2;
|
|
6805
6808
|
let reject;
|
|
6806
6809
|
return {
|
|
6807
6810
|
promise: new Promise((_resolve, _reject) => {
|
|
6808
|
-
resolve$
|
|
6811
|
+
resolve$2 = _resolve;
|
|
6809
6812
|
reject = _reject;
|
|
6810
6813
|
}),
|
|
6811
|
-
resolve: resolve$
|
|
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$
|
|
6868
|
-
timer = setTimeout(() => resolve$
|
|
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$
|
|
7088
|
+
let resolve$2;
|
|
7086
7089
|
let reject;
|
|
7087
7090
|
return {
|
|
7088
7091
|
promise: new Promise((res, rej) => {
|
|
7089
|
-
resolve$
|
|
7092
|
+
resolve$2 = res;
|
|
7090
7093
|
reject = rej;
|
|
7091
7094
|
}),
|
|
7092
|
-
resolve: resolve$
|
|
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$
|
|
8707
|
-
abortController$1.signal.onabort = () => resolve$
|
|
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
|
-
|
|
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
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
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
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
14522
|
+
return new Promise((resolve$2) => {
|
|
13662
14523
|
const server1 = createServer$1();
|
|
13663
14524
|
server1.once("error", () => {
|
|
13664
|
-
resolve$
|
|
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$
|
|
14531
|
+
resolve$2(false);
|
|
13671
14532
|
});
|
|
13672
14533
|
server2.once("listening", () => {
|
|
13673
|
-
server2.close(() => resolve$
|
|
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
|
|
13748
|
-
const content = readFileSync
|
|
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
|
|
14630
|
+
const content = readFileSync(indexPath, "utf-8");
|
|
13770
14631
|
return c.html(content);
|
|
13771
14632
|
}
|
|
13772
14633
|
}
|