@tidegap/cli 0.0.0-stage → 0.19.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 (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +136 -2
  3. package/dist/commands/clients.d.ts +20 -0
  4. package/dist/commands/clients.d.ts.map +1 -0
  5. package/dist/commands/clients.js +36 -0
  6. package/dist/commands/clients.js.map +1 -0
  7. package/dist/commands/env-file.d.ts +54 -0
  8. package/dist/commands/env-file.d.ts.map +1 -0
  9. package/dist/commands/env-file.js +153 -0
  10. package/dist/commands/env-file.js.map +1 -0
  11. package/dist/commands/install.d.ts +83 -0
  12. package/dist/commands/install.d.ts.map +1 -0
  13. package/dist/commands/install.js +297 -0
  14. package/dist/commands/install.js.map +1 -0
  15. package/dist/commands/queue.d.ts +139 -0
  16. package/dist/commands/queue.d.ts.map +1 -0
  17. package/dist/commands/queue.js +491 -0
  18. package/dist/commands/queue.js.map +1 -0
  19. package/dist/commands/receipts.d.ts +15 -0
  20. package/dist/commands/receipts.d.ts.map +1 -0
  21. package/dist/commands/receipts.js +70 -0
  22. package/dist/commands/receipts.js.map +1 -0
  23. package/dist/commands/register-wake.d.ts +21 -0
  24. package/dist/commands/register-wake.d.ts.map +1 -0
  25. package/dist/commands/register-wake.js +76 -0
  26. package/dist/commands/register-wake.js.map +1 -0
  27. package/dist/commands/stats.d.ts +31 -0
  28. package/dist/commands/stats.d.ts.map +1 -0
  29. package/dist/commands/stats.js +165 -0
  30. package/dist/commands/stats.js.map +1 -0
  31. package/dist/commands/tick.d.ts +73 -0
  32. package/dist/commands/tick.d.ts.map +1 -0
  33. package/dist/commands/tick.js +294 -0
  34. package/dist/commands/tick.js.map +1 -0
  35. package/dist/commands/ui.d.ts +33 -0
  36. package/dist/commands/ui.d.ts.map +1 -0
  37. package/dist/commands/ui.js +119 -0
  38. package/dist/commands/ui.js.map +1 -0
  39. package/dist/commands/verify.d.ts +32 -0
  40. package/dist/commands/verify.d.ts.map +1 -0
  41. package/dist/commands/verify.js +135 -0
  42. package/dist/commands/verify.js.map +1 -0
  43. package/dist/env.d.ts +57 -0
  44. package/dist/env.d.ts.map +1 -0
  45. package/dist/env.js +105 -0
  46. package/dist/env.js.map +1 -0
  47. package/dist/index.d.ts +25 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +366 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/platform/index.d.ts +30 -0
  52. package/dist/platform/index.d.ts.map +1 -0
  53. package/dist/platform/index.js +66 -0
  54. package/dist/platform/index.js.map +1 -0
  55. package/dist/platform/linux.d.ts +81 -0
  56. package/dist/platform/linux.d.ts.map +1 -0
  57. package/dist/platform/linux.js +158 -0
  58. package/dist/platform/linux.js.map +1 -0
  59. package/dist/platform/macos.d.ts +69 -0
  60. package/dist/platform/macos.d.ts.map +1 -0
  61. package/dist/platform/macos.js +151 -0
  62. package/dist/platform/macos.js.map +1 -0
  63. package/dist/platform/windows.d.ts +31 -0
  64. package/dist/platform/windows.d.ts.map +1 -0
  65. package/dist/platform/windows.js +58 -0
  66. package/dist/platform/windows.js.map +1 -0
  67. package/dist/ui/api.d.ts +66 -0
  68. package/dist/ui/api.d.ts.map +1 -0
  69. package/dist/ui/api.js +613 -0
  70. package/dist/ui/api.js.map +1 -0
  71. package/dist/ui/clients.d.ts +65 -0
  72. package/dist/ui/clients.d.ts.map +1 -0
  73. package/dist/ui/clients.js +179 -0
  74. package/dist/ui/clients.js.map +1 -0
  75. package/dist/ui/discovery.d.ts +25 -0
  76. package/dist/ui/discovery.d.ts.map +1 -0
  77. package/dist/ui/discovery.js +46 -0
  78. package/dist/ui/discovery.js.map +1 -0
  79. package/dist/ui/dispatcher.d.ts +84 -0
  80. package/dist/ui/dispatcher.d.ts.map +1 -0
  81. package/dist/ui/dispatcher.js +150 -0
  82. package/dist/ui/dispatcher.js.map +1 -0
  83. package/dist/ui/events.d.ts +27 -0
  84. package/dist/ui/events.d.ts.map +1 -0
  85. package/dist/ui/events.js +85 -0
  86. package/dist/ui/events.js.map +1 -0
  87. package/dist/ui/server.d.ts +84 -0
  88. package/dist/ui/server.d.ts.map +1 -0
  89. package/dist/ui/server.js +322 -0
  90. package/dist/ui/server.js.map +1 -0
  91. package/dist/ui/summary.d.ts +47 -0
  92. package/dist/ui/summary.d.ts.map +1 -0
  93. package/dist/ui/summary.js +120 -0
  94. package/dist/ui/summary.js.map +1 -0
  95. package/package.json +53 -3
  96. package/ui/agenda.js +256 -0
  97. package/ui/api.js +114 -0
  98. package/ui/app.css +713 -0
  99. package/ui/app.js +599 -0
  100. package/ui/clients.js +104 -0
  101. package/ui/detail.js +431 -0
  102. package/ui/dialog.js +284 -0
  103. package/ui/dnd.js +131 -0
  104. package/ui/dom.js +255 -0
  105. package/ui/index.html +38 -0
  106. package/ui/journal.js +207 -0
  107. package/ui/keys.js +92 -0
  108. package/ui/row.js +153 -0
  109. package/ui/state.js +412 -0
  110. package/ui/timeline.js +202 -0
  111. package/ui/toast.js +69 -0
  112. package/ui/tokens.css +108 -0
  113. package/ui/views.js +31 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Client token registry for `tidegap ui` (native clients, KTD2).
3
+ *
4
+ * A client token (`tgc_…`) lets a browser extension or the Mac app call the
5
+ * local API across `tidegap ui` restarts. The registry file
6
+ * `<tidegap home>/ui-clients.json` (mode 0600, written atomically) stores
7
+ * only the SHA-256 of each token, never the token itself. The server re-reads
8
+ * the file when it changes on disk, so `tidegap clients add` and `revoke`
9
+ * apply without a restart.
10
+ */
11
+ export declare const CLIENTS_FILE = "ui-clients.json";
12
+ export declare const CLIENT_TOKEN_PREFIX = "tgc_";
13
+ export declare const CLIENT_KINDS: readonly ["extension", "macos"];
14
+ export type ClientKind = (typeof CLIENT_KINDS)[number];
15
+ /** Longest client name, in characters. */
16
+ export declare const CLIENT_NAME_MAX = 64;
17
+ /** One registry entry, as stored. */
18
+ export interface ClientEntry {
19
+ id: string;
20
+ name: string;
21
+ kind: ClientKind;
22
+ token_sha256: string;
23
+ created_at: string;
24
+ }
25
+ /** What `list` and the API show: the entry without its hash. */
26
+ export type ClientInfo = Omit<ClientEntry, "token_sha256">;
27
+ /** The registry file is unreadable or malformed, or the input is invalid. */
28
+ export declare class ClientRegistryError extends Error {
29
+ constructor(message: string);
30
+ }
31
+ /** A clean client name, or the error message. */
32
+ export declare function cleanClientName(raw: unknown): string | ClientRegistryError;
33
+ export declare function isClientKind(v: unknown): v is ClientKind;
34
+ export declare class ClientRegistry {
35
+ readonly path: string;
36
+ private cache;
37
+ constructor(stateDir?: string);
38
+ /**
39
+ * Every entry. A missing file is zero clients; a malformed one throws
40
+ * `ClientRegistryError` (the CLI reports it, the server treats it as zero).
41
+ */
42
+ load(): ClientEntry[];
43
+ list(): ClientInfo[];
44
+ /** Mint a client token. The token is returned once and never stored. */
45
+ add(input: {
46
+ name: unknown;
47
+ kind: unknown;
48
+ }, now?: Date): {
49
+ client: ClientInfo;
50
+ token: string;
51
+ };
52
+ /** Remove a client; false when no client has that id. */
53
+ revoke(id: string): boolean;
54
+ /**
55
+ * The client a bearer token belongs to, or `undefined`. Hashes the bearer
56
+ * once and compares it with every stored hash in constant time, without
57
+ * stopping at the first match. Never throws: a bad registry means no
58
+ * clients, so the server stays up.
59
+ */
60
+ verify(bearer: string): ClientInfo | undefined;
61
+ /** Entries re-read only when the file changes (inode, mtime or size). */
62
+ private cached;
63
+ private write;
64
+ }
65
+ //# sourceMappingURL=clients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/ui/clients.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAC9C,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAC1C,eAAO,MAAM,YAAY,iCAAkC,CAAC;AAC5D,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,0CAA0C;AAC1C,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,qCAAqC;AACrC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;AAE3D,6EAA6E;AAC7E,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAuBD,iDAAiD;AACjD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,mBAAmB,CAM1E;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAExD;AAED,qBAAa,cAAc;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,KAAK,CAAsD;gBAEvD,QAAQ,GAAE,MAA6B;IAInD;;;OAGG;IACH,IAAI,IAAI,WAAW,EAAE;IAqBrB,IAAI,IAAI,UAAU,EAAE;IAIpB,wEAAwE;IACxE,GAAG,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE,GAAG,GAAE,IAAiB,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAiB3G,yDAAyD;IACzD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAQ3B;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAU9C,yEAAyE;IACzE,OAAO,CAAC,MAAM;IAoBd,OAAO,CAAC,KAAK;CAiBd"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Client token registry for `tidegap ui` (native clients, KTD2).
3
+ *
4
+ * A client token (`tgc_…`) lets a browser extension or the Mac app call the
5
+ * local API across `tidegap ui` restarts. The registry file
6
+ * `<tidegap home>/ui-clients.json` (mode 0600, written atomically) stores
7
+ * only the SHA-256 of each token, never the token itself. The server re-reads
8
+ * the file when it changes on disk, so `tidegap clients add` and `revoke`
9
+ * apply without a restart.
10
+ */
11
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
12
+ import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { resolveTidegapHome, stripControlChars } from "@tidegap/core";
15
+ export const CLIENTS_FILE = "ui-clients.json";
16
+ export const CLIENT_TOKEN_PREFIX = "tgc_";
17
+ export const CLIENT_KINDS = ["extension", "macos"];
18
+ /** Longest client name, in characters. */
19
+ export const CLIENT_NAME_MAX = 64;
20
+ /** The registry file is unreadable or malformed, or the input is invalid. */
21
+ export class ClientRegistryError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "ClientRegistryError";
25
+ }
26
+ }
27
+ function sha256Hex(s) {
28
+ return createHash("sha256").update(s).digest("hex");
29
+ }
30
+ function info(e) {
31
+ return { id: e.id, name: e.name, kind: e.kind, created_at: e.created_at };
32
+ }
33
+ function isEntry(v) {
34
+ if (!v || typeof v !== "object")
35
+ return false;
36
+ const e = v;
37
+ return (typeof e.id === "string" &&
38
+ typeof e.name === "string" &&
39
+ typeof e.created_at === "string" &&
40
+ typeof e.token_sha256 === "string" &&
41
+ /^[0-9a-f]{64}$/.test(e.token_sha256) &&
42
+ CLIENT_KINDS.includes(e.kind));
43
+ }
44
+ /** A clean client name, or the error message. */
45
+ export function cleanClientName(raw) {
46
+ const name = typeof raw === "string" ? stripControlChars(raw).trim() : "";
47
+ if (name.length === 0 || name.length > CLIENT_NAME_MAX) {
48
+ return new ClientRegistryError(`name must be 1 to ${CLIENT_NAME_MAX} characters`);
49
+ }
50
+ return name;
51
+ }
52
+ export function isClientKind(v) {
53
+ return CLIENT_KINDS.includes(v);
54
+ }
55
+ export class ClientRegistry {
56
+ path;
57
+ cache;
58
+ constructor(stateDir = resolveTidegapHome()) {
59
+ this.path = join(stateDir, CLIENTS_FILE);
60
+ }
61
+ /**
62
+ * Every entry. A missing file is zero clients; a malformed one throws
63
+ * `ClientRegistryError` (the CLI reports it, the server treats it as zero).
64
+ */
65
+ load() {
66
+ let text;
67
+ try {
68
+ text = readFileSync(this.path, "utf8");
69
+ }
70
+ catch (err) {
71
+ if (err.code === "ENOENT")
72
+ return [];
73
+ throw new ClientRegistryError(`cannot read ${this.path}: ${err.message}`);
74
+ }
75
+ let parsed;
76
+ try {
77
+ parsed = JSON.parse(text);
78
+ }
79
+ catch {
80
+ throw new ClientRegistryError(`${this.path} is not valid JSON; fix or delete it`);
81
+ }
82
+ const clients = parsed?.clients;
83
+ if (parsed?.version !== 1 || !Array.isArray(clients) || !clients.every(isEntry)) {
84
+ throw new ClientRegistryError(`${this.path} is malformed; fix or delete it`);
85
+ }
86
+ return clients;
87
+ }
88
+ list() {
89
+ return this.load().map(info);
90
+ }
91
+ /** Mint a client token. The token is returned once and never stored. */
92
+ add(input, now = new Date()) {
93
+ const name = cleanClientName(input.name);
94
+ if (name instanceof ClientRegistryError)
95
+ throw name;
96
+ if (!isClientKind(input.kind))
97
+ throw new ClientRegistryError(`kind must be one of ${CLIENT_KINDS.join(", ")}`);
98
+ const entries = this.load();
99
+ const token = `${CLIENT_TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
100
+ const entry = {
101
+ id: randomBytes(6).toString("hex"),
102
+ name,
103
+ kind: input.kind,
104
+ token_sha256: sha256Hex(token),
105
+ created_at: now.toISOString(),
106
+ };
107
+ this.write([...entries, entry]);
108
+ return { client: info(entry), token };
109
+ }
110
+ /** Remove a client; false when no client has that id. */
111
+ revoke(id) {
112
+ const entries = this.load();
113
+ const rest = entries.filter((e) => e.id !== id);
114
+ if (rest.length === entries.length)
115
+ return false;
116
+ this.write(rest);
117
+ return true;
118
+ }
119
+ /**
120
+ * The client a bearer token belongs to, or `undefined`. Hashes the bearer
121
+ * once and compares it with every stored hash in constant time, without
122
+ * stopping at the first match. Never throws: a bad registry means no
123
+ * clients, so the server stays up.
124
+ */
125
+ verify(bearer) {
126
+ if (!bearer.startsWith(CLIENT_TOKEN_PREFIX))
127
+ return undefined;
128
+ const presented = createHash("sha256").update(bearer).digest();
129
+ let match;
130
+ for (const e of this.cached()) {
131
+ if (timingSafeEqual(presented, Buffer.from(e.token_sha256, "hex")) && !match)
132
+ match = e;
133
+ }
134
+ return match ? info(match) : undefined;
135
+ }
136
+ /** Entries re-read only when the file changes (inode, mtime or size). */
137
+ cached() {
138
+ let key;
139
+ try {
140
+ const st = statSync(this.path);
141
+ key = `${st.ino}:${st.mtimeMs}:${st.size}`;
142
+ }
143
+ catch {
144
+ this.cache = undefined;
145
+ return [];
146
+ }
147
+ if (this.cache?.key === key)
148
+ return this.cache.entries;
149
+ let entries;
150
+ try {
151
+ entries = this.load();
152
+ }
153
+ catch {
154
+ entries = [];
155
+ }
156
+ this.cache = { key, entries };
157
+ return entries;
158
+ }
159
+ write(clients) {
160
+ const dir = join(this.path, "..");
161
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
162
+ const tmp = `${this.path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
163
+ try {
164
+ writeFileSync(tmp, `${JSON.stringify({ version: 1, clients }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
165
+ chmodSync(tmp, 0o600);
166
+ renameSync(tmp, this.path);
167
+ }
168
+ catch (err) {
169
+ try {
170
+ unlinkSync(tmp);
171
+ }
172
+ catch {
173
+ // nothing to clean up
174
+ }
175
+ throw err;
176
+ }
177
+ }
178
+ }
179
+ //# sourceMappingURL=clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/ui/clients.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9G,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEtE,MAAM,CAAC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAW,EAAE,OAAO,CAAU,CAAC;AAE5D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAclC,6EAA6E;AAC7E,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,IAAI,CAAC,CAAc;IAC1B,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;AAC5E,CAAC;AAED,SAAS,OAAO,CAAC,CAAU;IACzB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,OAAO,CACL,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QACxB,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAChC,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ;QAClC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;QACpC,YAAkC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAc,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,MAAM,IAAI,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACvD,OAAO,IAAI,mBAAmB,CAAC,qBAAqB,eAAe,aAAa,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,CAAU;IACrC,OAAQ,YAAmC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,OAAO,cAAc;IAChB,IAAI,CAAS;IACd,KAAK,CAAsD;IAEnE,YAAY,WAAmB,kBAAkB,EAAE;QACjD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,IAAI;QACF,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAC;YAChE,MAAM,IAAI,mBAAmB,CAAC,eAAe,IAAI,CAAC,IAAI,KAAM,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,CAAC,IAAI,sCAAsC,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,OAAO,GAAI,MAA0D,EAAE,OAAO,CAAC;QACrF,IAAK,MAAuC,EAAE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAClH,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,CAAC,IAAI,iCAAiC,CAAC,CAAC;QAC/E,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,wEAAwE;IACxE,GAAG,CAAC,KAAuC,EAAE,MAAY,IAAI,IAAI,EAAE;QACjE,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,IAAI,YAAY,mBAAmB;YAAE,MAAM,IAAI,CAAC;QACpD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,mBAAmB,CAAC,uBAAuB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/G,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAG,mBAAmB,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;YAClC,IAAI;YACJ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,YAAY,EAAE,SAAS,CAAC,KAAK,CAAC;YAC9B,UAAU,EAAE,GAAG,CAAC,WAAW,EAAE;SAC9B,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;IACxC,CAAC;IAED,yDAAyD;IACzD,MAAM,CAAC,EAAU;QACf,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAc;QACnB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9D,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;QAC/D,IAAI,KAA8B,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YAC9B,IAAI,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK;gBAAE,KAAK,GAAG,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzC,CAAC;IAED,yEAAyE;IACjE,MAAM;QACZ,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,GAAG,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;QACvD,IAAI,OAAsB,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,GAAG,EAAE,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,OAAsB;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;QAChF,IAAI,CAAC;YACH,aAAa,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC/G,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACtB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC;gBACH,UAAU,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Discovery file for `tidegap ui` (native clients KTD4). While the server
3
+ * runs, `<tidegap home>/ui.json` (mode 0600) names its pid and port so the
4
+ * Mac app can find it without the user typing anything. It holds no token.
5
+ * The file is removed on close only when it still names this process, so a
6
+ * second server's file is never deleted by the first one.
7
+ */
8
+ export declare const DISCOVERY_FILE = "ui.json";
9
+ /** Default port of `tidegap ui`; clients look here first. */
10
+ export declare const DEFAULT_UI_PORT = 47821;
11
+ export interface UiDiscovery {
12
+ version: 1;
13
+ pid: number;
14
+ port: number;
15
+ url: string;
16
+ started_at: string;
17
+ cli_version: string;
18
+ }
19
+ export declare function discoveryPath(stateDir: string): string;
20
+ export declare function writeDiscovery(stateDir: string, info: Omit<UiDiscovery, "version">): void;
21
+ /** The discovery record, or `undefined` when the file is missing or malformed. */
22
+ export declare function readDiscovery(stateDir: string): UiDiscovery | undefined;
23
+ /** Remove the file when it names `pid`; leave another server's file alone. */
24
+ export declare function removeDiscovery(stateDir: string, pid?: number): void;
25
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/ui/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,IAAI,CAOzF;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAOvE;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAoB,GAAG,IAAI,CAOjF"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Discovery file for `tidegap ui` (native clients KTD4). While the server
3
+ * runs, `<tidegap home>/ui.json` (mode 0600) names its pid and port so the
4
+ * Mac app can find it without the user typing anything. It holds no token.
5
+ * The file is removed on close only when it still names this process, so a
6
+ * second server's file is never deleted by the first one.
7
+ */
8
+ import { randomBytes } from "node:crypto";
9
+ import { chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ export const DISCOVERY_FILE = "ui.json";
12
+ /** Default port of `tidegap ui`; clients look here first. */
13
+ export const DEFAULT_UI_PORT = 47821;
14
+ export function discoveryPath(stateDir) {
15
+ return join(stateDir, DISCOVERY_FILE);
16
+ }
17
+ export function writeDiscovery(stateDir, info) {
18
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
19
+ const path = discoveryPath(stateDir);
20
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
21
+ writeFileSync(tmp, `${JSON.stringify({ version: 1, ...info }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
22
+ chmodSync(tmp, 0o600);
23
+ renameSync(tmp, path);
24
+ }
25
+ /** The discovery record, or `undefined` when the file is missing or malformed. */
26
+ export function readDiscovery(stateDir) {
27
+ try {
28
+ const d = JSON.parse(readFileSync(discoveryPath(stateDir), "utf8"));
29
+ return d.version === 1 && Number.isInteger(d.pid) && Number.isInteger(d.port) ? d : undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /** Remove the file when it names `pid`; leave another server's file alone. */
36
+ export function removeDiscovery(stateDir, pid = process.pid) {
37
+ if (readDiscovery(stateDir)?.pid !== pid)
38
+ return;
39
+ try {
40
+ unlinkSync(discoveryPath(stateDir));
41
+ }
42
+ catch {
43
+ // already gone
44
+ }
45
+ }
46
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/ui/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,cAAc,GAAG,SAAS,CAAC;AAExC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAWrC,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,IAAkC;IACjF,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3E,aAAa,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/G,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAyB,CAAC;QAC5F,OAAO,CAAC,CAAC,OAAO,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;IACjH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,MAAc,OAAO,CAAC,GAAG;IACzE,IAAI,aAAa,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,GAAG;QAAE,OAAO;IACjD,IAAI,CAAC;QACH,UAAU,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;AACH,CAAC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Dispatcher state and "Start dispatcher" for `tidegap ui` (ui v2, KTD13).
3
+ *
4
+ * The UI server never dispatches a task. When no dispatcher has run lately,
5
+ * the page can ask the server to start one: the same `tidegap` binary the
6
+ * install step resolves, spawned detached as `tidegap tick --daemon --interval
7
+ * 60 --db <ledger>`, with stdout and stderr appended to
8
+ * `~/.tidegap/logs/ui-tick.log` (directory 0700, file 0600). The child's
9
+ * pid, log path and start time are kept in ledger meta `ui_dispatcher`. A
10
+ * start is refused (409) while that child is alive or while any other
11
+ * ticker ticked within two intervals. The user stops it with `kill <pid>`.
12
+ *
13
+ * State: `running` when the UI-started child is alive (its pid exists and it
14
+ * started or ticked recently, so a recycled pid is not trusted) or a tick ran within
15
+ * two intervals (300 s when the daemon is installed, else 60 s); `stale`
16
+ * when a tick ran before that; `never` when no tick was ever recorded.
17
+ */
18
+ import { type TaskStore } from "@tidegap/core";
19
+ import type { DaemonStatus } from "../platform/index.js";
20
+ export declare const UI_DISPATCHER_META_KEY = "ui_dispatcher";
21
+ /** Tick interval of a dispatcher started from the page, in seconds. */
22
+ export declare const UI_DISPATCHER_INTERVAL_S = 60;
23
+ /** Interval of the installed daemon (`tidegap install` default), in seconds. */
24
+ export declare const DAEMON_INTERVAL_S = 300;
25
+ export interface DispatcherMeta {
26
+ pid: number;
27
+ log: string;
28
+ started_at: string;
29
+ }
30
+ /** Start `argv` detached with output appended to `logPath`; returns the pid. */
31
+ export type Spawner = (argv: string[], logPath: string) => number;
32
+ export interface DispatcherOptions {
33
+ /** The `tidegap` launcher (`[node, script]` or `[binary]`), from `resolveLauncher`. */
34
+ launcher: string[];
35
+ dbPath: string;
36
+ /** Default `~/.tidegap/logs`. */
37
+ logDir?: string;
38
+ spawn?: Spawner;
39
+ isAlive?: (pid: number) => boolean;
40
+ daemonStatus: () => DaemonStatus;
41
+ now?: () => Date;
42
+ }
43
+ export interface DispatcherState {
44
+ state: "running" | "stale" | "never";
45
+ last_tick_at: string | null;
46
+ interval_s: number;
47
+ ui_child: {
48
+ pid: number;
49
+ log: string;
50
+ started_at: string;
51
+ alive: boolean;
52
+ } | null;
53
+ }
54
+ export declare function defaultLogDir(): string;
55
+ /** True when a process with `pid` exists (EPERM still means it exists). */
56
+ export declare function pidAlive(pid: number): boolean;
57
+ export declare const defaultSpawner: Spawner;
58
+ export declare class DispatcherControl {
59
+ private readonly store;
60
+ private readonly opts;
61
+ constructor(store: TaskStore, opts: DispatcherOptions);
62
+ private meta;
63
+ private alive;
64
+ state(): DispatcherState;
65
+ /**
66
+ * Spawn the dispatcher. Refused (`ok: false`) when the child started from
67
+ * the page is alive (`ui_child`), or when another ticker (the installed
68
+ * daemon, an `tidegap tick` loop, OpenClaw) ticked recently (`ticking`): a
69
+ * second ticker would only race the first.
70
+ */
71
+ start(): {
72
+ ok: true;
73
+ meta: DispatcherMeta;
74
+ } | {
75
+ ok: false;
76
+ reason: "ui_child";
77
+ meta: DispatcherMeta;
78
+ } | {
79
+ ok: false;
80
+ reason: "ticking";
81
+ last_tick_at: string | null;
82
+ };
83
+ }
84
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/ui/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAElE,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,YAAY,EAAE,MAAM,YAAY,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IACrC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACnF;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO7C;AAED,eAAO,MAAM,cAAc,EAAE,OAkB5B,CAAC;AAEF,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,iBAAiB;IAG1C,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,KAAK;IAIb,KAAK,IAAI,eAAe;IAwBxB;;;;;OAKG;IACH,KAAK,IACD;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,cAAc,CAAA;KAAE,GAClC;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,cAAc,CAAA;KAAE,GACvD;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,SAAS,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAmClE"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Dispatcher state and "Start dispatcher" for `tidegap ui` (ui v2, KTD13).
3
+ *
4
+ * The UI server never dispatches a task. When no dispatcher has run lately,
5
+ * the page can ask the server to start one: the same `tidegap` binary the
6
+ * install step resolves, spawned detached as `tidegap tick --daemon --interval
7
+ * 60 --db <ledger>`, with stdout and stderr appended to
8
+ * `~/.tidegap/logs/ui-tick.log` (directory 0700, file 0600). The child's
9
+ * pid, log path and start time are kept in ledger meta `ui_dispatcher`. A
10
+ * start is refused (409) while that child is alive or while any other
11
+ * ticker ticked within two intervals. The user stops it with `kill <pid>`.
12
+ *
13
+ * State: `running` when the UI-started child is alive (its pid exists and it
14
+ * started or ticked recently, so a recycled pid is not trusted) or a tick ran within
15
+ * two intervals (300 s when the daemon is installed, else 60 s); `stale`
16
+ * when a tick ran before that; `never` when no tick was ever recorded.
17
+ */
18
+ import { spawn } from "node:child_process";
19
+ import { chmodSync, closeSync, fchmodSync, mkdirSync, openSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { LAST_TICK_META_KEY, resolveTidegapHome } from "@tidegap/core";
22
+ export const UI_DISPATCHER_META_KEY = "ui_dispatcher";
23
+ /** Tick interval of a dispatcher started from the page, in seconds. */
24
+ export const UI_DISPATCHER_INTERVAL_S = 60;
25
+ /** Interval of the installed daemon (`tidegap install` default), in seconds. */
26
+ export const DAEMON_INTERVAL_S = 300;
27
+ export function defaultLogDir() {
28
+ return join(resolveTidegapHome(), "logs");
29
+ }
30
+ /** True when a process with `pid` exists (EPERM still means it exists). */
31
+ export function pidAlive(pid) {
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ }
36
+ catch (err) {
37
+ return err.code === "EPERM";
38
+ }
39
+ }
40
+ export const defaultSpawner = (argv, logPath) => {
41
+ const fd = openSync(logPath, "a", 0o600);
42
+ try {
43
+ // The mode above applies only when the file is created.
44
+ fchmodSync(fd, 0o600);
45
+ const child = spawn(argv[0], argv.slice(1), {
46
+ detached: true,
47
+ stdio: ["ignore", fd, fd],
48
+ });
49
+ child.on("error", () => {
50
+ // A launch error surfaces as a missing pid below; nothing else to do.
51
+ });
52
+ if (child.pid === undefined)
53
+ throw new Error("could not start the dispatcher");
54
+ child.unref();
55
+ return child.pid;
56
+ }
57
+ finally {
58
+ closeSync(fd);
59
+ }
60
+ };
61
+ export class DispatcherControl {
62
+ store;
63
+ opts;
64
+ constructor(store, opts) {
65
+ this.store = store;
66
+ this.opts = opts;
67
+ }
68
+ meta() {
69
+ const raw = this.store.getMeta(UI_DISPATCHER_META_KEY);
70
+ if (!raw)
71
+ return undefined;
72
+ try {
73
+ const m = JSON.parse(raw);
74
+ if (typeof m.pid !== "number" || typeof m.log !== "string" || typeof m.started_at !== "string") {
75
+ return undefined;
76
+ }
77
+ return m;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ alive(pid) {
84
+ return (this.opts.isAlive ?? pidAlive)(pid);
85
+ }
86
+ state() {
87
+ const meta = this.meta();
88
+ const lastTick = this.store.getMeta(LAST_TICK_META_KEY) ?? null;
89
+ const interval = this.opts.daemonStatus() === "installed" ? DAEMON_INTERVAL_S : UI_DISPATCHER_INTERVAL_S;
90
+ const now = (this.opts.now?.() ?? new Date()).getTime();
91
+ const window = 2 * interval * 1000;
92
+ const recent = lastTick !== null && now - Date.parse(lastTick) <= window;
93
+ // A live pid alone is not proof: after a crash or reboot the OS can give
94
+ // the pid to another process. The child counts as alive while its pid
95
+ // exists AND it either started recently or a tick ran recently.
96
+ const child = meta
97
+ ? {
98
+ ...meta,
99
+ alive: this.alive(meta.pid) &&
100
+ (recent ||
101
+ now - Date.parse(meta.started_at) <= 2 * UI_DISPATCHER_INTERVAL_S * 1000),
102
+ }
103
+ : null;
104
+ const state = child?.alive || recent ? "running" : lastTick !== null ? "stale" : "never";
105
+ return { state, last_tick_at: lastTick, interval_s: interval, ui_child: child };
106
+ }
107
+ /**
108
+ * Spawn the dispatcher. Refused (`ok: false`) when the child started from
109
+ * the page is alive (`ui_child`), or when another ticker (the installed
110
+ * daemon, an `tidegap tick` loop, OpenClaw) ticked recently (`ticking`): a
111
+ * second ticker would only race the first.
112
+ */
113
+ start() {
114
+ const current = this.state();
115
+ if (current.ui_child?.alive) {
116
+ const { alive: _alive, ...meta } = current.ui_child;
117
+ return { ok: false, reason: "ui_child", meta };
118
+ }
119
+ if (current.state === "running") {
120
+ return { ok: false, reason: "ticking", last_tick_at: current.last_tick_at };
121
+ }
122
+ const logDir = this.opts.logDir ?? defaultLogDir();
123
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
124
+ try {
125
+ chmodSync(logDir, 0o700);
126
+ }
127
+ catch {
128
+ // best-effort on exotic filesystems
129
+ }
130
+ const log = join(logDir, "ui-tick.log");
131
+ const argv = [
132
+ ...this.opts.launcher,
133
+ "tick",
134
+ "--daemon",
135
+ "--interval",
136
+ String(UI_DISPATCHER_INTERVAL_S),
137
+ "--db",
138
+ this.opts.dbPath,
139
+ ];
140
+ const pid = (this.opts.spawn ?? defaultSpawner)(argv, log);
141
+ const meta = {
142
+ pid,
143
+ log,
144
+ started_at: (this.opts.now?.() ?? new Date()).toISOString(),
145
+ };
146
+ this.store.setMeta(UI_DISPATCHER_META_KEY, JSON.stringify(meta));
147
+ return { ok: true, meta };
148
+ }
149
+ }
150
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../src/ui/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Change feed for `tidegap ui` (KTD10). Polls `PRAGMA data_version`, which moves
3
+ * when another connection commits (the tick, the MCP server). On a move it
4
+ * re-lists the ledger and reports the ids whose status, read marker or
5
+ * scheduled time changed. The server's own writes do not move
6
+ * `data_version`, so the server calls `emit` itself after a mark-read.
7
+ */
8
+ import type { TaskStore } from "@tidegap/core";
9
+ export type TaskChangeListener = (ids: string[]) => void;
10
+ export declare class TaskEvents {
11
+ private readonly store;
12
+ private readonly intervalMs;
13
+ private readonly listeners;
14
+ private fingerprints;
15
+ private version;
16
+ private timer;
17
+ constructor(store: TaskStore, intervalMs?: number);
18
+ start(): void;
19
+ stop(): void;
20
+ subscribe(fn: TaskChangeListener): () => void;
21
+ /** Push a change the poller cannot see (the server's own write). */
22
+ emit(ids: string[]): void;
23
+ /** One poll step; public so tests need not wait on the timer. */
24
+ poll(): void;
25
+ private snapshot;
26
+ }
27
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/ui/events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAEzD,qBAAa,UAAU;IAOnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAP7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAC3D,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA6B;gBAGvB,KAAK,EAAE,SAAS,EAChB,UAAU,SAAQ;IAMrC,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAMZ,SAAS,CAAC,EAAE,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAK7C,oEAAoE;IACpE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAOzB,iEAAiE;IACjE,IAAI,IAAI,IAAI;IAuBZ,OAAO,CAAC,QAAQ;CAUjB"}