@shivaedev/work-board 0.7.1 → 0.8.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 (198) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +93 -2
  3. package/assets/agent.js +37 -0
  4. package/assets/native.js +32 -25
  5. package/dist/board.d.ts +1 -0
  6. package/dist/board.d.ts.map +1 -1
  7. package/dist/board.js +15 -2
  8. package/dist/board.js.map +1 -1
  9. package/dist/browser/client.d.ts +19 -79
  10. package/dist/browser/client.d.ts.map +1 -1
  11. package/dist/browser/client.js +12 -1
  12. package/dist/browser/client.js.map +1 -1
  13. package/dist/browser/entry.d.ts +3 -0
  14. package/dist/browser/entry.d.ts.map +1 -0
  15. package/dist/browser/entry.js +7 -0
  16. package/dist/browser/entry.js.map +1 -0
  17. package/dist/browser/responses/draftSession.d.ts +8 -0
  18. package/dist/browser/responses/draftSession.d.ts.map +1 -0
  19. package/dist/browser/responses/draftSession.js +92 -0
  20. package/dist/browser/responses/draftSession.js.map +1 -0
  21. package/dist/browser/responses/drafts.d.ts +16 -0
  22. package/dist/browser/responses/drafts.d.ts.map +1 -0
  23. package/dist/browser/responses/drafts.js +49 -0
  24. package/dist/browser/responses/drafts.js.map +1 -0
  25. package/dist/browser/responses/preview.d.ts +3 -0
  26. package/dist/browser/responses/preview.d.ts.map +1 -0
  27. package/dist/browser/responses/preview.js +4 -0
  28. package/dist/browser/responses/preview.js.map +1 -0
  29. package/dist/browser/responses/schema.d.ts +127 -0
  30. package/dist/browser/responses/schema.d.ts.map +1 -0
  31. package/dist/browser/responses/schema.js +47 -0
  32. package/dist/browser/responses/schema.js.map +1 -0
  33. package/dist/browser/responses/setup.d.ts +2 -0
  34. package/dist/browser/responses/setup.d.ts.map +1 -0
  35. package/dist/browser/responses/setup.js +99 -0
  36. package/dist/browser/responses/setup.js.map +1 -0
  37. package/dist/browser/responses/submit.d.ts +4 -0
  38. package/dist/browser/responses/submit.d.ts.map +1 -0
  39. package/dist/browser/responses/submit.js +36 -0
  40. package/dist/browser/responses/submit.js.map +1 -0
  41. package/dist/browser/session.d.ts +1 -79
  42. package/dist/browser/session.d.ts.map +1 -1
  43. package/dist/cli.d.ts.map +1 -1
  44. package/dist/cli.js +24 -15
  45. package/dist/cli.js.map +1 -1
  46. package/dist/files/changes.d.ts +1 -0
  47. package/dist/files/changes.d.ts.map +1 -1
  48. package/dist/files/changes.js +1 -1
  49. package/dist/files/changes.js.map +1 -1
  50. package/dist/http/respond.d.ts.map +1 -1
  51. package/dist/http/respond.js +2 -0
  52. package/dist/http/respond.js.map +1 -1
  53. package/dist/metadata/backlinks.d.ts.map +1 -1
  54. package/dist/metadata/backlinks.js +2 -0
  55. package/dist/metadata/backlinks.js.map +1 -1
  56. package/dist/metadata/fields.d.ts +17 -1
  57. package/dist/metadata/fields.d.ts.map +1 -1
  58. package/dist/metadata/problems.d.ts.map +1 -1
  59. package/dist/metadata/problems.js +2 -0
  60. package/dist/metadata/problems.js.map +1 -1
  61. package/dist/metadata/schema.d.ts +17 -1
  62. package/dist/metadata/schema.d.ts.map +1 -1
  63. package/dist/metadata/schema.js +6 -2
  64. package/dist/metadata/schema.js.map +1 -1
  65. package/dist/page/attentionDetails.d.ts.map +1 -1
  66. package/dist/page/attentionDetails.js +2 -1
  67. package/dist/page/attentionDetails.js.map +1 -1
  68. package/dist/page/metadataDetails.d.ts.map +1 -1
  69. package/dist/page/metadataDetails.js +6 -0
  70. package/dist/page/metadataDetails.js.map +1 -1
  71. package/dist/page/navigationScript.js +1 -1
  72. package/dist/page/shell.d.ts +1 -1
  73. package/dist/page/shell.d.ts.map +1 -1
  74. package/dist/page/shell.js.map +1 -1
  75. package/dist/page/style.d.ts.map +1 -1
  76. package/dist/page/style.js +2 -1
  77. package/dist/page/style.js.map +1 -1
  78. package/dist/page/swap.d.ts +1 -1
  79. package/dist/page/swap.d.ts.map +1 -1
  80. package/dist/page/swap.js +3 -0
  81. package/dist/page/swap.js.map +1 -1
  82. package/dist/path/Identity.d.ts +3 -0
  83. package/dist/path/Identity.d.ts.map +1 -0
  84. package/dist/path/Identity.js +3 -0
  85. package/dist/path/Identity.js.map +1 -0
  86. package/dist/responses/agent-cli.d.ts +2 -0
  87. package/dist/responses/agent-cli.d.ts.map +1 -0
  88. package/dist/responses/agent-cli.js +31 -0
  89. package/dist/responses/agent-cli.js.map +1 -0
  90. package/dist/responses/agent.d.ts +68 -0
  91. package/dist/responses/agent.d.ts.map +1 -0
  92. package/dist/responses/agent.js +49 -0
  93. package/dist/responses/agent.js.map +1 -0
  94. package/dist/responses/await.d.ts +33 -0
  95. package/dist/responses/await.d.ts.map +1 -0
  96. package/dist/responses/await.js +21 -0
  97. package/dist/responses/await.js.map +1 -0
  98. package/dist/responses/flags.d.ts +6 -0
  99. package/dist/responses/flags.d.ts.map +1 -0
  100. package/dist/responses/flags.js +23 -0
  101. package/dist/responses/flags.js.map +1 -0
  102. package/dist/responses/locate.d.ts +13 -0
  103. package/dist/responses/locate.d.ts.map +1 -0
  104. package/dist/responses/locate.js +50 -0
  105. package/dist/responses/locate.js.map +1 -0
  106. package/dist/responses/outcome.d.ts +65 -0
  107. package/dist/responses/outcome.d.ts.map +1 -0
  108. package/dist/responses/outcome.js +14 -0
  109. package/dist/responses/outcome.js.map +1 -0
  110. package/dist/responses/page.d.ts +6 -0
  111. package/dist/responses/page.d.ts.map +1 -0
  112. package/dist/responses/page.js +25 -0
  113. package/dist/responses/page.js.map +1 -0
  114. package/dist/responses/publish.d.ts +4 -0
  115. package/dist/responses/publish.d.ts.map +1 -0
  116. package/dist/responses/publish.js +56 -0
  117. package/dist/responses/publish.js.map +1 -0
  118. package/dist/responses/publishIn.d.ts +3 -0
  119. package/dist/responses/publishIn.d.ts.map +1 -0
  120. package/dist/responses/publishIn.js +69 -0
  121. package/dist/responses/publishIn.js.map +1 -0
  122. package/dist/responses/read.d.ts +30 -0
  123. package/dist/responses/read.d.ts.map +1 -0
  124. package/dist/responses/read.js +43 -0
  125. package/dist/responses/read.js.map +1 -0
  126. package/dist/responses/records.d.ts +12 -0
  127. package/dist/responses/records.d.ts.map +1 -0
  128. package/dist/responses/records.js +47 -0
  129. package/dist/responses/records.js.map +1 -0
  130. package/dist/responses/retry.d.ts +22 -0
  131. package/dist/responses/retry.d.ts.map +1 -0
  132. package/dist/responses/retry.js +14 -0
  133. package/dist/responses/retry.js.map +1 -0
  134. package/dist/responses/service.d.ts +106 -0
  135. package/dist/responses/service.d.ts.map +1 -0
  136. package/dist/responses/service.js +90 -0
  137. package/dist/responses/service.js.map +1 -0
  138. package/dist/responses/waitFor.d.ts +31 -0
  139. package/dist/responses/waitFor.d.ts.map +1 -0
  140. package/dist/responses/waitFor.js +47 -0
  141. package/dist/responses/waitFor.js.map +1 -0
  142. package/dist/rpc/contract.d.ts +101 -2
  143. package/dist/rpc/contract.d.ts.map +1 -1
  144. package/dist/rpc/contract.js +3 -2
  145. package/dist/rpc/contract.js.map +1 -1
  146. package/dist/rpc/responseContract.d.ts +212 -0
  147. package/dist/rpc/responseContract.d.ts.map +1 -0
  148. package/dist/rpc/responseContract.js +37 -0
  149. package/dist/rpc/responseContract.js.map +1 -0
  150. package/dist/rpc/server.d.ts +3 -1
  151. package/dist/rpc/server.d.ts.map +1 -1
  152. package/dist/rpc/server.js +6 -1
  153. package/dist/rpc/server.js.map +1 -1
  154. package/dist/serve-cli.d.ts +3 -0
  155. package/dist/serve-cli.d.ts.map +1 -0
  156. package/dist/serve-cli.js +20 -0
  157. package/dist/serve-cli.js.map +1 -0
  158. package/package.json +2 -2
  159. package/src/board.ts +16 -2
  160. package/src/browser/client.ts +30 -4
  161. package/src/browser/entry.ts +9 -0
  162. package/src/browser/responses/draftSession.ts +88 -0
  163. package/src/browser/responses/drafts.ts +58 -0
  164. package/src/browser/responses/preview.ts +5 -0
  165. package/src/browser/responses/schema.ts +52 -0
  166. package/src/browser/responses/setup.ts +102 -0
  167. package/src/browser/responses/submit.ts +41 -0
  168. package/src/cli.ts +14 -22
  169. package/src/files/changes.ts +2 -1
  170. package/src/http/respond.ts +2 -0
  171. package/src/metadata/backlinks.ts +2 -0
  172. package/src/metadata/problems.ts +2 -0
  173. package/src/metadata/schema.ts +6 -2
  174. package/src/page/attentionDetails.ts +2 -1
  175. package/src/page/metadataDetails.ts +6 -0
  176. package/src/page/navigationScript.ts +1 -1
  177. package/src/page/shell.ts +1 -1
  178. package/src/page/style.ts +2 -1
  179. package/src/page/swap.ts +3 -0
  180. package/src/path/Identity.ts +3 -0
  181. package/src/responses/agent-cli.ts +35 -0
  182. package/src/responses/agent.ts +59 -0
  183. package/src/responses/await.ts +21 -0
  184. package/src/responses/flags.ts +22 -0
  185. package/src/responses/locate.ts +55 -0
  186. package/src/responses/outcome.ts +15 -0
  187. package/src/responses/page.ts +41 -0
  188. package/src/responses/publish.ts +53 -0
  189. package/src/responses/publishIn.ts +64 -0
  190. package/src/responses/read.ts +45 -0
  191. package/src/responses/records.ts +53 -0
  192. package/src/responses/retry.ts +23 -0
  193. package/src/responses/service.ts +106 -0
  194. package/src/responses/waitFor.ts +58 -0
  195. package/src/rpc/contract.ts +3 -2
  196. package/src/rpc/responseContract.ts +46 -0
  197. package/src/rpc/server.ts +8 -1
  198. package/src/serve-cli.ts +37 -0
@@ -0,0 +1,106 @@
1
+ import { Clock, Effect, FileSystem, Semaphore } from "effect";
2
+ import { type DraftInput, type Question, type RecordedResponse, ResponseFailed } from "#browser/responses/schema.ts";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import type { Snapshot } from "#search/snapshot.ts";
5
+ import { awaitResponse } from "./await.ts";
6
+ import { locate as locateQuestion } from "./locate.ts";
7
+ import { publish } from "./publish.ts";
8
+ import { readResponses } from "./read.ts";
9
+ import { questionMarkdown, responseMarkdown } from "./records.ts";
10
+ import { retryResponse } from "./retry.ts";
11
+
12
+ export interface ResponseOptions {
13
+ readonly changes: Changes;
14
+ readonly enabled: boolean;
15
+ readonly index: Effect.Effect<Snapshot, unknown>;
16
+ readonly root: string;
17
+ }
18
+ function reject(code: ResponseFailed["code"], message: string) {
19
+ return Effect.fail(new ResponseFailed({ code, message }));
20
+ }
21
+ const HOURS_48 = 48 * 60 * 60 * 1000;
22
+ export const responseService = Effect.fn("WorkBoard.responseService")(function* (options: ResponseOptions) {
23
+ const fs = yield* FileSystem.FileSystem;
24
+ const lock = yield* Semaphore.make(1);
25
+ const snapshot = options.index.pipe(Effect.catch(() => reject("Unavailable", "The workspace could not be indexed.")));
26
+ const enabled = options.enabled ? Effect.void : reject("Disabled", "Start with --responses to explicitly enable local question/response writes.");
27
+ const locate = locateQuestion(options.root, options.changes.realRoot, snapshot, fs);
28
+ const read = readResponses(snapshot);
29
+ function register(input: { readonly item: string; readonly request: string; readonly revision: string }) {
30
+ return lock.withPermit(
31
+ Effect.gen(function* () {
32
+ yield* enabled;
33
+ const preview = yield* locate(input.item, input.request);
34
+ if (preview.reviewedRevision !== input.revision) {
35
+ return yield* reject("Stale", "Source changed since review. Read it again before registering this question.");
36
+ }
37
+ const data = yield* snapshot;
38
+ const matches = data.model.ids.get(preview.id);
39
+ if (matches?.length) {
40
+ if (matches.length !== 1 || matches[0]?.file !== `responses/${preview.id}.md`) {
41
+ return yield* reject("Conflict", "The registered identity moved or is duplicated. Read the existing record instead of retrying a write.");
42
+ }
43
+ const saved = (yield* read(preview.id)).question;
44
+ yield* publish(options.root, options.changes.realRoot, saved.id, questionMarkdown(saved));
45
+ return saved;
46
+ }
47
+ const now = yield* Clock.currentTimeMillis;
48
+ const question: Question = {
49
+ context: preview.context,
50
+ id: preview.id,
51
+ question: {
52
+ deadline: now + HOURS_48,
53
+ item: preview.item,
54
+ reason: preview.reason,
55
+ registeredAt: now,
56
+ request: preview.request,
57
+ reviewedRevision: preview.reviewedRevision,
58
+ source: preview.source,
59
+ },
60
+ };
61
+ yield* publish(options.root, options.changes.realRoot, question.id, questionMarkdown(question)).pipe(
62
+ Effect.ensuring(options.changes.refresh ?? Effect.void),
63
+ );
64
+ return question;
65
+ }),
66
+ );
67
+ }
68
+ function record(input: DraftInput) {
69
+ return lock.withPermit(
70
+ Effect.gen(function* () {
71
+ yield* enabled;
72
+ const result = yield* read(input.question);
73
+ const existing = result.responses.find((candidate) => candidate.id === input.id);
74
+ if (existing) {
75
+ yield* retryResponse(existing, input, snapshot, options);
76
+ return existing;
77
+ }
78
+ const { question } = result;
79
+ const current = yield* locate(question.question.item, question.question.request);
80
+ if (current.reviewedRevision !== question.question.reviewedRevision || current.source !== question.question.source) {
81
+ return yield* reject("Stale", "Source changed or moved. Keep the draft and review the new revision before responding.");
82
+ }
83
+ const data = yield* snapshot;
84
+ if (data.model.ids.has(input.id)) {
85
+ return yield* reject("Conflict", "This response identity is already used.");
86
+ }
87
+ const response: RecordedResponse = {
88
+ body: input.body,
89
+ id: input.id,
90
+ response: {
91
+ author: input.author,
92
+ question: input.question,
93
+ recordedAt: result.responses.reduce((time, saved) => Math.max(time, saved.response.recordedAt + 1), yield* Clock.currentTimeMillis),
94
+ reviewedRevision: question.question.reviewedRevision,
95
+ type: input.type,
96
+ },
97
+ };
98
+ yield* publish(options.root, options.changes.realRoot, response.id, responseMarkdown(response)).pipe(
99
+ Effect.ensuring(options.changes.refresh ?? Effect.void),
100
+ );
101
+ return response;
102
+ }),
103
+ );
104
+ }
105
+ return { awaitResponse: awaitResponse(read), locate, read, record, register };
106
+ });
@@ -0,0 +1,58 @@
1
+ import * as Clock from "effect/Clock";
2
+ import * as Effect from "effect/Effect";
3
+ import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
4
+ import { ResponseFailed, type WaitReading } from "#browser/responses/schema.ts";
5
+
6
+ function resultFor(reading: WaitReading) {
7
+ if (reading.response) {
8
+ return Effect.succeed({ kind: "response", question: reading.question, response: reading.response } as const);
9
+ }
10
+ if (reading.expired) {
11
+ return Effect.fail(
12
+ new ResponseFailed({
13
+ code: "Unanswered",
14
+ message:
15
+ "The human left this unanswered for 48 hours. Check in with them; they may have forgotten or want to postpone. The request and late replies remain available.",
16
+ }),
17
+ );
18
+ }
19
+ return Effect.succeed(undefined);
20
+ }
21
+
22
+ function retryable(error: ResponseFailed | RpcClientError) {
23
+ return error._tag !== "ResponseFailed" || error.code === "Unavailable";
24
+ }
25
+
26
+ export const waitForResponse = Effect.fn("WorkBoard.waitForResponse")(function* (
27
+ read: () => Effect.Effect<WaitReading, ResponseFailed | RpcClientError>,
28
+ diagnostic: (text: string) => void,
29
+ ) {
30
+ let known: WaitReading | undefined;
31
+ const leaseEnd = (yield* Clock.currentTimeMillis) + 48 * 60 * 60 * 1000;
32
+ let unavailable = false;
33
+ while (true) {
34
+ const result = yield* read().pipe(Effect.result);
35
+ if (result._tag === "Failure") {
36
+ if (!retryable(result.failure)) {
37
+ return yield* Effect.fail(result.failure);
38
+ }
39
+ if ((yield* Clock.currentTimeMillis) >= (known?.question.question.deadline ?? leaseEnd)) {
40
+ return yield* Effect.fail(
41
+ new Error("The service is unavailable at the deadline; unanswered status could not be verified. Re-read this question when it returns."),
42
+ );
43
+ }
44
+ if (!unavailable) {
45
+ diagnostic("Local service unavailable; reconnecting to the same question.");
46
+ }
47
+ unavailable = true;
48
+ yield* Effect.sleep("2 seconds");
49
+ continue;
50
+ }
51
+ known = result.success;
52
+ unavailable = false;
53
+ const complete = yield* resultFor(known);
54
+ if (complete) {
55
+ return complete;
56
+ }
57
+ }
58
+ });
@@ -4,6 +4,7 @@ import { contract } from "@shivaedev/effect-contract/contract.ts";
4
4
  import { LiveHint } from "@shivaedev/effect-contract/live.ts";
5
5
  import { query } from "@shivaedev/effect-contract/operation.ts";
6
6
  import { HistoryInput, HistoryOutput } from "#history/schema.ts";
7
+ import { responseContract } from "#rpc/responseContract.ts";
7
8
  import { index, navigation, watcher, workspace } from "./keys.ts";
8
9
  import { pageKeys } from "./pageKeys.ts";
9
10
 
@@ -18,7 +19,7 @@ const PageUrl = Schema.String.check(
18
19
  const path = new URL(value, "http://127.0.0.1").pathname;
19
20
  return (
20
21
  path === "/"
21
- || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start"].includes(path)
22
+ || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond"].includes(path)
22
23
  || /^\/_board\/item\/[^/]+\/$/u.exec(path) !== null
23
24
  || /\.md$/iu.exec(path) !== null
24
25
  || "Expected a local reading URL"
@@ -59,4 +60,4 @@ const ReadWatcher = query("watcher", {
59
60
  });
60
61
  export const workContract = contract("work-board", { queries: [ReadPage, ReadNavigation, ReadSearch, ReadHistory, ReadWatcher] });
61
62
  export const Subscribe = Rpc.make("work-board.subscribe", { error: ReadFailed, stream: true, success: LiveHint });
62
- export const workRpcs = workContract.add(Subscribe);
63
+ export const workRpcs = workContract.add(Subscribe).merge(responseContract);
@@ -0,0 +1,46 @@
1
+ import { Schema } from "effect";
2
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
3
+ import { command, query } from "@shivaedev/effect-contract/operation.ts";
4
+ import {
5
+ DraftInput,
6
+ Question,
7
+ QuestionPreview,
8
+ Reading,
9
+ RecordedResponse,
10
+ ResponseFailed,
11
+ Revision,
12
+ WaitReading,
13
+ } from "#browser/responses/schema.ts";
14
+ import { index, workspace } from "#rpc/keys.ts";
15
+
16
+ const locate = query("question", {
17
+ payload: { item: Schema.String, request: Schema.String },
18
+ reads: () => [workspace.list, index.list],
19
+ rejections: { ResponseFailed },
20
+ success: QuestionPreview,
21
+ });
22
+ const awaitResponse = query("awaitResponse", {
23
+ payload: { after: Schema.optional(Schema.String), question: Schema.String },
24
+ reads: () => [workspace.list, index.list],
25
+ rejections: { ResponseFailed },
26
+ success: WaitReading,
27
+ });
28
+ const read = query("responses", {
29
+ payload: { question: Schema.String },
30
+ reads: () => [workspace.list, index.list],
31
+ rejections: { ResponseFailed },
32
+ success: Reading,
33
+ });
34
+ const register = command("registerQuestion", {
35
+ invalidates: () => [workspace.list, index.list],
36
+ payload: { item: Schema.String, request: Schema.String, revision: Revision },
37
+ rejections: { ResponseFailed },
38
+ success: Question,
39
+ });
40
+ const respond = command("recordResponse", {
41
+ invalidates: () => [workspace.list, index.list],
42
+ payload: DraftInput,
43
+ rejections: { ResponseFailed },
44
+ success: RecordedResponse,
45
+ });
46
+ export const responseContract = contract("work-board", { commands: [register, respond], queries: [locate, read, awaitResponse] });
package/src/rpc/server.ts CHANGED
@@ -6,6 +6,7 @@ import { MAX_BYTES } from "#history/limits.ts";
6
6
  import { historyResponse } from "#history/response.ts";
7
7
  import { HEADERS } from "#http/respond.ts";
8
8
  import { navHtml } from "#page/nav.ts";
9
+ import type { responseService } from "#responses/service.ts";
9
10
  import { searchMatch } from "#search/match.ts";
10
11
  import type { Snapshot } from "#search/snapshot.ts";
11
12
  import { ReadFailed, workRpcs } from "./contract.ts";
@@ -16,10 +17,12 @@ export interface ServerOptions {
16
17
  readonly home: string | undefined;
17
18
  readonly index: Effect.Effect<Snapshot, unknown>;
18
19
  readonly page: (url: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown>;
20
+ readonly responses: Effect.Success<ReturnType<typeof responseService>>;
19
21
  }
20
22
 
21
- export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page }: ServerOptions) {
23
+ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page, responses }: ServerOptions) {
22
24
  const handlers = workRpcs.toLayer({
25
+ "work-board.awaitResponse": responses.awaitResponse,
23
26
  "work-board.history": ({ action, baseline }) =>
24
27
  Effect.gen(function* () {
25
28
  const now = yield* Clock.currentTimeMillis;
@@ -40,6 +43,10 @@ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home,
40
43
  }),
41
44
  Effect.catch((error) => Effect.fail(error instanceof ReadFailed ? error : new ReadFailed({ operation: "page", status: 500 }))),
42
45
  ),
46
+ "work-board.question": ({ item, request }) => responses.locate(item, request),
47
+ "work-board.recordResponse": responses.record,
48
+ "work-board.registerQuestion": responses.register,
49
+ "work-board.responses": ({ question }) => responses.read(question),
43
50
  "work-board.search": ({ query }) =>
44
51
  Effect.map(index, (snapshot) => ({ ...searchMatch(snapshot.entries, query), unavailable: snapshot.unavailable })).pipe(
45
52
  Effect.catch(() => Effect.fail(new ReadFailed({ operation: "search", status: 500 }))),
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { Effect, Layer, Option, Path } from "effect";
4
+ import { Argument, Command, Flag } from "effect/unstable/cli";
5
+ import { HOST, serveBoard } from "./serve.ts";
6
+
7
+ const VERSION = "0.8.0";
8
+
9
+ const serve = Effect.fn("WorkBoard.serve")(function* (input: {
10
+ readonly dir: string;
11
+ readonly port: number;
12
+ readonly responses: boolean;
13
+ readonly home: Option.Option<string>;
14
+ }) {
15
+ const path = yield* Path.Path;
16
+ return yield* Layer.launch(
17
+ serveBoard({ home: Option.getOrUndefined(input.home), port: input.port, responses: input.responses, root: path.resolve(input.dir) }),
18
+ );
19
+ });
20
+
21
+ const workBoard = Command.make(
22
+ "work-board",
23
+ {
24
+ dir: Argument.directory("dir", { mustExist: true }).pipe(Argument.withDescription("The folder of markdown files to serve")),
25
+ home: Flag.string("home").pipe(Flag.optional, Flag.withDescription("The file shown as a board at /, relative to the folder")),
26
+ port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
27
+ responses: Flag.boolean("responses").pipe(
28
+ Flag.withDefault(false),
29
+ Flag.withDescription("Explicitly enable local durable question and response writes (Linux)"),
30
+ ),
31
+ },
32
+ serve,
33
+ ).pipe(Command.withDescription("Serve a folder of markdown files as a live page that updates in place when a file changes."));
34
+
35
+ export function runServe() {
36
+ Command.run(workBoard, { version: VERSION }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
37
+ }