@openspecui/server 1.6.2 → 2.0.2
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/index.mjs +173 -52
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer as createServer$1 } from "node:net";
|
|
2
2
|
import { serve } from "@hono/node-server";
|
|
3
|
-
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized,
|
|
3
|
+
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli } from "@openspecui/core";
|
|
4
4
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
5
5
|
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|
6
6
|
import { Hono } from "hono";
|
|
@@ -13,7 +13,7 @@ import { initTRPC } from "@trpc/server";
|
|
|
13
13
|
import { observable } from "@trpc/server/observable";
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
15
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
16
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
16
|
+
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
17
17
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
import { z } from "zod";
|
|
@@ -962,6 +962,12 @@ const t = initTRPC.context().create();
|
|
|
962
962
|
const router = t.router;
|
|
963
963
|
const publicProcedure = t.procedure;
|
|
964
964
|
const execFileAsync = promisify(execFile);
|
|
965
|
+
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
966
|
+
"propose",
|
|
967
|
+
"explore",
|
|
968
|
+
"apply",
|
|
969
|
+
"archive"
|
|
970
|
+
];
|
|
965
971
|
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
966
972
|
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
967
973
|
const dashboardGitTaskStatus = {
|
|
@@ -993,48 +999,144 @@ function endDashboardGitTask(error) {
|
|
|
993
999
|
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
994
1000
|
emitDashboardGitTaskStatus();
|
|
995
1001
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1002
|
+
const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
|
|
1003
|
+
async function resolveGitMetadataDir(projectDir) {
|
|
1004
|
+
try {
|
|
1005
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
1006
|
+
cwd: projectDir,
|
|
1007
|
+
maxBuffer: 1024 * 1024,
|
|
1008
|
+
encoding: "utf8"
|
|
1009
|
+
});
|
|
1010
|
+
const gitDirRaw = stdout.trim();
|
|
1011
|
+
if (!gitDirRaw) return null;
|
|
1012
|
+
const gitDirPath = resolve(projectDir, gitDirRaw);
|
|
1013
|
+
if (!(await stat(gitDirPath)).isDirectory()) return null;
|
|
1014
|
+
return gitDirPath;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function resolveGitMetadataDirReactive(projectDir) {
|
|
1020
|
+
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
1021
|
+
if (!gitMetadataDir) return null;
|
|
1022
|
+
await reactiveReadDir(gitMetadataDir, { includeHidden: true });
|
|
1023
|
+
return gitMetadataDir;
|
|
1001
1024
|
}
|
|
1002
|
-
function getDashboardGitRefreshStampPath(
|
|
1003
|
-
return join(
|
|
1025
|
+
function getDashboardGitRefreshStampPath(gitMetadataDir) {
|
|
1026
|
+
return join(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME);
|
|
1004
1027
|
}
|
|
1005
1028
|
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
1006
|
-
const
|
|
1029
|
+
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
1030
|
+
if (!gitMetadataDir) return { skipped: true };
|
|
1031
|
+
const stampPath = getDashboardGitRefreshStampPath(gitMetadataDir);
|
|
1007
1032
|
await mkdir(dirname(stampPath), { recursive: true });
|
|
1008
1033
|
await writeFile(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
1034
|
+
return { skipped: false };
|
|
1009
1035
|
}
|
|
1010
1036
|
async function registerDashboardGitReactiveDeps(projectDir) {
|
|
1011
1037
|
await reactiveReadDir(projectDir, {
|
|
1012
1038
|
includeHidden: true,
|
|
1013
1039
|
exclude: ["node_modules"]
|
|
1014
1040
|
});
|
|
1015
|
-
await
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (!gitDirRaw) return;
|
|
1022
|
-
const gitDirPath = resolve(projectDir, gitDirRaw);
|
|
1023
|
-
await reactiveReadDir(gitDirPath, { includeHidden: true });
|
|
1024
|
-
await reactiveReadFile(join(gitDirPath, "HEAD"));
|
|
1025
|
-
await reactiveReadFile(join(gitDirPath, "index"));
|
|
1026
|
-
await reactiveReadFile(join(gitDirPath, "packed-refs"));
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
await reactiveReadDir(dotGitPath, { includeHidden: true });
|
|
1030
|
-
await reactiveReadFile(join(dotGitPath, "HEAD"));
|
|
1031
|
-
await reactiveReadFile(join(dotGitPath, "index"));
|
|
1032
|
-
await reactiveReadFile(join(dotGitPath, "packed-refs"));
|
|
1041
|
+
const gitMetadataDir = await resolveGitMetadataDirReactive(projectDir);
|
|
1042
|
+
if (!gitMetadataDir) return;
|
|
1043
|
+
await reactiveReadFile(getDashboardGitRefreshStampPath(gitMetadataDir));
|
|
1044
|
+
await reactiveReadFile(join(gitMetadataDir, "HEAD"));
|
|
1045
|
+
await reactiveReadFile(join(gitMetadataDir, "index"));
|
|
1046
|
+
await reactiveReadFile(join(gitMetadataDir, "packed-refs"));
|
|
1033
1047
|
}
|
|
1034
1048
|
function requireChangeId(changeId) {
|
|
1035
1049
|
if (!changeId) throw new Error("change is required");
|
|
1036
1050
|
return changeId;
|
|
1037
1051
|
}
|
|
1052
|
+
function parseOpsxProfileListJson(stdout) {
|
|
1053
|
+
try {
|
|
1054
|
+
const parsed = JSON.parse(stdout);
|
|
1055
|
+
const profile = parsed.profile === "custom" ? "custom" : "core";
|
|
1056
|
+
return {
|
|
1057
|
+
profile,
|
|
1058
|
+
delivery: parsed.delivery === "skills" || parsed.delivery === "commands" ? parsed.delivery : "both",
|
|
1059
|
+
workflows: Array.isArray(parsed.workflows) ? parsed.workflows.filter((item) => typeof item === "string" && item.length > 0) : profile === "core" ? [...OPSX_CORE_PROFILE_WORKFLOWS] : []
|
|
1060
|
+
};
|
|
1061
|
+
} catch {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function parseOpsxConfigDrift(output) {
|
|
1066
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1067
|
+
const warningLine = lines.find((line) => /global config.+not applied.+project/i.test(line)) ?? lines.find((line) => /out of sync/i.test(line)) ?? lines.find((line) => /run\s+`?openspec\s+update`?/i.test(line)) ?? null;
|
|
1068
|
+
return {
|
|
1069
|
+
drift: warningLine !== null,
|
|
1070
|
+
warningText: warningLine
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
async function fetchOpsxProfileState(ctx) {
|
|
1074
|
+
const configListJson = await ctx.cliExecutor.execute([
|
|
1075
|
+
"config",
|
|
1076
|
+
"list",
|
|
1077
|
+
"--json"
|
|
1078
|
+
]);
|
|
1079
|
+
if (!configListJson.success) return {
|
|
1080
|
+
available: false,
|
|
1081
|
+
profile: null,
|
|
1082
|
+
delivery: null,
|
|
1083
|
+
workflows: [],
|
|
1084
|
+
driftStatus: "unknown",
|
|
1085
|
+
warningText: null,
|
|
1086
|
+
error: configListJson.stderr || "Failed to load profile config."
|
|
1087
|
+
};
|
|
1088
|
+
const parsed = parseOpsxProfileListJson(configListJson.stdout);
|
|
1089
|
+
if (!parsed) return {
|
|
1090
|
+
available: false,
|
|
1091
|
+
profile: null,
|
|
1092
|
+
delivery: null,
|
|
1093
|
+
workflows: [],
|
|
1094
|
+
driftStatus: "unknown",
|
|
1095
|
+
warningText: null,
|
|
1096
|
+
error: "Invalid JSON from `openspec config list --json`."
|
|
1097
|
+
};
|
|
1098
|
+
const configListText = await ctx.cliExecutor.execute(["config", "list"]);
|
|
1099
|
+
if (!configListText.success) return {
|
|
1100
|
+
available: true,
|
|
1101
|
+
profile: parsed.profile,
|
|
1102
|
+
delivery: parsed.delivery,
|
|
1103
|
+
workflows: parsed.workflows,
|
|
1104
|
+
driftStatus: "unknown",
|
|
1105
|
+
warningText: null
|
|
1106
|
+
};
|
|
1107
|
+
const drift = parseOpsxConfigDrift(`${configListText.stdout}\n${configListText.stderr}`);
|
|
1108
|
+
return {
|
|
1109
|
+
available: true,
|
|
1110
|
+
profile: parsed.profile,
|
|
1111
|
+
delivery: parsed.delivery,
|
|
1112
|
+
workflows: parsed.workflows,
|
|
1113
|
+
driftStatus: drift.drift ? "drift" : "in-sync",
|
|
1114
|
+
warningText: drift.warningText
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async function resolveGlobalConfigPath(ctx) {
|
|
1118
|
+
const result = await ctx.cliExecutor.execute(["config", "path"]);
|
|
1119
|
+
if (!result.success) throw new Error(result.stderr || "Failed to resolve OpenSpec global config path.");
|
|
1120
|
+
const path = result.stdout.trim();
|
|
1121
|
+
if (!path) throw new Error("OpenSpec global config path is empty.");
|
|
1122
|
+
return path;
|
|
1123
|
+
}
|
|
1124
|
+
async function fetchGlobalConfigJson(ctx) {
|
|
1125
|
+
const result = await ctx.cliExecutor.execute([
|
|
1126
|
+
"config",
|
|
1127
|
+
"list",
|
|
1128
|
+
"--json"
|
|
1129
|
+
]);
|
|
1130
|
+
if (!result.success) throw new Error(result.stderr || "Failed to load OpenSpec global config.");
|
|
1131
|
+
try {
|
|
1132
|
+
const parsed = JSON.parse(result.stdout);
|
|
1133
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenSpec global config must be a JSON object.");
|
|
1134
|
+
return parsed;
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1137
|
+
throw new Error(`Invalid JSON from \`openspec config list --json\`: ${message}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1038
1140
|
function ensureEditableSource(source, label) {
|
|
1039
1141
|
if (source === "package") throw new Error(`${label} is read-only (package source)`);
|
|
1040
1142
|
}
|
|
@@ -1607,18 +1709,41 @@ const cliRouter = router({
|
|
|
1607
1709
|
successLabel: tool.successLabel
|
|
1608
1710
|
}));
|
|
1609
1711
|
}),
|
|
1712
|
+
getProfileState: publicProcedure.query(async ({ ctx }) => {
|
|
1713
|
+
return fetchOpsxProfileState(ctx);
|
|
1714
|
+
}),
|
|
1715
|
+
getGlobalConfigPath: publicProcedure.query(async ({ ctx }) => {
|
|
1716
|
+
return { path: await resolveGlobalConfigPath(ctx) };
|
|
1717
|
+
}),
|
|
1718
|
+
getGlobalConfig: publicProcedure.query(async ({ ctx }) => {
|
|
1719
|
+
return fetchGlobalConfigJson(ctx);
|
|
1720
|
+
}),
|
|
1721
|
+
setGlobalConfig: publicProcedure.input(z.object({ config: z.record(z.string(), z.unknown()) })).mutation(async ({ ctx, input }) => {
|
|
1722
|
+
const configPath = await resolveGlobalConfigPath(ctx);
|
|
1723
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1724
|
+
await writeFile(configPath, `${JSON.stringify(input.config, null, 2)}\n`, "utf8");
|
|
1725
|
+
return { success: true };
|
|
1726
|
+
}),
|
|
1610
1727
|
getConfiguredTools: publicProcedure.query(async ({ ctx }) => {
|
|
1611
1728
|
return getConfiguredTools(ctx.projectDir);
|
|
1612
1729
|
}),
|
|
1613
1730
|
subscribeConfiguredTools: publicProcedure.subscription(({ ctx }) => {
|
|
1614
1731
|
return createReactiveSubscription(() => getConfiguredTools(ctx.projectDir));
|
|
1615
1732
|
}),
|
|
1616
|
-
init: publicProcedure.input(z.object({
|
|
1617
|
-
z.
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1733
|
+
init: publicProcedure.input(z.object({
|
|
1734
|
+
tools: z.union([
|
|
1735
|
+
z.array(z.string()),
|
|
1736
|
+
z.literal("all"),
|
|
1737
|
+
z.literal("none")
|
|
1738
|
+
]).optional(),
|
|
1739
|
+
profile: z.enum(["core", "custom"]).optional(),
|
|
1740
|
+
force: z.boolean().optional()
|
|
1741
|
+
}).optional()).mutation(async ({ ctx, input }) => {
|
|
1742
|
+
return ctx.cliExecutor.init({
|
|
1743
|
+
tools: input?.tools,
|
|
1744
|
+
profile: input?.profile,
|
|
1745
|
+
force: input?.force
|
|
1746
|
+
});
|
|
1622
1747
|
}),
|
|
1623
1748
|
archive: publicProcedure.input(z.object({
|
|
1624
1749
|
changeId: z.string(),
|
|
@@ -1645,12 +1770,20 @@ const cliRouter = router({
|
|
|
1645
1770
|
execute: publicProcedure.input(z.object({ args: z.array(z.string()) })).mutation(async ({ ctx, input }) => {
|
|
1646
1771
|
return ctx.cliExecutor.execute(input.args);
|
|
1647
1772
|
}),
|
|
1648
|
-
initStream: publicProcedure.input(z.object({
|
|
1649
|
-
z.
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1773
|
+
initStream: publicProcedure.input(z.object({
|
|
1774
|
+
tools: z.union([
|
|
1775
|
+
z.array(z.string()),
|
|
1776
|
+
z.literal("all"),
|
|
1777
|
+
z.literal("none")
|
|
1778
|
+
]).optional(),
|
|
1779
|
+
profile: z.enum(["core", "custom"]).optional(),
|
|
1780
|
+
force: z.boolean().optional()
|
|
1781
|
+
}).optional()).subscription(({ ctx, input }) => {
|
|
1782
|
+
return createCliStreamObservable((onEvent) => ctx.cliExecutor.initStream({
|
|
1783
|
+
tools: input?.tools,
|
|
1784
|
+
profile: input?.profile,
|
|
1785
|
+
force: input?.force
|
|
1786
|
+
}, onEvent));
|
|
1654
1787
|
}),
|
|
1655
1788
|
archiveStream: publicProcedure.input(z.object({
|
|
1656
1789
|
changeId: z.string(),
|
|
@@ -1884,18 +2017,6 @@ const opsxRouter = router({
|
|
|
1884
2017
|
return ctx.kernel.getChangeIds();
|
|
1885
2018
|
});
|
|
1886
2019
|
}),
|
|
1887
|
-
changeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).query(async ({ ctx, input }) => {
|
|
1888
|
-
await ctx.kernel.waitForWarmup();
|
|
1889
|
-
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1890
|
-
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1891
|
-
}),
|
|
1892
|
-
subscribeChangeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).subscription(({ ctx, input }) => {
|
|
1893
|
-
return createReactiveSubscription(async () => {
|
|
1894
|
-
await ctx.kernel.waitForWarmup();
|
|
1895
|
-
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1896
|
-
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1897
|
-
});
|
|
1898
|
-
}),
|
|
1899
2020
|
readArtifactOutput: publicProcedure.input(z.object({
|
|
1900
2021
|
changeId: z.string(),
|
|
1901
2022
|
outputPath: z.string()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"yaml": "^2.8.0",
|
|
21
21
|
"yargs": "^18.0.0",
|
|
22
22
|
"zod": "^3.24.1",
|
|
23
|
-
"@openspecui/core": "
|
|
23
|
+
"@openspecui/core": "2.0.0",
|
|
24
24
|
"@openspecui/search": "1.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|