@openspecui/server 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +170 -32
- 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, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, reactiveExists, reactiveReadDir, reactiveReadFile, sniffGlobalCli } from "@openspecui/core";
|
|
3
|
+
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, reactiveExists, 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";
|
|
@@ -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 = {
|
|
@@ -1035,6 +1041,94 @@ function requireChangeId(changeId) {
|
|
|
1035
1041
|
if (!changeId) throw new Error("change is required");
|
|
1036
1042
|
return changeId;
|
|
1037
1043
|
}
|
|
1044
|
+
function parseOpsxProfileListJson(stdout) {
|
|
1045
|
+
try {
|
|
1046
|
+
const parsed = JSON.parse(stdout);
|
|
1047
|
+
const profile = parsed.profile === "custom" ? "custom" : "core";
|
|
1048
|
+
return {
|
|
1049
|
+
profile,
|
|
1050
|
+
delivery: parsed.delivery === "skills" || parsed.delivery === "commands" ? parsed.delivery : "both",
|
|
1051
|
+
workflows: Array.isArray(parsed.workflows) ? parsed.workflows.filter((item) => typeof item === "string" && item.length > 0) : profile === "core" ? [...OPSX_CORE_PROFILE_WORKFLOWS] : []
|
|
1052
|
+
};
|
|
1053
|
+
} catch {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function parseOpsxConfigDrift(output) {
|
|
1058
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1059
|
+
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;
|
|
1060
|
+
return {
|
|
1061
|
+
drift: warningLine !== null,
|
|
1062
|
+
warningText: warningLine
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
async function fetchOpsxProfileState(ctx) {
|
|
1066
|
+
const configListJson = await ctx.cliExecutor.execute([
|
|
1067
|
+
"config",
|
|
1068
|
+
"list",
|
|
1069
|
+
"--json"
|
|
1070
|
+
]);
|
|
1071
|
+
if (!configListJson.success) return {
|
|
1072
|
+
available: false,
|
|
1073
|
+
profile: null,
|
|
1074
|
+
delivery: null,
|
|
1075
|
+
workflows: [],
|
|
1076
|
+
driftStatus: "unknown",
|
|
1077
|
+
warningText: null,
|
|
1078
|
+
error: configListJson.stderr || "Failed to load profile config."
|
|
1079
|
+
};
|
|
1080
|
+
const parsed = parseOpsxProfileListJson(configListJson.stdout);
|
|
1081
|
+
if (!parsed) return {
|
|
1082
|
+
available: false,
|
|
1083
|
+
profile: null,
|
|
1084
|
+
delivery: null,
|
|
1085
|
+
workflows: [],
|
|
1086
|
+
driftStatus: "unknown",
|
|
1087
|
+
warningText: null,
|
|
1088
|
+
error: "Invalid JSON from `openspec config list --json`."
|
|
1089
|
+
};
|
|
1090
|
+
const configListText = await ctx.cliExecutor.execute(["config", "list"]);
|
|
1091
|
+
if (!configListText.success) return {
|
|
1092
|
+
available: true,
|
|
1093
|
+
profile: parsed.profile,
|
|
1094
|
+
delivery: parsed.delivery,
|
|
1095
|
+
workflows: parsed.workflows,
|
|
1096
|
+
driftStatus: "unknown",
|
|
1097
|
+
warningText: null
|
|
1098
|
+
};
|
|
1099
|
+
const drift = parseOpsxConfigDrift(`${configListText.stdout}\n${configListText.stderr}`);
|
|
1100
|
+
return {
|
|
1101
|
+
available: true,
|
|
1102
|
+
profile: parsed.profile,
|
|
1103
|
+
delivery: parsed.delivery,
|
|
1104
|
+
workflows: parsed.workflows,
|
|
1105
|
+
driftStatus: drift.drift ? "drift" : "in-sync",
|
|
1106
|
+
warningText: drift.warningText
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function resolveGlobalConfigPath(ctx) {
|
|
1110
|
+
const result = await ctx.cliExecutor.execute(["config", "path"]);
|
|
1111
|
+
if (!result.success) throw new Error(result.stderr || "Failed to resolve OpenSpec global config path.");
|
|
1112
|
+
const path = result.stdout.trim();
|
|
1113
|
+
if (!path) throw new Error("OpenSpec global config path is empty.");
|
|
1114
|
+
return path;
|
|
1115
|
+
}
|
|
1116
|
+
async function fetchGlobalConfigJson(ctx) {
|
|
1117
|
+
const result = await ctx.cliExecutor.execute([
|
|
1118
|
+
"config",
|
|
1119
|
+
"list",
|
|
1120
|
+
"--json"
|
|
1121
|
+
]);
|
|
1122
|
+
if (!result.success) throw new Error(result.stderr || "Failed to load OpenSpec global config.");
|
|
1123
|
+
try {
|
|
1124
|
+
const parsed = JSON.parse(result.stdout);
|
|
1125
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenSpec global config must be a JSON object.");
|
|
1126
|
+
return parsed;
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1129
|
+
throw new Error(`Invalid JSON from \`openspec config list --json\`: ${message}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1038
1132
|
function ensureEditableSource(source, label) {
|
|
1039
1133
|
if (source === "package") throw new Error(`${label} is read-only (package source)`);
|
|
1040
1134
|
}
|
|
@@ -1157,6 +1251,35 @@ async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1157
1251
|
ctx.adapter.listChangesWithMeta(),
|
|
1158
1252
|
ctx.adapter.listArchivedChangesWithMeta()
|
|
1159
1253
|
]);
|
|
1254
|
+
await ctx.kernel.waitForWarmup();
|
|
1255
|
+
await ctx.kernel.ensureStatusList();
|
|
1256
|
+
const statusList = ctx.kernel.getStatusList();
|
|
1257
|
+
const changeMetaMap = new Map(changeMetas.map((change) => [change.id, change]));
|
|
1258
|
+
const activeChangeIds = new Set([...changeMetas.map((change) => change.id), ...statusList.map((status) => status.changeName)]);
|
|
1259
|
+
const statusByChange = new Map(statusList.map((status) => [status.changeName, status]));
|
|
1260
|
+
const activeChanges = (await Promise.all([...activeChangeIds].map(async (changeId) => {
|
|
1261
|
+
const status = statusByChange.get(changeId);
|
|
1262
|
+
const changeMeta = changeMetaMap.get(changeId);
|
|
1263
|
+
const statInfo = await reactiveStat(join(ctx.projectDir, "openspec", "changes", changeId));
|
|
1264
|
+
let progress = changeMeta?.progress ?? {
|
|
1265
|
+
total: 0,
|
|
1266
|
+
completed: 0
|
|
1267
|
+
};
|
|
1268
|
+
if (status) try {
|
|
1269
|
+
await ctx.kernel.ensureApplyInstructions(changeId, status.schemaName);
|
|
1270
|
+
const apply = ctx.kernel.getApplyInstructions(changeId, status.schemaName);
|
|
1271
|
+
progress = {
|
|
1272
|
+
total: apply.progress.total,
|
|
1273
|
+
completed: apply.progress.complete
|
|
1274
|
+
};
|
|
1275
|
+
} catch {}
|
|
1276
|
+
return {
|
|
1277
|
+
id: changeId,
|
|
1278
|
+
name: changeMeta?.name ?? changeId,
|
|
1279
|
+
progress,
|
|
1280
|
+
updatedAt: changeMeta?.updatedAt ?? statInfo?.mtime ?? 0
|
|
1281
|
+
};
|
|
1282
|
+
}))).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1160
1283
|
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
1161
1284
|
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
1162
1285
|
if (!change) return null;
|
|
@@ -1177,12 +1300,6 @@ async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1177
1300
|
updatedAt: meta.updatedAt
|
|
1178
1301
|
};
|
|
1179
1302
|
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
1180
|
-
const activeChanges = changeMetas.map((change) => ({
|
|
1181
|
-
id: change.id,
|
|
1182
|
-
name: change.name,
|
|
1183
|
-
progress: change.progress,
|
|
1184
|
-
updatedAt: change.updatedAt
|
|
1185
|
-
}));
|
|
1186
1303
|
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
1187
1304
|
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
1188
1305
|
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
@@ -1513,6 +1630,7 @@ const configRouter = router({
|
|
|
1513
1630
|
"dark",
|
|
1514
1631
|
"system"
|
|
1515
1632
|
]).optional(),
|
|
1633
|
+
codeEditor: z.object({ theme: CodeEditorThemeSchema.optional() }).optional(),
|
|
1516
1634
|
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
1517
1635
|
dashboard: DashboardConfigSchema.partial().optional()
|
|
1518
1636
|
})).mutation(async ({ ctx, input }) => {
|
|
@@ -1520,8 +1638,9 @@ const configRouter = router({
|
|
|
1520
1638
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
1521
1639
|
if (hasCliCommand && !hasCliArgs) {
|
|
1522
1640
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
1523
|
-
if (input.theme !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
1641
|
+
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
1524
1642
|
theme: input.theme,
|
|
1643
|
+
codeEditor: input.codeEditor,
|
|
1525
1644
|
terminal: input.terminal,
|
|
1526
1645
|
dashboard: input.dashboard
|
|
1527
1646
|
});
|
|
@@ -1582,18 +1701,41 @@ const cliRouter = router({
|
|
|
1582
1701
|
successLabel: tool.successLabel
|
|
1583
1702
|
}));
|
|
1584
1703
|
}),
|
|
1704
|
+
getProfileState: publicProcedure.query(async ({ ctx }) => {
|
|
1705
|
+
return fetchOpsxProfileState(ctx);
|
|
1706
|
+
}),
|
|
1707
|
+
getGlobalConfigPath: publicProcedure.query(async ({ ctx }) => {
|
|
1708
|
+
return { path: await resolveGlobalConfigPath(ctx) };
|
|
1709
|
+
}),
|
|
1710
|
+
getGlobalConfig: publicProcedure.query(async ({ ctx }) => {
|
|
1711
|
+
return fetchGlobalConfigJson(ctx);
|
|
1712
|
+
}),
|
|
1713
|
+
setGlobalConfig: publicProcedure.input(z.object({ config: z.record(z.string(), z.unknown()) })).mutation(async ({ ctx, input }) => {
|
|
1714
|
+
const configPath = await resolveGlobalConfigPath(ctx);
|
|
1715
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1716
|
+
await writeFile(configPath, `${JSON.stringify(input.config, null, 2)}\n`, "utf8");
|
|
1717
|
+
return { success: true };
|
|
1718
|
+
}),
|
|
1585
1719
|
getConfiguredTools: publicProcedure.query(async ({ ctx }) => {
|
|
1586
1720
|
return getConfiguredTools(ctx.projectDir);
|
|
1587
1721
|
}),
|
|
1588
1722
|
subscribeConfiguredTools: publicProcedure.subscription(({ ctx }) => {
|
|
1589
1723
|
return createReactiveSubscription(() => getConfiguredTools(ctx.projectDir));
|
|
1590
1724
|
}),
|
|
1591
|
-
init: publicProcedure.input(z.object({
|
|
1592
|
-
z.
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1725
|
+
init: publicProcedure.input(z.object({
|
|
1726
|
+
tools: z.union([
|
|
1727
|
+
z.array(z.string()),
|
|
1728
|
+
z.literal("all"),
|
|
1729
|
+
z.literal("none")
|
|
1730
|
+
]).optional(),
|
|
1731
|
+
profile: z.enum(["core", "custom"]).optional(),
|
|
1732
|
+
force: z.boolean().optional()
|
|
1733
|
+
}).optional()).mutation(async ({ ctx, input }) => {
|
|
1734
|
+
return ctx.cliExecutor.init({
|
|
1735
|
+
tools: input?.tools,
|
|
1736
|
+
profile: input?.profile,
|
|
1737
|
+
force: input?.force
|
|
1738
|
+
});
|
|
1597
1739
|
}),
|
|
1598
1740
|
archive: publicProcedure.input(z.object({
|
|
1599
1741
|
changeId: z.string(),
|
|
@@ -1620,12 +1762,20 @@ const cliRouter = router({
|
|
|
1620
1762
|
execute: publicProcedure.input(z.object({ args: z.array(z.string()) })).mutation(async ({ ctx, input }) => {
|
|
1621
1763
|
return ctx.cliExecutor.execute(input.args);
|
|
1622
1764
|
}),
|
|
1623
|
-
initStream: publicProcedure.input(z.object({
|
|
1624
|
-
z.
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1765
|
+
initStream: publicProcedure.input(z.object({
|
|
1766
|
+
tools: z.union([
|
|
1767
|
+
z.array(z.string()),
|
|
1768
|
+
z.literal("all"),
|
|
1769
|
+
z.literal("none")
|
|
1770
|
+
]).optional(),
|
|
1771
|
+
profile: z.enum(["core", "custom"]).optional(),
|
|
1772
|
+
force: z.boolean().optional()
|
|
1773
|
+
}).optional()).subscription(({ ctx, input }) => {
|
|
1774
|
+
return createCliStreamObservable((onEvent) => ctx.cliExecutor.initStream({
|
|
1775
|
+
tools: input?.tools,
|
|
1776
|
+
profile: input?.profile,
|
|
1777
|
+
force: input?.force
|
|
1778
|
+
}, onEvent));
|
|
1629
1779
|
}),
|
|
1630
1780
|
archiveStream: publicProcedure.input(z.object({
|
|
1631
1781
|
changeId: z.string(),
|
|
@@ -1859,18 +2009,6 @@ const opsxRouter = router({
|
|
|
1859
2009
|
return ctx.kernel.getChangeIds();
|
|
1860
2010
|
});
|
|
1861
2011
|
}),
|
|
1862
|
-
changeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).query(async ({ ctx, input }) => {
|
|
1863
|
-
await ctx.kernel.waitForWarmup();
|
|
1864
|
-
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1865
|
-
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1866
|
-
}),
|
|
1867
|
-
subscribeChangeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).subscription(({ ctx, input }) => {
|
|
1868
|
-
return createReactiveSubscription(async () => {
|
|
1869
|
-
await ctx.kernel.waitForWarmup();
|
|
1870
|
-
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1871
|
-
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1872
|
-
});
|
|
1873
|
-
}),
|
|
1874
2012
|
readArtifactOutput: publicProcedure.input(z.object({
|
|
1875
2013
|
changeId: z.string(),
|
|
1876
2014
|
outputPath: z.string()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"yargs": "^18.0.0",
|
|
22
22
|
"zod": "^3.24.1",
|
|
23
23
|
"@openspecui/search": "1.1.0",
|
|
24
|
-
"@openspecui/core": "
|
|
24
|
+
"@openspecui/core": "2.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.10.2",
|