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.
Files changed (29) hide show
  1. package/dist/cli.mjs +2 -2
  2. package/dist/index.mjs +1 -1
  3. package/dist/{src-DfG_-u90.mjs → src-D5eBKdVF.mjs} +612 -244
  4. package/package.json +3 -3
  5. package/web/assets/{BufferResource-CVUoegR6.js → BufferResource-DMjllemV.js} +1 -1
  6. package/web/assets/{CanvasRenderer-BEIcB8i1.js → CanvasRenderer-kKbOzIca.js} +1 -1
  7. package/web/assets/{Filter-Bu_qhr6H.js → Filter-C5l7SDia.js} +1 -1
  8. package/web/assets/{RenderTargetSystem-DWouFDxU.js → RenderTargetSystem-BaFVF7ku.js} +1 -1
  9. package/web/assets/{WebGLRenderer-6FH_N1FV.js → WebGLRenderer-Ch58nD6Y.js} +1 -1
  10. package/web/assets/{WebGPURenderer-B8sJk3Sv.js → WebGPURenderer-Bpf-ane5.js} +1 -1
  11. package/web/assets/{browserAll-CLKeV1yb.js → browserAll-CiXGzqJc.js} +1 -1
  12. package/web/assets/{index-Bv7pWR8R.js → index-BQ6UeNz3.js} +1 -1
  13. package/web/assets/{index-BtNuxyw4.js → index-BTMNsWWi.js} +1 -1
  14. package/web/assets/{index-CEKSUzvw.js → index-BXWVYqbO.js} +1 -1
  15. package/web/assets/{index-BRp8MJ9v.js → index-BXqdCpuU.js} +1 -1
  16. package/web/assets/{index-BE5-y0_g.js → index-Bdb0Fpwv.js} +1 -1
  17. package/web/assets/{index-BPCTI2mG.js → index-BgHJ8w_f.js} +1 -1
  18. package/web/assets/{index-D4AU46yO.js → index-BwIIOUjO.js} +1 -1
  19. package/web/assets/{index-mWXhCp9j.js → index-CFkiyi1j.js} +1 -1
  20. package/web/assets/{index-eQZwF8qE.js → index-CLxF_OQQ.js} +1 -1
  21. package/web/assets/{index-DXRZmZm8.js → index-CckLtqno.js} +1 -1
  22. package/web/assets/{index-CEHMo0EU.js → index-CsXgfYOH.js} +252 -260
  23. package/web/assets/{index-CX13iBBs.js → index-D2Uig6TZ.js} +1 -1
  24. package/web/assets/{index-BlZ-sasH.js → index-DEWTHv2o.js} +1 -1
  25. package/web/assets/{index-Bp_dnlLF.js → index-DJZG7SGL.js} +1 -1
  26. package/web/assets/{index-CoOT7eZ9.js → index-L7IKyBGp.js} +1 -1
  27. package/web/assets/{index-Byr3HkRi.js → index-e2r1Tz_y.js} +1 -1
  28. package/web/assets/{webworkerAll-DjWoTx9g.js → webworkerAll-D0vlbEoH.js} +1 -1
  29. 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
- /** 默认的 fallback CLI 命令(数组形式) */
13930
- const FALLBACK_CLI_COMMAND = ["npx", "@fission-ai/openspec"];
13931
- /** 全局 openspec 命令(数组形式) */
13932
- const GLOBAL_CLI_COMMAND = ["openspec"];
13933
- /** 缓存检测到的 CLI 命令 */
13934
- let detectedCliCommand = null;
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. 简单字符串:用空格分割,如 `npx @fission-ai/openspec`
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
- return trimmed.split(/\s+/).filter(Boolean);
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 detectCliCommand();
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 detectCliCommand()).join(" ");
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({ command: stringType().optional() }).default({}),
14056
- ui: objectType({ theme: enumType([
14057
- "light",
14058
- "dark",
14059
- "system"
14060
- ]).default("system") }).default({})
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
- ui: { theme: "system" }
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
- ...config,
14108
- cli: {
14109
- ...current.cli,
14110
- ...config.cli
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
- await writeFile(this.configPath, JSON.stringify(merged, null, 2), "utf-8");
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
- const config = await this.readConfig();
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.getCliCommand()).join(" ");
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
- await this.writeConfig({ cli: { command } });
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
- * 命令前缀从 ConfigManager 获取,支持:
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
- * 执行 CLI 命令
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: this.getCleanEnv()
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 + "\n" + err.message,
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 result = await Promise.race([this.execute(["--version"]), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("CLI check timed out")), timeout))]);
14300
- if (result.success) return {
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: result.stdout.trim()
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: result.stderr || "Unknown 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 : "Unknown error"
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
- const fullCommand = await this.buildCommandArray(args);
14324
- const [cmd, ...cmdArgs] = fullCommand;
14325
- onEvent({
14326
- type: "command",
14327
- data: fullCommand.join(" ")
14328
- });
14329
- const child = spawn(cmd, cmdArgs, {
14330
- cwd: this.projectDir,
14331
- shell: false,
14332
- env: this.getCleanEnv()
14333
- });
14334
- child.stdout?.on("data", (data) => {
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: "stdout",
14337
- data: data.toString()
14690
+ type: "command",
14691
+ data: fullCommand.join(" ")
14338
14692
  });
14339
- });
14340
- child.stderr?.on("data", (data) => {
14341
- onEvent({
14342
- type: "stderr",
14343
- data: data.toString()
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
- child.on("close", (exitCode) => {
14347
- onEvent({
14348
- type: "exit",
14349
- exitCode
14699
+ activeChild = child;
14700
+ child.stdout?.on("data", (data) => {
14701
+ onEvent({
14702
+ type: "stdout",
14703
+ data: data.toString()
14704
+ });
14350
14705
  });
14351
- });
14352
- child.on("error", (err) => {
14353
- onEvent({
14354
- type: "stderr",
14355
- data: err.message
14706
+ child.stderr?.on("data", (data) => {
14707
+ onEvent({
14708
+ type: "stderr",
14709
+ data: data.toString()
14710
+ });
14356
14711
  });
14357
- onEvent({
14358
- type: "exit",
14359
- exitCode: null
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
- child.kill();
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: this.getCleanEnv()
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$2 = require_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$2.parse)(content);
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$1 = require_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$1.parse)(content);
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({ command: stringType() }).optional(),
22762
- ui: objectType({ theme: enumType([
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
- ]) }).optional()
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,