speckit-kit 0.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.
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/speckit-kit.ts
4
+ import fs3 from "fs";
5
+ import { createServer } from "http";
6
+ import path3 from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { HttpApiBuilder as HttpApiBuilder3, HttpMiddleware, HttpServer, HttpServerRequest, HttpServerResponse } from "@effect/platform";
9
+ import { NodeFileSystem, NodeHttpServer, NodePath, NodeRuntime } from "@effect/platform-node";
10
+ import { Effect as Effect5, Layer as Layer2 } from "effect";
11
+ import open from "open";
12
+
13
+ // src/server/app/effect-api.ts
14
+ import { HttpApi } from "@effect/platform";
15
+
16
+ // src/server/health/health.contract.ts
17
+ import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
18
+ import { Schema } from "effect";
19
+ var HealthResponse = Schema.Struct({
20
+ ok: Schema.Boolean
21
+ });
22
+ var HealthGroup = HttpApiGroup.make("Health").add(HttpApiEndpoint.get("health")`/health`.addSuccess(HealthResponse));
23
+
24
+ // src/server/specboard/specboard.contract.ts
25
+ import { HttpApiEndpoint as HttpApiEndpoint2, HttpApiGroup as HttpApiGroup2, HttpApiSchema } from "@effect/platform";
26
+ import { Schema as Schema2 } from "effect";
27
+ var ArtifactName = Schema2.Union(
28
+ Schema2.Literal("spec.md"),
29
+ Schema2.Literal("plan.md"),
30
+ Schema2.Literal("tasks.md"),
31
+ Schema2.Literal("quickstart.md"),
32
+ Schema2.Literal("data-model.md"),
33
+ Schema2.Literal("research.md")
34
+ );
35
+ var SpecTaskStats = Schema2.Struct({
36
+ total: Schema2.Number,
37
+ done: Schema2.Number,
38
+ todo: Schema2.Number
39
+ });
40
+ var SpecListItem = Schema2.Struct({
41
+ id: Schema2.String,
42
+ num: Schema2.Number,
43
+ title: Schema2.String,
44
+ taskStats: Schema2.optional(SpecTaskStats)
45
+ });
46
+ var SpecListResponse = Schema2.Struct({
47
+ items: Schema2.Array(SpecListItem)
48
+ });
49
+ var TaskItem = Schema2.Struct({
50
+ line: Schema2.Number,
51
+ checked: Schema2.Boolean,
52
+ raw: Schema2.String,
53
+ title: Schema2.String,
54
+ taskId: Schema2.optional(Schema2.String),
55
+ parallel: Schema2.optional(Schema2.Boolean),
56
+ story: Schema2.optional(Schema2.String)
57
+ });
58
+ var TaskListResponse = Schema2.Struct({
59
+ specId: Schema2.String,
60
+ tasks: Schema2.Array(TaskItem)
61
+ });
62
+ var TaskToggleRequest = Schema2.Struct({
63
+ line: Schema2.Number,
64
+ checked: Schema2.Boolean
65
+ });
66
+ var FileReadResponse = Schema2.Struct({
67
+ name: ArtifactName,
68
+ path: Schema2.String,
69
+ content: Schema2.String
70
+ });
71
+ var FileWriteRequest = Schema2.Struct({
72
+ content: Schema2.String
73
+ });
74
+ var FileWriteResponse = Schema2.Struct({
75
+ name: ArtifactName,
76
+ path: Schema2.String
77
+ });
78
+ var ValidationError = Schema2.Struct({
79
+ _tag: Schema2.Literal("ValidationError"),
80
+ message: Schema2.String
81
+ });
82
+ var NotFoundError = Schema2.Struct({
83
+ _tag: Schema2.Literal("NotFoundError"),
84
+ message: Schema2.String
85
+ });
86
+ var ForbiddenError = Schema2.Struct({
87
+ _tag: Schema2.Literal("ForbiddenError"),
88
+ message: Schema2.String
89
+ });
90
+ var ConflictError = Schema2.Struct({
91
+ _tag: Schema2.Literal("ConflictError"),
92
+ message: Schema2.String
93
+ });
94
+ var InternalError = Schema2.Struct({
95
+ _tag: Schema2.Literal("InternalError"),
96
+ message: Schema2.String
97
+ });
98
+ var SpecboardGroup = HttpApiGroup2.make("Specboard").addError(ValidationError, { status: 400 }).addError(ForbiddenError, { status: 403 }).addError(NotFoundError, { status: 404 }).addError(ConflictError, { status: 409 }).addError(InternalError, { status: 500 }).add(HttpApiEndpoint2.get("specList")`/specs`.addSuccess(SpecListResponse)).add(
99
+ HttpApiEndpoint2.get("taskList")`/specs/${HttpApiSchema.param("specId", Schema2.String)}/tasks`.addSuccess(
100
+ TaskListResponse
101
+ )
102
+ ).add(
103
+ HttpApiEndpoint2.post("taskToggle")`/specs/${HttpApiSchema.param("specId", Schema2.String)}/tasks/toggle`.setPayload(TaskToggleRequest).addSuccess(TaskListResponse)
104
+ ).add(
105
+ HttpApiEndpoint2.get(
106
+ "fileRead"
107
+ )`/specs/${HttpApiSchema.param("specId", Schema2.String)}/files/${HttpApiSchema.param("name", ArtifactName)}`.addSuccess(
108
+ FileReadResponse
109
+ )
110
+ ).add(
111
+ HttpApiEndpoint2.put(
112
+ "fileWrite"
113
+ )`/specs/${HttpApiSchema.param("specId", Schema2.String)}/files/${HttpApiSchema.param("name", ArtifactName)}`.setPayload(FileWriteRequest).addSuccess(FileWriteResponse)
114
+ );
115
+
116
+ // src/server/app/effect-api.ts
117
+ var EffectApi = HttpApi.make("EffectApi").add(HealthGroup).add(SpecboardGroup);
118
+
119
+ // src/server/health/health.http.live.ts
120
+ import { HttpApiBuilder } from "@effect/platform";
121
+ import { Effect } from "effect";
122
+ var HealthLive = HttpApiBuilder.group(
123
+ EffectApi,
124
+ "Health",
125
+ (handlers) => handlers.handle("health", () => Effect.succeed({ ok: true }))
126
+ );
127
+
128
+ // src/server/specboard/specboard.http.live.ts
129
+ import { HttpApiBuilder as HttpApiBuilder2 } from "@effect/platform";
130
+ import { Effect as Effect3 } from "effect";
131
+
132
+ // src/server/specboard/specboard.service.ts
133
+ import { Context } from "effect";
134
+ var Specboard = class extends Context.Tag("Specboard")() {
135
+ };
136
+
137
+ // src/server/specboard/specboard.http.live.ts
138
+ var SpecboardLive = HttpApiBuilder2.group(
139
+ EffectApi,
140
+ "Specboard",
141
+ (handlers) => handlers.handle(
142
+ "specList",
143
+ () => Effect3.gen(function* () {
144
+ const specboard = yield* Specboard;
145
+ return yield* specboard.listSpecs;
146
+ })
147
+ ).handle(
148
+ "taskList",
149
+ ({ path: path4 }) => Effect3.gen(function* () {
150
+ const specboard = yield* Specboard;
151
+ return yield* specboard.listTasks(path4.specId);
152
+ })
153
+ ).handle(
154
+ "taskToggle",
155
+ ({ path: path4, payload }) => Effect3.gen(function* () {
156
+ const specboard = yield* Specboard;
157
+ return yield* specboard.toggleTask({ specId: path4.specId, line: payload.line, checked: payload.checked });
158
+ })
159
+ ).handle(
160
+ "fileRead",
161
+ ({ path: path4 }) => Effect3.gen(function* () {
162
+ const specboard = yield* Specboard;
163
+ return yield* specboard.readFile({ specId: path4.specId, name: path4.name });
164
+ })
165
+ ).handle(
166
+ "fileWrite",
167
+ ({ path: path4, payload }) => Effect3.gen(function* () {
168
+ const specboard = yield* Specboard;
169
+ return yield* specboard.writeFile({ specId: path4.specId, name: path4.name, content: payload.content });
170
+ })
171
+ )
172
+ );
173
+
174
+ // src/server/specboard/specboard.service.live.ts
175
+ import fs2 from "fs/promises";
176
+ import path2 from "path";
177
+ import { Effect as Effect4, Layer } from "effect";
178
+
179
+ // src/server/util/repo-paths.ts
180
+ import fs from "fs";
181
+ import path from "path";
182
+ var RepoRootNotFoundError = class extends Error {
183
+ constructor() {
184
+ super(...arguments);
185
+ this.name = "RepoRootNotFoundError";
186
+ }
187
+ };
188
+ var UnsafePathSegmentError = class extends Error {
189
+ constructor() {
190
+ super(...arguments);
191
+ this.name = "UnsafePathSegmentError";
192
+ }
193
+ };
194
+ var PathEscapeError = class extends Error {
195
+ constructor() {
196
+ super(...arguments);
197
+ this.name = "PathEscapeError";
198
+ }
199
+ };
200
+ function findRepoPaths(startDir = process.cwd()) {
201
+ const envRepoRoot = process.env.SPECKIT_KIT_REPO_ROOT ?? process.env.SPECKIT_REPO_ROOT;
202
+ if (envRepoRoot) {
203
+ const repoRoot = path.resolve(envRepoRoot);
204
+ const specsRoot = path.join(repoRoot, "specs");
205
+ if (!fs.existsSync(specsRoot) || !fs.statSync(specsRoot).isDirectory()) {
206
+ throw new RepoRootNotFoundError(`Invalid repo root: specs/ not found under ${repoRoot}`);
207
+ }
208
+ return { repoRoot, specsRoot };
209
+ }
210
+ let dir = path.resolve(startDir);
211
+ while (true) {
212
+ const specsRoot = path.join(dir, "specs");
213
+ if (fs.existsSync(specsRoot) && fs.statSync(specsRoot).isDirectory()) {
214
+ return { repoRoot: dir, specsRoot };
215
+ }
216
+ const parent = path.dirname(dir);
217
+ if (parent === dir) {
218
+ break;
219
+ }
220
+ dir = parent;
221
+ }
222
+ throw new RepoRootNotFoundError(`Cannot locate repo root starting from: ${startDir}`);
223
+ }
224
+ function resolveWithinRoot(rootDir, relativePath) {
225
+ const rootAbs = path.resolve(rootDir);
226
+ const targetAbs = path.resolve(rootAbs, relativePath);
227
+ const rootPrefix = rootAbs.endsWith(path.sep) ? rootAbs : `${rootAbs}${path.sep}`;
228
+ if (!targetAbs.startsWith(rootPrefix)) {
229
+ throw new PathEscapeError(`Path escapes root: ${relativePath}`);
230
+ }
231
+ return targetAbs;
232
+ }
233
+ function assertSafePathSegment(segment) {
234
+ if (segment.includes("/") || segment.includes("\\")) {
235
+ throw new UnsafePathSegmentError("Path segment contains separator");
236
+ }
237
+ }
238
+
239
+ // src/server/specboard/specboard.tasks.ts
240
+ var CheckboxLine = /^(\s*-\s*\[)([ xX])(\]\s+)(.+)$/;
241
+ function parseTasksMarkdown(markdown) {
242
+ const tasks = [];
243
+ const lines = markdown.split(/\r?\n/);
244
+ for (let index = 0; index < lines.length; index++) {
245
+ const raw = lines[index] ?? "";
246
+ const match = raw.match(CheckboxLine);
247
+ if (!match) continue;
248
+ const checked = match[2]?.toLowerCase() === "x";
249
+ const title = match[4]?.trim() ?? "";
250
+ const taskId = title.match(/\bT\d{3}\b/)?.[0];
251
+ const parallel = /\[P\]/.test(title) ? true : void 0;
252
+ const story = title.match(/\[(US\d+)\]/)?.[1];
253
+ tasks.push({
254
+ line: index + 1,
255
+ checked,
256
+ raw,
257
+ title,
258
+ taskId,
259
+ parallel,
260
+ story
261
+ });
262
+ }
263
+ return tasks;
264
+ }
265
+ function updateCheckboxLine(rawLine, checked) {
266
+ const match = rawLine.match(CheckboxLine);
267
+ if (!match) return null;
268
+ const prefix = match[1] ?? "";
269
+ const suffix = `${match[3] ?? ""}${match[4] ?? ""}`;
270
+ return `${prefix}${checked ? "x" : " "}${suffix}`;
271
+ }
272
+
273
+ // src/server/specboard/specboard.service.live.ts
274
+ function toInternalError(message) {
275
+ return { _tag: "InternalError", message };
276
+ }
277
+ function toFsError(e) {
278
+ if (e instanceof UnsafePathSegmentError || e instanceof PathEscapeError) {
279
+ return { _tag: "ForbiddenError", message: "Forbidden path" };
280
+ }
281
+ if (typeof e === "object" && e !== null && "code" in e) {
282
+ const code = e.code;
283
+ if (code === "ENOENT") return { _tag: "NotFoundError", message: "File not found" };
284
+ if (code === "EACCES" || code === "EPERM") return { _tag: "ForbiddenError", message: "Permission denied" };
285
+ }
286
+ return toInternalError(e instanceof Error ? e.message : "Internal error");
287
+ }
288
+ function inferTitleFromMarkdown(markdown, fallback) {
289
+ const line = markdown.split(/\r?\n/).find((l) => l.startsWith("# "));
290
+ return line ? line.replace(/^#\s+/, "").trim() : fallback;
291
+ }
292
+ function parseSpecDirName(dirName) {
293
+ if (!/^\d{3}-/.test(dirName)) return null;
294
+ const num = Number.parseInt(dirName.slice(0, 3), 10);
295
+ if (Number.isNaN(num)) return null;
296
+ return { id: dirName, num };
297
+ }
298
+ function specFileRelPath(specId, name) {
299
+ return path2.posix.join("specs", specId, name);
300
+ }
301
+ function absSpecDir(paths, specId) {
302
+ assertSafePathSegment(specId);
303
+ return resolveWithinRoot(paths.specsRoot, specId);
304
+ }
305
+ function absSpecFile(paths, specId, name) {
306
+ return resolveWithinRoot(absSpecDir(paths, specId), name);
307
+ }
308
+ async function exists(p) {
309
+ try {
310
+ await fs2.stat(p);
311
+ return true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+ async function writeFileAtomic(absPath, content) {
317
+ const dir = path2.dirname(absPath);
318
+ const tmpPath = path2.join(dir, `.tmp.${path2.basename(absPath)}.${Date.now()}.${Math.random().toString(16).slice(2)}`);
319
+ await fs2.writeFile(tmpPath, content, "utf8");
320
+ await fs2.rename(tmpPath, absPath);
321
+ }
322
+ var SpecboardServiceLive = Layer.effect(
323
+ Specboard,
324
+ Effect4.sync(() => {
325
+ const pathsOrError = (() => {
326
+ try {
327
+ return findRepoPaths();
328
+ } catch (e) {
329
+ return toInternalError(e instanceof Error ? e.message : "Cannot find repo root");
330
+ }
331
+ })();
332
+ const isError = (u) => typeof u === "object" && u !== null && "_tag" in u;
333
+ const withPaths = (f) => isError(pathsOrError) ? Effect4.fail(pathsOrError) : f(pathsOrError);
334
+ const listSpecs = withPaths(
335
+ (paths) => Effect4.tryPromise({
336
+ try: async () => {
337
+ const dirents = await fs2.readdir(paths.specsRoot, { withFileTypes: true });
338
+ const specs = dirents.filter((d) => d.isDirectory()).map((d) => parseSpecDirName(d.name)).filter((v) => v !== null);
339
+ const items = await Promise.all(
340
+ specs.map(async ({ id, num }) => {
341
+ const specMdPath = absSpecFile(paths, id, "spec.md");
342
+ const specMd = await exists(specMdPath) ? await fs2.readFile(specMdPath, "utf8") : null;
343
+ const tasksMdPath = absSpecFile(paths, id, "tasks.md");
344
+ const tasksMd = await exists(tasksMdPath) ? await fs2.readFile(tasksMdPath, "utf8") : null;
345
+ const tasks = tasksMd ? parseTasksMarkdown(tasksMd) : [];
346
+ const done = tasks.filter((t) => t.checked).length;
347
+ const total = tasks.length;
348
+ return {
349
+ id,
350
+ num,
351
+ title: specMd ? inferTitleFromMarkdown(specMd, id) : id,
352
+ taskStats: tasksMd ? {
353
+ total,
354
+ done,
355
+ todo: total - done
356
+ } : void 0
357
+ };
358
+ })
359
+ );
360
+ items.sort((a, b) => b.num - a.num || b.id.localeCompare(a.id));
361
+ return { items };
362
+ },
363
+ catch: toFsError
364
+ })
365
+ );
366
+ const listTasks = (specId) => withPaths(
367
+ (paths) => Effect4.tryPromise({
368
+ try: async () => {
369
+ const tasksMdPath = absSpecFile(paths, specId, "tasks.md");
370
+ const tasksMd = await fs2.readFile(tasksMdPath, "utf8");
371
+ const tasks = parseTasksMarkdown(tasksMd);
372
+ return { specId, tasks };
373
+ },
374
+ catch: toFsError
375
+ })
376
+ );
377
+ const toggleTask = ({ specId, line, checked }) => withPaths(
378
+ (paths) => Effect4.tryPromise({
379
+ try: async () => {
380
+ if (!Number.isInteger(line) || line <= 0) {
381
+ return Promise.reject({ _tag: "ValidationError", message: "invalid line" });
382
+ }
383
+ const tasksMdPath = absSpecFile(paths, specId, "tasks.md");
384
+ const tasksMd = await fs2.readFile(tasksMdPath, "utf8");
385
+ const lines = tasksMd.split(/\r?\n/);
386
+ const idx = line - 1;
387
+ if (idx < 0 || idx >= lines.length) {
388
+ return Promise.reject({ _tag: "ValidationError", message: "line out of range" });
389
+ }
390
+ const updated = updateCheckboxLine(lines[idx] ?? "", checked);
391
+ if (!updated) {
392
+ return Promise.reject({
393
+ _tag: "ValidationError",
394
+ message: "target line is not a checkbox task"
395
+ });
396
+ }
397
+ lines[idx] = updated;
398
+ await writeFileAtomic(tasksMdPath, lines.join("\n"));
399
+ const nextTasksMd = await fs2.readFile(tasksMdPath, "utf8");
400
+ return { specId, tasks: parseTasksMarkdown(nextTasksMd) };
401
+ },
402
+ catch: (e) => {
403
+ if (typeof e === "object" && e !== null && "_tag" in e) {
404
+ const tag = e._tag;
405
+ if (tag === "ValidationError") {
406
+ return e;
407
+ }
408
+ }
409
+ return toFsError(e);
410
+ }
411
+ })
412
+ );
413
+ const readFile = ({ specId, name }) => withPaths(
414
+ (paths) => Effect4.tryPromise({
415
+ try: async () => {
416
+ const absPath = absSpecFile(paths, specId, name);
417
+ const content = await fs2.readFile(absPath, "utf8");
418
+ return { name, path: specFileRelPath(specId, name), content };
419
+ },
420
+ catch: toFsError
421
+ })
422
+ );
423
+ const writeFile = ({ specId, name, content }) => withPaths(
424
+ (paths) => Effect4.tryPromise({
425
+ try: async () => {
426
+ const specDir = absSpecDir(paths, specId);
427
+ if (!await exists(specDir)) {
428
+ return Promise.reject({ _tag: "NotFoundError", message: "Spec not found" });
429
+ }
430
+ const absPath = absSpecFile(paths, specId, name);
431
+ await writeFileAtomic(absPath, content);
432
+ return { name, path: specFileRelPath(specId, name) };
433
+ },
434
+ catch: (e) => {
435
+ if (typeof e === "object" && e !== null && "_tag" in e) {
436
+ const tag = e._tag;
437
+ if (tag === "NotFoundError") {
438
+ return e;
439
+ }
440
+ }
441
+ return toFsError(e);
442
+ }
443
+ })
444
+ );
445
+ return { listSpecs, listTasks, toggleTask, readFile, writeFile };
446
+ })
447
+ );
448
+
449
+ // src/bin/speckit-kit.ts
450
+ function printHelp() {
451
+ console.log(`speckit-kit
452
+
453
+ \u7528\u6CD5:
454
+ speckit-kit [kanban] [--repo-root <path>] [--host <host>] [--port <port>] [--no-open]
455
+
456
+ \u53C2\u6570:
457
+ --repo-root <path> \u6307\u5B9A\u76EE\u6807\u4ED3\u5E93\u6839\u76EE\u5F55\uFF08\u9700\u5305\u542B specs/\uFF09
458
+ --host <host> \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF09
459
+ --port <port> \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 0 \u968F\u673A\u7AEF\u53E3\uFF09
460
+ --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668
461
+ -h, --help \u663E\u793A\u5E2E\u52A9
462
+ `);
463
+ }
464
+ function parseArgs(argv) {
465
+ const args = [...argv];
466
+ const first = args[0];
467
+ let command = "kanban";
468
+ if (first && !first.startsWith("-")) {
469
+ if (first !== "kanban") {
470
+ throw new Error(`\u672A\u77E5\u547D\u4EE4\uFF1A${first}`);
471
+ }
472
+ command = "kanban";
473
+ args.shift();
474
+ }
475
+ let host = process.env.HOST ?? "127.0.0.1";
476
+ let port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : null;
477
+ let shouldOpen = !(process.env.NO_OPEN === "1" || process.env.NO_OPEN === "true");
478
+ let repoRoot = null;
479
+ const takeValue = (flag) => {
480
+ const value = args.shift();
481
+ if (!value) throw new Error(`${flag} \u7F3A\u5C11\u53C2\u6570`);
482
+ return value;
483
+ };
484
+ while (args.length > 0) {
485
+ const a = args.shift();
486
+ if (a === "-h" || a === "--help") return { help: true };
487
+ if (a === "--host") {
488
+ host = takeValue(a);
489
+ continue;
490
+ }
491
+ if (a === "--port") {
492
+ const raw = takeValue(a);
493
+ port = Number.parseInt(raw, 10);
494
+ if (Number.isNaN(port) || port < 0) {
495
+ throw new Error(`--port \u975E\u6CD5\uFF1A${raw}`);
496
+ }
497
+ continue;
498
+ }
499
+ if (a === "--repo-root") {
500
+ repoRoot = takeValue(a);
501
+ continue;
502
+ }
503
+ if (a === "--no-open") {
504
+ shouldOpen = false;
505
+ continue;
506
+ }
507
+ if (a === "--open") {
508
+ shouldOpen = true;
509
+ continue;
510
+ }
511
+ throw new Error(`\u672A\u77E5\u53C2\u6570\uFF1A${a}`);
512
+ }
513
+ return { command, host, port, open: shouldOpen, repoRoot };
514
+ }
515
+ var __filename = fileURLToPath(import.meta.url);
516
+ var __dirname = path3.dirname(__filename);
517
+ var UI_DIST = path3.resolve(__dirname, "..", "ui");
518
+ var UI_INDEX_HTML = path3.join(UI_DIST, "index.html");
519
+ function assertUiDistExists() {
520
+ if (!fs3.existsSync(UI_INDEX_HTML)) {
521
+ console.error(`UI dist \u4E0D\u5B58\u5728\uFF1A${UI_DIST}`);
522
+ console.error(`\u8BF7\u5148\u6784\u5EFA\uFF1Apnpm -C packages/speckit-kit build`);
523
+ process.exit(1);
524
+ }
525
+ }
526
+ function stripApiPrefix(req) {
527
+ let url;
528
+ try {
529
+ url = new URL(req.url, "http://localhost");
530
+ } catch {
531
+ return req;
532
+ }
533
+ if (!(url.pathname === "/api" || url.pathname.startsWith("/api/"))) {
534
+ return req;
535
+ }
536
+ url.pathname = url.pathname.replace(/^\/api/, "") || "/";
537
+ const nextUrl = `${url.pathname}${url.search}`;
538
+ return req.modify({ url: nextUrl });
539
+ }
540
+ function resolveUiFilePath(pathname) {
541
+ const decoded = (() => {
542
+ try {
543
+ return decodeURIComponent(pathname);
544
+ } catch {
545
+ return pathname;
546
+ }
547
+ })();
548
+ const withoutLeadingSlash = decoded.startsWith("/") ? decoded.slice(1) : decoded;
549
+ const relative = withoutLeadingSlash.length === 0 ? "index.html" : withoutLeadingSlash;
550
+ const candidate = path3.resolve(UI_DIST, relative);
551
+ if (!candidate.startsWith(`${UI_DIST}${path3.sep}`) && candidate !== UI_DIST) {
552
+ return null;
553
+ }
554
+ if (decoded.endsWith("/") || !path3.extname(candidate)) {
555
+ return UI_INDEX_HTML;
556
+ }
557
+ return candidate;
558
+ }
559
+ function runKanban(opts) {
560
+ if (opts.repoRoot) {
561
+ process.env.SPECKIT_KIT_REPO_ROOT = path3.resolve(opts.repoRoot);
562
+ }
563
+ assertUiDistExists();
564
+ const ApiLive = HttpApiBuilder3.api(EffectApi).pipe(
565
+ Layer2.provide(HealthLive),
566
+ Layer2.provide(SpecboardLive),
567
+ Layer2.provide(SpecboardServiceLive)
568
+ );
569
+ const NodeServerLive = NodeHttpServer.layer(createServer, {
570
+ port: opts.port === null || Number.isNaN(opts.port) ? 0 : opts.port,
571
+ host: opts.host
572
+ });
573
+ const RuntimeLive = Layer2.mergeAll(
574
+ ApiLive,
575
+ HttpApiBuilder3.Router.Live,
576
+ HttpApiBuilder3.Middleware.layer,
577
+ NodeFileSystem.layer,
578
+ NodePath.layer,
579
+ NodeServerLive
580
+ );
581
+ const program = Effect5.gen(function* () {
582
+ const apiApp = yield* HttpApiBuilder3.httpApp;
583
+ const httpApp = Effect5.gen(function* () {
584
+ const req = yield* HttpServerRequest.HttpServerRequest;
585
+ let url;
586
+ try {
587
+ url = new URL(req.url, "http://localhost");
588
+ } catch {
589
+ url = new URL("http://localhost/");
590
+ }
591
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
592
+ const rewritten = stripApiPrefix(req);
593
+ return yield* apiApp.pipe(Effect5.provideService(HttpServerRequest.HttpServerRequest, rewritten));
594
+ }
595
+ if (req.method !== "GET" && req.method !== "HEAD") {
596
+ return HttpServerResponse.text("Not Found", { status: 404 });
597
+ }
598
+ const filePath = resolveUiFilePath(url.pathname);
599
+ if (!filePath) {
600
+ return HttpServerResponse.text("Not Found", { status: 404 });
601
+ }
602
+ return yield* HttpServerResponse.file(filePath).pipe(
603
+ Effect5.catchAll(() => Effect5.succeed(HttpServerResponse.text("Not Found", { status: 404 })))
604
+ );
605
+ });
606
+ yield* HttpServer.serveEffect(httpApp, HttpMiddleware.logger);
607
+ const address = yield* HttpServer.addressWith(Effect5.succeed);
608
+ if (address._tag === "TcpAddress") {
609
+ const publicUrl = `http://localhost:${address.port}`;
610
+ yield* Effect5.sync(() => {
611
+ console.log(`Server running at ${publicUrl}`);
612
+ });
613
+ if (opts.open) {
614
+ yield* Effect5.tryPromise({
615
+ try: () => open(publicUrl),
616
+ catch: () => void 0
617
+ }).pipe(Effect5.ignore);
618
+ }
619
+ } else {
620
+ yield* Effect5.sync(() => {
621
+ console.log(`Server running at ${HttpServer.formatAddress(address)}`);
622
+ });
623
+ }
624
+ return yield* Effect5.never;
625
+ }).pipe(Effect5.provide(RuntimeLive), Effect5.scoped);
626
+ NodeRuntime.runMain(program);
627
+ }
628
+ async function main() {
629
+ const parsed = parseArgs(process.argv.slice(2));
630
+ if ("help" in parsed) {
631
+ printHelp();
632
+ return;
633
+ }
634
+ if (parsed.command === "kanban") {
635
+ runKanban(parsed);
636
+ return;
637
+ }
638
+ }
639
+ void main().catch((e) => {
640
+ console.error(e);
641
+ process.exit(1);
642
+ });
643
+ //# sourceMappingURL=speckit-kit.js.map