ework-web 0.4.4 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import { setConfig, initDB } from "./db";
9
9
  import { testMysqlConnection, migrateSqliteToMysql, writeMysqlEnv, migrateMysqlToSqlite, writeSqliteEnv, migrateDaemonSqliteToMysql } from "./db-admin";
10
10
  import type { MysqlTargetOpts } from "./db-admin";
11
11
  import { checkAuth, makeAuthCookieHeader, clearAuthCookieHeader, loginHTML, sanitizeNext, ensureBootstrapAdmin, ensureBootstrapSystem, isReservedSystemLogin } from "./auth";
12
- import { OpencodeClient, OpencodeError } from "./opencode";
12
+ import { OpencodeError, createOpencodeClient } from "./opencode";
13
13
  import { renderMarkdown } from "./render/markdown";
14
14
  import { log, uptimeSeconds, version } from "./logger";
15
15
  import { buildIssueThread, fetchIssuePage, fetchIssueSince, errorPage } from "./views/issueThread";
@@ -95,7 +95,7 @@ const STATIC_DIR = join(__dirname, "static");
95
95
 
96
96
  await initDB();
97
97
  const cfg: Config = await loadConfig();
98
- const opencode = new OpencodeClient(cfg);
98
+ const opencode = createOpencodeClient(cfg, cfg.daemonWebhookUrl);
99
99
 
100
100
  async function autoWireDaemon(projectId: number, origin: string): Promise<void> {
101
101
  if (!cfg.autowireActive) {
@@ -235,9 +235,11 @@ function daemonEnvPath(): string | null {
235
235
  const p = join(dataDir, ".env");
236
236
  return existsSync(p) ? p : null;
237
237
  }
238
- // Conventional paths: standalone install, then ework-aio managed layout
239
- // (~/.local/share/ework-aio/ework-daemon/.env).
238
+ // Sibling layout (ework-aio managed): web's cwd is <dataDir>/ework-web/,
239
+ // daemon is at <dataDir>/ework-daemon/.env. Checked first because it works
240
+ // regardless of where --data-dir points (Docker, custom paths, etc.).
240
241
  const candidates = [
242
+ join(process.cwd(), "..", "ework-daemon", ".env"),
241
243
  join(homedir(), ".local", "share", "ework-daemon", ".env"),
242
244
  join(homedir(), ".local", "share", "ework-aio", "ework-daemon", ".env"),
243
245
  ];
@@ -250,8 +252,12 @@ function daemonEnvPath(): string | null {
250
252
  function readDaemonSqlitePath(envPath: string): string | null {
251
253
  try {
252
254
  const content = readFileSync(envPath, "utf8");
253
- const m = content.match(/^DAEMON_DB_PATH\s*=\s*(.+)$/m);
254
- return m?.[1]?.trim() ?? null;
255
+ const m = content.match(/^(?:WORK_DB_PATH|DAEMON_DB_PATH)\s*=\s*(.+)$/m);
256
+ if (m?.[1]) return m[1].trim();
257
+ // Neither key in .env — replicate the daemon's PRODUCTION_DB_DEFAULT
258
+ // (config.ts:61) so we can find its SQLite DB for migration.
259
+ const xdg = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
260
+ return join(xdg, "ework-daemon", "ework-daemon.db");
255
261
  } catch {
256
262
  return null;
257
263
  }
package/src/opencode.ts CHANGED
@@ -250,6 +250,61 @@ export class OpencodeClient {
250
250
  }
251
251
  }
252
252
 
253
+ export interface OpencodeClientInterface {
254
+ listSessions(limit: number): Promise<SessionListItem[]>;
255
+ exportSession(id: string): Promise<SessionExport>;
256
+ exportSessionRaw(id: string): Promise<string>;
257
+ listModels(): Promise<string[]>;
258
+ }
259
+
260
+ export class RemoteOpencodeClient implements OpencodeClientInterface {
261
+ private readonly base: string;
262
+
263
+ constructor(endpoint: string) {
264
+ this.base = endpoint.replace(/\/$/, "");
265
+ }
266
+
267
+ async listSessions(limit: number): Promise<SessionListItem[]> {
268
+ const res = await fetch(`${this.base}/api/opencode/sessions?limit=${limit}`);
269
+ if (!res.ok) throw new OpencodeError(`remote listSessions → ${res.status}`, res.status);
270
+ return (await res.json()) as SessionListItem[];
271
+ }
272
+
273
+ async exportSession(id: string): Promise<SessionExport> {
274
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new OpencodeError(`bad session id: ${id}`, 400);
275
+ const res = await fetch(`${this.base}/api/opencode/sessions/${id}/export`);
276
+ if (!res.ok) throw new OpencodeError(`remote exportSession → ${res.status}`, res.status);
277
+ const raw = await res.json();
278
+ const exp = parseSessionExport(raw);
279
+ if (!exp) throw new OpencodeError(`malformed remote export for ${id}`, 502);
280
+ return exp;
281
+ }
282
+
283
+ async exportSessionRaw(id: string): Promise<string> {
284
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new OpencodeError(`bad session id: ${id}`, 400);
285
+ const res = await fetch(`${this.base}/api/opencode/sessions/${id}/raw`);
286
+ if (!res.ok) throw new OpencodeError(`remote exportSessionRaw → ${res.status}`, res.status);
287
+ const data = (await res.json()) as { raw?: string };
288
+ return data.raw ?? "";
289
+ }
290
+
291
+ async listModels(): Promise<string[]> {
292
+ return [];
293
+ }
294
+ }
295
+
296
+ function isLocalhost(endpoint: string): boolean {
297
+ const host = endpoint.replace(/^https?:\/\//, "").split(":")[0] ?? "";
298
+ return /^(127\.|localhost$|0\.0\.0\.0$|::1$|\[::1\]$)/.test(host);
299
+ }
300
+
301
+ export function createOpencodeClient(cfg: Config, daemonEndpoint?: string): OpencodeClientInterface {
302
+ if (daemonEndpoint && !isLocalhost(daemonEndpoint)) {
303
+ return new RemoteOpencodeClient(daemonEndpoint);
304
+ }
305
+ return new OpencodeClient(cfg);
306
+ }
307
+
253
308
  async function readCapped(stream: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<string> {
254
309
  if (!stream) return "";
255
310
  const reader = stream.getReader();
@@ -1,4 +1,4 @@
1
- import type { OpencodeClient, SessionListItem, SessionExport, SessionMessage, MessagePart, ToolState } from "../opencode";
1
+ import type { OpencodeClientInterface, SessionListItem, SessionExport, SessionMessage, MessagePart, ToolState } from "../opencode";
2
2
  import { THEME_CSS, escapeHtml, escapeAttr, tabNavHTML } from "../render/layout";
3
3
  import { renderMarkdown, linkifySessionIDs, linkifyAbsPaths } from "../render/markdown";
4
4
  import { BUILD_ID } from "../build";
@@ -8,7 +8,7 @@ import { homedir } from "os";
8
8
 
9
9
  const LIST_LIMIT = 100;
10
10
 
11
- export async function buildSessionList(client: OpencodeClient, q: string): Promise<{ html: string }> {
11
+ export async function buildSessionList(client: OpencodeClientInterface, q: string): Promise<{ html: string }> {
12
12
  let sessions = await client.listSessions(LIST_LIMIT);
13
13
  const needle = q.trim().toLowerCase();
14
14
  if (needle) {
@@ -57,7 +57,7 @@ function sessionRow(s: SessionListItem): string {
57
57
  </a>`;
58
58
  }
59
59
 
60
- export async function buildSessionView(client: OpencodeClient, id: string, desc: boolean, collapseLines: number, limit = 30, all = false): Promise<{ html: string }> {
60
+ export async function buildSessionView(client: OpencodeClientInterface, id: string, desc: boolean, collapseLines: number, limit = 30, all = false): Promise<{ html: string }> {
61
61
  const data = await client.exportSession(id);
62
62
  const info = data.info;
63
63
  const title = info.title || id;