@nickmeriano/task 0.11.0 → 0.12.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 (73) hide show
  1. package/README.md +175 -1
  2. package/dist/board/github.d.ts +68 -0
  3. package/dist/board/github.d.ts.map +1 -0
  4. package/dist/board/github.js +112 -0
  5. package/dist/board/github.js.map +1 -0
  6. package/dist/board/handler.d.ts +50 -0
  7. package/dist/board/handler.d.ts.map +1 -0
  8. package/dist/board/handler.js +183 -0
  9. package/dist/board/handler.js.map +1 -0
  10. package/dist/board/handler.test.d.ts +11 -0
  11. package/dist/board/handler.test.d.ts.map +1 -0
  12. package/dist/board/handler.test.js +229 -0
  13. package/dist/board/handler.test.js.map +1 -0
  14. package/dist/board/pages-function.d.ts +23 -0
  15. package/dist/board/pages-function.d.ts.map +1 -0
  16. package/dist/board/pages-function.js +34 -0
  17. package/dist/board/pages-function.js.map +1 -0
  18. package/dist/board/source.d.ts +118 -0
  19. package/dist/board/source.d.ts.map +1 -0
  20. package/dist/board/source.js +333 -0
  21. package/dist/board/source.js.map +1 -0
  22. package/dist/board/source.test.d.ts +12 -0
  23. package/dist/board/source.test.d.ts.map +1 -0
  24. package/dist/board/source.test.js +165 -0
  25. package/dist/board/source.test.js.map +1 -0
  26. package/dist/board/tar.d.ts +28 -0
  27. package/dist/board/tar.d.ts.map +1 -0
  28. package/dist/board/tar.js +188 -0
  29. package/dist/board/tar.js.map +1 -0
  30. package/dist/board/tar.test.d.ts +9 -0
  31. package/dist/board/tar.test.d.ts.map +1 -0
  32. package/dist/board/tar.test.js +110 -0
  33. package/dist/board/tar.test.js.map +1 -0
  34. package/dist/cli.js +37 -2
  35. package/dist/cli.js.map +1 -1
  36. package/dist/export.d.ts +31 -0
  37. package/dist/export.d.ts.map +1 -1
  38. package/dist/export.js +61 -1
  39. package/dist/export.js.map +1 -1
  40. package/dist/export.test.js +80 -1
  41. package/dist/export.test.js.map +1 -1
  42. package/dist/functions/board.js +709 -0
  43. package/dist/git-serve.d.ts +14 -1
  44. package/dist/git-serve.d.ts.map +1 -1
  45. package/dist/git-serve.js +30 -2
  46. package/dist/git-serve.js.map +1 -1
  47. package/dist/git-serve.test.d.ts +1 -0
  48. package/dist/git-serve.test.d.ts.map +1 -1
  49. package/dist/git-serve.test.js +36 -1
  50. package/dist/git-serve.test.js.map +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +6 -0
  54. package/dist/index.js.map +1 -1
  55. package/package.json +3 -3
  56. package/skill/SKILL.md +10 -3
  57. package/src/board/github.ts +151 -0
  58. package/src/board/handler.test.ts +276 -0
  59. package/src/board/handler.ts +228 -0
  60. package/src/board/pages-function.ts +42 -0
  61. package/src/board/source.test.ts +203 -0
  62. package/src/board/source.ts +422 -0
  63. package/src/board/tar.test.ts +128 -0
  64. package/src/board/tar.ts +199 -0
  65. package/src/cli.ts +36 -2
  66. package/src/export.test.ts +108 -1
  67. package/src/export.ts +79 -1
  68. package/src/git-serve.test.ts +41 -1
  69. package/src/git-serve.ts +30 -2
  70. package/src/index.ts +10 -0
  71. package/ui/dist/assets/index-B8M_DaOt.js +229 -0
  72. package/ui/dist/index.html +1 -1
  73. package/ui/dist/assets/index-mJmm4sWq.js +0 -229
@@ -11,11 +11,12 @@
11
11
  * runs; the board's own origin and origin-less clients pass.
12
12
  * 4. Degradation: a repo with no origin advertises no `git` capability and
13
13
  * 404s the whole route group — the self-hiding rule the UI builds on.
14
+ * 5. A board reached through a symlinked path is still a board (TAS-qn8td).
14
15
  */
15
16
 
16
17
  import assert from "node:assert/strict"
17
18
  import { after, test } from "node:test"
18
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
19
+ import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
19
20
  import { tmpdir } from "node:os"
20
21
  import { join } from "node:path"
21
22
  import { spawnSync } from "node:child_process"
@@ -232,6 +233,45 @@ test("a plain directory (no git at all) behaves the same", async () => {
232
233
  assert.equal("git" in project.body, false)
233
234
  })
234
235
 
236
+ test("a repo reached through a symlink still finds its boards", async () => {
237
+ // `git rev-parse --show-toplevel` resolves symlinks and a Store's taskDir
238
+ // does not, so comparing them unresolved drops every board — commit 400s and
239
+ // status calls board files `otherDirty`, quietly. The symlink is explicit
240
+ // here rather than inherited from tmpdir(): on macOS /var/folders is already
241
+ // one (which is how this was found), on Linux it is not, and the property
242
+ // has to hold on both.
243
+ const home = tempDir()
244
+ const real = join(home, "real")
245
+ mkdirSync(real)
246
+ const bare = join(real, "origin.git")
247
+ sh(real, "git", "init", "--quiet", "--bare", "-b", "main", bare)
248
+ sh(real, "git", "clone", "--quiet", bare, join(real, "clone"))
249
+ const link = join(home, "link")
250
+ symlinkSync(real, link)
251
+
252
+ const clone = join(link, "clone")
253
+ sh(clone, "git", "config", "user.name", "Test")
254
+ sh(clone, "git", "config", "user.email", "test@example.com")
255
+ const store = initProject(clone, { name: "linked", prefix: "LNK" })
256
+ const first = store.create({ title: "First", status: "todo" }).key
257
+ sh(clone, "git", "add", "-A")
258
+ sh(clone, "git", "commit", "--quiet", "-m", "board")
259
+ sh(clone, "git", "push", "--quiet", "-u", "origin", "main")
260
+
261
+ const base = await serve(clone)
262
+ await call(base, `/api/tasks/${first}`, Object.assign(post({ status: "in_progress" }), { method: "PATCH" }))
263
+
264
+ const status = await call(base, "/api/git/status")
265
+ assert.equal(status.status, 200)
266
+ const dirty = status.body.dirty as { path: string }[]
267
+ assert.ok(dirty.length > 0, `board edits must read as board dirt: ${JSON.stringify(status.body)}`)
268
+ assert.ok(dirty.every((f) => f.path.startsWith(".task/")))
269
+ assert.equal(status.body.otherDirty, 0, "and must not be counted as other people's mess")
270
+
271
+ const commit = await call(base, "/api/git/commit", post({ message: "chore(board): through a link" }))
272
+ assert.equal(commit.status, 200, JSON.stringify(commit.body))
273
+ })
274
+
235
275
  test("the gh-less PR fallback is the exact GitHub compare URL, branches escaped", () => {
236
276
  assert.equal(
237
277
  compareUrl({ owner: "nicmeriano", repo: "nickmeriano.com" }, "main", "board/2026-09-01"),
package/src/git-serve.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { spawnSync } from "node:child_process"
22
+ import { realpathSync } from "node:fs"
22
23
  import { relative, sep } from "node:path"
23
24
  import { claimBranch, claimNamespace, release } from "./claim.ts"
24
25
  import { remoteClaimTips, syncClaimRefs } from "./claim-io.ts"
@@ -31,6 +32,19 @@ export interface ServedBoard {
31
32
  store: Store
32
33
  }
33
34
 
35
+ /**
36
+ * A path with its symlinks resolved, or the path itself when it cannot be —
37
+ * a board directory that does not exist on disk yet makes `realpathSync`
38
+ * throw ENOENT, and that is not a reason to lose the board.
39
+ */
40
+ function realPath(path: string): string {
41
+ try {
42
+ return realpathSync(path)
43
+ } catch {
44
+ return path
45
+ }
46
+ }
47
+
34
48
  /** A failure the route turns into a JSON error with this status. */
35
49
  export class GitServeError extends Error {
36
50
  readonly status: number
@@ -194,11 +208,25 @@ export class GitServe {
194
208
  return records
195
209
  }
196
210
 
197
- /** Each board's `.task` dir as a toplevel-relative posix pathspec. */
211
+ /**
212
+ * Each board's `.task` dir as a toplevel-relative posix pathspec.
213
+ *
214
+ * Both sides are resolved through {@link realPath} before they are
215
+ * compared. `git rev-parse --show-toplevel` already returns a symlink-free
216
+ * path, while a store's `taskDir` is built from whatever path the process
217
+ * was given — so on a checkout reached through a symlink the two disagree,
218
+ * `relative()` answers `../../…`, and the guard below silently drops every
219
+ * board. That is not a corner case on macOS, where `os.tmpdir()` is
220
+ * `/var/folders/…` → `/private/var/folders/…`; it is also any repo under a
221
+ * symlinked parent. Dropping every board turns `commit` into a 400 and,
222
+ * worse, makes `status` report board files as `otherDirty` with an empty
223
+ * `dirty` list — a board that looks clean while `.task/` is full of edits.
224
+ */
198
225
  private boardDirs(top: string): Map<string, string> {
226
+ const root = realPath(top)
199
227
  const dirs = new Map<string, string>()
200
228
  for (const { id, store } of this.boards()) {
201
- const rel = relative(top, store.taskDir).split(sep).join("/")
229
+ const rel = relative(root, realPath(store.taskDir)).split(sep).join("/")
202
230
  if (rel === "" || rel.startsWith("..")) continue
203
231
  dirs.set(id, rel)
204
232
  }
package/src/index.ts CHANGED
@@ -42,5 +42,15 @@ export {
42
42
  type ReleaseResult,
43
43
  } from "./claim.ts"
44
44
  export { createTaskServer } from "./server.ts"
45
+ // The live proxy for an exported board: the same read routes `task serve`
46
+ // answers, from a GitHub repository, as one platform-neutral fetch handler.
47
+ // `task export` ships it as a Cloudflare Pages Function beside the snapshot;
48
+ // any other host gets this and a three-line adapter — see the README.
49
+ export {
50
+ createBoardHandler,
51
+ type BoardHandler,
52
+ type BoardHandlerOptions,
53
+ } from "./board/handler.ts"
54
+ export { buildSnapshot, writeExport, type ExportSummary, type WriteExportOptions } from "./export.ts"
45
55
  export { resolveAuthor, ANONYMOUS, type Author, type AuthorSource } from "./author.ts"
46
56
  export * from "./types.ts"