@openspecui/server 1.1.0 → 1.2.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.
Files changed (2) hide show
  1. package/dist/index.mjs +106 -266
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { serve } from "@hono/node-server";
2
- import { ApplyInstructionsSchema, ArtifactInstructionsSchema, ChangeStatusSchema, CliExecutor, ConfigManager, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, TemplatesSchema, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, initWatcherPool, isWatcherPoolInitialized, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli } from "@openspecui/core";
2
+ import { CliExecutor, ConfigManager, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, initWatcherPool, isWatcherPoolInitialized, sniffGlobalCli } from "@openspecui/core";
3
3
  import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
4
4
  import { applyWSSHandler } from "@trpc/server/adapters/ws";
5
5
  import { Hono } from "hono";
@@ -12,9 +12,8 @@ import { SearchQuerySchema } from "@openspecui/search";
12
12
  import { initTRPC } from "@trpc/server";
13
13
  import { observable } from "@trpc/server/observable";
14
14
  import { mkdir, rm, writeFile } from "node:fs/promises";
15
- import { dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
15
+ import { dirname, join, resolve, sep } from "node:path";
16
16
  import { z } from "zod";
17
- import { parse } from "yaml";
18
17
  import { EventEmitter as EventEmitter$1 } from "node:events";
19
18
  import { NodeWorkerSearchProvider } from "@openspecui/search/node";
20
19
 
@@ -81,6 +80,8 @@ var PtySession = class extends EventEmitter {
81
80
  command;
82
81
  args;
83
82
  platform;
83
+ closeTip;
84
+ closeCallbackUrl;
84
85
  createdAt;
85
86
  process;
86
87
  titleInterval = null;
@@ -104,6 +105,8 @@ var PtySession = class extends EventEmitter {
104
105
  this.command = resolvedCommand.command;
105
106
  this.args = resolvedCommand.args;
106
107
  this.platform = opts.platform;
108
+ this.closeTip = opts.closeTip;
109
+ this.closeCallbackUrl = opts.closeCallbackUrl;
107
110
  this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
108
111
  this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
109
112
  this.process = pty.spawn(this.command, this.args, {
@@ -184,6 +187,8 @@ var PtySession = class extends EventEmitter {
184
187
  platform: this.platform,
185
188
  isExited: this.isExited,
186
189
  exitCode: this.exitCode,
190
+ closeTip: this.closeTip,
191
+ closeCallbackUrl: this.closeCallbackUrl,
187
192
  createdAt: this.createdAt
188
193
  };
189
194
  }
@@ -203,6 +208,8 @@ var PtyManager = class {
203
208
  rows: opts.rows,
204
209
  command: opts.command,
205
210
  args: opts.args,
211
+ closeTip: opts.closeTip,
212
+ closeCallbackUrl: opts.closeCallbackUrl,
206
213
  cwd: this.defaultCwd,
207
214
  scrollback: opts.scrollback,
208
215
  maxBufferBytes: opts.maxBufferBytes,
@@ -307,11 +314,14 @@ function createPtyWebSocketHandler(ptyManager) {
307
314
  switch (msg.type) {
308
315
  case "create":
309
316
  try {
317
+ const createMessage = msg;
310
318
  const session = ptyManager.create({
311
319
  cols: msg.cols,
312
320
  rows: msg.rows,
313
321
  command: msg.command,
314
- args: msg.args
322
+ args: msg.args,
323
+ closeTip: createMessage.closeTip,
324
+ closeCallbackUrl: createMessage.closeCallbackUrl
315
325
  });
316
326
  send({
317
327
  type: "created",
@@ -367,7 +377,9 @@ function createPtyWebSocketHandler(ptyManager) {
367
377
  args: s.args,
368
378
  platform: s.platform,
369
379
  isExited: s.isExited,
370
- exitCode: s.exitCode
380
+ exitCode: s.exitCode,
381
+ closeTip: s.closeTip,
382
+ closeCallbackUrl: s.closeCallbackUrl
371
383
  }))
372
384
  });
373
385
  break;
@@ -465,53 +477,6 @@ function createCliStreamObservable(startStream) {
465
477
  });
466
478
  }
467
479
 
468
- //#endregion
469
- //#region src/opsx-schema.ts
470
- const SchemaYamlArtifactSchema = z.object({
471
- id: z.string(),
472
- generates: z.string(),
473
- description: z.string().optional(),
474
- template: z.string().optional(),
475
- instruction: z.string().optional(),
476
- requires: z.array(z.string()).optional()
477
- });
478
- const SchemaYamlSchema = z.object({
479
- name: z.string(),
480
- version: z.union([z.string(), z.number()]).optional(),
481
- description: z.string().optional(),
482
- artifacts: z.array(SchemaYamlArtifactSchema),
483
- apply: z.object({
484
- requires: z.array(z.string()).optional(),
485
- tracks: z.string().optional(),
486
- instruction: z.string().optional()
487
- }).optional()
488
- });
489
- function parseSchemaYaml(content) {
490
- const raw = parse(content);
491
- const parsed = SchemaYamlSchema.safeParse(raw);
492
- if (!parsed.success) throw new Error(`Invalid schema.yaml: ${parsed.error.message}`);
493
- const { artifacts, apply, name, description, version } = parsed.data;
494
- const detail = {
495
- name,
496
- description,
497
- version,
498
- artifacts: artifacts.map((artifact) => ({
499
- id: artifact.id,
500
- outputPath: artifact.generates,
501
- description: artifact.description,
502
- template: artifact.template,
503
- instruction: artifact.instruction,
504
- requires: artifact.requires ?? []
505
- })),
506
- applyRequires: apply?.requires ?? [],
507
- applyTracks: apply?.tracks,
508
- applyInstruction: apply?.instruction
509
- };
510
- const validated = SchemaDetailSchema.safeParse(detail);
511
- if (!validated.success) throw new Error(`Invalid schema detail: ${validated.error.message}`);
512
- return validated.data;
513
- }
514
-
515
480
  //#endregion
516
481
  //#region src/reactive-kv.ts
517
482
  /**
@@ -639,20 +604,6 @@ function requireChangeId(changeId) {
639
604
  function ensureEditableSource(source, label) {
640
605
  if (source === "package") throw new Error(`${label} is read-only (package source)`);
641
606
  }
642
- function parseCliJson(raw, schema, label) {
643
- const trimmed = raw.trim();
644
- if (!trimmed) throw new Error(`${label} returned empty output`);
645
- let parsed;
646
- try {
647
- parsed = JSON.parse(trimmed);
648
- } catch (err) {
649
- const message = err instanceof Error ? err.message : String(err);
650
- throw new Error(`${label} returned invalid JSON: ${message}`);
651
- }
652
- const result = schema.safeParse(parsed);
653
- if (!result.success) throw new Error(`${label} returned unexpected JSON: ${result.error.message}`);
654
- return result.data;
655
- }
656
607
  function resolveEntryPath(root, entryPath) {
657
608
  const normalizedRoot = resolve(root);
658
609
  const resolvedPath = resolve(normalizedRoot, entryPath);
@@ -660,153 +611,62 @@ function resolveEntryPath(root, entryPath) {
660
611
  if (resolvedPath !== normalizedRoot && !resolvedPath.startsWith(rootPrefix)) throw new Error("Invalid path: outside schema root");
661
612
  return resolvedPath;
662
613
  }
663
- function toRelativePath(root, absolutePath) {
664
- return relative(root, absolutePath).split(sep).join("/");
665
- }
666
- async function readEntriesUnderRoot(root) {
667
- if (!(await reactiveStat(root))?.isDirectory) return [];
668
- const collectEntries = async (dir) => {
669
- const names = await reactiveReadDir(dir, { includeHidden: false });
670
- const entries = [];
671
- for (const name of names) {
672
- const fullPath = join(dir, name);
673
- const statInfo = await reactiveStat(fullPath);
674
- if (!statInfo) continue;
675
- const relativePath = toRelativePath(root, fullPath);
676
- if (statInfo.isDirectory) {
677
- entries.push({
678
- path: relativePath,
679
- type: "directory"
680
- });
681
- entries.push(...await collectEntries(fullPath));
682
- } else {
683
- const content = await reactiveReadFile(fullPath);
684
- const size = content ? Buffer.byteLength(content, "utf-8") : void 0;
685
- entries.push({
686
- path: relativePath,
687
- type: "file",
688
- content: content ?? void 0,
689
- size
690
- });
691
- }
692
- }
693
- return entries;
694
- };
695
- return collectEntries(root);
696
- }
697
- async function touchOpsxProjectDeps(projectDir) {
698
- const openspecDir = join(projectDir, "openspec");
699
- await reactiveReadFile(join(openspecDir, "config.yaml"));
700
- const schemaRoot = join(openspecDir, "schemas");
701
- const schemaDirs = await reactiveReadDir(schemaRoot, {
702
- directoriesOnly: true,
703
- includeHidden: true
704
- });
705
- await Promise.all(schemaDirs.map((name) => reactiveReadFile(join(schemaRoot, name, "schema.yaml"))));
706
- await reactiveReadDir(join(openspecDir, "changes"), {
707
- directoriesOnly: true,
708
- includeHidden: true,
709
- exclude: ["archive"]
710
- });
711
- }
712
- async function touchOpsxChangeDeps(projectDir, changeId) {
713
- const changeDir = join(projectDir, "openspec", "changes", changeId);
714
- await reactiveReadDir(changeDir, { includeHidden: true });
715
- await reactiveReadFile(join(changeDir, ".openspec.yaml"));
716
- }
717
- async function readGlobArtifactFiles(projectDir, changeId, outputPath) {
718
- return (await readEntriesUnderRoot(join(projectDir, "openspec", "changes", changeId))).filter((entry) => entry.type === "file" && matchesGlob(entry.path, outputPath)).map((entry) => ({
719
- path: entry.path,
720
- type: "file",
721
- content: entry.content ?? ""
722
- }));
723
- }
724
614
  async function fetchOpsxStatus(ctx, input) {
725
615
  const changeId = requireChangeId(input.change);
726
- await touchOpsxProjectDeps(ctx.projectDir);
727
- await touchOpsxChangeDeps(ctx.projectDir, changeId);
728
- const args = [
729
- "status",
730
- "--json",
731
- "--change",
732
- changeId
733
- ];
734
- if (input.schema) args.push("--schema", input.schema);
735
- const result = await ctx.cliExecutor.execute(args);
736
- if (!result.success) throw new Error(result.stderr || `openspec status failed (exit ${result.exitCode ?? "null"})`);
737
- const status = parseCliJson(result.stdout, ChangeStatusSchema, "openspec status");
738
- const changeRelDir = `openspec/changes/${changeId}`;
739
- for (const artifact of status.artifacts) artifact.relativePath = `${changeRelDir}/${artifact.outputPath}`;
740
- return status;
616
+ await ctx.kernel.waitForWarmup();
617
+ await ctx.kernel.ensureStatus(changeId, input.schema);
618
+ return ctx.kernel.getStatus(changeId, input.schema);
741
619
  }
742
620
  async function fetchOpsxStatusList(ctx) {
743
- const changeIds = await reactiveReadDir(join(ctx.projectDir, "openspec", "changes"), {
744
- directoriesOnly: true,
745
- includeHidden: false,
746
- exclude: ["archive"]
747
- });
748
- return await Promise.all(changeIds.map((changeId) => fetchOpsxStatus(ctx, { change: changeId })));
621
+ await ctx.kernel.waitForWarmup();
622
+ await ctx.kernel.ensureStatusList();
623
+ return ctx.kernel.getStatusList();
749
624
  }
750
625
  async function fetchOpsxInstructions(ctx, input) {
751
626
  const changeId = requireChangeId(input.change);
752
- await touchOpsxProjectDeps(ctx.projectDir);
753
- await touchOpsxChangeDeps(ctx.projectDir, changeId);
754
- const args = [
755
- "instructions",
756
- input.artifact,
757
- "--json",
758
- "--change",
759
- changeId
760
- ];
761
- if (input.schema) args.push("--schema", input.schema);
762
- const result = await ctx.cliExecutor.execute(args);
763
- if (!result.success) throw new Error(result.stderr || `openspec instructions failed (exit ${result.exitCode ?? "null"})`);
764
- return parseCliJson(result.stdout, ArtifactInstructionsSchema, "openspec instructions");
627
+ await ctx.kernel.waitForWarmup();
628
+ await ctx.kernel.ensureInstructions(changeId, input.artifact, input.schema);
629
+ return ctx.kernel.getInstructions(changeId, input.artifact, input.schema);
765
630
  }
766
631
  async function fetchOpsxApplyInstructions(ctx, input) {
767
632
  const changeId = requireChangeId(input.change);
768
- await touchOpsxProjectDeps(ctx.projectDir);
769
- await touchOpsxChangeDeps(ctx.projectDir, changeId);
770
- const args = [
771
- "instructions",
772
- "apply",
773
- "--json",
774
- "--change",
775
- changeId
776
- ];
777
- if (input.schema) args.push("--schema", input.schema);
778
- const result = await ctx.cliExecutor.execute(args);
779
- if (!result.success) throw new Error(result.stderr || `openspec instructions apply failed (exit ${result.exitCode ?? "null"})`);
780
- return parseCliJson(result.stdout, ApplyInstructionsSchema, "openspec instructions apply");
633
+ await ctx.kernel.waitForWarmup();
634
+ await ctx.kernel.ensureApplyInstructions(changeId, input.schema);
635
+ return ctx.kernel.getApplyInstructions(changeId, input.schema);
781
636
  }
782
- async function fetchOpsxSchemas(ctx) {
783
- await touchOpsxProjectDeps(ctx.projectDir);
784
- const result = await ctx.cliExecutor.schemas();
785
- if (!result.success) throw new Error(result.stderr || `openspec schemas failed (exit ${result.exitCode ?? "null"})`);
786
- return parseCliJson(result.stdout, z.array(SchemaInfoSchema), "openspec schemas");
637
+ async function fetchOpsxConfigBundle(ctx) {
638
+ await ctx.kernel.ensureSchemas();
639
+ const schemas = ctx.kernel.getSchemas();
640
+ for (const schema of schemas) {
641
+ ctx.kernel.ensureSchemaDetail(schema.name).catch(() => {});
642
+ ctx.kernel.ensureSchemaResolution(schema.name).catch(() => {});
643
+ }
644
+ const schemaDetails = {};
645
+ const schemaResolutions = {};
646
+ for (const schema of schemas) {
647
+ schemaDetails[schema.name] = ctx.kernel.peekSchemaDetail(schema.name);
648
+ schemaResolutions[schema.name] = ctx.kernel.peekSchemaResolution(schema.name);
649
+ }
650
+ return {
651
+ schemas,
652
+ schemaDetails,
653
+ schemaResolutions
654
+ };
787
655
  }
788
656
  async function fetchOpsxSchemaResolution(ctx, name) {
789
- await touchOpsxProjectDeps(ctx.projectDir);
790
- const result = await ctx.cliExecutor.schemaWhich(name);
791
- if (!result.success) throw new Error(result.stderr || `openspec schema which failed (exit ${result.exitCode ?? "null"})`);
792
- return parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
657
+ await ctx.kernel.waitForWarmup();
658
+ await ctx.kernel.ensureSchemaResolution(name);
659
+ return ctx.kernel.getSchemaResolution(name);
793
660
  }
794
661
  async function fetchOpsxTemplates(ctx, schema) {
795
- await touchOpsxProjectDeps(ctx.projectDir);
796
- const result = await ctx.cliExecutor.templates(schema);
797
- if (!result.success) throw new Error(result.stderr || `openspec templates failed (exit ${result.exitCode ?? "null"})`);
798
- return parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
662
+ await ctx.kernel.waitForWarmup();
663
+ await ctx.kernel.ensureTemplates(schema);
664
+ return ctx.kernel.getTemplates(schema);
799
665
  }
800
666
  async function fetchOpsxTemplateContents(ctx, schema) {
801
- const templates = await fetchOpsxTemplates(ctx, schema);
802
- const entries = await Promise.all(Object.entries(templates).map(async ([artifactId, info]) => {
803
- return [artifactId, {
804
- content: await reactiveReadFile(info.path),
805
- path: info.path,
806
- source: info.source
807
- }];
808
- }));
809
- return Object.fromEntries(entries);
667
+ await ctx.kernel.waitForWarmup();
668
+ await ctx.kernel.ensureTemplateContents(schema);
669
+ return ctx.kernel.getTemplateContents(schema);
810
670
  }
811
671
  /**
812
672
  * Spec router - spec CRUD operations
@@ -1196,11 +1056,11 @@ const opsxRouter = router({
1196
1056
  })).subscription(({ ctx, input }) => {
1197
1057
  return createReactiveSubscription(() => fetchOpsxApplyInstructions(ctx, input));
1198
1058
  }),
1199
- schemas: publicProcedure.query(async ({ ctx }) => {
1200
- return fetchOpsxSchemas(ctx);
1059
+ configBundle: publicProcedure.query(async ({ ctx }) => {
1060
+ return fetchOpsxConfigBundle(ctx);
1201
1061
  }),
1202
- subscribeSchemas: publicProcedure.subscription(({ ctx }) => {
1203
- return createReactiveSubscription(() => fetchOpsxSchemas(ctx));
1062
+ subscribeConfigBundle: publicProcedure.subscription(({ ctx }) => {
1063
+ return createReactiveSubscription(() => fetchOpsxConfigBundle(ctx));
1204
1064
  }),
1205
1065
  templates: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).query(async ({ ctx, input }) => {
1206
1066
  return fetchOpsxTemplates(ctx, input?.schema);
@@ -1214,53 +1074,34 @@ const opsxRouter = router({
1214
1074
  subscribeTemplateContents: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).subscription(({ ctx, input }) => {
1215
1075
  return createReactiveSubscription(() => fetchOpsxTemplateContents(ctx, input?.schema));
1216
1076
  }),
1217
- schemaResolution: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
1218
- return fetchOpsxSchemaResolution(ctx, input.name);
1219
- }),
1220
- subscribeSchemaResolution: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
1221
- return createReactiveSubscription(() => fetchOpsxSchemaResolution(ctx, input.name));
1222
- }),
1223
- schemaDetail: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
1224
- await touchOpsxProjectDeps(ctx.projectDir);
1225
- const schemaPath = join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml");
1226
- const content = await reactiveReadFile(schemaPath);
1227
- if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
1228
- return parseSchemaYaml(content);
1229
- }),
1230
- subscribeSchemaDetail: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
1231
- return createReactiveSubscription(async () => {
1232
- await touchOpsxProjectDeps(ctx.projectDir);
1233
- const schemaPath = join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml");
1234
- const content = await reactiveReadFile(schemaPath);
1235
- if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
1236
- return parseSchemaYaml(content);
1237
- });
1238
- }),
1239
1077
  schemaFiles: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
1240
- await touchOpsxProjectDeps(ctx.projectDir);
1241
- return readEntriesUnderRoot((await fetchOpsxSchemaResolution(ctx, input.name)).path);
1078
+ await ctx.kernel.waitForWarmup();
1079
+ await ctx.kernel.ensureSchemaFiles(input.name);
1080
+ return ctx.kernel.getSchemaFiles(input.name);
1242
1081
  }),
1243
1082
  subscribeSchemaFiles: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
1244
1083
  return createReactiveSubscription(async () => {
1245
- await touchOpsxProjectDeps(ctx.projectDir);
1246
- return readEntriesUnderRoot((await fetchOpsxSchemaResolution(ctx, input.name)).path);
1084
+ await ctx.kernel.waitForWarmup();
1085
+ await ctx.kernel.ensureSchemaFiles(input.name);
1086
+ return ctx.kernel.getSchemaFiles(input.name);
1247
1087
  });
1248
1088
  }),
1249
1089
  schemaYaml: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
1250
- await touchOpsxProjectDeps(ctx.projectDir);
1251
- return reactiveReadFile(join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml"));
1090
+ await ctx.kernel.waitForWarmup();
1091
+ await ctx.kernel.ensureSchemaYaml(input.name);
1092
+ return ctx.kernel.getSchemaYaml(input.name);
1252
1093
  }),
1253
1094
  subscribeSchemaYaml: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
1254
1095
  return createReactiveSubscription(async () => {
1255
- await touchOpsxProjectDeps(ctx.projectDir);
1256
- return reactiveReadFile(join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml"));
1096
+ await ctx.kernel.waitForWarmup();
1097
+ await ctx.kernel.ensureSchemaYaml(input.name);
1098
+ return ctx.kernel.getSchemaYaml(input.name);
1257
1099
  });
1258
1100
  }),
1259
1101
  writeSchemaYaml: publicProcedure.input(z.object({
1260
1102
  name: z.string(),
1261
1103
  content: z.string()
1262
1104
  })).mutation(async ({ ctx, input }) => {
1263
- await touchOpsxProjectDeps(ctx.projectDir);
1264
1105
  const resolution = await fetchOpsxSchemaResolution(ctx, input.name);
1265
1106
  ensureEditableSource(resolution.source, "schema.yaml");
1266
1107
  const schemaPath = join(resolution.path, "schema.yaml");
@@ -1273,7 +1114,6 @@ const opsxRouter = router({
1273
1114
  path: z.string(),
1274
1115
  content: z.string()
1275
1116
  })).mutation(async ({ ctx, input }) => {
1276
- await touchOpsxProjectDeps(ctx.projectDir);
1277
1117
  const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
1278
1118
  ensureEditableSource(resolution.source, "schema file");
1279
1119
  if (!input.path.trim()) throw new Error("path is required");
@@ -1287,7 +1127,6 @@ const opsxRouter = router({
1287
1127
  path: z.string(),
1288
1128
  content: z.string().optional()
1289
1129
  })).mutation(async ({ ctx, input }) => {
1290
- await touchOpsxProjectDeps(ctx.projectDir);
1291
1130
  const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
1292
1131
  ensureEditableSource(resolution.source, "schema file");
1293
1132
  if (!input.path.trim()) throw new Error("path is required");
@@ -1300,7 +1139,6 @@ const opsxRouter = router({
1300
1139
  schema: z.string(),
1301
1140
  path: z.string()
1302
1141
  })).mutation(async ({ ctx, input }) => {
1303
- await touchOpsxProjectDeps(ctx.projectDir);
1304
1142
  const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
1305
1143
  ensureEditableSource(resolution.source, "schema directory");
1306
1144
  if (!input.path.trim()) throw new Error("path is required");
@@ -1311,7 +1149,6 @@ const opsxRouter = router({
1311
1149
  schema: z.string(),
1312
1150
  path: z.string()
1313
1151
  })).mutation(async ({ ctx, input }) => {
1314
- await touchOpsxProjectDeps(ctx.projectDir);
1315
1152
  const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
1316
1153
  ensureEditableSource(resolution.source, "schema entry");
1317
1154
  if (!input.path.trim()) throw new Error("path is required");
@@ -1327,26 +1164,18 @@ const opsxRouter = router({
1327
1164
  schema: z.string(),
1328
1165
  artifactId: z.string()
1329
1166
  })).query(async ({ ctx, input }) => {
1330
- const info = (await fetchOpsxTemplates(ctx, input.schema))[input.artifactId];
1167
+ const info = (await fetchOpsxTemplateContents(ctx, input.schema))[input.artifactId];
1331
1168
  if (!info) throw new Error(`Template not found for ${input.schema}:${input.artifactId}`);
1332
- return {
1333
- content: await reactiveReadFile(info.path),
1334
- path: info.path,
1335
- source: info.source
1336
- };
1169
+ return info;
1337
1170
  }),
1338
1171
  subscribeTemplateContent: publicProcedure.input(z.object({
1339
1172
  schema: z.string(),
1340
1173
  artifactId: z.string()
1341
1174
  })).subscription(({ ctx, input }) => {
1342
1175
  return createReactiveSubscription(async () => {
1343
- const info = (await fetchOpsxTemplates(ctx, input.schema))[input.artifactId];
1176
+ const info = (await fetchOpsxTemplateContents(ctx, input.schema))[input.artifactId];
1344
1177
  if (!info) throw new Error(`Template not found for ${input.schema}:${input.artifactId}`);
1345
- return {
1346
- content: await reactiveReadFile(info.path),
1347
- path: info.path,
1348
- source: info.source
1349
- };
1178
+ return info;
1350
1179
  });
1351
1180
  }),
1352
1181
  writeTemplateContent: publicProcedure.input(z.object({
@@ -1362,7 +1191,6 @@ const opsxRouter = router({
1362
1191
  return { success: true };
1363
1192
  }),
1364
1193
  deleteSchema: publicProcedure.input(z.object({ name: z.string() })).mutation(async ({ ctx, input }) => {
1365
- await touchOpsxProjectDeps(ctx.projectDir);
1366
1194
  const resolution = await fetchOpsxSchemaResolution(ctx, input.name);
1367
1195
  ensureEditableSource(resolution.source, "schema");
1368
1196
  await rm(resolution.path, {
@@ -1372,11 +1200,15 @@ const opsxRouter = router({
1372
1200
  return { success: true };
1373
1201
  }),
1374
1202
  projectConfig: publicProcedure.query(async ({ ctx }) => {
1375
- return reactiveReadFile(join(ctx.projectDir, "openspec", "config.yaml"));
1203
+ await ctx.kernel.waitForWarmup();
1204
+ await ctx.kernel.ensureProjectConfig();
1205
+ return ctx.kernel.getProjectConfig();
1376
1206
  }),
1377
1207
  subscribeProjectConfig: publicProcedure.subscription(({ ctx }) => {
1378
1208
  return createReactiveSubscription(async () => {
1379
- return reactiveReadFile(join(ctx.projectDir, "openspec", "config.yaml"));
1209
+ await ctx.kernel.waitForWarmup();
1210
+ await ctx.kernel.ensureProjectConfig();
1211
+ return ctx.kernel.getProjectConfig();
1380
1212
  });
1381
1213
  }),
1382
1214
  writeProjectConfig: publicProcedure.input(z.object({ content: z.string() })).mutation(async ({ ctx, input }) => {
@@ -1386,55 +1218,63 @@ const opsxRouter = router({
1386
1218
  return { success: true };
1387
1219
  }),
1388
1220
  listChanges: publicProcedure.query(async ({ ctx }) => {
1389
- return reactiveReadDir(join(ctx.projectDir, "openspec", "changes"), {
1390
- directoriesOnly: true,
1391
- exclude: ["archive"],
1392
- includeHidden: false
1393
- });
1221
+ await ctx.kernel.waitForWarmup();
1222
+ await ctx.kernel.ensureChangeIds();
1223
+ return ctx.kernel.getChangeIds();
1394
1224
  }),
1395
1225
  subscribeChanges: publicProcedure.subscription(({ ctx }) => {
1396
1226
  return createReactiveSubscription(async () => {
1397
- return reactiveReadDir(join(ctx.projectDir, "openspec", "changes"), {
1398
- directoriesOnly: true,
1399
- exclude: ["archive"],
1400
- includeHidden: false
1401
- });
1227
+ await ctx.kernel.waitForWarmup();
1228
+ await ctx.kernel.ensureChangeIds();
1229
+ return ctx.kernel.getChangeIds();
1402
1230
  });
1403
1231
  }),
1404
1232
  changeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).query(async ({ ctx, input }) => {
1405
- return reactiveReadFile(join(ctx.projectDir, "openspec", "changes", input.changeId, ".openspec.yaml"));
1233
+ await ctx.kernel.waitForWarmup();
1234
+ await ctx.kernel.ensureChangeMetadata(input.changeId);
1235
+ return ctx.kernel.getChangeMetadata(input.changeId);
1406
1236
  }),
1407
1237
  subscribeChangeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).subscription(({ ctx, input }) => {
1408
1238
  return createReactiveSubscription(async () => {
1409
- return reactiveReadFile(join(ctx.projectDir, "openspec", "changes", input.changeId, ".openspec.yaml"));
1239
+ await ctx.kernel.waitForWarmup();
1240
+ await ctx.kernel.ensureChangeMetadata(input.changeId);
1241
+ return ctx.kernel.getChangeMetadata(input.changeId);
1410
1242
  });
1411
1243
  }),
1412
1244
  readArtifactOutput: publicProcedure.input(z.object({
1413
1245
  changeId: z.string(),
1414
1246
  outputPath: z.string()
1415
1247
  })).query(async ({ ctx, input }) => {
1416
- return reactiveReadFile(join(ctx.projectDir, "openspec", "changes", input.changeId, input.outputPath));
1248
+ await ctx.kernel.waitForWarmup();
1249
+ await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
1250
+ return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
1417
1251
  }),
1418
1252
  subscribeArtifactOutput: publicProcedure.input(z.object({
1419
1253
  changeId: z.string(),
1420
1254
  outputPath: z.string()
1421
1255
  })).subscription(({ ctx, input }) => {
1422
1256
  return createReactiveSubscription(async () => {
1423
- return reactiveReadFile(join(ctx.projectDir, "openspec", "changes", input.changeId, input.outputPath));
1257
+ await ctx.kernel.waitForWarmup();
1258
+ await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
1259
+ return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
1424
1260
  });
1425
1261
  }),
1426
1262
  readGlobArtifactFiles: publicProcedure.input(z.object({
1427
1263
  changeId: z.string(),
1428
1264
  outputPath: z.string()
1429
1265
  })).query(async ({ ctx, input }) => {
1430
- return readGlobArtifactFiles(ctx.projectDir, input.changeId, input.outputPath);
1266
+ await ctx.kernel.waitForWarmup();
1267
+ await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
1268
+ return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
1431
1269
  }),
1432
1270
  subscribeGlobArtifactFiles: publicProcedure.input(z.object({
1433
1271
  changeId: z.string(),
1434
1272
  outputPath: z.string()
1435
1273
  })).subscription(({ ctx, input }) => {
1436
1274
  return createReactiveSubscription(async () => {
1437
- return readGlobArtifactFiles(ctx.projectDir, input.changeId, input.outputPath);
1275
+ await ctx.kernel.waitForWarmup();
1276
+ await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
1277
+ return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
1438
1278
  });
1439
1279
  }),
1440
1280
  writeArtifactOutput: publicProcedure.input(z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openspecui/server",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {
@@ -20,8 +20,8 @@
20
20
  "yaml": "^2.8.0",
21
21
  "yargs": "^18.0.0",
22
22
  "zod": "^3.24.1",
23
- "@openspecui/search": "1.1.0",
24
- "@openspecui/core": "1.1.0"
23
+ "@openspecui/core": "1.2.0",
24
+ "@openspecui/search": "1.1.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.10.2",