openspecui 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{src-DfG_-u90.mjs → src-D5eBKdVF.mjs} +612 -244
- package/package.json +3 -3
- package/web/assets/{BufferResource-CVUoegR6.js → BufferResource-DMjllemV.js} +1 -1
- package/web/assets/{CanvasRenderer-BEIcB8i1.js → CanvasRenderer-kKbOzIca.js} +1 -1
- package/web/assets/{Filter-Bu_qhr6H.js → Filter-C5l7SDia.js} +1 -1
- package/web/assets/{RenderTargetSystem-DWouFDxU.js → RenderTargetSystem-BaFVF7ku.js} +1 -1
- package/web/assets/{WebGLRenderer-6FH_N1FV.js → WebGLRenderer-Ch58nD6Y.js} +1 -1
- package/web/assets/{WebGPURenderer-B8sJk3Sv.js → WebGPURenderer-Bpf-ane5.js} +1 -1
- package/web/assets/{browserAll-CLKeV1yb.js → browserAll-CiXGzqJc.js} +1 -1
- package/web/assets/{index-Bv7pWR8R.js → index-BQ6UeNz3.js} +1 -1
- package/web/assets/{index-BtNuxyw4.js → index-BTMNsWWi.js} +1 -1
- package/web/assets/{index-CEKSUzvw.js → index-BXWVYqbO.js} +1 -1
- package/web/assets/{index-BRp8MJ9v.js → index-BXqdCpuU.js} +1 -1
- package/web/assets/{index-BE5-y0_g.js → index-Bdb0Fpwv.js} +1 -1
- package/web/assets/{index-BPCTI2mG.js → index-BgHJ8w_f.js} +1 -1
- package/web/assets/{index-D4AU46yO.js → index-BwIIOUjO.js} +1 -1
- package/web/assets/{index-mWXhCp9j.js → index-CFkiyi1j.js} +1 -1
- package/web/assets/{index-eQZwF8qE.js → index-CLxF_OQQ.js} +1 -1
- package/web/assets/{index-DXRZmZm8.js → index-CckLtqno.js} +1 -1
- package/web/assets/{index-CEHMo0EU.js → index-CsXgfYOH.js} +252 -260
- package/web/assets/{index-CX13iBBs.js → index-D2Uig6TZ.js} +1 -1
- package/web/assets/{index-BlZ-sasH.js → index-DEWTHv2o.js} +1 -1
- package/web/assets/{index-Bp_dnlLF.js → index-DJZG7SGL.js} +1 -1
- package/web/assets/{index-CoOT7eZ9.js → index-L7IKyBGp.js} +1 -1
- package/web/assets/{index-Byr3HkRi.js → index-e2r1Tz_y.js} +1 -1
- package/web/assets/{webworkerAll-DjWoTx9g.js → webworkerAll-D0vlbEoH.js} +1 -1
- package/web/index.html +1 -1
|
@@ -5,7 +5,7 @@ import { Readable } from "stream";
|
|
|
5
5
|
import crypto from "crypto";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8
|
-
import { join } from "path";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
10
|
import { mkdir as mkdir$1, readFile as readFile$1, readdir, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
|
|
11
11
|
import { dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
@@ -9628,6 +9628,15 @@ async function reactiveReadFile(filepath) {
|
|
|
9628
9628
|
return state.get();
|
|
9629
9629
|
}
|
|
9630
9630
|
/**
|
|
9631
|
+
* 主动更新响应式文件缓存(用于写入后立即推送订阅)
|
|
9632
|
+
*
|
|
9633
|
+
* 仅当该文件已有缓存状态时生效;不会创建新的监听器。
|
|
9634
|
+
*/
|
|
9635
|
+
function updateReactiveFileCache(filepath, content) {
|
|
9636
|
+
const key = `file:${resolve$1(filepath)}`;
|
|
9637
|
+
stateCache$1.get(key)?.set(content);
|
|
9638
|
+
}
|
|
9639
|
+
/**
|
|
9631
9640
|
* 响应式读取目录内容
|
|
9632
9641
|
*
|
|
9633
9642
|
* 特性:
|
|
@@ -13926,20 +13935,124 @@ var OpenSpecWatcher = class extends EventEmitter {
|
|
|
13926
13935
|
//#endregion
|
|
13927
13936
|
//#region ../core/src/config.ts
|
|
13928
13937
|
const execAsync = promisify(exec);
|
|
13929
|
-
|
|
13930
|
-
const
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13938
|
+
const CLI_PROBE_TIMEOUT_MS = 2e4;
|
|
13939
|
+
const THEME_VALUES = [
|
|
13940
|
+
"light",
|
|
13941
|
+
"dark",
|
|
13942
|
+
"system"
|
|
13943
|
+
];
|
|
13944
|
+
const CURSOR_STYLE_VALUES = [
|
|
13945
|
+
"block",
|
|
13946
|
+
"underline",
|
|
13947
|
+
"bar"
|
|
13948
|
+
];
|
|
13949
|
+
const BASE_PACKAGE_MANAGER_RUNNERS = [
|
|
13950
|
+
{
|
|
13951
|
+
id: "npx",
|
|
13952
|
+
source: "npx",
|
|
13953
|
+
commandParts: [
|
|
13954
|
+
"npx",
|
|
13955
|
+
"-y",
|
|
13956
|
+
"@fission-ai/openspec"
|
|
13957
|
+
]
|
|
13958
|
+
},
|
|
13959
|
+
{
|
|
13960
|
+
id: "bunx",
|
|
13961
|
+
source: "bunx",
|
|
13962
|
+
commandParts: ["bunx", "@fission-ai/openspec"]
|
|
13963
|
+
},
|
|
13964
|
+
{
|
|
13965
|
+
id: "deno",
|
|
13966
|
+
source: "deno",
|
|
13967
|
+
commandParts: [
|
|
13968
|
+
"deno",
|
|
13969
|
+
"run",
|
|
13970
|
+
"-A",
|
|
13971
|
+
"npm:@fission-ai/openspec"
|
|
13972
|
+
]
|
|
13973
|
+
},
|
|
13974
|
+
{
|
|
13975
|
+
id: "pnpm",
|
|
13976
|
+
source: "pnpm",
|
|
13977
|
+
commandParts: [
|
|
13978
|
+
"pnpm",
|
|
13979
|
+
"dlx",
|
|
13980
|
+
"@fission-ai/openspec"
|
|
13981
|
+
]
|
|
13982
|
+
},
|
|
13983
|
+
{
|
|
13984
|
+
id: "yarn",
|
|
13985
|
+
source: "yarn",
|
|
13986
|
+
commandParts: [
|
|
13987
|
+
"yarn",
|
|
13988
|
+
"dlx",
|
|
13989
|
+
"@fission-ai/openspec"
|
|
13990
|
+
]
|
|
13991
|
+
}
|
|
13992
|
+
];
|
|
13993
|
+
function tokenizeCliCommand(input) {
|
|
13994
|
+
const tokens = [];
|
|
13995
|
+
let current = "";
|
|
13996
|
+
let quote = null;
|
|
13997
|
+
let tokenStarted = false;
|
|
13998
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
13999
|
+
const char = input[index];
|
|
14000
|
+
if (quote) {
|
|
14001
|
+
if (char === quote) {
|
|
14002
|
+
quote = null;
|
|
14003
|
+
tokenStarted = true;
|
|
14004
|
+
continue;
|
|
14005
|
+
}
|
|
14006
|
+
if (char === "\\") {
|
|
14007
|
+
const next = input[index + 1];
|
|
14008
|
+
if (next && (next === quote || next === "\\")) {
|
|
14009
|
+
current += next;
|
|
14010
|
+
tokenStarted = true;
|
|
14011
|
+
index += 1;
|
|
14012
|
+
continue;
|
|
14013
|
+
}
|
|
14014
|
+
}
|
|
14015
|
+
current += char;
|
|
14016
|
+
tokenStarted = true;
|
|
14017
|
+
continue;
|
|
14018
|
+
}
|
|
14019
|
+
if (char === "\"" || char === "'") {
|
|
14020
|
+
quote = char;
|
|
14021
|
+
tokenStarted = true;
|
|
14022
|
+
continue;
|
|
14023
|
+
}
|
|
14024
|
+
if (char === "\\") {
|
|
14025
|
+
const next = input[index + 1];
|
|
14026
|
+
if (next && /[\s"'\\]/.test(next)) {
|
|
14027
|
+
current += next;
|
|
14028
|
+
tokenStarted = true;
|
|
14029
|
+
index += 1;
|
|
14030
|
+
continue;
|
|
14031
|
+
}
|
|
14032
|
+
current += char;
|
|
14033
|
+
tokenStarted = true;
|
|
14034
|
+
continue;
|
|
14035
|
+
}
|
|
14036
|
+
if (/\s/.test(char)) {
|
|
14037
|
+
if (tokenStarted) {
|
|
14038
|
+
tokens.push(current);
|
|
14039
|
+
current = "";
|
|
14040
|
+
tokenStarted = false;
|
|
14041
|
+
}
|
|
14042
|
+
continue;
|
|
14043
|
+
}
|
|
14044
|
+
current += char;
|
|
14045
|
+
tokenStarted = true;
|
|
14046
|
+
}
|
|
14047
|
+
if (tokenStarted) tokens.push(current);
|
|
14048
|
+
return tokens;
|
|
14049
|
+
}
|
|
13935
14050
|
/**
|
|
13936
14051
|
* 解析 CLI 命令字符串为数组
|
|
13937
14052
|
*
|
|
13938
14053
|
* 支持两种格式:
|
|
13939
14054
|
* 1. JSON 数组:以 `[` 开头,如 `["npx", "@fission-ai/openspec"]`
|
|
13940
|
-
* 2.
|
|
13941
|
-
*
|
|
13942
|
-
* 注意:简单字符串解析不支持带引号的参数,如需复杂命令请使用 JSON 数组格式
|
|
14055
|
+
* 2. shell-like 字符串:支持引号与基础转义
|
|
13943
14056
|
*/
|
|
13944
14057
|
function parseCliCommand(command) {
|
|
13945
14058
|
const trimmed = command.trim();
|
|
@@ -13950,7 +14063,144 @@ function parseCliCommand(command) {
|
|
|
13950
14063
|
} catch (err) {
|
|
13951
14064
|
throw new Error(`Failed to parse CLI command as JSON array: ${err instanceof Error ? err.message : err}`);
|
|
13952
14065
|
}
|
|
13953
|
-
|
|
14066
|
+
const tokens = tokenizeCliCommand(trimmed);
|
|
14067
|
+
if (tokens.length !== 1) return tokens;
|
|
14068
|
+
const firstChar = trimmed[0];
|
|
14069
|
+
const lastChar = trimmed[trimmed.length - 1];
|
|
14070
|
+
if (firstChar !== "\"" && firstChar !== "'" || firstChar !== lastChar) return tokens;
|
|
14071
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
14072
|
+
if (!inner) return tokens;
|
|
14073
|
+
const innerTokens = tokenizeCliCommand(inner.replace(/\\(["'])/g, "$1"));
|
|
14074
|
+
if (innerTokens.length > 1 && innerTokens.slice(1).some((token) => token.startsWith("-"))) return innerTokens;
|
|
14075
|
+
return tokens;
|
|
14076
|
+
}
|
|
14077
|
+
function commandToString(commandParts) {
|
|
14078
|
+
const formatToken = (token) => {
|
|
14079
|
+
if (!token) return "\"\"";
|
|
14080
|
+
if (!/[\s"'\\]/.test(token)) return token;
|
|
14081
|
+
return JSON.stringify(token);
|
|
14082
|
+
};
|
|
14083
|
+
return commandParts.map(formatToken).join(" ").trim();
|
|
14084
|
+
}
|
|
14085
|
+
function getRunnerPriorityFromUserAgent(userAgent) {
|
|
14086
|
+
if (!userAgent) return null;
|
|
14087
|
+
if (userAgent.startsWith("bun")) return "bunx";
|
|
14088
|
+
if (userAgent.startsWith("npm")) return "npx";
|
|
14089
|
+
if (userAgent.startsWith("deno")) return "deno";
|
|
14090
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
14091
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
14092
|
+
return null;
|
|
14093
|
+
}
|
|
14094
|
+
function buildCliRunnerCandidates(options) {
|
|
14095
|
+
const candidates = [];
|
|
14096
|
+
const configuredCommandParts = options.configuredCommandParts?.filter(Boolean) ?? [];
|
|
14097
|
+
if (configuredCommandParts.length > 0) candidates.push({
|
|
14098
|
+
id: "configured",
|
|
14099
|
+
source: "config.cli.command",
|
|
14100
|
+
commandParts: configuredCommandParts
|
|
14101
|
+
});
|
|
14102
|
+
candidates.push({
|
|
14103
|
+
id: "openspec",
|
|
14104
|
+
source: "openspec",
|
|
14105
|
+
commandParts: ["openspec"]
|
|
14106
|
+
});
|
|
14107
|
+
const packageRunners = [...BASE_PACKAGE_MANAGER_RUNNERS];
|
|
14108
|
+
const preferred = getRunnerPriorityFromUserAgent(options.userAgent);
|
|
14109
|
+
if (preferred) {
|
|
14110
|
+
const index = packageRunners.findIndex((item) => item.id === preferred);
|
|
14111
|
+
if (index > 0) {
|
|
14112
|
+
const [runner] = packageRunners.splice(index, 1);
|
|
14113
|
+
packageRunners.unshift(runner);
|
|
14114
|
+
}
|
|
14115
|
+
}
|
|
14116
|
+
return [...candidates, ...packageRunners];
|
|
14117
|
+
}
|
|
14118
|
+
function createCleanCliEnv(baseEnv = process.env) {
|
|
14119
|
+
const env = { ...baseEnv };
|
|
14120
|
+
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];
|
|
14121
|
+
return env;
|
|
14122
|
+
}
|
|
14123
|
+
async function probeCliRunner(candidate, cwd, env) {
|
|
14124
|
+
const [cmd, ...cmdArgs] = candidate.commandParts;
|
|
14125
|
+
return new Promise((resolve$2) => {
|
|
14126
|
+
let stdout = "";
|
|
14127
|
+
let stderr = "";
|
|
14128
|
+
let timedOut = false;
|
|
14129
|
+
const timer = setTimeout(() => {
|
|
14130
|
+
timedOut = true;
|
|
14131
|
+
child.kill();
|
|
14132
|
+
}, CLI_PROBE_TIMEOUT_MS);
|
|
14133
|
+
const child = spawn(cmd, [...cmdArgs, "--version"], {
|
|
14134
|
+
cwd,
|
|
14135
|
+
shell: false,
|
|
14136
|
+
env
|
|
14137
|
+
});
|
|
14138
|
+
child.stdout?.on("data", (data) => {
|
|
14139
|
+
stdout += data.toString();
|
|
14140
|
+
});
|
|
14141
|
+
child.stderr?.on("data", (data) => {
|
|
14142
|
+
stderr += data.toString();
|
|
14143
|
+
});
|
|
14144
|
+
child.on("error", (err) => {
|
|
14145
|
+
clearTimeout(timer);
|
|
14146
|
+
const code = err.code;
|
|
14147
|
+
const suffix = code ? ` (${code})` : "";
|
|
14148
|
+
resolve$2({
|
|
14149
|
+
source: candidate.source,
|
|
14150
|
+
command: commandToString(candidate.commandParts),
|
|
14151
|
+
success: false,
|
|
14152
|
+
error: `${err.message}${suffix}`,
|
|
14153
|
+
exitCode: null
|
|
14154
|
+
});
|
|
14155
|
+
});
|
|
14156
|
+
child.on("close", (exitCode) => {
|
|
14157
|
+
clearTimeout(timer);
|
|
14158
|
+
if (timedOut) {
|
|
14159
|
+
resolve$2({
|
|
14160
|
+
source: candidate.source,
|
|
14161
|
+
command: commandToString(candidate.commandParts),
|
|
14162
|
+
success: false,
|
|
14163
|
+
error: "CLI probe timed out",
|
|
14164
|
+
exitCode
|
|
14165
|
+
});
|
|
14166
|
+
return;
|
|
14167
|
+
}
|
|
14168
|
+
if (exitCode === 0) {
|
|
14169
|
+
const version = stdout.trim().split("\n")[0] || void 0;
|
|
14170
|
+
resolve$2({
|
|
14171
|
+
source: candidate.source,
|
|
14172
|
+
command: commandToString(candidate.commandParts),
|
|
14173
|
+
success: true,
|
|
14174
|
+
version,
|
|
14175
|
+
exitCode
|
|
14176
|
+
});
|
|
14177
|
+
return;
|
|
14178
|
+
}
|
|
14179
|
+
resolve$2({
|
|
14180
|
+
source: candidate.source,
|
|
14181
|
+
command: commandToString(candidate.commandParts),
|
|
14182
|
+
success: false,
|
|
14183
|
+
error: stderr.trim() || `Exit code ${exitCode ?? "null"}`,
|
|
14184
|
+
exitCode
|
|
14185
|
+
});
|
|
14186
|
+
});
|
|
14187
|
+
});
|
|
14188
|
+
}
|
|
14189
|
+
async function resolveCliRunner(candidates, cwd, env) {
|
|
14190
|
+
const attempts = [];
|
|
14191
|
+
for (const candidate of candidates) {
|
|
14192
|
+
const attempt = await probeCliRunner(candidate, cwd, env);
|
|
14193
|
+
attempts.push(attempt);
|
|
14194
|
+
if (attempt.success) return {
|
|
14195
|
+
source: attempt.source,
|
|
14196
|
+
command: attempt.command,
|
|
14197
|
+
commandParts: candidate.commandParts,
|
|
14198
|
+
version: attempt.version,
|
|
14199
|
+
attempts
|
|
14200
|
+
};
|
|
14201
|
+
}
|
|
14202
|
+
const details = attempts.map((attempt) => `- ${attempt.command}: ${attempt.error ?? "failed"}`).join("\n");
|
|
14203
|
+
throw new Error(`No available OpenSpec CLI runner.\n${details}`);
|
|
13954
14204
|
}
|
|
13955
14205
|
/**
|
|
13956
14206
|
* 比较两个语义化版本号
|
|
@@ -13977,7 +14227,7 @@ function compareVersions(a, b) {
|
|
|
13977
14227
|
*/
|
|
13978
14228
|
async function fetchLatestVersion() {
|
|
13979
14229
|
try {
|
|
13980
|
-
const { stdout } = await execAsync("npx @fission-ai/openspec --version", { timeout: 6e4 });
|
|
14230
|
+
const { stdout } = await execAsync("npx -y @fission-ai/openspec --version", { timeout: 6e4 });
|
|
13981
14231
|
return stdout.trim();
|
|
13982
14232
|
} catch {
|
|
13983
14233
|
return;
|
|
@@ -14007,7 +14257,6 @@ async function sniffGlobalCli() {
|
|
|
14007
14257
|
};
|
|
14008
14258
|
}
|
|
14009
14259
|
const version = localResult.stdout.trim();
|
|
14010
|
-
detectedCliCommand = GLOBAL_CLI_COMMAND;
|
|
14011
14260
|
return {
|
|
14012
14261
|
hasGlobal: true,
|
|
14013
14262
|
version,
|
|
@@ -14016,53 +14265,44 @@ async function sniffGlobalCli() {
|
|
|
14016
14265
|
};
|
|
14017
14266
|
}
|
|
14018
14267
|
/**
|
|
14019
|
-
* 检测全局安装的 openspec 命令
|
|
14020
|
-
* 优先使用全局命令,fallback 到 npx
|
|
14021
|
-
*
|
|
14022
|
-
* @returns CLI 命令数组
|
|
14023
|
-
*/
|
|
14024
|
-
async function detectCliCommand() {
|
|
14025
|
-
if (detectedCliCommand !== null) return detectedCliCommand;
|
|
14026
|
-
try {
|
|
14027
|
-
await execAsync(`${process.platform === "win32" ? "where" : "which"} openspec`);
|
|
14028
|
-
detectedCliCommand = GLOBAL_CLI_COMMAND;
|
|
14029
|
-
return detectedCliCommand;
|
|
14030
|
-
} catch {
|
|
14031
|
-
detectedCliCommand = FALLBACK_CLI_COMMAND;
|
|
14032
|
-
return detectedCliCommand;
|
|
14033
|
-
}
|
|
14034
|
-
}
|
|
14035
|
-
/**
|
|
14036
14268
|
* 获取默认 CLI 命令(异步,带检测)
|
|
14037
14269
|
*
|
|
14038
14270
|
* @returns CLI 命令数组,如 `['openspec']` 或 `['npx', '@fission-ai/openspec']`
|
|
14039
14271
|
*/
|
|
14040
14272
|
async function getDefaultCliCommand() {
|
|
14041
|
-
return
|
|
14273
|
+
return (await resolveCliRunner(buildCliRunnerCandidates({ userAgent: process.env.npm_config_user_agent }).filter((candidate) => candidate.id !== "configured"), process.cwd(), createCleanCliEnv())).commandParts;
|
|
14042
14274
|
}
|
|
14043
14275
|
/**
|
|
14044
14276
|
* 获取默认 CLI 命令的字符串形式(用于 UI 显示)
|
|
14045
14277
|
*/
|
|
14046
14278
|
async function getDefaultCliCommandString() {
|
|
14047
|
-
return (await
|
|
14279
|
+
return commandToString(await getDefaultCliCommand());
|
|
14048
14280
|
}
|
|
14281
|
+
const TerminalConfigSchema = objectType({
|
|
14282
|
+
fontSize: numberType().min(8).max(32).default(13),
|
|
14283
|
+
fontFamily: stringType().default(""),
|
|
14284
|
+
cursorBlink: booleanType().default(true),
|
|
14285
|
+
cursorStyle: enumType(CURSOR_STYLE_VALUES).default("block"),
|
|
14286
|
+
scrollback: numberType().min(0).max(1e5).default(1e3)
|
|
14287
|
+
});
|
|
14049
14288
|
/**
|
|
14050
14289
|
* OpenSpecUI 配置 Schema
|
|
14051
14290
|
*
|
|
14052
14291
|
* 存储在 openspec/.openspecui.json 中,利用文件监听实现响应式更新
|
|
14053
14292
|
*/
|
|
14054
14293
|
const OpenSpecUIConfigSchema = objectType({
|
|
14055
|
-
cli: objectType({
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14294
|
+
cli: objectType({
|
|
14295
|
+
command: stringType().optional(),
|
|
14296
|
+
args: arrayType(stringType()).optional()
|
|
14297
|
+
}).default({}),
|
|
14298
|
+
theme: enumType(THEME_VALUES).default("system"),
|
|
14299
|
+
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
|
|
14061
14300
|
});
|
|
14062
14301
|
/** 默认配置(静态,用于测试和类型) */
|
|
14063
14302
|
const DEFAULT_CONFIG = {
|
|
14064
14303
|
cli: {},
|
|
14065
|
-
|
|
14304
|
+
theme: "system",
|
|
14305
|
+
terminal: TerminalConfigSchema.parse({})
|
|
14066
14306
|
};
|
|
14067
14307
|
/**
|
|
14068
14308
|
* 配置管理器
|
|
@@ -14072,7 +14312,11 @@ const DEFAULT_CONFIG = {
|
|
|
14072
14312
|
*/
|
|
14073
14313
|
var ConfigManager = class {
|
|
14074
14314
|
configPath;
|
|
14315
|
+
projectDir;
|
|
14316
|
+
resolvedRunner = null;
|
|
14317
|
+
resolvingRunnerPromise = null;
|
|
14075
14318
|
constructor(projectDir) {
|
|
14319
|
+
this.projectDir = projectDir;
|
|
14076
14320
|
this.configPath = join(projectDir, "openspec", ".openspecui.json");
|
|
14077
14321
|
}
|
|
14078
14322
|
/**
|
|
@@ -14102,43 +14346,145 @@ var ConfigManager = class {
|
|
|
14102
14346
|
*/
|
|
14103
14347
|
async writeConfig(config) {
|
|
14104
14348
|
const current = await this.readConfig();
|
|
14349
|
+
const nextCli = { ...current.cli };
|
|
14350
|
+
if (config.cli && Object.prototype.hasOwnProperty.call(config.cli, "command")) {
|
|
14351
|
+
const trimmed = config.cli.command?.trim();
|
|
14352
|
+
if (trimmed) nextCli.command = trimmed;
|
|
14353
|
+
else {
|
|
14354
|
+
delete nextCli.command;
|
|
14355
|
+
delete nextCli.args;
|
|
14356
|
+
}
|
|
14357
|
+
}
|
|
14358
|
+
if (config.cli && Object.prototype.hasOwnProperty.call(config.cli, "args")) {
|
|
14359
|
+
const args = (config.cli.args ?? []).map((arg) => arg.trim()).filter(Boolean);
|
|
14360
|
+
if (args.length > 0) nextCli.args = args;
|
|
14361
|
+
else delete nextCli.args;
|
|
14362
|
+
}
|
|
14363
|
+
if (!nextCli.command) delete nextCli.args;
|
|
14105
14364
|
const merged = {
|
|
14106
14365
|
...current,
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
...
|
|
14111
|
-
|
|
14112
|
-
ui: {
|
|
14113
|
-
...current.ui,
|
|
14114
|
-
...config.ui
|
|
14366
|
+
cli: nextCli,
|
|
14367
|
+
theme: config.theme ?? current.theme,
|
|
14368
|
+
terminal: {
|
|
14369
|
+
...current.terminal,
|
|
14370
|
+
...config.terminal
|
|
14115
14371
|
}
|
|
14116
14372
|
};
|
|
14117
|
-
|
|
14373
|
+
const serialized = JSON.stringify(merged, null, 2);
|
|
14374
|
+
await mkdir(dirname(this.configPath), { recursive: true });
|
|
14375
|
+
await writeFile(this.configPath, serialized, "utf-8");
|
|
14376
|
+
updateReactiveFileCache(this.configPath, serialized);
|
|
14377
|
+
this.invalidateResolvedCliRunner();
|
|
14378
|
+
}
|
|
14379
|
+
/**
|
|
14380
|
+
* 解析并缓存可用 CLI runner。
|
|
14381
|
+
*/
|
|
14382
|
+
async resolveCliRunner() {
|
|
14383
|
+
if (this.resolvedRunner) return this.resolvedRunner;
|
|
14384
|
+
if (this.resolvingRunnerPromise) return this.resolvingRunnerPromise;
|
|
14385
|
+
this.resolvingRunnerPromise = this.resolveCliRunnerUncached().then((runner) => {
|
|
14386
|
+
this.resolvedRunner = runner;
|
|
14387
|
+
return runner;
|
|
14388
|
+
}).finally(() => {
|
|
14389
|
+
this.resolvingRunnerPromise = null;
|
|
14390
|
+
});
|
|
14391
|
+
return this.resolvingRunnerPromise;
|
|
14392
|
+
}
|
|
14393
|
+
async resolveCliRunnerUncached() {
|
|
14394
|
+
const config = await this.readConfig();
|
|
14395
|
+
const configuredCommandParts = this.getConfiguredCommandParts(config.cli);
|
|
14396
|
+
const hasConfiguredCommand = configuredCommandParts.length > 0;
|
|
14397
|
+
const resolved = await resolveCliRunner(hasConfiguredCommand ? [{
|
|
14398
|
+
id: "configured",
|
|
14399
|
+
source: "config.cli.command",
|
|
14400
|
+
commandParts: configuredCommandParts
|
|
14401
|
+
}] : buildCliRunnerCandidates({
|
|
14402
|
+
configuredCommandParts,
|
|
14403
|
+
userAgent: process.env.npm_config_user_agent
|
|
14404
|
+
}), this.projectDir, createCleanCliEnv());
|
|
14405
|
+
if (!hasConfiguredCommand) {
|
|
14406
|
+
const [resolvedCommand, ...resolvedArgs] = resolved.commandParts;
|
|
14407
|
+
const currentCommand = config.cli.command?.trim();
|
|
14408
|
+
const currentArgs = config.cli.args ?? [];
|
|
14409
|
+
if (currentCommand !== resolvedCommand || currentArgs.length !== resolvedArgs.length || currentArgs.some((arg, index) => arg !== resolvedArgs[index])) try {
|
|
14410
|
+
await this.writeConfig({ cli: {
|
|
14411
|
+
command: resolvedCommand,
|
|
14412
|
+
args: resolvedArgs
|
|
14413
|
+
} });
|
|
14414
|
+
} catch (err) {
|
|
14415
|
+
console.warn("Failed to persist auto-detected CLI command:", err);
|
|
14416
|
+
}
|
|
14417
|
+
}
|
|
14418
|
+
return resolved;
|
|
14118
14419
|
}
|
|
14119
14420
|
/**
|
|
14120
14421
|
* 获取 CLI 命令(数组形式)
|
|
14121
|
-
*
|
|
14122
|
-
* 优先级:配置文件 > 全局 openspec 命令 > npx fallback
|
|
14123
|
-
*
|
|
14124
|
-
* @returns CLI 命令数组,如 `['openspec']` 或 `['npx', '@fission-ai/openspec']`
|
|
14125
14422
|
*/
|
|
14126
14423
|
async getCliCommand() {
|
|
14127
|
-
|
|
14128
|
-
if (config.cli.command) return parseCliCommand(config.cli.command);
|
|
14129
|
-
return getDefaultCliCommand();
|
|
14424
|
+
return (await this.resolveCliRunner()).commandParts;
|
|
14130
14425
|
}
|
|
14131
14426
|
/**
|
|
14132
14427
|
* 获取 CLI 命令的字符串形式(用于 UI 显示)
|
|
14133
14428
|
*/
|
|
14134
14429
|
async getCliCommandString() {
|
|
14135
|
-
return (await this.
|
|
14430
|
+
return (await this.resolveCliRunner()).command;
|
|
14431
|
+
}
|
|
14432
|
+
/**
|
|
14433
|
+
* 获取 CLI 解析结果(用于诊断)
|
|
14434
|
+
*/
|
|
14435
|
+
async getResolvedCliRunner() {
|
|
14436
|
+
return this.resolveCliRunner();
|
|
14437
|
+
}
|
|
14438
|
+
/**
|
|
14439
|
+
* 清理 CLI 解析缓存(用于 ENOENT 自愈)
|
|
14440
|
+
*/
|
|
14441
|
+
invalidateResolvedCliRunner() {
|
|
14442
|
+
this.resolvedRunner = null;
|
|
14443
|
+
this.resolvingRunnerPromise = null;
|
|
14136
14444
|
}
|
|
14137
14445
|
/**
|
|
14138
14446
|
* 设置 CLI 命令
|
|
14139
14447
|
*/
|
|
14140
14448
|
async setCliCommand(command) {
|
|
14141
|
-
|
|
14449
|
+
const trimmed = command.trim();
|
|
14450
|
+
if (!trimmed) {
|
|
14451
|
+
await this.writeConfig({ cli: {
|
|
14452
|
+
command: null,
|
|
14453
|
+
args: null
|
|
14454
|
+
} });
|
|
14455
|
+
return;
|
|
14456
|
+
}
|
|
14457
|
+
const commandParts = parseCliCommand(trimmed);
|
|
14458
|
+
if (commandParts.length === 0) {
|
|
14459
|
+
await this.writeConfig({ cli: {
|
|
14460
|
+
command: null,
|
|
14461
|
+
args: null
|
|
14462
|
+
} });
|
|
14463
|
+
return;
|
|
14464
|
+
}
|
|
14465
|
+
const [resolvedCommand, ...resolvedArgs] = commandParts;
|
|
14466
|
+
await this.writeConfig({ cli: {
|
|
14467
|
+
command: resolvedCommand,
|
|
14468
|
+
args: resolvedArgs
|
|
14469
|
+
} });
|
|
14470
|
+
}
|
|
14471
|
+
getConfiguredCommandParts(cli) {
|
|
14472
|
+
const command = cli.command?.trim();
|
|
14473
|
+
if (!command) return [];
|
|
14474
|
+
if (Array.isArray(cli.args) && cli.args.length > 0) return [command, ...cli.args];
|
|
14475
|
+
return parseCliCommand(command);
|
|
14476
|
+
}
|
|
14477
|
+
/**
|
|
14478
|
+
* 设置主题
|
|
14479
|
+
*/
|
|
14480
|
+
async setTheme(theme) {
|
|
14481
|
+
await this.writeConfig({ theme });
|
|
14482
|
+
}
|
|
14483
|
+
/**
|
|
14484
|
+
* 设置终端配置(部分更新)
|
|
14485
|
+
*/
|
|
14486
|
+
async setTerminalConfig(terminal) {
|
|
14487
|
+
await this.writeConfig({ terminal });
|
|
14142
14488
|
}
|
|
14143
14489
|
};
|
|
14144
14490
|
|
|
@@ -14147,50 +14493,24 @@ var ConfigManager = class {
|
|
|
14147
14493
|
/**
|
|
14148
14494
|
* CLI 执行器
|
|
14149
14495
|
*
|
|
14150
|
-
* 负责调用外部 openspec CLI
|
|
14151
|
-
*
|
|
14152
|
-
* - ['npx', '@fission-ai/openspec'] (默认)
|
|
14153
|
-
* - ['openspec'] (全局安装)
|
|
14154
|
-
* - 自定义数组或字符串
|
|
14155
|
-
*
|
|
14156
|
-
* 注意:所有命令都使用 shell: false 执行,避免 shell 注入风险
|
|
14496
|
+
* 负责调用外部 openspec CLI 命令,统一通过 ConfigManager 的 runner 解析结果执行。
|
|
14497
|
+
* 所有命令都使用 shell: false,避免 shell 注入风险。
|
|
14157
14498
|
*/
|
|
14158
14499
|
var CliExecutor = class {
|
|
14159
14500
|
constructor(configManager, projectDir) {
|
|
14160
14501
|
this.configManager = configManager;
|
|
14161
14502
|
this.projectDir = projectDir;
|
|
14162
14503
|
}
|
|
14163
|
-
/**
|
|
14164
|
-
* 创建干净的环境变量,移除 pnpm 特有的配置
|
|
14165
|
-
* 避免 pnpm 环境变量污染 npx/npm 执行
|
|
14166
|
-
*/
|
|
14167
|
-
getCleanEnv() {
|
|
14168
|
-
const env = { ...process.env };
|
|
14169
|
-
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];
|
|
14170
|
-
return env;
|
|
14171
|
-
}
|
|
14172
|
-
/**
|
|
14173
|
-
* 构建完整命令数组
|
|
14174
|
-
*
|
|
14175
|
-
* @param args CLI 参数,如 ['init'] 或 ['archive', 'change-id']
|
|
14176
|
-
* @returns [command, ...commandArgs, ...args]
|
|
14177
|
-
*/
|
|
14178
14504
|
async buildCommandArray(args) {
|
|
14179
14505
|
return [...await this.configManager.getCliCommand(), ...args];
|
|
14180
14506
|
}
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
*
|
|
14184
|
-
* @param args CLI 参数,如 ['init'] 或 ['archive', 'change-id']
|
|
14185
|
-
* @returns 执行结果
|
|
14186
|
-
*/
|
|
14187
|
-
async execute(args) {
|
|
14188
|
-
const [cmd, ...cmdArgs] = await this.buildCommandArray(args);
|
|
14507
|
+
runCommandOnce(fullCommand) {
|
|
14508
|
+
const [cmd, ...cmdArgs] = fullCommand;
|
|
14189
14509
|
return new Promise((resolve$2) => {
|
|
14190
14510
|
const child = spawn(cmd, cmdArgs, {
|
|
14191
14511
|
cwd: this.projectDir,
|
|
14192
14512
|
shell: false,
|
|
14193
|
-
env:
|
|
14513
|
+
env: createCleanCliEnv()
|
|
14194
14514
|
});
|
|
14195
14515
|
let stdout = "";
|
|
14196
14516
|
let stderr = "";
|
|
@@ -14209,19 +14529,50 @@ var CliExecutor = class {
|
|
|
14209
14529
|
});
|
|
14210
14530
|
});
|
|
14211
14531
|
child.on("error", (err) => {
|
|
14532
|
+
const errorCode = err.code;
|
|
14533
|
+
const errorMessage = err.message + (errorCode ? ` (${errorCode})` : "");
|
|
14212
14534
|
resolve$2({
|
|
14213
14535
|
success: false,
|
|
14214
14536
|
stdout,
|
|
14215
|
-
stderr: stderr
|
|
14216
|
-
exitCode: null
|
|
14537
|
+
stderr: stderr ? `${stderr}\n${errorMessage}` : errorMessage,
|
|
14538
|
+
exitCode: null,
|
|
14539
|
+
errorCode
|
|
14217
14540
|
});
|
|
14218
14541
|
});
|
|
14219
14542
|
});
|
|
14220
14543
|
}
|
|
14544
|
+
async executeInternal(args, allowRetry) {
|
|
14545
|
+
let fullCommand;
|
|
14546
|
+
try {
|
|
14547
|
+
fullCommand = await this.buildCommandArray(args);
|
|
14548
|
+
} catch (err) {
|
|
14549
|
+
return {
|
|
14550
|
+
success: false,
|
|
14551
|
+
stdout: "",
|
|
14552
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
14553
|
+
exitCode: null
|
|
14554
|
+
};
|
|
14555
|
+
}
|
|
14556
|
+
const result = await this.runCommandOnce(fullCommand);
|
|
14557
|
+
if (allowRetry && result.errorCode === "ENOENT") {
|
|
14558
|
+
this.configManager.invalidateResolvedCliRunner();
|
|
14559
|
+
return this.executeInternal(args, false);
|
|
14560
|
+
}
|
|
14561
|
+
return {
|
|
14562
|
+
success: result.success,
|
|
14563
|
+
stdout: result.stdout,
|
|
14564
|
+
stderr: result.stderr,
|
|
14565
|
+
exitCode: result.exitCode
|
|
14566
|
+
};
|
|
14567
|
+
}
|
|
14568
|
+
/**
|
|
14569
|
+
* 执行 CLI 命令
|
|
14570
|
+
*/
|
|
14571
|
+
async execute(args) {
|
|
14572
|
+
return this.executeInternal(args, true);
|
|
14573
|
+
}
|
|
14221
14574
|
/**
|
|
14222
14575
|
* 执行 openspec init(非交互式)
|
|
14223
|
-
*
|
|
14224
|
-
* @param tools 工具列表,如 ['claude', 'cursor'] 或 'all' 或 'none'
|
|
14225
14576
|
*/
|
|
14226
14577
|
async init(tools = "all") {
|
|
14227
14578
|
const toolsArg = Array.isArray(tools) ? tools.join(",") : tools;
|
|
@@ -14233,9 +14584,6 @@ var CliExecutor = class {
|
|
|
14233
14584
|
}
|
|
14234
14585
|
/**
|
|
14235
14586
|
* 执行 openspec archive <changeId>(非交互式)
|
|
14236
|
-
*
|
|
14237
|
-
* @param changeId 要归档的 change ID
|
|
14238
|
-
* @param options 选项
|
|
14239
14587
|
*/
|
|
14240
14588
|
async archive(changeId, options = {}) {
|
|
14241
14589
|
const args = [
|
|
@@ -14292,75 +14640,108 @@ var CliExecutor = class {
|
|
|
14292
14640
|
}
|
|
14293
14641
|
/**
|
|
14294
14642
|
* 检查 CLI 是否可用
|
|
14295
|
-
* @param timeout 超时时间(毫秒),默认 10 秒
|
|
14296
14643
|
*/
|
|
14297
14644
|
async checkAvailability(timeout = 1e4) {
|
|
14298
14645
|
try {
|
|
14299
|
-
const
|
|
14300
|
-
|
|
14646
|
+
const resolved = await Promise.race([this.configManager.getResolvedCliRunner(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("CLI runner resolve timed out")), timeout))]);
|
|
14647
|
+
const versionResult = await Promise.race([this.runCommandOnce([...resolved.commandParts, "--version"]), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("CLI check timed out")), timeout))]);
|
|
14648
|
+
if (versionResult.success) return {
|
|
14301
14649
|
available: true,
|
|
14302
|
-
version:
|
|
14650
|
+
version: versionResult.stdout.trim() || resolved.version,
|
|
14651
|
+
effectiveCommand: resolved.command,
|
|
14652
|
+
tried: resolved.attempts.map((attempt) => attempt.command)
|
|
14303
14653
|
};
|
|
14304
14654
|
return {
|
|
14305
14655
|
available: false,
|
|
14306
|
-
error:
|
|
14656
|
+
error: versionResult.stderr || "Unknown error",
|
|
14657
|
+
effectiveCommand: resolved.command,
|
|
14658
|
+
tried: resolved.attempts.map((attempt) => attempt.command)
|
|
14307
14659
|
};
|
|
14308
14660
|
} catch (err) {
|
|
14309
14661
|
return {
|
|
14310
14662
|
available: false,
|
|
14311
|
-
error: err instanceof Error ? err.message :
|
|
14663
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14312
14664
|
};
|
|
14313
14665
|
}
|
|
14314
14666
|
}
|
|
14315
14667
|
/**
|
|
14316
14668
|
* 流式执行 CLI 命令
|
|
14317
|
-
*
|
|
14318
|
-
* @param args CLI 参数
|
|
14319
|
-
* @param onEvent 事件回调
|
|
14320
|
-
* @returns 取消函数
|
|
14321
14669
|
*/
|
|
14322
14670
|
async executeStream(args, onEvent) {
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14671
|
+
let cancelled = false;
|
|
14672
|
+
let activeChild = null;
|
|
14673
|
+
const start = async (allowRetry) => {
|
|
14674
|
+
if (cancelled) return;
|
|
14675
|
+
let fullCommand;
|
|
14676
|
+
try {
|
|
14677
|
+
fullCommand = await this.buildCommandArray(args);
|
|
14678
|
+
} catch (err) {
|
|
14679
|
+
onEvent({
|
|
14680
|
+
type: "stderr",
|
|
14681
|
+
data: err instanceof Error ? err.message : String(err)
|
|
14682
|
+
});
|
|
14683
|
+
onEvent({
|
|
14684
|
+
type: "exit",
|
|
14685
|
+
exitCode: null
|
|
14686
|
+
});
|
|
14687
|
+
return;
|
|
14688
|
+
}
|
|
14335
14689
|
onEvent({
|
|
14336
|
-
type: "
|
|
14337
|
-
data:
|
|
14690
|
+
type: "command",
|
|
14691
|
+
data: fullCommand.join(" ")
|
|
14338
14692
|
});
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14693
|
+
const [cmd, ...cmdArgs] = fullCommand;
|
|
14694
|
+
const child = spawn(cmd, cmdArgs, {
|
|
14695
|
+
cwd: this.projectDir,
|
|
14696
|
+
shell: false,
|
|
14697
|
+
env: createCleanCliEnv()
|
|
14344
14698
|
});
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14699
|
+
activeChild = child;
|
|
14700
|
+
child.stdout?.on("data", (data) => {
|
|
14701
|
+
onEvent({
|
|
14702
|
+
type: "stdout",
|
|
14703
|
+
data: data.toString()
|
|
14704
|
+
});
|
|
14350
14705
|
});
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14706
|
+
child.stderr?.on("data", (data) => {
|
|
14707
|
+
onEvent({
|
|
14708
|
+
type: "stderr",
|
|
14709
|
+
data: data.toString()
|
|
14710
|
+
});
|
|
14356
14711
|
});
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14712
|
+
child.on("close", (exitCode) => {
|
|
14713
|
+
if (activeChild !== child) return;
|
|
14714
|
+
activeChild = null;
|
|
14715
|
+
onEvent({
|
|
14716
|
+
type: "exit",
|
|
14717
|
+
exitCode
|
|
14718
|
+
});
|
|
14360
14719
|
});
|
|
14361
|
-
|
|
14720
|
+
child.on("error", (err) => {
|
|
14721
|
+
if (activeChild !== child) return;
|
|
14722
|
+
activeChild = null;
|
|
14723
|
+
const code = err.code;
|
|
14724
|
+
const message = err.message + (code ? ` (${code})` : "");
|
|
14725
|
+
if (allowRetry && code === "ENOENT" && !cancelled) {
|
|
14726
|
+
this.configManager.invalidateResolvedCliRunner();
|
|
14727
|
+
start(false);
|
|
14728
|
+
return;
|
|
14729
|
+
}
|
|
14730
|
+
onEvent({
|
|
14731
|
+
type: "stderr",
|
|
14732
|
+
data: message
|
|
14733
|
+
});
|
|
14734
|
+
onEvent({
|
|
14735
|
+
type: "exit",
|
|
14736
|
+
exitCode: null
|
|
14737
|
+
});
|
|
14738
|
+
});
|
|
14739
|
+
};
|
|
14740
|
+
await start(true);
|
|
14362
14741
|
return () => {
|
|
14363
|
-
|
|
14742
|
+
cancelled = true;
|
|
14743
|
+
activeChild?.kill();
|
|
14744
|
+
activeChild = null;
|
|
14364
14745
|
};
|
|
14365
14746
|
}
|
|
14366
14747
|
/**
|
|
@@ -14389,13 +14770,6 @@ var CliExecutor = class {
|
|
|
14389
14770
|
}
|
|
14390
14771
|
/**
|
|
14391
14772
|
* 流式执行任意命令(数组形式)
|
|
14392
|
-
*
|
|
14393
|
-
* 用于执行不需要 openspec CLI 前缀的命令,如 npm install。
|
|
14394
|
-
* 使用 shell: false 避免 shell 注入风险。
|
|
14395
|
-
*
|
|
14396
|
-
* @param command 命令数组,如 ['npm', 'install', '-g', '@fission-ai/openspec']
|
|
14397
|
-
* @param onEvent 事件回调
|
|
14398
|
-
* @returns 取消函数
|
|
14399
14773
|
*/
|
|
14400
14774
|
executeCommandStream(command, onEvent) {
|
|
14401
14775
|
const [cmd, ...cmdArgs] = command;
|
|
@@ -14406,7 +14780,7 @@ var CliExecutor = class {
|
|
|
14406
14780
|
const child = spawn(cmd, cmdArgs, {
|
|
14407
14781
|
cwd: this.projectDir,
|
|
14408
14782
|
shell: false,
|
|
14409
|
-
env:
|
|
14783
|
+
env: createCleanCliEnv()
|
|
14410
14784
|
});
|
|
14411
14785
|
child.stdout?.on("data", (data) => {
|
|
14412
14786
|
onEvent({
|
|
@@ -14427,9 +14801,10 @@ var CliExecutor = class {
|
|
|
14427
14801
|
});
|
|
14428
14802
|
});
|
|
14429
14803
|
child.on("error", (err) => {
|
|
14804
|
+
const code = err.code;
|
|
14430
14805
|
onEvent({
|
|
14431
14806
|
type: "stderr",
|
|
14432
|
-
data: err.message
|
|
14807
|
+
data: err.message + (code ? ` (${code})` : "")
|
|
14433
14808
|
});
|
|
14434
14809
|
onEvent({
|
|
14435
14810
|
type: "exit",
|
|
@@ -21477,7 +21852,7 @@ var require_dist = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnpm/yaml
|
|
|
21477
21852
|
|
|
21478
21853
|
//#endregion
|
|
21479
21854
|
//#region ../core/src/opsx-kernel.ts
|
|
21480
|
-
var import_dist$
|
|
21855
|
+
var import_dist$1 = require_dist();
|
|
21481
21856
|
function parseCliJson$1(raw$1, schema$6, label) {
|
|
21482
21857
|
const trimmed = raw$1.trim();
|
|
21483
21858
|
if (!trimmed) throw new Error(`${label} returned empty output`);
|
|
@@ -22027,7 +22402,7 @@ const SchemaYamlSchema$1 = objectType({
|
|
|
22027
22402
|
}).optional()
|
|
22028
22403
|
});
|
|
22029
22404
|
function parseSchemaYamlInline(content) {
|
|
22030
|
-
const raw$1 = (0, import_dist$
|
|
22405
|
+
const raw$1 = (0, import_dist$1.parse)(content);
|
|
22031
22406
|
const parsed = SchemaYamlSchema$1.safeParse(raw$1);
|
|
22032
22407
|
if (!parsed.success) throw new Error(`Invalid schema.yaml: ${parsed.error.message}`);
|
|
22033
22408
|
const { artifacts, apply, name, description, version } = parsed.data;
|
|
@@ -22158,64 +22533,6 @@ const PtyServerMessageSchema = discriminatedUnionType("type", [
|
|
|
22158
22533
|
PtyErrorResponseSchema
|
|
22159
22534
|
]);
|
|
22160
22535
|
|
|
22161
|
-
//#endregion
|
|
22162
|
-
//#region ../server/src/reactive-subscription.ts
|
|
22163
|
-
/**
|
|
22164
|
-
* 创建响应式订阅
|
|
22165
|
-
*
|
|
22166
|
-
* 自动追踪 task 中的文件依赖,当依赖变更时自动重新执行并推送新数据。
|
|
22167
|
-
*
|
|
22168
|
-
* @param task 要执行的异步任务,内部的文件读取会被自动追踪
|
|
22169
|
-
* @returns tRPC observable
|
|
22170
|
-
*
|
|
22171
|
-
* @example
|
|
22172
|
-
* ```typescript
|
|
22173
|
-
* // 在 router 中使用
|
|
22174
|
-
* subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
22175
|
-
* return createReactiveSubscription(() => ctx.adapter.listSpecsWithMeta())
|
|
22176
|
-
* })
|
|
22177
|
-
* ```
|
|
22178
|
-
*/
|
|
22179
|
-
function createReactiveSubscription(task) {
|
|
22180
|
-
return observable((emit) => {
|
|
22181
|
-
const context = new ReactiveContext();
|
|
22182
|
-
const controller = new AbortController();
|
|
22183
|
-
(async () => {
|
|
22184
|
-
try {
|
|
22185
|
-
for await (const data of context.stream(task, controller.signal)) emit.next(data);
|
|
22186
|
-
} catch (err) {
|
|
22187
|
-
if (!controller.signal.aborted) emit.error(err);
|
|
22188
|
-
}
|
|
22189
|
-
})();
|
|
22190
|
-
return () => {
|
|
22191
|
-
controller.abort();
|
|
22192
|
-
};
|
|
22193
|
-
});
|
|
22194
|
-
}
|
|
22195
|
-
/**
|
|
22196
|
-
* 创建带输入参数的响应式订阅
|
|
22197
|
-
*
|
|
22198
|
-
* @param task 接收输入参数的异步任务
|
|
22199
|
-
* @returns 返回一个函数,接收输入参数并返回 tRPC observable
|
|
22200
|
-
*
|
|
22201
|
-
* @example
|
|
22202
|
-
* ```typescript
|
|
22203
|
-
* // 在 router 中使用
|
|
22204
|
-
* subscribeOne: publicProcedure
|
|
22205
|
-
* .input(z.object({ id: z.string() }))
|
|
22206
|
-
* .subscription(({ ctx, input }) => {
|
|
22207
|
-
* return createReactiveSubscriptionWithInput(
|
|
22208
|
-
* (id: string) => ctx.adapter.readSpec(id)
|
|
22209
|
-
* )(input.id)
|
|
22210
|
-
* })
|
|
22211
|
-
* ```
|
|
22212
|
-
*/
|
|
22213
|
-
function createReactiveSubscriptionWithInput(task) {
|
|
22214
|
-
return (input) => {
|
|
22215
|
-
return createReactiveSubscription(() => task(input));
|
|
22216
|
-
};
|
|
22217
|
-
}
|
|
22218
|
-
|
|
22219
22536
|
//#endregion
|
|
22220
22537
|
//#region ../server/src/cli-stream-observable.ts
|
|
22221
22538
|
/**
|
|
@@ -22275,7 +22592,7 @@ function createCliStreamObservable(startStream) {
|
|
|
22275
22592
|
|
|
22276
22593
|
//#endregion
|
|
22277
22594
|
//#region ../server/src/opsx-schema.ts
|
|
22278
|
-
var import_dist
|
|
22595
|
+
var import_dist = require_dist();
|
|
22279
22596
|
const SchemaYamlArtifactSchema = objectType({
|
|
22280
22597
|
id: stringType(),
|
|
22281
22598
|
generates: stringType(),
|
|
@@ -22296,7 +22613,7 @@ const SchemaYamlSchema = objectType({
|
|
|
22296
22613
|
}).optional()
|
|
22297
22614
|
});
|
|
22298
22615
|
function parseSchemaYaml(content) {
|
|
22299
|
-
const raw$1 = (0, import_dist
|
|
22616
|
+
const raw$1 = (0, import_dist.parse)(content);
|
|
22300
22617
|
const parsed = SchemaYamlSchema.safeParse(raw$1);
|
|
22301
22618
|
if (!parsed.success) throw new Error(`Invalid schema.yaml: ${parsed.error.message}`);
|
|
22302
22619
|
const { artifacts, apply, name, description, version } = parsed.data;
|
|
@@ -22378,9 +22695,66 @@ var ReactiveKV = class {
|
|
|
22378
22695
|
/** Singleton instance shared across the server lifetime */
|
|
22379
22696
|
const reactiveKV = new ReactiveKV();
|
|
22380
22697
|
|
|
22698
|
+
//#endregion
|
|
22699
|
+
//#region ../server/src/reactive-subscription.ts
|
|
22700
|
+
/**
|
|
22701
|
+
* 创建响应式订阅
|
|
22702
|
+
*
|
|
22703
|
+
* 自动追踪 task 中的文件依赖,当依赖变更时自动重新执行并推送新数据。
|
|
22704
|
+
*
|
|
22705
|
+
* @param task 要执行的异步任务,内部的文件读取会被自动追踪
|
|
22706
|
+
* @returns tRPC observable
|
|
22707
|
+
*
|
|
22708
|
+
* @example
|
|
22709
|
+
* ```typescript
|
|
22710
|
+
* // 在 router 中使用
|
|
22711
|
+
* subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
22712
|
+
* return createReactiveSubscription(() => ctx.adapter.listSpecsWithMeta())
|
|
22713
|
+
* })
|
|
22714
|
+
* ```
|
|
22715
|
+
*/
|
|
22716
|
+
function createReactiveSubscription(task) {
|
|
22717
|
+
return observable((emit) => {
|
|
22718
|
+
const context = new ReactiveContext();
|
|
22719
|
+
const controller = new AbortController();
|
|
22720
|
+
(async () => {
|
|
22721
|
+
try {
|
|
22722
|
+
for await (const data of context.stream(task, controller.signal)) emit.next(data);
|
|
22723
|
+
} catch (err) {
|
|
22724
|
+
if (!controller.signal.aborted) emit.error(err);
|
|
22725
|
+
}
|
|
22726
|
+
})();
|
|
22727
|
+
return () => {
|
|
22728
|
+
controller.abort();
|
|
22729
|
+
};
|
|
22730
|
+
});
|
|
22731
|
+
}
|
|
22732
|
+
/**
|
|
22733
|
+
* 创建带输入参数的响应式订阅
|
|
22734
|
+
*
|
|
22735
|
+
* @param task 接收输入参数的异步任务
|
|
22736
|
+
* @returns 返回一个函数,接收输入参数并返回 tRPC observable
|
|
22737
|
+
*
|
|
22738
|
+
* @example
|
|
22739
|
+
* ```typescript
|
|
22740
|
+
* // 在 router 中使用
|
|
22741
|
+
* subscribeOne: publicProcedure
|
|
22742
|
+
* .input(z.object({ id: z.string() }))
|
|
22743
|
+
* .subscription(({ ctx, input }) => {
|
|
22744
|
+
* return createReactiveSubscriptionWithInput(
|
|
22745
|
+
* (id: string) => ctx.adapter.readSpec(id)
|
|
22746
|
+
* )(input.id)
|
|
22747
|
+
* })
|
|
22748
|
+
* ```
|
|
22749
|
+
*/
|
|
22750
|
+
function createReactiveSubscriptionWithInput(task) {
|
|
22751
|
+
return (input) => {
|
|
22752
|
+
return createReactiveSubscription(() => task(input));
|
|
22753
|
+
};
|
|
22754
|
+
}
|
|
22755
|
+
|
|
22381
22756
|
//#endregion
|
|
22382
22757
|
//#region ../server/src/router.ts
|
|
22383
|
-
var import_dist = /* @__PURE__ */ __toESM$1(require_dist(), 1);
|
|
22384
22758
|
const t = initTRPC.context().create();
|
|
22385
22759
|
const router = t.router;
|
|
22386
22760
|
const publicProcedure = t.procedure;
|
|
@@ -22758,20 +23132,40 @@ const configRouter = router({
|
|
|
22758
23132
|
return getDefaultCliCommandString();
|
|
22759
23133
|
}),
|
|
22760
23134
|
update: publicProcedure.input(objectType({
|
|
22761
|
-
cli: objectType({
|
|
22762
|
-
|
|
23135
|
+
cli: objectType({
|
|
23136
|
+
command: stringType().nullable().optional(),
|
|
23137
|
+
args: arrayType(stringType()).nullable().optional()
|
|
23138
|
+
}).optional(),
|
|
23139
|
+
theme: enumType([
|
|
22763
23140
|
"light",
|
|
22764
23141
|
"dark",
|
|
22765
23142
|
"system"
|
|
22766
|
-
])
|
|
23143
|
+
]).optional(),
|
|
23144
|
+
terminal: objectType({
|
|
23145
|
+
fontSize: numberType().min(8).max(32).optional(),
|
|
23146
|
+
fontFamily: stringType().optional(),
|
|
23147
|
+
cursorBlink: booleanType().optional(),
|
|
23148
|
+
cursorStyle: enumType([
|
|
23149
|
+
"block",
|
|
23150
|
+
"underline",
|
|
23151
|
+
"bar"
|
|
23152
|
+
]).optional(),
|
|
23153
|
+
scrollback: numberType().min(0).max(1e5).optional()
|
|
23154
|
+
}).optional()
|
|
22767
23155
|
})).mutation(async ({ ctx, input }) => {
|
|
23156
|
+
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
23157
|
+
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
23158
|
+
if (hasCliCommand && !hasCliArgs) {
|
|
23159
|
+
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
23160
|
+
if (input.theme !== void 0 || input.terminal !== void 0) await ctx.configManager.writeConfig({
|
|
23161
|
+
theme: input.theme,
|
|
23162
|
+
terminal: input.terminal
|
|
23163
|
+
});
|
|
23164
|
+
return { success: true };
|
|
23165
|
+
}
|
|
22768
23166
|
await ctx.configManager.writeConfig(input);
|
|
22769
23167
|
return { success: true };
|
|
22770
23168
|
}),
|
|
22771
|
-
setCliCommand: publicProcedure.input(objectType({ command: stringType() })).mutation(async ({ ctx, input }) => {
|
|
22772
|
-
await ctx.configManager.setCliCommand(input.command);
|
|
22773
|
-
return { success: true };
|
|
22774
|
-
}),
|
|
22775
23169
|
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
22776
23170
|
return createReactiveSubscription(() => ctx.configManager.readConfig());
|
|
22777
23171
|
})
|
|
@@ -23117,32 +23511,6 @@ const opsxRouter = router({
|
|
|
23117
23511
|
await writeFile$1(join$1(openspecDir, "config.yaml"), input.content, "utf-8");
|
|
23118
23512
|
return { success: true };
|
|
23119
23513
|
}),
|
|
23120
|
-
updateProjectConfigUi: publicProcedure.input(objectType({
|
|
23121
|
-
"font-size": numberType().min(8).max(32).optional(),
|
|
23122
|
-
"font-families": arrayType(stringType()).optional(),
|
|
23123
|
-
"cursor-blink": booleanType().optional(),
|
|
23124
|
-
"cursor-style": enumType([
|
|
23125
|
-
"block",
|
|
23126
|
-
"underline",
|
|
23127
|
-
"bar"
|
|
23128
|
-
]).optional(),
|
|
23129
|
-
scrollback: numberType().min(0).max(1e5).optional()
|
|
23130
|
-
})).mutation(async ({ ctx, input }) => {
|
|
23131
|
-
const configPath = join$1(ctx.projectDir, "openspec", "config.yaml");
|
|
23132
|
-
let doc;
|
|
23133
|
-
try {
|
|
23134
|
-
const content = await readFile$1(configPath, "utf-8");
|
|
23135
|
-
doc = import_dist.parseDocument(content);
|
|
23136
|
-
} catch {
|
|
23137
|
-
doc = new import_dist.Document({});
|
|
23138
|
-
}
|
|
23139
|
-
if (!doc.has("ui")) doc.set("ui", doc.createNode({}));
|
|
23140
|
-
const uiNode = doc.get("ui", true);
|
|
23141
|
-
for (const [key, value] of Object.entries(input)) if (value !== void 0) uiNode.set(key, value);
|
|
23142
|
-
await mkdir$1(join$1(ctx.projectDir, "openspec"), { recursive: true });
|
|
23143
|
-
await writeFile$1(configPath, doc.toString(), "utf-8");
|
|
23144
|
-
return { success: true };
|
|
23145
|
-
}),
|
|
23146
23514
|
listChanges: publicProcedure.query(async ({ ctx }) => {
|
|
23147
23515
|
return reactiveReadDir(join$1(ctx.projectDir, "openspec", "changes"), {
|
|
23148
23516
|
directoriesOnly: true,
|