zooid 0.6.1 → 0.7.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 (78) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +36 -444
  3. package/dist/bin.js +1797 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/chunk-N5POSZX5.js +31000 -0
  6. package/dist/chunk-N5POSZX5.js.map +1 -0
  7. package/dist/index.d.ts +40 -0
  8. package/dist/index.js +4 -3790
  9. package/dist/index.js.map +1 -0
  10. package/dist/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  11. package/dist/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  12. package/dist/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  13. package/dist/web/assets/index-BT_v3DKu.js +295 -0
  14. package/dist/web/assets/index-DJTghnF-.css +1 -0
  15. package/dist/web/assets/index-JZMMlqDP.js +118066 -0
  16. package/dist/web/assets/reaction-picker-emoji-Kh7emgFa.js +716 -0
  17. package/dist/web/favicon.svg +1 -0
  18. package/dist/web/index.html +14 -0
  19. package/package.json +42 -22
  20. package/src/bin.ts +112 -0
  21. package/src/bootstrap/admin.test.ts +69 -0
  22. package/src/bootstrap/admin.ts +37 -0
  23. package/src/bootstrap/configs.test.ts +24 -0
  24. package/src/bootstrap/configs.ts +64 -0
  25. package/src/bootstrap/data-layout.test.ts +25 -0
  26. package/src/bootstrap/data-layout.ts +20 -0
  27. package/src/bootstrap/derive.test.ts +74 -0
  28. package/src/bootstrap/derive.ts +40 -0
  29. package/src/bootstrap/paths.test.ts +22 -0
  30. package/src/bootstrap/paths.ts +27 -0
  31. package/src/bootstrap/tokens.test.ts +44 -0
  32. package/src/bootstrap/tokens.ts +43 -0
  33. package/src/build-registry.context.test.ts +61 -0
  34. package/src/build-registry.test.ts +59 -0
  35. package/src/build-registry.ts +142 -0
  36. package/src/build-registry.zod043.test.ts +64 -0
  37. package/src/commands/dev-cascade.test.ts +38 -0
  38. package/src/commands/dev-cascade.ts +35 -0
  39. package/src/commands/dev-data-layout.test.ts +55 -0
  40. package/src/commands/dev.ts +308 -0
  41. package/src/commands/init/generators.test.ts +97 -0
  42. package/src/commands/init/generators.ts +119 -0
  43. package/src/commands/init/prompts.ts +123 -0
  44. package/src/commands/init/registry.test.ts +33 -0
  45. package/src/commands/init/registry.ts +75 -0
  46. package/src/commands/init/sniff.test.ts +41 -0
  47. package/src/commands/init/sniff.ts +30 -0
  48. package/src/commands/init.test.ts +131 -0
  49. package/src/commands/init.ts +158 -0
  50. package/src/commands/logs.test.ts +105 -0
  51. package/src/commands/logs.ts +108 -0
  52. package/src/commands/start.ts +28 -0
  53. package/src/commands/status.test.ts +73 -0
  54. package/src/commands/status.ts +100 -0
  55. package/src/daemon/start-daemon.test.ts +67 -0
  56. package/src/daemon/start-daemon.ts +243 -0
  57. package/src/index.ts +2 -0
  58. package/src/observability/capture-agent.ts +58 -0
  59. package/src/observability/capture-tuwunel.test.ts +57 -0
  60. package/src/observability/capture-tuwunel.ts +24 -0
  61. package/src/observability/file-sink.test.ts +68 -0
  62. package/src/observability/file-sink.ts +78 -0
  63. package/src/observability/observability.integration.test.ts +162 -0
  64. package/src/observability/paths.test.ts +104 -0
  65. package/src/observability/paths.ts +94 -0
  66. package/src/services/tuwunel.test.ts +64 -0
  67. package/src/services/tuwunel.ts +91 -0
  68. package/src/web/resolve.test.ts +50 -0
  69. package/src/web/resolve.ts +31 -0
  70. package/src/web/static.test.ts +51 -0
  71. package/src/web/static.ts +61 -0
  72. package/src/web/watch.ts +103 -0
  73. package/dist/chunk-67ZRMVHO.js +0 -174
  74. package/dist/chunk-AR456MHY.js +0 -29
  75. package/dist/chunk-VBGU2NST.js +0 -139
  76. package/dist/client-4VMFEFDX.js +0 -22
  77. package/dist/config-2KK5GX42.js +0 -27
  78. package/dist/template-T5IB4YWC.js +0 -92
@@ -0,0 +1,50 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { resolveWebRoot, webSourcePackage } from './resolve.js'
6
+
7
+ let root: string
8
+ beforeEach(() => {
9
+ root = mkdtempSync(join(tmpdir(), 'zooid-resolve-'))
10
+ })
11
+ afterEach(() => rmSync(root, { recursive: true, force: true }))
12
+
13
+ describe('resolveWebRoot', () => {
14
+ it('returns dist/web when present (published-package layout)', () => {
15
+ const distWeb = join(root, 'dist', 'web')
16
+ mkdirSync(distWeb, { recursive: true })
17
+ writeFileSync(join(distWeb, 'index.html'), '<html/>')
18
+ expect(resolveWebRoot(root)).toBe(distWeb)
19
+ })
20
+
21
+ it('falls back to ../../zoon/packages/web/dist when running from source', () => {
22
+ const cliRoot = join(root, 'zooid', 'packages', 'cli')
23
+ mkdirSync(cliRoot, { recursive: true })
24
+ const webPkg = join(root, 'zoon', 'packages', 'web')
25
+ const webDist = join(webPkg, 'dist')
26
+ mkdirSync(webDist, { recursive: true })
27
+ writeFileSync(join(webPkg, 'package.json'), '{"name":"@zoon/web"}')
28
+ writeFileSync(join(webDist, 'index.html'), '<html/>')
29
+ expect(resolveWebRoot(cliRoot)).toBe(webDist)
30
+ })
31
+
32
+ it('throws a helpful error when neither layout has been built', () => {
33
+ expect(() => resolveWebRoot(root)).toThrow(/@zoon\/web.*build/i)
34
+ })
35
+ })
36
+
37
+ describe('webSourcePackage', () => {
38
+ it('returns the source package dir when sibling zoon/packages/web exists', () => {
39
+ const cliRoot = join(root, 'zooid', 'packages', 'cli')
40
+ mkdirSync(cliRoot, { recursive: true })
41
+ const webPkg = join(root, 'zoon', 'packages', 'web')
42
+ mkdirSync(webPkg, { recursive: true })
43
+ writeFileSync(join(webPkg, 'package.json'), '{"name":"@zoon/web"}')
44
+ expect(webSourcePackage(cliRoot)).toBe(webPkg)
45
+ })
46
+
47
+ it('returns null outside the monorepo', () => {
48
+ expect(webSourcePackage(root)).toBeNull()
49
+ })
50
+ })
@@ -0,0 +1,31 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ const ENV_OVERRIDE = 'ZOOID_DEV_WEB_ROOT_OVERRIDE'
5
+
6
+ export function resolveWebRoot(cliRoot: string): string {
7
+ const override = process.env[ENV_OVERRIDE]
8
+ if (override && existsSync(join(override, 'index.html'))) return resolve(override)
9
+
10
+ const published = join(cliRoot, 'dist', 'web')
11
+ if (existsSync(join(published, 'index.html'))) return published
12
+
13
+ const fromSource = webSourcePackage(cliRoot)
14
+ if (fromSource && existsSync(join(fromSource, 'dist', 'index.html'))) {
15
+ return join(fromSource, 'dist')
16
+ }
17
+
18
+ throw new Error(
19
+ `@zoon/web build not found.\n Tried: ${published}\n Tried: ${fromSource ?? '(no monorepo source)'}\n` +
20
+ `Run \`pnpm -C zoon/packages/web build\` (or set ${ENV_OVERRIDE} to a built dist).`,
21
+ )
22
+ }
23
+
24
+ // Returns the path to the in-tree @zoon/web package (zoon/packages/web) if the
25
+ // CLI is running from the monorepo source. Returns null when running from an
26
+ // installed package (no sibling zoon/ tree).
27
+ export function webSourcePackage(cliRoot: string): string | null {
28
+ const workspaceRoot = dirname(dirname(dirname(cliRoot)))
29
+ const candidate = join(workspaceRoot, 'zoon', 'packages', 'web')
30
+ return existsSync(join(candidate, 'package.json')) ? candidate : null
31
+ }
@@ -0,0 +1,51 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { webStatic } from './static.js'
6
+
7
+ let dir: string
8
+ beforeEach(() => {
9
+ dir = mkdtempSync(join(tmpdir(), 'zooid-web-'))
10
+ mkdirSync(join(dir, 'assets'), { recursive: true })
11
+ writeFileSync(join(dir, 'index.html'), '<!doctype html><div id=root></div>')
12
+ writeFileSync(join(dir, 'assets', 'main.js'), 'console.log("ui")')
13
+ })
14
+ afterEach(() => rmSync(dir, { recursive: true, force: true }))
15
+
16
+ describe('webStatic', () => {
17
+ it('serves index.html at /', async () => {
18
+ const app = webStatic({ webRoot: dir, homeserverUrl: 'http://localhost:8448' })
19
+ const r = await app.request('/')
20
+ expect(r.status).toBe(200)
21
+ expect(await r.text()).toContain('id=root')
22
+ })
23
+
24
+ it('serves static assets', async () => {
25
+ const app = webStatic({ webRoot: dir, homeserverUrl: 'http://localhost:8448' })
26
+ const r = await app.request('/assets/main.js')
27
+ expect(r.status).toBe(200)
28
+ expect(await r.text()).toContain('console.log("ui")')
29
+ })
30
+
31
+ it('synthesizes /config.json with the homeserver URL', async () => {
32
+ const app = webStatic({ webRoot: dir, homeserverUrl: 'http://localhost:8448' })
33
+ const r = await app.request('/config.json')
34
+ expect(r.status).toBe(200)
35
+ expect(r.headers.get('content-type')).toMatch(/application\/json/)
36
+ expect(await r.json()).toEqual({ homeserver_url: 'http://localhost:8448' })
37
+ })
38
+
39
+ it('falls back to index.html for unknown routes (SPA history)', async () => {
40
+ const app = webStatic({ webRoot: dir, homeserverUrl: 'http://localhost:8448' })
41
+ const r = await app.request('/room/!abc:localhost')
42
+ expect(r.status).toBe(200)
43
+ expect(await r.text()).toContain('id=root')
44
+ })
45
+
46
+ it('returns 404 for a missing asset', async () => {
47
+ const app = webStatic({ webRoot: dir, homeserverUrl: 'http://localhost:8448' })
48
+ const r = await app.request('/assets/nope.js')
49
+ expect(r.status).toBe(404)
50
+ })
51
+ })
@@ -0,0 +1,61 @@
1
+ import { readFileSync, statSync } from 'node:fs'
2
+ import { extname, join, normalize } from 'node:path'
3
+ import { Hono } from 'hono'
4
+
5
+ export interface WebStaticOpts {
6
+ webRoot: string
7
+ homeserverUrl: string
8
+ }
9
+
10
+ const MIME: Record<string, string> = {
11
+ '.html': 'text/html; charset=utf-8',
12
+ '.js': 'application/javascript; charset=utf-8',
13
+ '.mjs': 'application/javascript; charset=utf-8',
14
+ '.css': 'text/css; charset=utf-8',
15
+ '.json': 'application/json; charset=utf-8',
16
+ '.svg': 'image/svg+xml',
17
+ '.png': 'image/png',
18
+ '.jpg': 'image/jpeg',
19
+ '.jpeg': 'image/jpeg',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ }
23
+
24
+ function isAssetPath(p: string): boolean {
25
+ return p.startsWith('/assets/') || /\.[a-z0-9]+$/i.test(p)
26
+ }
27
+
28
+ export function webStatic(opts: WebStaticOpts): Hono {
29
+ const app = new Hono()
30
+
31
+ app.get('/config.json', (c) => c.json({ homeserver_url: opts.homeserverUrl }))
32
+
33
+ app.get('*', (c) => {
34
+ const url = new URL(c.req.url)
35
+ const requested = decodeURIComponent(url.pathname)
36
+ const wantFile = requested === '/' ? '/index.html' : requested
37
+
38
+ const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, '')
39
+ const filePath = join(opts.webRoot, safe)
40
+ if (!filePath.startsWith(opts.webRoot)) return c.notFound()
41
+
42
+ try {
43
+ const stat = statSync(filePath)
44
+ if (stat.isFile()) {
45
+ const body = readFileSync(filePath)
46
+ const ct = MIME[extname(filePath)] ?? 'application/octet-stream'
47
+ return c.body(body as unknown as ArrayBuffer, 200, { 'content-type': ct })
48
+ }
49
+ } catch {
50
+ // fall through to history fallback
51
+ }
52
+
53
+ if (isAssetPath(requested)) return c.notFound()
54
+ const indexBytes = readFileSync(join(opts.webRoot, 'index.html'))
55
+ return c.body(indexBytes as unknown as ArrayBuffer, 200, {
56
+ 'content-type': MIME['.html']!,
57
+ })
58
+ })
59
+
60
+ return app
61
+ }
@@ -0,0 +1,103 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { existsSync, statSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ export interface WebWatchHandle {
6
+ distPath: string
7
+ // Pipe captured child output onwards. Call once Listr / interactive
8
+ // renderers are done, otherwise vite output will get garbled.
9
+ attachStdio: () => void
10
+ stop: () => Promise<void>
11
+ }
12
+
13
+ export interface WebWatchOpts {
14
+ webPackageDir: string
15
+ // Wait up to this many ms for vite's first build to finish before resolving.
16
+ firstBuildTimeoutMs?: number
17
+ }
18
+
19
+ // Spawns `vite build --watch` in webPackageDir and resolves once the first
20
+ // build has completed (detected via index.html mtime > spawn time).
21
+ export async function startWebWatch(opts: WebWatchOpts): Promise<WebWatchHandle> {
22
+ const distPath = join(opts.webPackageDir, 'dist')
23
+ const indexPath = join(distPath, 'index.html')
24
+ const timeoutMs = opts.firstBuildTimeoutMs ?? 60_000
25
+ const spawnTime = Date.now()
26
+
27
+ const child: ChildProcess = spawn(
28
+ 'pnpm',
29
+ ['-C', opts.webPackageDir, 'exec', 'vite', 'build', '--watch'],
30
+ { stdio: ['ignore', 'pipe', 'pipe'] },
31
+ )
32
+
33
+ let buffered = ''
34
+ const onData = (chunk: Buffer): void => {
35
+ buffered += chunk.toString('utf8')
36
+ }
37
+ child.stdout?.on('data', onData)
38
+ child.stderr?.on('data', onData)
39
+
40
+ let attached = false
41
+ const attachStdio = (): void => {
42
+ if (attached) return
43
+ attached = true
44
+ child.stdout?.off('data', onData)
45
+ child.stderr?.off('data', onData)
46
+ if (buffered) process.stdout.write(buffered)
47
+ buffered = ''
48
+ child.stdout?.pipe(process.stdout)
49
+ child.stderr?.pipe(process.stderr)
50
+ }
51
+
52
+ let exited = false
53
+ child.once('exit', () => {
54
+ exited = true
55
+ })
56
+
57
+ while (Date.now() - spawnTime < timeoutMs) {
58
+ if (exited) {
59
+ attachStdio()
60
+ throw new Error(`vite build --watch exited before first build (code ${child.exitCode})`)
61
+ }
62
+ if (existsSync(indexPath)) {
63
+ const m = statSync(indexPath).mtimeMs
64
+ // Slack for clock granularity on macOS HFS+/APFS.
65
+ if (m >= spawnTime - 500) return makeHandle()
66
+ }
67
+ await new Promise((r) => setTimeout(r, 200))
68
+ }
69
+ // Timed out: clean up and surface what vite said.
70
+ attachStdio()
71
+ await stopChild(child)
72
+ throw new Error(`vite build --watch did not produce ${indexPath} within ${timeoutMs}ms`)
73
+
74
+ function makeHandle(): WebWatchHandle {
75
+ return {
76
+ distPath,
77
+ attachStdio,
78
+ stop: () => stopChild(child),
79
+ }
80
+ }
81
+ }
82
+
83
+ async function stopChild(child: ChildProcess): Promise<void> {
84
+ if (child.exitCode !== null) return
85
+ child.kill('SIGTERM')
86
+ await new Promise<void>((resolve) => {
87
+ let done = false
88
+ const finish = (): void => {
89
+ if (done) return
90
+ done = true
91
+ resolve()
92
+ }
93
+ child.once('exit', finish)
94
+ setTimeout(() => {
95
+ try {
96
+ child.kill('SIGKILL')
97
+ } catch {
98
+ // already gone
99
+ }
100
+ finish()
101
+ }, 2000)
102
+ })
103
+ }
@@ -1,174 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/lib/config.ts
4
- import fs from "fs";
5
- import os from "os";
6
- import path from "path";
7
-
8
- // src/lib/constants.ts
9
- var STATE_FILENAME = "state.json";
10
- var LEGACY_STATE_FILENAME = "config.json";
11
-
12
- // src/lib/config.ts
13
- function getConfigDir() {
14
- return process.env.ZOOID_CONFIG_DIR ?? path.join(os.homedir(), ".zooid");
15
- }
16
- function getStatePath() {
17
- const dir = getConfigDir();
18
- const statePath = path.join(dir, STATE_FILENAME);
19
- if (!fs.existsSync(statePath)) {
20
- const legacyPath = path.join(dir, LEGACY_STATE_FILENAME);
21
- if (fs.existsSync(legacyPath)) {
22
- fs.renameSync(legacyPath, statePath);
23
- }
24
- }
25
- return statePath;
26
- }
27
- var getConfigPath = getStatePath;
28
- function loadConfigFile() {
29
- try {
30
- const raw = fs.readFileSync(getStatePath(), "utf-8");
31
- const parsed = JSON.parse(raw);
32
- if (parsed.server && !parsed.servers) {
33
- const serverUrl = parsed.server;
34
- const migrated = {
35
- current: serverUrl,
36
- servers: {
37
- [serverUrl]: {
38
- worker_url: parsed.worker_url,
39
- admin_token: parsed.admin_token,
40
- channels: parsed.channels
41
- }
42
- }
43
- };
44
- const dir = getConfigDir();
45
- fs.mkdirSync(dir, { recursive: true });
46
- fs.writeFileSync(
47
- getConfigPath(),
48
- JSON.stringify(migrated, null, 2) + "\n"
49
- );
50
- return migrated;
51
- }
52
- return parsed;
53
- } catch {
54
- return {};
55
- }
56
- }
57
- var serverNoteShown = false;
58
- function resolveServer() {
59
- const file = loadConfigFile();
60
- let cwdUrl;
61
- try {
62
- const zooidJsonPath = path.join(process.cwd(), "zooid.json");
63
- if (fs.existsSync(zooidJsonPath)) {
64
- const raw = fs.readFileSync(zooidJsonPath, "utf-8");
65
- const parsed = JSON.parse(raw);
66
- cwdUrl = parsed.url || void 0;
67
- }
68
- } catch {
69
- }
70
- if (cwdUrl) {
71
- if (file.current && file.current !== cwdUrl && !serverNoteShown) {
72
- serverNoteShown = true;
73
- console.log(
74
- ` Note: using server from zooid.json (${cwdUrl}), current is ${file.current}`
75
- );
76
- }
77
- return cwdUrl;
78
- }
79
- return file.current;
80
- }
81
- function loadConfig() {
82
- const file = loadConfigFile();
83
- const serverUrl = resolveServer();
84
- const entry = serverUrl ? file.servers?.[serverUrl] : void 0;
85
- return {
86
- server: serverUrl,
87
- worker_url: entry?.worker_url,
88
- admin_token: entry?.admin_token,
89
- channels: entry?.channels
90
- };
91
- }
92
- function saveConfig(partial, serverUrl, options) {
93
- const dir = getConfigDir();
94
- fs.mkdirSync(dir, { recursive: true });
95
- const file = loadConfigFile();
96
- const url = serverUrl ?? resolveServer();
97
- if (!url) {
98
- throw new Error(
99
- "No server URL to save config for. Deploy first or set url in zooid.json."
100
- );
101
- }
102
- if (!file.servers) file.servers = {};
103
- const existing = file.servers[url] ?? {};
104
- const merged = { ...existing, ...partial };
105
- if (partial.channels && existing.channels) {
106
- merged.channels = { ...existing.channels };
107
- for (const [chId, chData] of Object.entries(partial.channels)) {
108
- merged.channels[chId] = { ...existing.channels[chId], ...chData };
109
- }
110
- }
111
- file.servers[url] = merged;
112
- if (options?.setCurrent !== false) {
113
- file.current = url;
114
- }
115
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
116
- }
117
- function loadDirectoryToken() {
118
- const file = loadConfigFile();
119
- return file.directory_token;
120
- }
121
- function saveDirectoryToken(token) {
122
- const dir = getConfigDir();
123
- fs.mkdirSync(dir, { recursive: true });
124
- const file = loadConfigFile();
125
- file.directory_token = token;
126
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
127
- }
128
- function recordTailHistory(channelId, serverUrl, name, lastEventId) {
129
- const url = serverUrl ?? resolveServer();
130
- if (!url) return;
131
- const file = loadConfigFile();
132
- if (!file.servers) file.servers = {};
133
- if (!file.servers[url]) file.servers[url] = {};
134
- if (!file.servers[url].channels) file.servers[url].channels = {};
135
- const channel = file.servers[url].channels[channelId] ?? {};
136
- const now = (/* @__PURE__ */ new Date()).toISOString();
137
- const existing = channel.stats;
138
- channel.stats = {
139
- num_tails: (existing?.num_tails ?? 0) + 1,
140
- last_tailed_at: now,
141
- first_tailed_at: existing?.first_tailed_at ?? now,
142
- last_event_id: lastEventId ?? existing?.last_event_id
143
- };
144
- if (name) {
145
- channel.name = name;
146
- }
147
- file.servers[url].channels[channelId] = channel;
148
- const dir = getConfigDir();
149
- fs.mkdirSync(dir, { recursive: true });
150
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
151
- }
152
- function switchServer(url) {
153
- const dir = getConfigDir();
154
- fs.mkdirSync(dir, { recursive: true });
155
- const file = loadConfigFile();
156
- if (!file.servers) file.servers = {};
157
- if (!file.servers[url]) file.servers[url] = {};
158
- file.current = url;
159
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
160
- }
161
-
162
- export {
163
- getConfigDir,
164
- getStatePath,
165
- getConfigPath,
166
- loadConfigFile,
167
- resolveServer,
168
- loadConfig,
169
- saveConfig,
170
- loadDirectoryToken,
171
- saveDirectoryToken,
172
- recordTailHistory,
173
- switchServer
174
- };
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/lib/github.ts
4
- function parseGitHubUrl(url) {
5
- try {
6
- const u = new URL(url);
7
- if (u.hostname !== "github.com") return null;
8
- const parts = u.pathname.replace(/^\/|\/$/g, "").split("/");
9
- if (parts.length < 2) return null;
10
- const owner = parts[0];
11
- const repo = parts[1];
12
- if (!owner || !repo) return null;
13
- if (parts.length === 2) {
14
- return { owner, repo, ref: "main", path: "" };
15
- }
16
- if (parts[2] === "tree" && parts.length >= 4) {
17
- const ref = parts[3];
18
- const subPath = parts.slice(4).join("/");
19
- return { owner, repo, ref, path: subPath };
20
- }
21
- return null;
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- export {
28
- parseGitHubUrl
29
- };
@@ -1,139 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadConfig,
4
- loadConfigFile,
5
- saveConfig
6
- } from "./chunk-67ZRMVHO.js";
7
-
8
- // src/lib/client.ts
9
- import { ZooidClient } from "@zooid/sdk";
10
- function createClient(token) {
11
- const config = loadConfig();
12
- const server = config.server;
13
- if (!server) {
14
- throw new Error(
15
- "No server configured. Run: npx zooid config set server <url>"
16
- );
17
- }
18
- return new ZooidClient({ server, token: token ?? config.admin_token });
19
- }
20
- function getChannelToken(channelTokens, tokenType) {
21
- if (!channelTokens) return void 0;
22
- if (channelTokens.token) return channelTokens.token;
23
- if (tokenType === "publish") return channelTokens.publish_token;
24
- if (tokenType === "subscribe") return channelTokens.subscribe_token;
25
- return channelTokens.publish_token ?? channelTokens.subscribe_token;
26
- }
27
- function createChannelClient(channelId, tokenType) {
28
- const config = loadConfig();
29
- const server = config.server;
30
- if (!server) {
31
- throw new Error(
32
- "No server configured. Run: npx zooid config set server <url>"
33
- );
34
- }
35
- const channelToken = getChannelToken(config.channels?.[channelId], tokenType);
36
- return new ZooidClient({ server, token: channelToken ?? config.admin_token });
37
- }
38
- var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
39
- var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
40
- var PRIVATE_HOST_RE = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$/;
41
- function normalizeServerUrl(url) {
42
- let normalized = url.replace(/\/+$/, "");
43
- try {
44
- const parsed = new URL(normalized);
45
- if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
46
- normalized = normalized.replace(/^http:\/\//, "https://");
47
- }
48
- } catch {
49
- }
50
- return normalized;
51
- }
52
- function parseChannelUrl(channel) {
53
- let raw = channel;
54
- if (!raw.startsWith("http") && raw.includes("/")) {
55
- if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
56
- raw = `http://${raw}`;
57
- } else if (raw.includes(".")) {
58
- raw = `https://${raw}`;
59
- } else if (/^[^/]+:\d+\//.test(raw)) {
60
- raw = `http://${raw}`;
61
- }
62
- }
63
- if (!raw.startsWith("http")) return null;
64
- try {
65
- const url = new URL(raw);
66
- const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
67
- if (channelsMatch) {
68
- return { server: url.origin, channelId: channelsMatch[1] };
69
- }
70
- const segments = url.pathname.split("/").filter(Boolean);
71
- if (segments.length === 1) {
72
- return { server: url.origin, channelId: segments[0] };
73
- }
74
- } catch {
75
- }
76
- return null;
77
- }
78
- function resolveChannel(channel, opts) {
79
- const parsed = parseChannelUrl(channel);
80
- if (parsed) {
81
- const { server: server2, channelId } = parsed;
82
- let token2 = opts?.token;
83
- let tokenSaved2 = false;
84
- if (token2) {
85
- saveConfig({ channels: { [channelId]: { token: token2 } } }, server2, {
86
- setCurrent: false
87
- });
88
- tokenSaved2 = true;
89
- }
90
- if (!token2) {
91
- const file = loadConfigFile();
92
- const channelTokens = file.servers?.[server2]?.channels?.[channelId];
93
- token2 = getChannelToken(channelTokens, opts?.tokenType);
94
- }
95
- return {
96
- client: new ZooidClient({ server: server2, token: token2 }),
97
- channelId,
98
- server: server2,
99
- tokenSaved: tokenSaved2
100
- };
101
- }
102
- const config = loadConfig();
103
- const server = config.server;
104
- if (!server) {
105
- throw new Error(
106
- "No server configured. Run: npx zooid config set server <url>"
107
- );
108
- }
109
- let token = opts?.token;
110
- let tokenSaved = false;
111
- if (token) {
112
- saveConfig({ channels: { [channel]: { token } } });
113
- tokenSaved = true;
114
- }
115
- if (!token) {
116
- const channelToken = getChannelToken(
117
- config.channels?.[channel],
118
- opts?.tokenType
119
- );
120
- token = channelToken ?? config.admin_token;
121
- }
122
- return {
123
- client: new ZooidClient({ server, token }),
124
- channelId: channel,
125
- server,
126
- tokenSaved
127
- };
128
- }
129
-
130
- export {
131
- createClient,
132
- createChannelClient,
133
- createPublishClient,
134
- createSubscribeClient,
135
- PRIVATE_HOST_RE,
136
- normalizeServerUrl,
137
- parseChannelUrl,
138
- resolveChannel
139
- };
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- PRIVATE_HOST_RE,
4
- createChannelClient,
5
- createClient,
6
- createPublishClient,
7
- createSubscribeClient,
8
- normalizeServerUrl,
9
- parseChannelUrl,
10
- resolveChannel
11
- } from "./chunk-VBGU2NST.js";
12
- import "./chunk-67ZRMVHO.js";
13
- export {
14
- PRIVATE_HOST_RE,
15
- createChannelClient,
16
- createClient,
17
- createPublishClient,
18
- createSubscribeClient,
19
- normalizeServerUrl,
20
- parseChannelUrl,
21
- resolveChannel
22
- };
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- getConfigDir,
4
- getConfigPath,
5
- getStatePath,
6
- loadConfig,
7
- loadConfigFile,
8
- loadDirectoryToken,
9
- recordTailHistory,
10
- resolveServer,
11
- saveConfig,
12
- saveDirectoryToken,
13
- switchServer
14
- } from "./chunk-67ZRMVHO.js";
15
- export {
16
- getConfigDir,
17
- getConfigPath,
18
- getStatePath,
19
- loadConfig,
20
- loadConfigFile,
21
- loadDirectoryToken,
22
- recordTailHistory,
23
- resolveServer,
24
- saveConfig,
25
- saveDirectoryToken,
26
- switchServer
27
- };