sharednet 0.1.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/LICENSE +21 -0
- package/README.md +28 -0
- package/bin/sharednet.js +5 -0
- package/dist/api-client.d.ts +11 -0
- package/dist/api-client.js +82 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +328 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +20 -0
- package/dist/guest.d.ts +29 -0
- package/dist/guest.js +676 -0
- package/dist/instance-computation.d.ts +10 -0
- package/dist/instance-computation.js +26 -0
- package/dist/login.d.ts +20 -0
- package/dist/login.js +137 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +11 -0
- package/dist/runtime-detection.d.ts +43 -0
- package/dist/runtime-detection.js +165 -0
- package/dist/session.d.ts +80 -0
- package/dist/session.js +182 -0
- package/dist/storage.d.ts +74 -0
- package/dist/storage.js +368 -0
- package/package.json +51 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type Environment = Record<string, string | undefined>;
|
|
2
|
+
export interface StoragePaths {
|
|
3
|
+
configDir: string;
|
|
4
|
+
credentialsFile: string;
|
|
5
|
+
installationFile: string;
|
|
6
|
+
stateDir: string;
|
|
7
|
+
sessionsDir: string;
|
|
8
|
+
/** One file per seat: rooms/<room_id>/<member_id>.json, so two Agents on one machine can sit in one Room. */
|
|
9
|
+
roomsDir: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A guest seat in one Room: the member token the join handed back, owner-only
|
|
13
|
+
* under the config directory, never inside a project tree.
|
|
14
|
+
*/
|
|
15
|
+
export interface StoredRoomCredential {
|
|
16
|
+
schema_version: 1;
|
|
17
|
+
base_url: string;
|
|
18
|
+
room_id: string;
|
|
19
|
+
member_id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
member_token: string;
|
|
22
|
+
joined_at: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The per-project cursor: which Room this checkout is in and the last
|
|
26
|
+
* sequence it has seen. Nothing here is secret, and the directory ignores
|
|
27
|
+
* itself so it never rides into a commit.
|
|
28
|
+
*/
|
|
29
|
+
export interface ProjectRoomState {
|
|
30
|
+
schema_version: 1;
|
|
31
|
+
base_url: string;
|
|
32
|
+
room_id: string;
|
|
33
|
+
member_id: string;
|
|
34
|
+
last_sequence: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const PROJECT_STATE_DIR = ".sharednet";
|
|
37
|
+
export interface StoredApiCredential {
|
|
38
|
+
schema_version: 1;
|
|
39
|
+
base_url: string;
|
|
40
|
+
principal_id: string;
|
|
41
|
+
api_key_id: string;
|
|
42
|
+
api_key: string;
|
|
43
|
+
installation_secret: string;
|
|
44
|
+
created_at: string;
|
|
45
|
+
expires_at: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface StoredSession {
|
|
48
|
+
schema_version: 1;
|
|
49
|
+
base_url: string;
|
|
50
|
+
principal_id: string;
|
|
51
|
+
/** The tag this Instance was under when the session file was written; null when untagged. */
|
|
52
|
+
agent_id: string | null;
|
|
53
|
+
instance_id: string;
|
|
54
|
+
local_instance_key: string | null;
|
|
55
|
+
instance_token: string;
|
|
56
|
+
created_at: string;
|
|
57
|
+
lease_expires_at: string;
|
|
58
|
+
/** Always null since Instances became permanent; kept so old files still parse. */
|
|
59
|
+
expires_at: string | null;
|
|
60
|
+
}
|
|
61
|
+
export declare function getStoragePaths(env?: Environment): StoragePaths;
|
|
62
|
+
export declare function readStoredApiCredential(paths: StoragePaths): Promise<StoredApiCredential | null>;
|
|
63
|
+
/** Written by `sharednet login`; owner-only, and never printed back. */
|
|
64
|
+
export declare function writeStoredApiCredential(paths: StoragePaths, credential: StoredApiCredential): Promise<void>;
|
|
65
|
+
export declare function getOrCreateInstallationSecret(paths: StoragePaths): Promise<string>;
|
|
66
|
+
export declare function writeSession(paths: StoragePaths, session: StoredSession): Promise<void>;
|
|
67
|
+
export declare function listSessions(paths: StoragePaths): Promise<StoredSession[]>;
|
|
68
|
+
export declare function readSessionById(paths: StoragePaths, instanceId: string): Promise<StoredSession | null>;
|
|
69
|
+
export declare function deleteSession(paths: StoragePaths, instanceId: string): Promise<void>;
|
|
70
|
+
export declare function writeRoomCredential(paths: StoragePaths, credential: StoredRoomCredential): Promise<void>;
|
|
71
|
+
export declare function readRoomCredential(paths: StoragePaths, roomId: string, memberId: string): Promise<StoredRoomCredential | null>;
|
|
72
|
+
export declare function readProjectRoomState(cwd: string): Promise<ProjectRoomState | null>;
|
|
73
|
+
export declare function writeProjectRoomState(cwd: string, state: ProjectRoomState): Promise<void>;
|
|
74
|
+
export {};
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, link, mkdir, open, readdir, readFile, rename, unlink, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { localError } from "./errors.js";
|
|
7
|
+
export const PROJECT_STATE_DIR = ".sharednet";
|
|
8
|
+
function unsafeStorage() {
|
|
9
|
+
throw localError("unsafe_credential_storage", "SharedNet local credential storage is not owner-only.");
|
|
10
|
+
}
|
|
11
|
+
export function getStoragePaths(env = process.env) {
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
const localAppData = env.LOCALAPPDATA;
|
|
14
|
+
if (!localAppData)
|
|
15
|
+
unsafeStorage();
|
|
16
|
+
const root = join(localAppData, "SharedNet");
|
|
17
|
+
return {
|
|
18
|
+
configDir: root,
|
|
19
|
+
credentialsFile: join(root, "credentials.json"),
|
|
20
|
+
installationFile: join(root, "installation.json"),
|
|
21
|
+
stateDir: root,
|
|
22
|
+
sessionsDir: join(root, "sessions"),
|
|
23
|
+
roomsDir: join(root, "rooms"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const home = env.HOME || homedir();
|
|
27
|
+
if (!home)
|
|
28
|
+
unsafeStorage();
|
|
29
|
+
const configRoot = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
30
|
+
const stateRoot = env.XDG_STATE_HOME || join(home, ".local", "state");
|
|
31
|
+
const configDir = join(configRoot, "sharednet");
|
|
32
|
+
const stateDir = join(stateRoot, "sharednet");
|
|
33
|
+
return {
|
|
34
|
+
configDir,
|
|
35
|
+
credentialsFile: join(configDir, "credentials.json"),
|
|
36
|
+
installationFile: join(configDir, "installation.json"),
|
|
37
|
+
stateDir,
|
|
38
|
+
sessionsDir: join(stateDir, "sessions"),
|
|
39
|
+
roomsDir: join(configDir, "rooms"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function ensureSecureDirectory(path) {
|
|
43
|
+
if (process.platform === "win32")
|
|
44
|
+
unsafeStorage();
|
|
45
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
46
|
+
const info = await lstat(path);
|
|
47
|
+
if (info.isSymbolicLink() ||
|
|
48
|
+
!info.isDirectory() ||
|
|
49
|
+
info.uid !== process.getuid?.() ||
|
|
50
|
+
(info.mode & 0o077) !== 0) {
|
|
51
|
+
unsafeStorage();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function readSecureFile(path) {
|
|
55
|
+
let info;
|
|
56
|
+
try {
|
|
57
|
+
info = await lstat(path);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === "ENOENT")
|
|
61
|
+
return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (info.isSymbolicLink() ||
|
|
65
|
+
!info.isFile() ||
|
|
66
|
+
info.uid !== process.getuid?.() ||
|
|
67
|
+
(info.mode & 0o077) !== 0) {
|
|
68
|
+
unsafeStorage();
|
|
69
|
+
}
|
|
70
|
+
let handle;
|
|
71
|
+
try {
|
|
72
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
73
|
+
handle = await open(path, constants.O_RDONLY | noFollow);
|
|
74
|
+
const openedInfo = await handle.stat();
|
|
75
|
+
if (!openedInfo.isFile() ||
|
|
76
|
+
openedInfo.dev !== info.dev ||
|
|
77
|
+
openedInfo.ino !== info.ino ||
|
|
78
|
+
openedInfo.uid !== process.getuid?.() ||
|
|
79
|
+
(openedInfo.mode & 0o077) !== 0) {
|
|
80
|
+
unsafeStorage();
|
|
81
|
+
}
|
|
82
|
+
return await handle.readFile("utf8");
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await handle?.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function writeSecureJson(path, value) {
|
|
89
|
+
const parent = dirname(path);
|
|
90
|
+
await ensureSecureDirectory(parent);
|
|
91
|
+
// Validate an existing target before an atomic replacement. Missing is fine.
|
|
92
|
+
await readSecureFile(path);
|
|
93
|
+
const temp = join(parent, `.sharednet-${randomBytes(12).toString("hex")}.tmp`);
|
|
94
|
+
let handle;
|
|
95
|
+
try {
|
|
96
|
+
handle = await open(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
97
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
|
|
98
|
+
await handle.sync();
|
|
99
|
+
await handle.close();
|
|
100
|
+
handle = undefined;
|
|
101
|
+
await chmod(temp, 0o600);
|
|
102
|
+
await rename(temp, path);
|
|
103
|
+
const directory = await open(parent, constants.O_RDONLY);
|
|
104
|
+
try {
|
|
105
|
+
await directory.sync();
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await directory.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
await handle?.close();
|
|
113
|
+
await unlink(temp).catch(() => undefined);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function createSecureJsonIfAbsent(path, value) {
|
|
118
|
+
const parent = dirname(path);
|
|
119
|
+
await ensureSecureDirectory(parent);
|
|
120
|
+
const temp = join(parent, `.sharednet-${randomBytes(12).toString("hex")}.tmp`);
|
|
121
|
+
let handle;
|
|
122
|
+
try {
|
|
123
|
+
handle = await open(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
124
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
|
|
125
|
+
await handle.sync();
|
|
126
|
+
await handle.close();
|
|
127
|
+
handle = undefined;
|
|
128
|
+
await chmod(temp, 0o600);
|
|
129
|
+
try {
|
|
130
|
+
// link(2) is an atomic create-if-absent operation. Unlike opening the
|
|
131
|
+
// destination with O_EXCL and then writing it, readers can never observe
|
|
132
|
+
// an empty or partially written installation secret.
|
|
133
|
+
await link(temp, path);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error.code === "EEXIST")
|
|
137
|
+
return false;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const directory = await open(parent, constants.O_RDONLY);
|
|
141
|
+
try {
|
|
142
|
+
await directory.sync();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await directory.close();
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await handle?.close();
|
|
151
|
+
await unlink(temp).catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function parseJsonObject(raw, source) {
|
|
155
|
+
try {
|
|
156
|
+
const value = JSON.parse(raw);
|
|
157
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
158
|
+
throw new Error();
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
throw localError("invalid_local_state", `${source} is not valid SharedNet state.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function requireString(value, source) {
|
|
166
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
167
|
+
throw localError("invalid_local_state", `${source} is not valid SharedNet state.`);
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function parseCredential(raw) {
|
|
172
|
+
const value = parseJsonObject(raw, "The credential file");
|
|
173
|
+
if (value.schema_version !== 1) {
|
|
174
|
+
throw localError("invalid_local_state", "The credential file has an unsupported version.");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
schema_version: 1,
|
|
178
|
+
base_url: requireString(value.base_url, "The credential file"),
|
|
179
|
+
principal_id: requireString(value.principal_id, "The credential file"),
|
|
180
|
+
api_key_id: requireString(value.api_key_id, "The credential file"),
|
|
181
|
+
api_key: requireString(value.api_key, "The credential file"),
|
|
182
|
+
installation_secret: requireString(value.installation_secret, "The credential file"),
|
|
183
|
+
created_at: requireString(value.created_at, "The credential file"),
|
|
184
|
+
expires_at: value.expires_at === null ? null : requireString(value.expires_at, "The credential file"),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function parseSession(raw) {
|
|
188
|
+
const value = parseJsonObject(raw, "A session file");
|
|
189
|
+
if (value.schema_version !== 1) {
|
|
190
|
+
throw localError("invalid_local_state", "A session file has an unsupported version.");
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
schema_version: 1,
|
|
194
|
+
base_url: requireString(value.base_url, "A session file"),
|
|
195
|
+
principal_id: requireString(value.principal_id, "A session file"),
|
|
196
|
+
agent_id: value.agent_id === null ? null : requireString(value.agent_id, "A session file"),
|
|
197
|
+
instance_id: requireString(value.instance_id, "A session file"),
|
|
198
|
+
local_instance_key: value.local_instance_key === null
|
|
199
|
+
? null
|
|
200
|
+
: requireString(value.local_instance_key, "A session file"),
|
|
201
|
+
instance_token: requireString(value.instance_token, "A session file"),
|
|
202
|
+
created_at: requireString(value.created_at, "A session file"),
|
|
203
|
+
lease_expires_at: requireString(value.lease_expires_at, "A session file"),
|
|
204
|
+
expires_at: value.expires_at === null || value.expires_at === undefined ? null : requireString(value.expires_at, "A session file"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export async function readStoredApiCredential(paths) {
|
|
208
|
+
const raw = await readSecureFile(paths.credentialsFile);
|
|
209
|
+
return raw === null ? null : parseCredential(raw);
|
|
210
|
+
}
|
|
211
|
+
/** Written by `sharednet login`; owner-only, and never printed back. */
|
|
212
|
+
export async function writeStoredApiCredential(paths, credential) {
|
|
213
|
+
await writeSecureJson(paths.credentialsFile, credential);
|
|
214
|
+
}
|
|
215
|
+
export async function getOrCreateInstallationSecret(paths) {
|
|
216
|
+
const credential = await readStoredApiCredential(paths);
|
|
217
|
+
if (credential)
|
|
218
|
+
return credential.installation_secret;
|
|
219
|
+
const raw = await readSecureFile(paths.installationFile);
|
|
220
|
+
if (raw !== null) {
|
|
221
|
+
const value = parseJsonObject(raw, "The installation file");
|
|
222
|
+
if (value.schema_version !== 1) {
|
|
223
|
+
throw localError("invalid_local_state", "The installation file has an unsupported version.");
|
|
224
|
+
}
|
|
225
|
+
return requireString(value.installation_secret, "The installation file");
|
|
226
|
+
}
|
|
227
|
+
const installation = {
|
|
228
|
+
schema_version: 1,
|
|
229
|
+
installation_secret: randomBytes(32).toString("base64url"),
|
|
230
|
+
};
|
|
231
|
+
if (await createSecureJsonIfAbsent(paths.installationFile, installation)) {
|
|
232
|
+
return installation.installation_secret;
|
|
233
|
+
}
|
|
234
|
+
const winnerRaw = await readSecureFile(paths.installationFile);
|
|
235
|
+
if (winnerRaw === null) {
|
|
236
|
+
throw localError("invalid_local_state", "The installation secret could not be created.");
|
|
237
|
+
}
|
|
238
|
+
const winner = parseJsonObject(winnerRaw, "The installation file");
|
|
239
|
+
if (winner.schema_version !== 1) {
|
|
240
|
+
throw localError("invalid_local_state", "The installation file has an unsupported version.");
|
|
241
|
+
}
|
|
242
|
+
return requireString(winner.installation_secret, "The installation file");
|
|
243
|
+
}
|
|
244
|
+
export async function writeSession(paths, session) {
|
|
245
|
+
if (!/^i_[A-Za-z0-9_-]+$/.test(session.instance_id)) {
|
|
246
|
+
throw localError("invalid_local_state", "The Instance ID is invalid.");
|
|
247
|
+
}
|
|
248
|
+
await writeSecureJson(join(paths.sessionsDir, `${session.instance_id}.json`), session);
|
|
249
|
+
}
|
|
250
|
+
export async function listSessions(paths) {
|
|
251
|
+
let entries;
|
|
252
|
+
try {
|
|
253
|
+
await ensureSecureDirectory(paths.sessionsDir);
|
|
254
|
+
entries = await readdir(paths.sessionsDir, { withFileTypes: true });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error.code === "ENOENT")
|
|
258
|
+
return [];
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
const sessions = [];
|
|
262
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
263
|
+
if (!entry.name.endsWith(".json"))
|
|
264
|
+
continue;
|
|
265
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
266
|
+
unsafeStorage();
|
|
267
|
+
const raw = await readSecureFile(join(paths.sessionsDir, entry.name));
|
|
268
|
+
if (raw !== null)
|
|
269
|
+
sessions.push(parseSession(raw));
|
|
270
|
+
}
|
|
271
|
+
return sessions;
|
|
272
|
+
}
|
|
273
|
+
export async function readSessionById(paths, instanceId) {
|
|
274
|
+
if (!/^i_[A-Za-z0-9_-]+$/.test(instanceId)) {
|
|
275
|
+
throw localError("invalid_session_id", "The Instance ID is invalid.");
|
|
276
|
+
}
|
|
277
|
+
const raw = await readSecureFile(join(paths.sessionsDir, `${instanceId}.json`));
|
|
278
|
+
return raw === null ? null : parseSession(raw);
|
|
279
|
+
}
|
|
280
|
+
export async function deleteSession(paths, instanceId) {
|
|
281
|
+
if (!/^i_[A-Za-z0-9_-]+$/.test(instanceId))
|
|
282
|
+
return;
|
|
283
|
+
const path = join(paths.sessionsDir, `${instanceId}.json`);
|
|
284
|
+
const raw = await readSecureFile(path);
|
|
285
|
+
if (raw === null)
|
|
286
|
+
return;
|
|
287
|
+
await unlink(path);
|
|
288
|
+
}
|
|
289
|
+
const ROOM_ID_PATTERN = /^rom_[A-Za-z0-9]+$/;
|
|
290
|
+
/** A seat is an Instance (`i_`); `mem_` files written before migration 0007 still open. */
|
|
291
|
+
const MEMBER_ID_PATTERN = /^(?:i|mem)_[A-Za-z0-9]+$/;
|
|
292
|
+
/**
|
|
293
|
+
* A seat is one member in one Room. Two Agents on the same machine that join
|
|
294
|
+
* the same Room are two seats and must never share a file, or the second join
|
|
295
|
+
* would silently take over the first one's credential.
|
|
296
|
+
*/
|
|
297
|
+
function roomCredentialFile(paths, roomId, memberId) {
|
|
298
|
+
if (!ROOM_ID_PATTERN.test(roomId)) {
|
|
299
|
+
throw localError("invalid_local_state", "The Room ID is invalid.");
|
|
300
|
+
}
|
|
301
|
+
if (!MEMBER_ID_PATTERN.test(memberId)) {
|
|
302
|
+
throw localError("invalid_local_state", "The member ID is invalid.");
|
|
303
|
+
}
|
|
304
|
+
return join(paths.roomsDir, roomId, `${memberId}.json`);
|
|
305
|
+
}
|
|
306
|
+
function parseRoomCredential(raw) {
|
|
307
|
+
const value = parseJsonObject(raw, "A Room credential file");
|
|
308
|
+
if (value.schema_version !== 1) {
|
|
309
|
+
throw localError("invalid_local_state", "A Room credential file has an unsupported version.");
|
|
310
|
+
}
|
|
311
|
+
const source = "A Room credential file";
|
|
312
|
+
return {
|
|
313
|
+
schema_version: 1,
|
|
314
|
+
base_url: requireString(value.base_url, source),
|
|
315
|
+
room_id: requireString(value.room_id, source),
|
|
316
|
+
member_id: requireString(value.member_id, source),
|
|
317
|
+
name: requireString(value.name, source),
|
|
318
|
+
member_token: requireString(value.member_token, source),
|
|
319
|
+
joined_at: requireString(value.joined_at, source),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export async function writeRoomCredential(paths, credential) {
|
|
323
|
+
await writeSecureJson(roomCredentialFile(paths, credential.room_id, credential.member_id), credential);
|
|
324
|
+
}
|
|
325
|
+
export async function readRoomCredential(paths, roomId, memberId) {
|
|
326
|
+
const raw = await readSecureFile(roomCredentialFile(paths, roomId, memberId));
|
|
327
|
+
return raw === null ? null : parseRoomCredential(raw);
|
|
328
|
+
}
|
|
329
|
+
function projectStateFile(cwd) {
|
|
330
|
+
return join(cwd, PROJECT_STATE_DIR, "room.json");
|
|
331
|
+
}
|
|
332
|
+
export async function readProjectRoomState(cwd) {
|
|
333
|
+
let raw;
|
|
334
|
+
try {
|
|
335
|
+
raw = await readFile(projectStateFile(cwd), "utf8");
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (error.code === "ENOENT")
|
|
339
|
+
return null;
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
const value = parseJsonObject(raw, "The project Room state");
|
|
343
|
+
if (value.schema_version !== 1) {
|
|
344
|
+
throw localError("invalid_local_state", "The project Room state has an unsupported version.");
|
|
345
|
+
}
|
|
346
|
+
const source = "The project Room state";
|
|
347
|
+
const lastSequence = value.last_sequence;
|
|
348
|
+
if (!Number.isSafeInteger(lastSequence) || lastSequence < 0) {
|
|
349
|
+
throw localError("invalid_local_state", `${source} is not valid SharedNet state.`);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
schema_version: 1,
|
|
353
|
+
base_url: requireString(value.base_url, source),
|
|
354
|
+
room_id: requireString(value.room_id, source),
|
|
355
|
+
member_id: requireString(value.member_id, source),
|
|
356
|
+
last_sequence: lastSequence,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export async function writeProjectRoomState(cwd, state) {
|
|
360
|
+
const directory = join(cwd, PROJECT_STATE_DIR);
|
|
361
|
+
await mkdir(directory, { recursive: true });
|
|
362
|
+
// The directory ignores itself, so a project that has no .gitignore entry for
|
|
363
|
+
// it still never commits a cursor file by accident.
|
|
364
|
+
await writeFile(join(directory, ".gitignore"), "*\n", { flag: "w" });
|
|
365
|
+
const temp = join(directory, `.room-${randomBytes(6).toString("hex")}.tmp`);
|
|
366
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
367
|
+
await rename(temp, projectStateFile(cwd));
|
|
368
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharednet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The SharedNet CLI: join a Room, say, wait, watch, and form groups of coding Agents by Instance id.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sharednet": "bin/sharednet.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/cli.d.ts",
|
|
13
|
+
"import": "./dist/cli.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"prepack": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.18"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Aicoo-Team/SharedNet.git",
|
|
33
|
+
"directory": "packages/cli"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://www.sharednet.ai/skills",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Aicoo-Team/SharedNet/issues"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"sharednet",
|
|
41
|
+
"agents",
|
|
42
|
+
"claude-code",
|
|
43
|
+
"codex",
|
|
44
|
+
"rooms",
|
|
45
|
+
"cli"
|
|
46
|
+
],
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "26.4.1",
|
|
49
|
+
"typescript": "7.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|