openspecui 2.1.2 → 2.1.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 (33) hide show
  1. package/dist/cli.mjs +3 -8
  2. package/dist/index.mjs +1 -1
  3. package/dist/{open-DfwCb8mL.mjs → open-CLEEmeZr.mjs} +1 -1
  4. package/dist/{src-Nn_MJz41.mjs → src-DFu4IXMV.mjs} +320 -72
  5. package/package.json +1 -1
  6. package/web/assets/{BufferResource-CSbEbiLP.js → BufferResource-CYUkNl6e.js} +1 -1
  7. package/web/assets/{CanvasRenderer-CvpAMPzA.js → CanvasRenderer-B3FZcTbO.js} +1 -1
  8. package/web/assets/{Filter-BqwVCa6B.js → Filter-B6m1PS4e.js} +1 -1
  9. package/web/assets/{RenderTargetSystem-mD_Y2dUA.js → RenderTargetSystem-D-tQO-WR.js} +1 -1
  10. package/web/assets/{WebGLRenderer-DsvDwXyL.js → WebGLRenderer-CJA_VvyN.js} +1 -1
  11. package/web/assets/{WebGPURenderer-YcBYXAol.js → WebGPURenderer-CL8THZq8.js} +1 -1
  12. package/web/assets/{browserAll-MbWxSnea.js → browserAll-Bp7d2QAW.js} +1 -1
  13. package/web/assets/{ghostty-web-cM8zjOdb.js → ghostty-web-Dn3qPWne.js} +1 -1
  14. package/web/assets/{index-kjHuAdn9.js → index--lAEzTl3.js} +1 -1
  15. package/web/assets/{index-CskIUxS-.js → index-B_QlxcT9.js} +1 -1
  16. package/web/assets/{index-BCMJS8eq.js → index-CKG6_6g8.js} +1 -1
  17. package/web/assets/{index-DuZDcW_P.js → index-CXQzuBsO.js} +1 -1
  18. package/web/assets/{index-Cr4OFm9E.js → index-CeCKFMBL.js} +1 -1
  19. package/web/assets/{index-Cq4njHD4.js → index-Cy7P7qsp.js} +193 -193
  20. package/web/assets/{index-JHrsE4PU.js → index-D3_KWKkX.js} +1 -1
  21. package/web/assets/{index-KaodXPu0.js → index-D64umkMM.js} +1 -1
  22. package/web/assets/{index-TGQYcfbH.js → index-D7SV9jDS.js} +1 -1
  23. package/web/assets/index-D8ZU3Gye.css +1 -0
  24. package/web/assets/{index-DyZwNTgF.js → index-DF-P0r8_.js} +1 -1
  25. package/web/assets/{index-DijRYB99.js → index-DIk8Jt_d.js} +1 -1
  26. package/web/assets/{index-BMKX_bGe.js → index-DVQLid3N.js} +1 -1
  27. package/web/assets/{index-B0lABsCj.js → index-DbAAfBjU.js} +1 -1
  28. package/web/assets/{index-BY40EQxT.js → index-G43sk1RJ.js} +1 -1
  29. package/web/assets/{index-D0sXMPhu.js → index-QwKm9rik.js} +1 -1
  30. package/web/assets/{index-B1J3yPM2.js → index-oMDefHHq.js} +1 -1
  31. package/web/assets/{webworkerAll-BE_Ec3Rk.js → webworkerAll-iiZBXOCn.js} +1 -1
  32. package/web/index.html +2 -2
  33. package/web/assets/index-DHeR2UYq.css +0 -1
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as SchemaInfoSchema, c as toOpsxDisplayPath, d as CliExecutor, f as ConfigManager, g as __toESM, h as __commonJS, i as SchemaDetailSchema, l as buildHostedLaunchUrl, m as OpenSpecAdapter, o as SchemaResolutionSchema, p as DEFAULT_CONFIG, r as require_dist, s as TemplatesSchema, t as startServer, u as resolveHostedAppBaseUrl } from "./src-Nn_MJz41.mjs";
2
+ import { a as SchemaInfoSchema, c as toOpsxDisplayPath, d as CliExecutor, f as ConfigManager, g as __toESM, h as __commonJS, i as SchemaDetailSchema, l as buildHostedLaunchUrl, m as OpenSpecAdapter, o as SchemaResolutionSchema, p as DEFAULT_CONFIG, r as require_dist, s as TemplatesSchema, t as startServer, u as resolveHostedAppBaseUrl } from "./src-DFu4IXMV.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { createServer } from "node:net";
5
5
  import { basename, dirname, extname, join, normalize, relative, resolve } from "path";
@@ -4508,7 +4508,7 @@ var yargs_default = Yargs;
4508
4508
  //#endregion
4509
4509
  //#region package.json
4510
4510
  var import_dist = require_dist();
4511
- var version = "2.1.2";
4511
+ var version = "2.1.3";
4512
4512
 
4513
4513
  //#endregion
4514
4514
  //#region src/export.ts
@@ -4811,13 +4811,9 @@ async function generateSnapshot(projectDir) {
4811
4811
  };
4812
4812
  }));
4813
4813
  let projectMd;
4814
- let agentsMd;
4815
4814
  try {
4816
4815
  projectMd = await adapter.readProjectMd() ?? void 0;
4817
4816
  } catch {}
4818
- try {
4819
- agentsMd = await adapter.readAgentsMd() ?? void 0;
4820
- } catch {}
4821
4817
  let configYaml;
4822
4818
  let schemas = [];
4823
4819
  const schemaDetails = {};
@@ -4920,7 +4916,6 @@ async function generateSnapshot(projectDir) {
4920
4916
  changes,
4921
4917
  archives,
4922
4918
  projectMd,
4923
- agentsMd,
4924
4919
  opsx: {
4925
4920
  configYaml,
4926
4921
  schemas,
@@ -5462,7 +5457,7 @@ async function main() {
5462
5457
  }
5463
5458
  console.log("");
5464
5459
  if (argv.open) {
5465
- await (await import("./open-DfwCb8mL.mjs")).default(browserUrl);
5460
+ await (await import("./open-CLEEmeZr.mjs")).default(browserUrl);
5466
5461
  console.log(useHostedApp ? "🌐 Hosted app opened" : "🌐 Browser opened");
5467
5462
  }
5468
5463
  console.log("");
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as createServer, t as startServer } from "./src-Nn_MJz41.mjs";
1
+ import { n as createServer, t as startServer } from "./src-DFu4IXMV.mjs";
2
2
 
3
3
  export { createServer, startServer };
@@ -1,12 +1,12 @@
1
1
  import fs, { constants } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import fs$1 from "node:fs";
4
+ import os from "node:os";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import childProcess, { execFile } from "node:child_process";
6
7
  import { promisify } from "node:util";
7
8
  import process from "node:process";
8
9
  import { Buffer } from "node:buffer";
9
- import os from "node:os";
10
10
 
11
11
  //#region ../../node_modules/.pnpm/is-docker@3.0.0/node_modules/is-docker/index.js
12
12
  let isDockerCached;
@@ -14,6 +14,7 @@ import { EventEmitter } from "events";
14
14
  import { watch } from "fs";
15
15
  import { exec, spawn } from "child_process";
16
16
  import { promisify } from "util";
17
+ import { homedir } from "node:os";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import { EventEmitter as EventEmitter$1 } from "node:events";
19
20
  import { execFile } from "node:child_process";
@@ -1669,6 +1670,23 @@ async function reactiveStat(path$1) {
1669
1670
  }
1670
1671
  return state.get();
1671
1672
  }
1673
+ /**
1674
+ * 清除指定路径的缓存(用于测试)
1675
+ */
1676
+ function clearCache(path$1) {
1677
+ if (path$1) {
1678
+ const normalizedPath = resolve$1(path$1);
1679
+ for (const [key, release] of releaseCache$1) if (key.includes(normalizedPath)) {
1680
+ release();
1681
+ releaseCache$1.delete(key);
1682
+ stateCache$1.delete(key);
1683
+ }
1684
+ } else {
1685
+ for (const release of releaseCache$1.values()) release();
1686
+ releaseCache$1.clear();
1687
+ stateCache$1.clear();
1688
+ }
1689
+ }
1672
1690
 
1673
1691
  //#endregion
1674
1692
  //#region ../core/src/validator.ts
@@ -1869,23 +1887,11 @@ var OpenSpecAdapter = class {
1869
1887
  return reactiveReadFile(join(this.openspecDir, "project.md"));
1870
1888
  }
1871
1889
  /**
1872
- * Read AGENTS.md content (reactive)
1873
- */
1874
- async readAgentsMd() {
1875
- return reactiveReadFile(join(this.openspecDir, "AGENTS.md"));
1876
- }
1877
- /**
1878
1890
  * Write project.md content
1879
1891
  */
1880
1892
  async writeProjectMd(content) {
1881
1893
  await writeFile(join(this.openspecDir, "project.md"), content, "utf-8");
1882
1894
  }
1883
- /**
1884
- * Write AGENTS.md content
1885
- */
1886
- async writeAgentsMd(content) {
1887
- await writeFile(join(this.openspecDir, "AGENTS.md"), content, "utf-8");
1888
- }
1889
1895
  async readSpec(specId) {
1890
1896
  try {
1891
1897
  const content = await this.readSpecRaw(specId);
@@ -2056,24 +2062,6 @@ This project uses OpenSpec for spec-driven development.
2056
2062
  - \`specs/\` - Source of truth specifications
2057
2063
  - \`changes/\` - Active change proposals
2058
2064
  - \`changes/archive/\` - Completed changes
2059
- `, "utf-8");
2060
- await writeFile(join(this.openspecDir, "AGENTS.md"), `# AI Agent Instructions
2061
-
2062
- This project uses OpenSpec for spec-driven development.
2063
-
2064
- ## Available Commands
2065
- - \`openspec list\` - List changes or specs
2066
- - \`openspec view\` - Dashboard view
2067
- - \`openspec show <name>\` - Show change or spec details
2068
- - \`openspec validate <name>\` - Validate change or spec
2069
- - \`openspec archive <change>\` - Archive completed change
2070
-
2071
- ## Workflow
2072
- 1. Create a change proposal in \`changes/<change-id>/proposal.md\`
2073
- 2. Define delta specs in \`changes/<change-id>/specs/\`
2074
- 3. Track tasks in \`changes/<change-id>/tasks.md\`
2075
- 4. Implement and mark tasks complete
2076
- 5. Archive when done: \`openspec archive <change-id>\`
2077
2065
  `, "utf-8");
2078
2066
  }
2079
2067
  /**
@@ -5879,7 +5867,7 @@ var OpenSpecWatcher = class extends EventEmitter {
5879
5867
  });
5880
5868
  });
5881
5869
  this.watchDir(this.openspecDir, (filename, eventType) => {
5882
- if (filename === "project.md" || filename === "AGENTS.md") this.emitDebounced(`project:${filename}`, {
5870
+ if (filename === "project.md") this.emitDebounced(`project:${filename}`, {
5883
5871
  type: "project",
5884
5872
  action: eventType === "rename" ? "create" : "update",
5885
5873
  path: join(this.openspecDir, filename),
@@ -6321,11 +6309,68 @@ const DEFAULT_CONFIG = {
6321
6309
  terminal: TerminalConfigSchema.parse({}),
6322
6310
  dashboard: DashboardConfigSchema.parse({})
6323
6311
  };
6312
+ function areStringArraysEqual(left, right) {
6313
+ if (left === right) return true;
6314
+ if (!left || !right) return !left && !right;
6315
+ if (left.length !== right.length) return false;
6316
+ return left.every((value, index) => value === right[index]);
6317
+ }
6318
+ function pruneNullish(value) {
6319
+ if (value === null || value === void 0) return;
6320
+ if (Array.isArray(value)) return value.map((entry) => pruneNullish(entry)).filter((entry) => entry !== void 0);
6321
+ if (typeof value === "object") {
6322
+ const normalizedEntries = Object.entries(value).flatMap(([key, entryValue]) => {
6323
+ const nextValue = pruneNullish(entryValue);
6324
+ return nextValue === void 0 ? [] : [[key, nextValue]];
6325
+ });
6326
+ return Object.fromEntries(normalizedEntries);
6327
+ }
6328
+ return value;
6329
+ }
6330
+ function hasOwnEntries(value) {
6331
+ return Object.keys(value).length > 0;
6332
+ }
6333
+ function toPersistedConfig(config, options = {}) {
6334
+ const persisted = {};
6335
+ const command = config.cli.command?.trim();
6336
+ const args = (config.cli.args ?? []).map((arg) => arg.trim()).filter(Boolean);
6337
+ const cliCommandParts = command ? [command, ...args] : void 0;
6338
+ if (cliCommandParts && !areStringArraysEqual(cliCommandParts, options.defaultCliCommandParts)) {
6339
+ const persistedCommand = cliCommandParts[0];
6340
+ persisted.cli = args.length > 0 ? {
6341
+ command: persistedCommand,
6342
+ args
6343
+ } : { command: persistedCommand };
6344
+ }
6345
+ if (config.theme !== DEFAULT_CONFIG.theme) persisted.theme = config.theme;
6346
+ const codeEditor = {};
6347
+ if (config.codeEditor.theme !== DEFAULT_CONFIG.codeEditor.theme) codeEditor.theme = config.codeEditor.theme;
6348
+ if (hasOwnEntries(codeEditor)) persisted.codeEditor = codeEditor;
6349
+ if (config.appBaseUrl !== DEFAULT_CONFIG.appBaseUrl) persisted.appBaseUrl = config.appBaseUrl;
6350
+ const terminal = {};
6351
+ if (config.terminal.fontSize !== DEFAULT_CONFIG.terminal.fontSize) terminal.fontSize = config.terminal.fontSize;
6352
+ if (config.terminal.fontFamily !== DEFAULT_CONFIG.terminal.fontFamily) terminal.fontFamily = config.terminal.fontFamily;
6353
+ if (config.terminal.cursorBlink !== DEFAULT_CONFIG.terminal.cursorBlink) terminal.cursorBlink = config.terminal.cursorBlink;
6354
+ if (config.terminal.cursorStyle !== DEFAULT_CONFIG.terminal.cursorStyle) terminal.cursorStyle = config.terminal.cursorStyle;
6355
+ if (config.terminal.scrollback !== DEFAULT_CONFIG.terminal.scrollback) terminal.scrollback = config.terminal.scrollback;
6356
+ if (config.terminal.rendererEngine !== DEFAULT_CONFIG.terminal.rendererEngine) terminal.rendererEngine = config.terminal.rendererEngine;
6357
+ if (hasOwnEntries(terminal)) persisted.terminal = terminal;
6358
+ const dashboard = {};
6359
+ if (config.dashboard.trendPointLimit !== DEFAULT_CONFIG.dashboard.trendPointLimit) dashboard.trendPointLimit = config.dashboard.trendPointLimit;
6360
+ if (hasOwnEntries(dashboard)) persisted.dashboard = dashboard;
6361
+ return persisted;
6362
+ }
6363
+ function isPersistedConfigEmpty(config) {
6364
+ return !hasOwnEntries(config);
6365
+ }
6324
6366
  /**
6325
6367
  * 配置管理器
6326
6368
  *
6327
6369
  * 负责读写 openspec/.openspecui.json 配置文件。
6328
6370
  * 读取操作使用 reactiveReadFile,支持响应式更新。
6371
+ *
6372
+ * `.openspecui.json` 是预期中的项目级 UI 配置文件,但只有显式偏离默认值的
6373
+ * override 才会落盘。仅启动 openspecui 或仅依赖默认配置时,不应触发文件写入。
6329
6374
  */
6330
6375
  var ConfigManager = class {
6331
6376
  configPath;
@@ -6336,18 +6381,11 @@ var ConfigManager = class {
6336
6381
  this.projectDir = projectDir;
6337
6382
  this.configPath = join(projectDir, "openspec", ".openspecui.json");
6338
6383
  }
6339
- /**
6340
- * 读取配置(响应式)
6341
- *
6342
- * 如果配置文件不存在,返回默认配置。
6343
- * 如果配置文件格式错误,返回默认配置并打印警告。
6344
- */
6345
- async readConfig() {
6346
- const content = await reactiveReadFile(this.configPath);
6384
+ parseConfigContent(content) {
6347
6385
  if (!content) return DEFAULT_CONFIG;
6348
6386
  try {
6349
- const parsed = JSON.parse(content);
6350
- const result = OpenSpecUIConfigSchema.safeParse(parsed);
6387
+ const normalized = pruneNullish(JSON.parse(content)) ?? {};
6388
+ const result = OpenSpecUIConfigSchema.safeParse(normalized);
6351
6389
  if (result.success) return result.data;
6352
6390
  console.warn("Invalid config format, using defaults:", result.error.message);
6353
6391
  return DEFAULT_CONFIG;
@@ -6357,12 +6395,24 @@ var ConfigManager = class {
6357
6395
  }
6358
6396
  }
6359
6397
  /**
6398
+ * 读取配置(响应式)
6399
+ *
6400
+ * 如果配置文件不存在,返回默认配置。
6401
+ * 如果配置文件格式错误,返回默认配置并打印警告。
6402
+ */
6403
+ async readConfig() {
6404
+ const content = await reactiveReadFile(this.configPath);
6405
+ return this.parseConfigContent(content);
6406
+ }
6407
+ /**
6360
6408
  * 写入配置
6361
6409
  *
6362
6410
  * 会触发文件监听,自动更新订阅者。
6363
6411
  */
6364
6412
  async writeConfig(config) {
6365
- const current = await this.readConfig();
6413
+ const currentContent = await reactiveReadFile(this.configPath);
6414
+ const fileExists = currentContent !== null;
6415
+ const current = this.parseConfigContent(currentContent);
6366
6416
  const nextCli = { ...current.cli };
6367
6417
  if (config.cli && Object.prototype.hasOwnProperty.call(config.cli, "command")) {
6368
6418
  const trimmed = config.cli.command?.trim();
@@ -6378,7 +6428,7 @@ var ConfigManager = class {
6378
6428
  else delete nextCli.args;
6379
6429
  }
6380
6430
  if (!nextCli.command) delete nextCli.args;
6381
- const merged = {
6431
+ const persisted = toPersistedConfig({
6382
6432
  ...current,
6383
6433
  cli: nextCli,
6384
6434
  theme: config.theme ?? current.theme,
@@ -6395,13 +6445,25 @@ var ConfigManager = class {
6395
6445
  ...current.dashboard,
6396
6446
  ...config.dashboard
6397
6447
  }
6398
- };
6399
- const serialized = JSON.stringify(merged, null, 2);
6448
+ });
6449
+ if (isPersistedConfigEmpty(persisted) && !fileExists) return;
6450
+ const serialized = isPersistedConfigEmpty(persisted) ? "{}" : JSON.stringify(persisted, null, 2);
6451
+ if (currentContent === serialized) return;
6400
6452
  await mkdir(dirname(this.configPath), { recursive: true });
6401
6453
  await writeFile(this.configPath, serialized, "utf-8");
6402
6454
  updateReactiveFileCache(this.configPath, serialized);
6403
6455
  this.invalidateResolvedCliRunner();
6404
6456
  }
6457
+ async resolveDefaultCliCommandParts() {
6458
+ return (await resolveCliRunner(buildCliRunnerCandidates({ userAgent: process.env.npm_config_user_agent }).filter((candidate) => candidate.id !== "configured"), this.projectDir, createCleanCliEnv())).commandParts;
6459
+ }
6460
+ async isDefaultCliCommand(commandParts) {
6461
+ try {
6462
+ return areStringArraysEqual(commandParts, await this.resolveDefaultCliCommandParts());
6463
+ } catch {
6464
+ return false;
6465
+ }
6466
+ }
6405
6467
  /**
6406
6468
  * 解析并缓存可用 CLI runner。
6407
6469
  */
@@ -6419,8 +6481,7 @@ var ConfigManager = class {
6419
6481
  async resolveCliRunnerUncached() {
6420
6482
  const config = await this.readConfig();
6421
6483
  const configuredCommandParts = this.getConfiguredCommandParts(config.cli);
6422
- const hasConfiguredCommand = configuredCommandParts.length > 0;
6423
- const resolved = await resolveCliRunner(hasConfiguredCommand ? [{
6484
+ return await resolveCliRunner(configuredCommandParts.length > 0 ? [{
6424
6485
  id: "configured",
6425
6486
  source: "config.cli.command",
6426
6487
  commandParts: configuredCommandParts
@@ -6428,20 +6489,6 @@ var ConfigManager = class {
6428
6489
  configuredCommandParts,
6429
6490
  userAgent: process.env.npm_config_user_agent
6430
6491
  }), this.projectDir, createCleanCliEnv());
6431
- if (!hasConfiguredCommand) {
6432
- const [resolvedCommand, ...resolvedArgs] = resolved.commandParts;
6433
- const currentCommand = config.cli.command?.trim();
6434
- const currentArgs = config.cli.args ?? [];
6435
- if (currentCommand !== resolvedCommand || currentArgs.length !== resolvedArgs.length || currentArgs.some((arg, index) => arg !== resolvedArgs[index])) try {
6436
- await this.writeConfig({ cli: {
6437
- command: resolvedCommand,
6438
- args: resolvedArgs
6439
- } });
6440
- } catch (err) {
6441
- console.warn("Failed to persist auto-detected CLI command:", err);
6442
- }
6443
- }
6444
- return resolved;
6445
6492
  }
6446
6493
  /**
6447
6494
  * 获取 CLI 命令(数组形式)
@@ -6488,6 +6535,13 @@ var ConfigManager = class {
6488
6535
  } });
6489
6536
  return;
6490
6537
  }
6538
+ if (await this.isDefaultCliCommand(commandParts)) {
6539
+ await this.writeConfig({ cli: {
6540
+ command: null,
6541
+ args: null
6542
+ } });
6543
+ return;
6544
+ }
6491
6545
  const [resolvedCommand, ...resolvedArgs] = commandParts;
6492
6546
  await this.writeConfig({ cli: {
6493
6547
  command: resolvedCommand,
@@ -6800,9 +6854,36 @@ var CliExecutor = class {
6800
6854
  }
6801
6855
  /**
6802
6856
  * 流式执行任意命令(数组形式)
6857
+ *
6858
+ * 字面量 `openspec` 会自动通过已解析的 CLI runner 执行,
6859
+ * 其它命令保持原始 spawn 行为。
6803
6860
  */
6804
6861
  executeCommandStream(command, onEvent) {
6805
6862
  const [cmd, ...cmdArgs] = command;
6863
+ if (cmd === "openspec") {
6864
+ let cancelResolved = null;
6865
+ let cancelled = false;
6866
+ this.executeStream([...cmdArgs], onEvent).then((cancel) => {
6867
+ if (cancelled) {
6868
+ cancel();
6869
+ return;
6870
+ }
6871
+ cancelResolved = cancel;
6872
+ }).catch((err) => {
6873
+ onEvent({
6874
+ type: "stderr",
6875
+ data: err instanceof Error ? err.message : String(err)
6876
+ });
6877
+ onEvent({
6878
+ type: "exit",
6879
+ exitCode: null
6880
+ });
6881
+ });
6882
+ return () => {
6883
+ cancelled = true;
6884
+ cancelResolved?.();
6885
+ };
6886
+ }
6806
6887
  onEvent({
6807
6888
  type: "command",
6808
6889
  data: command.join(" ")
@@ -7044,12 +7125,6 @@ const AI_TOOLS = [
7044
7125
  available: true,
7045
7126
  successLabel: "Windsurf",
7046
7127
  skillsDir: ".windsurf"
7047
- },
7048
- {
7049
- name: "AGENTS.md (works with Amp, VS Code, …)",
7050
- value: "agents",
7051
- available: false,
7052
- successLabel: "your AGENTS.md-compatible assistant"
7053
7128
  }
7054
7129
  ];
7055
7130
  /**
@@ -7059,11 +7134,23 @@ function getAvailableTools() {
7059
7134
  return AI_TOOLS.filter((tool) => tool.available);
7060
7135
  }
7061
7136
  /**
7062
- * 获取所有工具(包括 available: false 的)
7137
+ * 获取所有工具
7063
7138
  */
7064
7139
  function getAllTools() {
7065
7140
  return AI_TOOLS;
7066
7141
  }
7142
+ /**
7143
+ * 检测当前项目中已经存在的工具目录。
7144
+ *
7145
+ * 这里对齐 OpenSpec 官方 `getAvailableTools(projectPath)` 的语义:
7146
+ * 仅根据项目根目录下的工具目录是否存在来判断,不读取全局命令安装状态。
7147
+ */
7148
+ async function getDetectedProjectTools(projectDir) {
7149
+ return (await Promise.all(AI_TOOLS.map(async (tool) => {
7150
+ if (!tool.skillsDir) return null;
7151
+ return await reactiveExists(join$1(projectDir, tool.skillsDir)) ? tool : null;
7152
+ }))).filter((tool) => tool !== null);
7153
+ }
7067
7154
  /** 状态缓存:projectDir -> ReactiveState */
7068
7155
  const stateCache = /* @__PURE__ */ new Map();
7069
7156
  /** 监听器释放函数缓存 */
@@ -7126,6 +7213,143 @@ async function getConfiguredTools(projectDir) {
7126
7213
  return state.get();
7127
7214
  }
7128
7215
 
7216
+ //#endregion
7217
+ //#region ../core/src/tool-init-state.ts
7218
+ const TOOL_WORKFLOW_TO_SKILL_DIR = {
7219
+ propose: "openspec-propose",
7220
+ explore: "openspec-explore",
7221
+ new: "openspec-new-change",
7222
+ continue: "openspec-continue-change",
7223
+ apply: "openspec-apply-change",
7224
+ ff: "openspec-ff-change",
7225
+ sync: "openspec-sync-specs",
7226
+ archive: "openspec-archive-change",
7227
+ "bulk-archive": "openspec-bulk-archive-change",
7228
+ verify: "openspec-verify-change",
7229
+ onboard: "openspec-onboard"
7230
+ };
7231
+ const ALL_TOOL_WORKFLOWS = Object.keys(TOOL_WORKFLOW_TO_SKILL_DIR);
7232
+ function toKnownWorkflows(workflows) {
7233
+ return workflows.filter((workflow) => workflow in TOOL_WORKFLOW_TO_SKILL_DIR);
7234
+ }
7235
+ function resolveCodexHome() {
7236
+ const configuredHome = process.env.CODEX_HOME?.trim();
7237
+ return resolve$1(configuredHome ? configuredHome : join$1(homedir(), ".codex"));
7238
+ }
7239
+ function resolveToolCommandPath(projectDir, toolId, workflow) {
7240
+ switch (toolId) {
7241
+ case "amazon-q": return resolve$1(projectDir, ".amazonq", "prompts", `opsx-${workflow}.md`);
7242
+ case "antigravity": return resolve$1(projectDir, ".agent", "workflows", `opsx-${workflow}.md`);
7243
+ case "auggie": return resolve$1(projectDir, ".augment", "commands", `opsx-${workflow}.md`);
7244
+ case "claude": return resolve$1(projectDir, ".claude", "commands", "opsx", `${workflow}.md`);
7245
+ case "cline": return resolve$1(projectDir, ".clinerules", "workflows", `opsx-${workflow}.md`);
7246
+ case "codebuddy": return resolve$1(projectDir, ".codebuddy", "commands", "opsx", `${workflow}.md`);
7247
+ case "codex": return resolve$1(resolveCodexHome(), "prompts", `opsx-${workflow}.md`);
7248
+ case "continue": return resolve$1(projectDir, ".continue", "prompts", `opsx-${workflow}.prompt`);
7249
+ case "costrict": return resolve$1(projectDir, ".cospec", "openspec", "commands", `opsx-${workflow}.md`);
7250
+ case "crush": return resolve$1(projectDir, ".crush", "commands", "opsx", `${workflow}.md`);
7251
+ case "cursor": return resolve$1(projectDir, ".cursor", "commands", `opsx-${workflow}.md`);
7252
+ case "factory": return resolve$1(projectDir, ".factory", "commands", `opsx-${workflow}.md`);
7253
+ case "gemini": return resolve$1(projectDir, ".gemini", "commands", "opsx", `${workflow}.toml`);
7254
+ case "github-copilot": return resolve$1(projectDir, ".github", "prompts", `opsx-${workflow}.prompt.md`);
7255
+ case "iflow": return resolve$1(projectDir, ".iflow", "commands", `opsx-${workflow}.md`);
7256
+ case "kilocode": return resolve$1(projectDir, ".kilocode", "workflows", `opsx-${workflow}.md`);
7257
+ case "kiro": return resolve$1(projectDir, ".kiro", "prompts", `opsx-${workflow}.prompt.md`);
7258
+ case "opencode": return resolve$1(projectDir, ".opencode", "command", `opsx-${workflow}.md`);
7259
+ case "pi": return resolve$1(projectDir, ".pi", "prompts", `opsx-${workflow}.md`);
7260
+ case "qoder": return resolve$1(projectDir, ".qoder", "commands", "opsx", `${workflow}.md`);
7261
+ case "qwen": return resolve$1(projectDir, ".qwen", "commands", `opsx-${workflow}.toml`);
7262
+ case "roocode": return resolve$1(projectDir, ".roo", "commands", `opsx-${workflow}.md`);
7263
+ case "windsurf": return resolve$1(projectDir, ".windsurf", "workflows", `opsx-${workflow}.md`);
7264
+ default: return null;
7265
+ }
7266
+ }
7267
+ function getSkillArtifacts(projectDir, skillsDir) {
7268
+ return ALL_TOOL_WORKFLOWS.map((workflow) => ({
7269
+ workflow,
7270
+ path: resolve$1(projectDir, skillsDir, "skills", TOOL_WORKFLOW_TO_SKILL_DIR[workflow], "SKILL.md")
7271
+ }));
7272
+ }
7273
+ function getCommandArtifacts(projectDir, toolId) {
7274
+ return ALL_TOOL_WORKFLOWS.flatMap((workflow) => {
7275
+ const path$1 = resolveToolCommandPath(projectDir, toolId, workflow);
7276
+ return path$1 ? [{
7277
+ workflow,
7278
+ path: path$1
7279
+ }] : [];
7280
+ });
7281
+ }
7282
+ function invalidateToolInitCaches(projectDir) {
7283
+ const cacheRoots = /* @__PURE__ */ new Set();
7284
+ for (const tool of AI_TOOLS) {
7285
+ if (tool.skillsDir) cacheRoots.add(resolve$1(projectDir, tool.skillsDir));
7286
+ for (const workflow of ALL_TOOL_WORKFLOWS) {
7287
+ const commandPath = resolveToolCommandPath(projectDir, tool.value, workflow);
7288
+ if (commandPath) cacheRoots.add(dirname$1(commandPath));
7289
+ }
7290
+ }
7291
+ for (const root of cacheRoots) clearCache(root);
7292
+ }
7293
+ async function getExistingArtifactPaths(entries) {
7294
+ const presence = await Promise.all(entries.map(async (entry) => ({
7295
+ path: entry.path,
7296
+ exists: await reactiveExists(entry.path)
7297
+ })));
7298
+ return new Set(presence.filter((entry) => entry.exists).map((entry) => entry.path));
7299
+ }
7300
+ function countExisting(entries, existingPaths) {
7301
+ return entries.reduce((count, entry) => count + (existingPaths.has(entry.path) ? 1 : 0), 0);
7302
+ }
7303
+ function collectMissingWorkflows(entries, existingPaths) {
7304
+ return entries.filter((entry) => !existingPaths.has(entry.path)).map((entry) => entry.workflow);
7305
+ }
7306
+ function collectUnexpectedWorkflows(entries, desiredWorkflowSet, existingPaths) {
7307
+ return entries.filter((entry) => !desiredWorkflowSet.has(entry.workflow) && existingPaths.has(entry.path)).map((entry) => entry.workflow);
7308
+ }
7309
+ async function getToolInitStates(projectDir, options) {
7310
+ invalidateToolInitCaches(projectDir);
7311
+ const desiredWorkflows = toKnownWorkflows(options.workflows);
7312
+ const desiredWorkflowSet = new Set(desiredWorkflows);
7313
+ const shouldGenerateSkills = options.delivery !== "commands";
7314
+ const shouldGenerateCommands = options.delivery !== "skills";
7315
+ return Promise.all(AI_TOOLS.filter((tool) => tool.skillsDir).map(async (tool) => {
7316
+ const skillArtifacts = getSkillArtifacts(projectDir, tool.skillsDir);
7317
+ const commandArtifacts = getCommandArtifacts(projectDir, tool.value);
7318
+ const existingSkillPaths = await getExistingArtifactPaths(skillArtifacts);
7319
+ const existingCommandPaths = await getExistingArtifactPaths(commandArtifacts);
7320
+ const expectedSkillArtifacts = shouldGenerateSkills ? skillArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
7321
+ const expectedCommandArtifacts = shouldGenerateCommands ? commandArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
7322
+ const missingSkillWorkflows = collectMissingWorkflows(expectedSkillArtifacts, existingSkillPaths);
7323
+ const missingCommandWorkflows = collectMissingWorkflows(expectedCommandArtifacts, existingCommandPaths);
7324
+ const unexpectedSkillWorkflows = collectUnexpectedWorkflows(shouldGenerateSkills ? skillArtifacts : skillArtifacts, shouldGenerateSkills ? desiredWorkflowSet : /* @__PURE__ */ new Set(), existingSkillPaths);
7325
+ const unexpectedCommandWorkflows = collectUnexpectedWorkflows(shouldGenerateCommands ? commandArtifacts : commandArtifacts, shouldGenerateCommands ? desiredWorkflowSet : /* @__PURE__ */ new Set(), existingCommandPaths);
7326
+ const expectedSkillCount = expectedSkillArtifacts.length;
7327
+ const presentExpectedSkillCount = expectedSkillCount - missingSkillWorkflows.length;
7328
+ const detectedSkillCount = countExisting(skillArtifacts, existingSkillPaths);
7329
+ const expectedCommandCount = expectedCommandArtifacts.length;
7330
+ const presentExpectedCommandCount = expectedCommandCount - missingCommandWorkflows.length;
7331
+ const detectedCommandCount = countExisting(commandArtifacts, existingCommandPaths);
7332
+ const hasAnyArtifacts = detectedSkillCount + detectedCommandCount > 0;
7333
+ const isInitialized = missingSkillWorkflows.length === 0 && missingCommandWorkflows.length === 0 && unexpectedSkillWorkflows.length === 0 && unexpectedCommandWorkflows.length === 0;
7334
+ return {
7335
+ toolId: tool.value,
7336
+ toolName: tool.name,
7337
+ status: !hasAnyArtifacts ? "uninitialized" : isInitialized ? "initialized" : "partial",
7338
+ hasAnyArtifacts,
7339
+ expectedSkillCount,
7340
+ presentExpectedSkillCount,
7341
+ detectedSkillCount,
7342
+ expectedCommandCount,
7343
+ presentExpectedCommandCount,
7344
+ detectedCommandCount,
7345
+ missingSkillWorkflows,
7346
+ missingCommandWorkflows,
7347
+ unexpectedSkillWorkflows,
7348
+ unexpectedCommandWorkflows
7349
+ };
7350
+ }));
7351
+ }
7352
+
7129
7353
  //#endregion
7130
7354
  //#region ../core/src/dashboard-types.ts
7131
7355
  const DASHBOARD_METRIC_KEYS = [
@@ -24846,9 +25070,30 @@ const cliRouter = router({
24846
25070
  successLabel: tool.successLabel
24847
25071
  }));
24848
25072
  }),
25073
+ getDetectedProjectTools: publicProcedure.query(async ({ ctx }) => {
25074
+ return (await getDetectedProjectTools(ctx.projectDir)).map((tool) => ({
25075
+ name: tool.name,
25076
+ value: tool.value,
25077
+ available: tool.available,
25078
+ successLabel: tool.successLabel
25079
+ }));
25080
+ }),
24849
25081
  getProfileState: publicProcedure.query(async ({ ctx }) => {
24850
25082
  return fetchOpsxProfileState(ctx);
24851
25083
  }),
25084
+ getToolInitStates: publicProcedure.input(objectType({
25085
+ delivery: enumType([
25086
+ "both",
25087
+ "skills",
25088
+ "commands"
25089
+ ]),
25090
+ workflows: arrayType(stringType()).default([])
25091
+ })).query(async ({ ctx, input }) => {
25092
+ return getToolInitStates(ctx.projectDir, {
25093
+ delivery: input.delivery,
25094
+ workflows: input.workflows
25095
+ });
25096
+ }),
24852
25097
  getGlobalConfigPath: publicProcedure.query(async ({ ctx }) => {
24853
25098
  return { path: await resolveGlobalConfigPath(ctx) };
24854
25099
  }),
@@ -25743,14 +25988,17 @@ async function startServer(config, setupApp) {
25743
25988
  };
25744
25989
  }
25745
25990
 
25991
+ //#endregion
25992
+ //#region src/web-assets.ts
25993
+ function getWebAssetsDirCandidates(runtimeDir) {
25994
+ return [join$1(runtimeDir, "..", "web"), join$1(runtimeDir, "..", "..", "web", "dist")];
25995
+ }
25996
+
25746
25997
  //#endregion
25747
25998
  //#region src/index.ts
25748
25999
  const __dirname = dirname$1(fileURLToPath(import.meta.url));
25749
26000
  function getWebAssetsDir() {
25750
- const devPath = join$1(__dirname, "..", "..", "web", "dist");
25751
- const prodPath = join$1(__dirname, "..", "web");
25752
- if (existsSync(prodPath)) return prodPath;
25753
- if (existsSync(devPath)) return devPath;
26001
+ for (const candidate of getWebAssetsDirCandidates(__dirname)) if (existsSync(candidate)) return candidate;
25754
26002
  throw new Error("Web assets not found. Make sure to build the web package first.");
25755
26003
  }
25756
26004
  function setupStaticFiles(app) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspecui",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "OpenSpec UI - Visual interface for spec-driven development",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -1,4 +1,4 @@
1
- import{y as U,z as g,A as c,B as S,D as _,F as m,H as I,J as p}from"./index-Cq4njHD4.js";const x={name:"local-uniform-bit",vertex:{header:`
1
+ import{y as U,z as g,A as c,B as S,D as _,F as m,H as I,J as p}from"./index-Cy7P7qsp.js";const x={name:"local-uniform-bit",vertex:{header:`
2
2
 
3
3
  struct LocalUniforms {
4
4
  uTransformMatrix:mat3x3<f32>,