zooid 0.1.1 → 0.2.1
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/dist/chunk-67ZRMVHO.js +174 -0
- package/dist/chunk-EEA3FCBS.js +142 -0
- package/dist/client-QPT54SNG.js +22 -0
- package/dist/config-2KK5GX42.js +27 -0
- package/dist/index.js +183 -382
- package/package.json +4 -4
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
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 createChannelClient(channelId, tokenType) {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
const server = config.server;
|
|
23
|
+
if (!server) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const tokenKey = tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
29
|
+
const channelToken = config.channels?.[channelId]?.[tokenKey];
|
|
30
|
+
return new ZooidClient({ server, token: channelToken ?? config.admin_token });
|
|
31
|
+
}
|
|
32
|
+
var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
|
|
33
|
+
var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
|
|
34
|
+
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+)$/;
|
|
35
|
+
function normalizeServerUrl(url) {
|
|
36
|
+
let normalized = url.replace(/\/+$/, "");
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(normalized);
|
|
39
|
+
if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
|
|
40
|
+
normalized = normalized.replace(/^http:\/\//, "https://");
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
function parseChannelUrl(channel) {
|
|
47
|
+
let raw = channel;
|
|
48
|
+
if (!raw.startsWith("http") && raw.includes("/")) {
|
|
49
|
+
if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
|
|
50
|
+
raw = `http://${raw}`;
|
|
51
|
+
} else if (raw.includes(".")) {
|
|
52
|
+
raw = `https://${raw}`;
|
|
53
|
+
} else if (/^[^/]+:\d+\//.test(raw)) {
|
|
54
|
+
raw = `http://${raw}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!raw.startsWith("http")) return null;
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(raw);
|
|
60
|
+
const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
|
|
61
|
+
if (channelsMatch) {
|
|
62
|
+
return { server: url.origin, channelId: channelsMatch[1] };
|
|
63
|
+
}
|
|
64
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
65
|
+
if (segments.length === 1) {
|
|
66
|
+
return { server: url.origin, channelId: segments[0] };
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function resolveChannel(channel, opts) {
|
|
73
|
+
const parsed = parseChannelUrl(channel);
|
|
74
|
+
if (parsed) {
|
|
75
|
+
const { server: server2, channelId } = parsed;
|
|
76
|
+
let token2 = opts?.token;
|
|
77
|
+
let tokenSaved2 = false;
|
|
78
|
+
if (token2 && opts?.tokenType) {
|
|
79
|
+
const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
80
|
+
saveConfig({ channels: { [channelId]: { [tokenKey]: token2 } } }, server2, {
|
|
81
|
+
setCurrent: false
|
|
82
|
+
});
|
|
83
|
+
tokenSaved2 = true;
|
|
84
|
+
}
|
|
85
|
+
if (!token2) {
|
|
86
|
+
const file = loadConfigFile();
|
|
87
|
+
const channelTokens = file.servers?.[server2]?.channels?.[channelId];
|
|
88
|
+
if (opts?.tokenType === "publish") {
|
|
89
|
+
token2 = channelTokens?.publish_token;
|
|
90
|
+
} else {
|
|
91
|
+
token2 = channelTokens?.subscribe_token;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
client: new ZooidClient({ server: server2, token: token2 }),
|
|
96
|
+
channelId,
|
|
97
|
+
server: server2,
|
|
98
|
+
tokenSaved: tokenSaved2
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
const server = config.server;
|
|
103
|
+
if (!server) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let token = opts?.token;
|
|
109
|
+
let tokenSaved = false;
|
|
110
|
+
if (token && opts?.tokenType) {
|
|
111
|
+
const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
112
|
+
saveConfig({ channels: { [channel]: { [tokenKey]: token } } });
|
|
113
|
+
tokenSaved = true;
|
|
114
|
+
}
|
|
115
|
+
if (!token) {
|
|
116
|
+
const channelTokens = config.channels?.[channel];
|
|
117
|
+
if (opts?.tokenType === "publish") {
|
|
118
|
+
token = channelTokens?.publish_token ?? config.admin_token;
|
|
119
|
+
} else if (opts?.tokenType === "subscribe") {
|
|
120
|
+
token = channelTokens?.subscribe_token ?? config.admin_token;
|
|
121
|
+
} else {
|
|
122
|
+
token = config.admin_token;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
client: new ZooidClient({ server, token }),
|
|
127
|
+
channelId: channel,
|
|
128
|
+
server,
|
|
129
|
+
tokenSaved
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export {
|
|
134
|
+
createClient,
|
|
135
|
+
createChannelClient,
|
|
136
|
+
createPublishClient,
|
|
137
|
+
createSubscribeClient,
|
|
138
|
+
PRIVATE_HOST_RE,
|
|
139
|
+
normalizeServerUrl,
|
|
140
|
+
parseChannelUrl,
|
|
141
|
+
resolveChannel
|
|
142
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
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-EEA3FCBS.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
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,162 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
createClient,
|
|
4
|
+
createPublishClient,
|
|
5
|
+
createSubscribeClient,
|
|
6
|
+
normalizeServerUrl,
|
|
7
|
+
resolveChannel
|
|
8
|
+
} from "./chunk-EEA3FCBS.js";
|
|
9
|
+
import {
|
|
10
|
+
getConfigDir,
|
|
11
|
+
getStatePath,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadConfigFile,
|
|
14
|
+
loadDirectoryToken,
|
|
15
|
+
recordTailHistory,
|
|
16
|
+
resolveServer,
|
|
17
|
+
saveConfig,
|
|
18
|
+
saveDirectoryToken,
|
|
19
|
+
switchServer
|
|
20
|
+
} from "./chunk-67ZRMVHO.js";
|
|
8
21
|
|
|
9
22
|
// src/index.ts
|
|
10
23
|
import { Command } from "commander";
|
|
11
24
|
|
|
12
|
-
// src/lib/
|
|
25
|
+
// src/lib/telemetry.ts
|
|
13
26
|
import fs from "fs";
|
|
14
|
-
import os from "os";
|
|
15
27
|
import path from "path";
|
|
16
|
-
function getConfigDir() {
|
|
17
|
-
return process.env.ZOOID_CONFIG_DIR ?? path.join(os.homedir(), ".zooid");
|
|
18
|
-
}
|
|
19
|
-
function getConfigPath() {
|
|
20
|
-
return path.join(getConfigDir(), "config.json");
|
|
21
|
-
}
|
|
22
|
-
function loadConfigFile() {
|
|
23
|
-
try {
|
|
24
|
-
const raw = fs.readFileSync(getConfigPath(), "utf-8");
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
if (parsed.server && !parsed.servers) {
|
|
27
|
-
const serverUrl = parsed.server;
|
|
28
|
-
const migrated = {
|
|
29
|
-
current: serverUrl,
|
|
30
|
-
servers: {
|
|
31
|
-
[serverUrl]: {
|
|
32
|
-
worker_url: parsed.worker_url,
|
|
33
|
-
admin_token: parsed.admin_token,
|
|
34
|
-
channels: parsed.channels
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const dir = getConfigDir();
|
|
39
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
-
fs.writeFileSync(
|
|
41
|
-
getConfigPath(),
|
|
42
|
-
JSON.stringify(migrated, null, 2) + "\n"
|
|
43
|
-
);
|
|
44
|
-
return migrated;
|
|
45
|
-
}
|
|
46
|
-
return parsed;
|
|
47
|
-
} catch {
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function resolveServer() {
|
|
52
|
-
const file = loadConfigFile();
|
|
53
|
-
let cwdUrl;
|
|
54
|
-
try {
|
|
55
|
-
const zooidJsonPath = path.join(process.cwd(), "zooid.json");
|
|
56
|
-
if (fs.existsSync(zooidJsonPath)) {
|
|
57
|
-
const raw = fs.readFileSync(zooidJsonPath, "utf-8");
|
|
58
|
-
const parsed = JSON.parse(raw);
|
|
59
|
-
cwdUrl = parsed.url || void 0;
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
if (cwdUrl) {
|
|
64
|
-
if (file.current && file.current !== cwdUrl) {
|
|
65
|
-
console.log(
|
|
66
|
-
` Note: using server from zooid.json (${cwdUrl}), current is ${file.current}`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return cwdUrl;
|
|
70
|
-
}
|
|
71
|
-
return file.current;
|
|
72
|
-
}
|
|
73
|
-
function loadConfig() {
|
|
74
|
-
const file = loadConfigFile();
|
|
75
|
-
const serverUrl = resolveServer();
|
|
76
|
-
const entry = serverUrl ? file.servers?.[serverUrl] : void 0;
|
|
77
|
-
return {
|
|
78
|
-
server: serverUrl,
|
|
79
|
-
worker_url: entry?.worker_url,
|
|
80
|
-
admin_token: entry?.admin_token,
|
|
81
|
-
channels: entry?.channels
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
function saveConfig(partial, serverUrl, options) {
|
|
85
|
-
const dir = getConfigDir();
|
|
86
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
-
const file = loadConfigFile();
|
|
88
|
-
const url = serverUrl ?? resolveServer();
|
|
89
|
-
if (!url) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
"No server URL to save config for. Deploy first or set url in zooid.json."
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
if (!file.servers) file.servers = {};
|
|
95
|
-
const existing = file.servers[url] ?? {};
|
|
96
|
-
const merged = { ...existing, ...partial };
|
|
97
|
-
if (partial.channels && existing.channels) {
|
|
98
|
-
merged.channels = { ...existing.channels };
|
|
99
|
-
for (const [chId, chData] of Object.entries(partial.channels)) {
|
|
100
|
-
merged.channels[chId] = { ...existing.channels[chId], ...chData };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
file.servers[url] = merged;
|
|
104
|
-
if (options?.setCurrent !== false) {
|
|
105
|
-
file.current = url;
|
|
106
|
-
}
|
|
107
|
-
fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
|
|
108
|
-
}
|
|
109
|
-
function loadDirectoryToken() {
|
|
110
|
-
const file = loadConfigFile();
|
|
111
|
-
return file.directory_token;
|
|
112
|
-
}
|
|
113
|
-
function saveDirectoryToken(token) {
|
|
114
|
-
const dir = getConfigDir();
|
|
115
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
116
|
-
const file = loadConfigFile();
|
|
117
|
-
file.directory_token = token;
|
|
118
|
-
fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
|
|
119
|
-
}
|
|
120
|
-
function recordTailHistory(channelId, serverUrl, name) {
|
|
121
|
-
const url = serverUrl ?? resolveServer();
|
|
122
|
-
if (!url) return;
|
|
123
|
-
const file = loadConfigFile();
|
|
124
|
-
if (!file.servers) file.servers = {};
|
|
125
|
-
if (!file.servers[url]) file.servers[url] = {};
|
|
126
|
-
if (!file.servers[url].channels) file.servers[url].channels = {};
|
|
127
|
-
const channel = file.servers[url].channels[channelId] ?? {};
|
|
128
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
-
const existing = channel.stats;
|
|
130
|
-
channel.stats = {
|
|
131
|
-
num_tails: (existing?.num_tails ?? 0) + 1,
|
|
132
|
-
last_tailed_at: now,
|
|
133
|
-
first_tailed_at: existing?.first_tailed_at ?? now
|
|
134
|
-
};
|
|
135
|
-
if (name) {
|
|
136
|
-
channel.name = name;
|
|
137
|
-
}
|
|
138
|
-
file.servers[url].channels[channelId] = channel;
|
|
139
|
-
const dir = getConfigDir();
|
|
140
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
-
fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
|
|
142
|
-
}
|
|
143
|
-
function switchServer(url) {
|
|
144
|
-
const dir = getConfigDir();
|
|
145
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
146
|
-
const file = loadConfigFile();
|
|
147
|
-
if (!file.servers) file.servers = {};
|
|
148
|
-
if (!file.servers[url]) file.servers[url] = {};
|
|
149
|
-
file.current = url;
|
|
150
|
-
fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/lib/telemetry.ts
|
|
154
|
-
import fs2 from "fs";
|
|
155
|
-
import path2 from "path";
|
|
156
28
|
import { randomUUID } from "crypto";
|
|
29
|
+
import { spawn } from "child_process";
|
|
157
30
|
var TELEMETRY_ENDPOINT = "https://telemetry.zooid.dev/v1/events";
|
|
158
31
|
var QUEUE_FILENAME = "telemetry.json";
|
|
159
|
-
var CONFIG_FILENAME = "config.json";
|
|
160
32
|
var MAX_QUEUE_SIZE = 1e3;
|
|
161
33
|
function isEnabled() {
|
|
162
34
|
const envVar = process.env.ZOOID_TELEMETRY;
|
|
@@ -185,10 +57,10 @@ function writeEvent(event) {
|
|
|
185
57
|
const queuePath = getQueuePath();
|
|
186
58
|
const dir = getConfigDir();
|
|
187
59
|
try {
|
|
188
|
-
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
189
61
|
let queue = [];
|
|
190
62
|
try {
|
|
191
|
-
const raw =
|
|
63
|
+
const raw = fs.readFileSync(queuePath, "utf-8");
|
|
192
64
|
queue = JSON.parse(raw);
|
|
193
65
|
} catch {
|
|
194
66
|
}
|
|
@@ -196,22 +68,21 @@ function writeEvent(event) {
|
|
|
196
68
|
if (queue.length > MAX_QUEUE_SIZE) {
|
|
197
69
|
queue = queue.slice(-MAX_QUEUE_SIZE);
|
|
198
70
|
}
|
|
199
|
-
|
|
71
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2) + "\n");
|
|
200
72
|
} catch {
|
|
201
73
|
}
|
|
202
74
|
}
|
|
203
75
|
function flushInBackground() {
|
|
204
76
|
const queuePath = getQueuePath();
|
|
205
77
|
try {
|
|
206
|
-
const raw =
|
|
78
|
+
const raw = fs.readFileSync(queuePath, "utf-8");
|
|
207
79
|
const parsed = JSON.parse(raw);
|
|
208
80
|
if (!Array.isArray(parsed) || parsed.length === 0) return;
|
|
209
81
|
} catch {
|
|
210
82
|
return;
|
|
211
83
|
}
|
|
212
84
|
try {
|
|
213
|
-
const
|
|
214
|
-
const child = spawn2(process.execPath, ["-e", flushScript(queuePath)], {
|
|
85
|
+
const child = spawn(process.execPath, ["-e", flushScript(queuePath)], {
|
|
215
86
|
detached: true,
|
|
216
87
|
stdio: "ignore",
|
|
217
88
|
env: { ...process.env }
|
|
@@ -221,9 +92,9 @@ function flushInBackground() {
|
|
|
221
92
|
}
|
|
222
93
|
}
|
|
223
94
|
function getInstallId() {
|
|
224
|
-
const configPath =
|
|
95
|
+
const configPath = getStatePath();
|
|
225
96
|
try {
|
|
226
|
-
const raw =
|
|
97
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
227
98
|
const parsed = JSON.parse(raw);
|
|
228
99
|
if (parsed.install_id) return parsed.install_id;
|
|
229
100
|
} catch {
|
|
@@ -233,12 +104,12 @@ function getInstallId() {
|
|
|
233
104
|
return id;
|
|
234
105
|
}
|
|
235
106
|
function getQueuePath() {
|
|
236
|
-
return
|
|
107
|
+
return path.join(getConfigDir(), QUEUE_FILENAME);
|
|
237
108
|
}
|
|
238
109
|
function readTelemetryFlag() {
|
|
239
|
-
const configPath =
|
|
110
|
+
const configPath = getStatePath();
|
|
240
111
|
try {
|
|
241
|
-
const raw =
|
|
112
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
242
113
|
const parsed = JSON.parse(raw);
|
|
243
114
|
if (parsed.telemetry === true) return true;
|
|
244
115
|
if (parsed.telemetry === false) return false;
|
|
@@ -252,17 +123,17 @@ function writeTelemetryFlag(enabled) {
|
|
|
252
123
|
}
|
|
253
124
|
function persistToConfig(key, value) {
|
|
254
125
|
const dir = getConfigDir();
|
|
255
|
-
const configPath =
|
|
126
|
+
const configPath = getStatePath();
|
|
256
127
|
try {
|
|
257
|
-
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
129
|
let config = {};
|
|
259
130
|
try {
|
|
260
|
-
const raw =
|
|
131
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
261
132
|
config = JSON.parse(raw);
|
|
262
133
|
} catch {
|
|
263
134
|
}
|
|
264
135
|
config[key] = value;
|
|
265
|
-
|
|
136
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
266
137
|
} catch {
|
|
267
138
|
}
|
|
268
139
|
}
|
|
@@ -331,131 +202,6 @@ function runConfigGet(key) {
|
|
|
331
202
|
);
|
|
332
203
|
}
|
|
333
204
|
|
|
334
|
-
// src/lib/client.ts
|
|
335
|
-
import { ZooidClient } from "@zooid/sdk";
|
|
336
|
-
function createClient(token) {
|
|
337
|
-
const config = loadConfig();
|
|
338
|
-
const server = config.server;
|
|
339
|
-
if (!server) {
|
|
340
|
-
throw new Error(
|
|
341
|
-
"No server configured. Run: npx zooid config set server <url>"
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
return new ZooidClient({ server, token: token ?? config.admin_token });
|
|
345
|
-
}
|
|
346
|
-
function createChannelClient(channelId, tokenType) {
|
|
347
|
-
const config = loadConfig();
|
|
348
|
-
const server = config.server;
|
|
349
|
-
if (!server) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
"No server configured. Run: npx zooid config set server <url>"
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
const tokenKey = tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
355
|
-
const channelToken = config.channels?.[channelId]?.[tokenKey];
|
|
356
|
-
return new ZooidClient({ server, token: channelToken ?? config.admin_token });
|
|
357
|
-
}
|
|
358
|
-
var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
|
|
359
|
-
var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
|
|
360
|
-
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+)$/;
|
|
361
|
-
function normalizeServerUrl(url) {
|
|
362
|
-
let normalized = url.replace(/\/+$/, "");
|
|
363
|
-
try {
|
|
364
|
-
const parsed = new URL(normalized);
|
|
365
|
-
if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
|
|
366
|
-
normalized = normalized.replace(/^http:\/\//, "https://");
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
}
|
|
370
|
-
return normalized;
|
|
371
|
-
}
|
|
372
|
-
function parseChannelUrl(channel) {
|
|
373
|
-
let raw = channel;
|
|
374
|
-
if (!raw.startsWith("http") && raw.includes("/")) {
|
|
375
|
-
if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
|
|
376
|
-
raw = `http://${raw}`;
|
|
377
|
-
} else if (raw.includes(".")) {
|
|
378
|
-
raw = `https://${raw}`;
|
|
379
|
-
} else if (/^[^/]+:\d+\//.test(raw)) {
|
|
380
|
-
raw = `http://${raw}`;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (!raw.startsWith("http")) return null;
|
|
384
|
-
try {
|
|
385
|
-
const url = new URL(raw);
|
|
386
|
-
const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
|
|
387
|
-
if (channelsMatch) {
|
|
388
|
-
return { server: url.origin, channelId: channelsMatch[1] };
|
|
389
|
-
}
|
|
390
|
-
const segments = url.pathname.split("/").filter(Boolean);
|
|
391
|
-
if (segments.length === 1) {
|
|
392
|
-
return { server: url.origin, channelId: segments[0] };
|
|
393
|
-
}
|
|
394
|
-
} catch {
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
function resolveChannel(channel, opts) {
|
|
399
|
-
const parsed = parseChannelUrl(channel);
|
|
400
|
-
if (parsed) {
|
|
401
|
-
const { server: server2, channelId } = parsed;
|
|
402
|
-
let token2 = opts?.token;
|
|
403
|
-
let tokenSaved2 = false;
|
|
404
|
-
if (token2 && opts?.tokenType) {
|
|
405
|
-
const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
406
|
-
saveConfig({ channels: { [channelId]: { [tokenKey]: token2 } } }, server2, {
|
|
407
|
-
setCurrent: false
|
|
408
|
-
});
|
|
409
|
-
tokenSaved2 = true;
|
|
410
|
-
}
|
|
411
|
-
if (!token2) {
|
|
412
|
-
const file = loadConfigFile();
|
|
413
|
-
const channelTokens = file.servers?.[server2]?.channels?.[channelId];
|
|
414
|
-
if (opts?.tokenType === "publish") {
|
|
415
|
-
token2 = channelTokens?.publish_token;
|
|
416
|
-
} else {
|
|
417
|
-
token2 = channelTokens?.subscribe_token;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
client: new ZooidClient({ server: server2, token: token2 }),
|
|
422
|
-
channelId,
|
|
423
|
-
server: server2,
|
|
424
|
-
tokenSaved: tokenSaved2
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
const config = loadConfig();
|
|
428
|
-
const server = config.server;
|
|
429
|
-
if (!server) {
|
|
430
|
-
throw new Error(
|
|
431
|
-
"No server configured. Run: npx zooid config set server <url>"
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
let token = opts?.token;
|
|
435
|
-
let tokenSaved = false;
|
|
436
|
-
if (token && opts?.tokenType) {
|
|
437
|
-
const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
|
|
438
|
-
saveConfig({ channels: { [channel]: { [tokenKey]: token } } });
|
|
439
|
-
tokenSaved = true;
|
|
440
|
-
}
|
|
441
|
-
if (!token) {
|
|
442
|
-
const channelTokens = config.channels?.[channel];
|
|
443
|
-
if (opts?.tokenType === "publish") {
|
|
444
|
-
token = channelTokens?.publish_token ?? config.admin_token;
|
|
445
|
-
} else if (opts?.tokenType === "subscribe") {
|
|
446
|
-
token = channelTokens?.subscribe_token ?? config.admin_token;
|
|
447
|
-
} else {
|
|
448
|
-
token = config.admin_token;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
client: new ZooidClient({ server, token }),
|
|
453
|
-
channelId: channel,
|
|
454
|
-
server,
|
|
455
|
-
tokenSaved
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
205
|
// src/commands/channel.ts
|
|
460
206
|
async function runChannelCreate(id, options, client) {
|
|
461
207
|
const c = client ?? createClient();
|
|
@@ -465,7 +211,7 @@ async function runChannelCreate(id, options, client) {
|
|
|
465
211
|
description: options.description,
|
|
466
212
|
is_public: options.public ?? true,
|
|
467
213
|
strict: options.strict,
|
|
468
|
-
|
|
214
|
+
config: options.config
|
|
469
215
|
});
|
|
470
216
|
if (!client) {
|
|
471
217
|
const config = loadConfig();
|
|
@@ -494,20 +240,20 @@ async function runChannelDelete(channelId, client) {
|
|
|
494
240
|
const serverUrl = resolveServer();
|
|
495
241
|
if (serverUrl && file.servers?.[serverUrl]?.channels?.[channelId]) {
|
|
496
242
|
delete file.servers[serverUrl].channels[channelId];
|
|
497
|
-
const
|
|
498
|
-
|
|
243
|
+
const fs6 = await import("fs");
|
|
244
|
+
fs6.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
|
|
499
245
|
}
|
|
500
246
|
}
|
|
501
247
|
}
|
|
502
248
|
|
|
503
249
|
// src/commands/publish.ts
|
|
504
|
-
import
|
|
250
|
+
import fs2 from "fs";
|
|
505
251
|
async function runPublish(channelId, options, client) {
|
|
506
252
|
const c = client ?? createPublishClient(channelId);
|
|
507
253
|
let type;
|
|
508
254
|
let data;
|
|
509
255
|
if (options.file) {
|
|
510
|
-
const raw =
|
|
256
|
+
const raw = fs2.readFileSync(options.file, "utf-8");
|
|
511
257
|
const parsed = JSON.parse(raw);
|
|
512
258
|
type = parsed.type;
|
|
513
259
|
data = parsed.data ?? parsed;
|
|
@@ -708,13 +454,13 @@ async function deviceAuth() {
|
|
|
708
454
|
"Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
|
|
709
455
|
);
|
|
710
456
|
}
|
|
711
|
-
async function directoryFetch(
|
|
457
|
+
async function directoryFetch(path5, options = {}) {
|
|
712
458
|
let token = await ensureDirectoryToken();
|
|
713
459
|
const doFetch = (t) => {
|
|
714
460
|
const headers = new Headers(options.headers);
|
|
715
461
|
headers.set("Authorization", `Bearer ${t}`);
|
|
716
462
|
headers.set("Content-Type", "application/json");
|
|
717
|
-
return fetch(`${DIRECTORY_BASE_URL}${
|
|
463
|
+
return fetch(`${DIRECTORY_BASE_URL}${path5}`, { ...options, headers });
|
|
718
464
|
};
|
|
719
465
|
let res = await doFetch(token);
|
|
720
466
|
if (res.status === 401) {
|
|
@@ -958,10 +704,10 @@ async function runTokenMint(scope, options) {
|
|
|
958
704
|
}
|
|
959
705
|
|
|
960
706
|
// src/commands/dev.ts
|
|
961
|
-
import { execSync, spawn } from "child_process";
|
|
707
|
+
import { execSync, spawn as spawn2 } from "child_process";
|
|
962
708
|
import crypto2 from "crypto";
|
|
963
|
-
import
|
|
964
|
-
import
|
|
709
|
+
import fs3 from "fs";
|
|
710
|
+
import path2 from "path";
|
|
965
711
|
import { fileURLToPath } from "url";
|
|
966
712
|
|
|
967
713
|
// src/lib/crypto.ts
|
|
@@ -1027,25 +773,25 @@ async function createEdDSAAdminToken(privateKeyJwk, kid) {
|
|
|
1027
773
|
|
|
1028
774
|
// src/commands/dev.ts
|
|
1029
775
|
function findServerDir() {
|
|
1030
|
-
const cliDir =
|
|
1031
|
-
return
|
|
776
|
+
const cliDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
777
|
+
return path2.resolve(cliDir, "../../server");
|
|
1032
778
|
}
|
|
1033
779
|
async function runDev(port = 8787) {
|
|
1034
780
|
const serverDir = findServerDir();
|
|
1035
|
-
if (!
|
|
781
|
+
if (!fs3.existsSync(path2.join(serverDir, "wrangler.toml"))) {
|
|
1036
782
|
throw new Error(
|
|
1037
783
|
`Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
|
|
1038
784
|
);
|
|
1039
785
|
}
|
|
1040
786
|
const jwtSecret = crypto2.randomUUID();
|
|
1041
|
-
const devVarsPath =
|
|
1042
|
-
|
|
787
|
+
const devVarsPath = path2.join(serverDir, ".dev.vars");
|
|
788
|
+
fs3.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
|
|
1043
789
|
`);
|
|
1044
790
|
const adminToken = await createAdminToken(jwtSecret);
|
|
1045
791
|
const serverUrl = `http://localhost:${port}`;
|
|
1046
792
|
saveConfig({ admin_token: adminToken }, serverUrl);
|
|
1047
|
-
const schemaPath =
|
|
1048
|
-
if (
|
|
793
|
+
const schemaPath = path2.join(serverDir, "src/db/schema.sql");
|
|
794
|
+
if (fs3.existsSync(schemaPath)) {
|
|
1049
795
|
try {
|
|
1050
796
|
execSync(
|
|
1051
797
|
`npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
|
|
@@ -1062,11 +808,11 @@ async function runDev(port = 8787) {
|
|
|
1062
808
|
printSuccess("Local server configured");
|
|
1063
809
|
printInfo("Server", serverUrl);
|
|
1064
810
|
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
1065
|
-
printInfo("Config saved to", "~/.zooid/
|
|
811
|
+
printInfo("Config saved to", "~/.zooid/state.json");
|
|
1066
812
|
console.log("");
|
|
1067
813
|
console.log("Starting wrangler dev...");
|
|
1068
814
|
console.log("");
|
|
1069
|
-
const child =
|
|
815
|
+
const child = spawn2(
|
|
1070
816
|
"npx",
|
|
1071
817
|
["wrangler", "dev", "--local", "--port", String(port)],
|
|
1072
818
|
{
|
|
@@ -1085,25 +831,25 @@ async function runDev(port = 8787) {
|
|
|
1085
831
|
}
|
|
1086
832
|
|
|
1087
833
|
// src/commands/init.ts
|
|
1088
|
-
import
|
|
1089
|
-
import
|
|
834
|
+
import fs4 from "fs";
|
|
835
|
+
import path3 from "path";
|
|
1090
836
|
import readline2 from "readline/promises";
|
|
1091
|
-
var
|
|
1092
|
-
function
|
|
1093
|
-
return
|
|
837
|
+
var CONFIG_FILENAME = "zooid.json";
|
|
838
|
+
function getConfigPath() {
|
|
839
|
+
return path3.join(process.cwd(), CONFIG_FILENAME);
|
|
1094
840
|
}
|
|
1095
841
|
function loadServerConfig() {
|
|
1096
|
-
const configPath =
|
|
1097
|
-
if (!
|
|
1098
|
-
const raw =
|
|
842
|
+
const configPath = getConfigPath();
|
|
843
|
+
if (!fs4.existsSync(configPath)) return null;
|
|
844
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
1099
845
|
return JSON.parse(raw);
|
|
1100
846
|
}
|
|
1101
847
|
function saveServerConfig(config) {
|
|
1102
|
-
const configPath =
|
|
1103
|
-
|
|
848
|
+
const configPath = getConfigPath();
|
|
849
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1104
850
|
}
|
|
1105
851
|
async function runInit() {
|
|
1106
|
-
const configPath =
|
|
852
|
+
const configPath = getConfigPath();
|
|
1107
853
|
const existing = loadServerConfig();
|
|
1108
854
|
if (existing) {
|
|
1109
855
|
printInfo("Found existing", configPath);
|
|
@@ -1146,9 +892,9 @@ async function runInit() {
|
|
|
1146
892
|
tags,
|
|
1147
893
|
url
|
|
1148
894
|
};
|
|
1149
|
-
|
|
895
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1150
896
|
console.log("");
|
|
1151
|
-
printSuccess(`Saved ${
|
|
897
|
+
printSuccess(`Saved ${CONFIG_FILENAME}`);
|
|
1152
898
|
console.log("");
|
|
1153
899
|
printInfo("Name", config.name || "(not set)");
|
|
1154
900
|
printInfo("Description", config.description || "(not set)");
|
|
@@ -1169,63 +915,63 @@ async function runInit() {
|
|
|
1169
915
|
// src/commands/deploy.ts
|
|
1170
916
|
import { execSync as execSync2, spawnSync } from "child_process";
|
|
1171
917
|
import crypto3 from "crypto";
|
|
1172
|
-
import
|
|
1173
|
-
import
|
|
1174
|
-
import
|
|
918
|
+
import fs5 from "fs";
|
|
919
|
+
import os from "os";
|
|
920
|
+
import path4 from "path";
|
|
1175
921
|
import readline3 from "readline/promises";
|
|
1176
922
|
import { createRequire } from "module";
|
|
1177
|
-
import { ZooidClient
|
|
923
|
+
import { ZooidClient } from "@zooid/sdk";
|
|
1178
924
|
var require2 = createRequire(import.meta.url);
|
|
1179
925
|
function resolvePackageDir(packageName) {
|
|
1180
926
|
const pkgJson = require2.resolve(`${packageName}/package.json`);
|
|
1181
|
-
return
|
|
927
|
+
return path4.dirname(pkgJson);
|
|
1182
928
|
}
|
|
1183
929
|
function prepareStagingDir() {
|
|
1184
930
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
1185
|
-
const serverRequire = createRequire(
|
|
1186
|
-
const webDir =
|
|
1187
|
-
const webDistDir =
|
|
1188
|
-
if (!
|
|
931
|
+
const serverRequire = createRequire(path4.join(serverDir, "package.json"));
|
|
932
|
+
const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
|
|
933
|
+
const webDistDir = path4.join(webDir, "dist");
|
|
934
|
+
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
1189
935
|
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
1190
936
|
}
|
|
1191
|
-
if (!
|
|
937
|
+
if (!fs5.existsSync(webDistDir)) {
|
|
1192
938
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
1193
939
|
}
|
|
1194
|
-
const tmpDir =
|
|
1195
|
-
copyDirSync(
|
|
1196
|
-
copyDirSync(webDistDir,
|
|
1197
|
-
let toml =
|
|
940
|
+
const tmpDir = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
|
|
941
|
+
copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
|
|
942
|
+
copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
|
|
943
|
+
let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
|
|
1198
944
|
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
1199
|
-
|
|
945
|
+
fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
|
|
1200
946
|
const nodeModules = findServerNodeModules(serverDir);
|
|
1201
947
|
if (nodeModules) {
|
|
1202
|
-
|
|
948
|
+
fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
|
|
1203
949
|
}
|
|
1204
950
|
return tmpDir;
|
|
1205
951
|
}
|
|
1206
952
|
function findServerNodeModules(serverDir) {
|
|
1207
|
-
const local =
|
|
1208
|
-
if (
|
|
1209
|
-
const storeNodeModules =
|
|
1210
|
-
if (
|
|
953
|
+
const local = path4.join(serverDir, "node_modules");
|
|
954
|
+
if (fs5.existsSync(path4.join(local, "hono"))) return local;
|
|
955
|
+
const storeNodeModules = path4.resolve(serverDir, "..", "..");
|
|
956
|
+
if (fs5.existsSync(path4.join(storeNodeModules, "hono")))
|
|
1211
957
|
return storeNodeModules;
|
|
1212
958
|
let dir = serverDir;
|
|
1213
|
-
while (dir !==
|
|
1214
|
-
dir =
|
|
1215
|
-
const nm =
|
|
1216
|
-
if (
|
|
959
|
+
while (dir !== path4.dirname(dir)) {
|
|
960
|
+
dir = path4.dirname(dir);
|
|
961
|
+
const nm = path4.join(dir, "node_modules");
|
|
962
|
+
if (fs5.existsSync(path4.join(nm, "hono"))) return nm;
|
|
1217
963
|
}
|
|
1218
964
|
return null;
|
|
1219
965
|
}
|
|
1220
966
|
function copyDirSync(src, dest) {
|
|
1221
|
-
|
|
1222
|
-
for (const entry of
|
|
1223
|
-
const srcPath =
|
|
1224
|
-
const destPath =
|
|
967
|
+
fs5.mkdirSync(dest, { recursive: true });
|
|
968
|
+
for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
|
|
969
|
+
const srcPath = path4.join(src, entry.name);
|
|
970
|
+
const destPath = path4.join(dest, entry.name);
|
|
1225
971
|
if (entry.isDirectory()) {
|
|
1226
972
|
copyDirSync(srcPath, destPath);
|
|
1227
973
|
} else {
|
|
1228
|
-
|
|
974
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1229
975
|
}
|
|
1230
976
|
}
|
|
1231
977
|
}
|
|
@@ -1279,9 +1025,9 @@ function parseDeployUrls(output) {
|
|
|
1279
1025
|
};
|
|
1280
1026
|
}
|
|
1281
1027
|
function loadDotEnv() {
|
|
1282
|
-
const envPath =
|
|
1283
|
-
if (!
|
|
1284
|
-
const content =
|
|
1028
|
+
const envPath = path4.join(process.cwd(), ".env");
|
|
1029
|
+
if (!fs5.existsSync(envPath)) return {};
|
|
1030
|
+
const content = fs5.readFileSync(envPath, "utf-8");
|
|
1285
1031
|
const tokenMatch = content.match(/^CLOUDFLARE_API_TOKEN=(.+)$/m);
|
|
1286
1032
|
const accountMatch = content.match(/^CLOUDFLARE_ACCOUNT_ID=(.+)$/m);
|
|
1287
1033
|
return {
|
|
@@ -1386,8 +1132,8 @@ async function runDeploy() {
|
|
|
1386
1132
|
}
|
|
1387
1133
|
const databaseId = dbIdMatch[1];
|
|
1388
1134
|
printSuccess(`D1 database created (${databaseId})`);
|
|
1389
|
-
const wranglerTomlPath =
|
|
1390
|
-
let tomlContent =
|
|
1135
|
+
const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
|
|
1136
|
+
let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
|
|
1391
1137
|
tomlContent = tomlContent.replace(
|
|
1392
1138
|
/name = "[^"]*"/,
|
|
1393
1139
|
`name = "${workerName}"`
|
|
@@ -1404,10 +1150,10 @@ async function runDeploy() {
|
|
|
1404
1150
|
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
1405
1151
|
`ZOOID_SERVER_ID = "${serverSlug}"`
|
|
1406
1152
|
);
|
|
1407
|
-
|
|
1153
|
+
fs5.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1408
1154
|
printSuccess("Configured wrangler.toml");
|
|
1409
|
-
const schemaPath =
|
|
1410
|
-
if (
|
|
1155
|
+
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1156
|
+
if (fs5.existsSync(schemaPath)) {
|
|
1411
1157
|
printStep("Running database schema migration...");
|
|
1412
1158
|
wrangler(
|
|
1413
1159
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -1462,8 +1208,8 @@ async function runDeploy() {
|
|
|
1462
1208
|
console.log("");
|
|
1463
1209
|
printInfo("Deploy type", "Redeploying existing server");
|
|
1464
1210
|
console.log("");
|
|
1465
|
-
const wranglerTomlPath =
|
|
1466
|
-
let tomlContent =
|
|
1211
|
+
const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
|
|
1212
|
+
let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
|
|
1467
1213
|
tomlContent = tomlContent.replace(
|
|
1468
1214
|
/name = "[^"]*"/,
|
|
1469
1215
|
`name = "${workerName}"`
|
|
@@ -1488,9 +1234,9 @@ async function runDeploy() {
|
|
|
1488
1234
|
}
|
|
1489
1235
|
} catch {
|
|
1490
1236
|
}
|
|
1491
|
-
|
|
1492
|
-
const schemaPath =
|
|
1493
|
-
if (
|
|
1237
|
+
fs5.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1238
|
+
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1239
|
+
if (fs5.existsSync(schemaPath)) {
|
|
1494
1240
|
printStep("Running schema migration...");
|
|
1495
1241
|
wrangler(
|
|
1496
1242
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -1499,7 +1245,10 @@ async function runDeploy() {
|
|
|
1499
1245
|
);
|
|
1500
1246
|
printSuccess("Schema up to date");
|
|
1501
1247
|
}
|
|
1502
|
-
const migrations = [
|
|
1248
|
+
const migrations = [
|
|
1249
|
+
"ALTER TABLE events ADD COLUMN publisher_name TEXT",
|
|
1250
|
+
"ALTER TABLE channels ADD COLUMN config TEXT"
|
|
1251
|
+
];
|
|
1503
1252
|
for (const sql of migrations) {
|
|
1504
1253
|
try {
|
|
1505
1254
|
wrangler(
|
|
@@ -1510,6 +1259,15 @@ async function runDeploy() {
|
|
|
1510
1259
|
} catch {
|
|
1511
1260
|
}
|
|
1512
1261
|
}
|
|
1262
|
+
try {
|
|
1263
|
+
const dataMigrationSql = `UPDATE channels SET config = json_object('types', (SELECT json_group_object(key, json_object('schema', json_each.value)) FROM json_each(schema))) WHERE schema IS NOT NULL AND config IS NULL`;
|
|
1264
|
+
wrangler(
|
|
1265
|
+
`d1 execute ${dbName} --remote --command="${dataMigrationSql}"`,
|
|
1266
|
+
stagingDir,
|
|
1267
|
+
creds
|
|
1268
|
+
);
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1513
1271
|
try {
|
|
1514
1272
|
const keysOutput = wrangler(
|
|
1515
1273
|
`d1 execute ${dbName} --remote --json --command="SELECT kid FROM trusted_keys WHERE issuer = 'local' LIMIT 1"`,
|
|
@@ -1571,9 +1329,7 @@ async function runDeploy() {
|
|
|
1571
1329
|
adminToken = existingConfig.admin_token;
|
|
1572
1330
|
}
|
|
1573
1331
|
if (!adminToken) {
|
|
1574
|
-
printError(
|
|
1575
|
-
"No admin token found in ~/.zooid/config.json for this server"
|
|
1576
|
-
);
|
|
1332
|
+
printError("No admin token found in ~/.zooid/state.json for this server");
|
|
1577
1333
|
console.log(
|
|
1578
1334
|
"If this is a first deploy, remove the D1 database and try again."
|
|
1579
1335
|
);
|
|
@@ -1595,7 +1351,7 @@ async function runDeploy() {
|
|
|
1595
1351
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
1596
1352
|
if (canonicalUrl && adminToken) {
|
|
1597
1353
|
try {
|
|
1598
|
-
const client = new
|
|
1354
|
+
const client = new ZooidClient({
|
|
1599
1355
|
server: canonicalUrl,
|
|
1600
1356
|
token: adminToken
|
|
1601
1357
|
});
|
|
@@ -1627,7 +1383,7 @@ async function runDeploy() {
|
|
|
1627
1383
|
configToSave.channels = {};
|
|
1628
1384
|
}
|
|
1629
1385
|
saveConfig(configToSave, canonicalUrl || void 0);
|
|
1630
|
-
printSuccess("Saved connection config to ~/.zooid/
|
|
1386
|
+
printSuccess("Saved connection config to ~/.zooid/state.json");
|
|
1631
1387
|
cleanup(stagingDir);
|
|
1632
1388
|
console.log("");
|
|
1633
1389
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
@@ -1641,7 +1397,7 @@ async function runDeploy() {
|
|
|
1641
1397
|
if (isFirstDeploy) {
|
|
1642
1398
|
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
1643
1399
|
}
|
|
1644
|
-
printInfo("Config", "~/.zooid/
|
|
1400
|
+
printInfo("Config", "~/.zooid/state.json");
|
|
1645
1401
|
console.log("");
|
|
1646
1402
|
if (isFirstDeploy) {
|
|
1647
1403
|
console.log(" Next steps:");
|
|
@@ -1654,7 +1410,7 @@ async function runDeploy() {
|
|
|
1654
1410
|
}
|
|
1655
1411
|
function cleanup(dir) {
|
|
1656
1412
|
try {
|
|
1657
|
-
|
|
1413
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
1658
1414
|
} catch {
|
|
1659
1415
|
}
|
|
1660
1416
|
}
|
|
@@ -1682,7 +1438,7 @@ async function resolveAndRecord(channel, opts) {
|
|
|
1682
1438
|
return result;
|
|
1683
1439
|
}
|
|
1684
1440
|
var program = new Command();
|
|
1685
|
-
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.
|
|
1441
|
+
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.2.0");
|
|
1686
1442
|
var telemetryCtx = { startTime: 0 };
|
|
1687
1443
|
function setTelemetryChannel(channelId) {
|
|
1688
1444
|
telemetryCtx.channelId = channelId;
|
|
@@ -1790,18 +1546,23 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1790
1546
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1791
1547
|
).action(async (id, opts) => {
|
|
1792
1548
|
try {
|
|
1793
|
-
let
|
|
1549
|
+
let config;
|
|
1794
1550
|
if (opts.schema) {
|
|
1795
|
-
const
|
|
1796
|
-
const raw =
|
|
1797
|
-
|
|
1551
|
+
const fs6 = await import("fs");
|
|
1552
|
+
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1553
|
+
const parsed = JSON.parse(raw);
|
|
1554
|
+
const types = {};
|
|
1555
|
+
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
1556
|
+
types[eventType] = { schema: schemaDef };
|
|
1557
|
+
}
|
|
1558
|
+
config = { types };
|
|
1798
1559
|
}
|
|
1799
1560
|
const result = await runChannelCreate(id, {
|
|
1800
1561
|
name: opts.name,
|
|
1801
1562
|
description: opts.description,
|
|
1802
1563
|
public: opts.private ? false : true,
|
|
1803
1564
|
strict: opts.strict,
|
|
1804
|
-
|
|
1565
|
+
config
|
|
1805
1566
|
});
|
|
1806
1567
|
printSuccess(`Created channel: ${id}`);
|
|
1807
1568
|
printInfo("Publish token", result.publish_token);
|
|
@@ -1823,9 +1584,14 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1823
1584
|
if (opts.public) fields.is_public = true;
|
|
1824
1585
|
if (opts.private) fields.is_public = false;
|
|
1825
1586
|
if (opts.schema) {
|
|
1826
|
-
const
|
|
1827
|
-
const raw =
|
|
1828
|
-
|
|
1587
|
+
const fs6 = await import("fs");
|
|
1588
|
+
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1589
|
+
const parsed = JSON.parse(raw);
|
|
1590
|
+
const types = {};
|
|
1591
|
+
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
1592
|
+
types[eventType] = { schema: schemaDef };
|
|
1593
|
+
}
|
|
1594
|
+
fields.config = { types };
|
|
1829
1595
|
}
|
|
1830
1596
|
if (opts.strict !== void 0) fields.strict = opts.strict;
|
|
1831
1597
|
if (Object.keys(fields).length === 0) {
|
|
@@ -1908,9 +1674,40 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
|
|
|
1908
1674
|
"--mode <mode>",
|
|
1909
1675
|
"Transport mode for follow: auto, ws, or poll",
|
|
1910
1676
|
"auto"
|
|
1911
|
-
).option("--interval <ms>", "Poll interval in ms for follow mode", "5000").option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, opts) => {
|
|
1677
|
+
).option("--interval <ms>", "Poll interval in ms for follow mode", "5000").option("--unseen", "Only show events since your last tail").option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, opts) => {
|
|
1912
1678
|
try {
|
|
1913
|
-
|
|
1679
|
+
let unseenCursor;
|
|
1680
|
+
let unseenSince;
|
|
1681
|
+
if (opts.unseen) {
|
|
1682
|
+
const file = loadConfigFile();
|
|
1683
|
+
const { parseChannelUrl } = await import("./client-QPT54SNG.js");
|
|
1684
|
+
const { resolveServer: resolveServer2 } = await import("./config-2KK5GX42.js");
|
|
1685
|
+
const parsed = parseChannelUrl(channel);
|
|
1686
|
+
const channelId2 = parsed?.channelId ?? channel;
|
|
1687
|
+
const serverUrl = parsed?.server ?? resolveServer2();
|
|
1688
|
+
const stats = serverUrl ? file.servers?.[serverUrl]?.channels?.[channelId2]?.stats : void 0;
|
|
1689
|
+
if (!stats) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
`No tail history for ${channelId2} \u2014 tail it at least once first`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
if (stats.last_event_id) {
|
|
1695
|
+
unseenCursor = stats.last_event_id;
|
|
1696
|
+
} else if (stats.last_tailed_at) {
|
|
1697
|
+
unseenSince = stats.last_tailed_at;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const { client, channelId, server } = await resolveAndRecord(
|
|
1701
|
+
channel,
|
|
1702
|
+
opts
|
|
1703
|
+
);
|
|
1704
|
+
if (opts.unseen) {
|
|
1705
|
+
if (unseenCursor) {
|
|
1706
|
+
opts.cursor = unseenCursor;
|
|
1707
|
+
} else if (unseenSince) {
|
|
1708
|
+
opts.since = unseenSince;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1914
1711
|
if (opts.follow) {
|
|
1915
1712
|
const mode = opts.mode;
|
|
1916
1713
|
const transport = mode === "auto" ? "auto (WebSocket \u2192 poll fallback)" : mode;
|
|
@@ -1939,6 +1736,10 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
|
|
|
1939
1736
|
},
|
|
1940
1737
|
client
|
|
1941
1738
|
);
|
|
1739
|
+
if (result.events.length > 0) {
|
|
1740
|
+
const lastEvent = result.events[result.events.length - 1];
|
|
1741
|
+
recordTailHistory(channelId, server, void 0, lastEvent.id);
|
|
1742
|
+
}
|
|
1942
1743
|
if (result.events.length === 0) {
|
|
1943
1744
|
console.log("No events.");
|
|
1944
1745
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Open-source pub/sub server for AI agents. Publish signals, subscribe via webhook, WebSocket, polling, or RSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@inquirer/checkbox": "^5.0.7",
|
|
26
26
|
"commander": "^14.0.3",
|
|
27
|
-
"@zooid/sdk": "^0.
|
|
28
|
-
"@zooid/types": "^0.
|
|
29
|
-
"@zooid/server": "^0.
|
|
27
|
+
"@zooid/sdk": "^0.2.0",
|
|
28
|
+
"@zooid/types": "^0.2.0",
|
|
29
|
+
"@zooid/server": "^0.2.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@cloudflare/vitest-pool-workers": "^0.8.34",
|