@babylen/legion 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.
Files changed (3) hide show
  1. package/README.md +86 -0
  2. package/dist/index.js +361 -0
  3. package/package.json +66 -0
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Tanuki Legion
2
+
3
+ Legion is a lightweight agent that connects your devices to Tanuki Cloud, enabling remote development and automation capabilities.
4
+
5
+ ## Installation
6
+
7
+ ### Quick Install
8
+
9
+ ```bash
10
+ curl -sL https://tanuki.sabw.ru/install-legion | LEGION_TOKEN=your_token_here bash
11
+ ```
12
+
13
+ ### Manual Installation
14
+
15
+ 1. **Install dependencies:**
16
+
17
+ ```bash
18
+ bun install
19
+ ```
20
+
21
+ 2. **Configure:**
22
+
23
+ Create `~/.tanuki/config.json`:
24
+
25
+ ```json
26
+ {
27
+ "token": "your_legion_token",
28
+ "serverUrl": "wss://tanuki.sabw.ru"
29
+ }
30
+ ```
31
+
32
+ Or set environment variables:
33
+
34
+ ```bash
35
+ export LEGION_TOKEN=your_token_here
36
+ export TANUKI_SERVER_URL=wss://tanuki.sabw.ru
37
+ ```
38
+
39
+ 3. **Run:**
40
+
41
+ ```bash
42
+ bun run dev
43
+ ```
44
+
45
+ ## Features
46
+
47
+ - 🔗 **Cloud Connection**: Seamlessly connect to Tanuki Cloud via WebSocket
48
+ - 🛡️ **Secure Authentication**: Token-based authentication for device access
49
+ - 🔄 **Auto-Reconnection**: Automatic reconnection on network interruptions
50
+ - 📊 **Device Management**: Manage multiple devices from Tanuki Cloud dashboard
51
+
52
+ ## Configuration
53
+
54
+ Configuration is stored in `~/.tanuki/config.json`:
55
+
56
+ - `token`: Your Legion device token (required)
57
+ - `serverUrl`: Tanuki Cloud server URL (default: `wss://tanuki.sabw.ru`)
58
+
59
+ ## Getting a Token
60
+
61
+ 1. Log in to [Tanuki Cloud](https://tanuki.sabw.ru)
62
+ 2. Go to Settings → Devices
63
+ 3. Click "Connect New Device"
64
+ 4. Copy the installation command or token
65
+
66
+ ## Development
67
+
68
+ This project uses [Bun](https://bun.sh) as the JavaScript runtime.
69
+
70
+ ```bash
71
+ # Install dependencies
72
+ bun install
73
+
74
+ # Run in development mode
75
+ bun run dev
76
+
77
+ # Type check
78
+ bun run typecheck
79
+
80
+ # Build
81
+ bun run build
82
+ ```
83
+
84
+ ## License
85
+
86
+ Proprietary - Tanuki Cloud
package/dist/index.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_socket = require("socket.io-client");
28
+ var os3 = __toESM(require("os"));
29
+
30
+ // src/core/config.ts
31
+ var import_os = __toESM(require("os"));
32
+ var import_path = __toESM(require("path"));
33
+ var import_promises = __toESM(require("fs/promises"));
34
+ var import_zod = require("zod");
35
+ var ConfigSchema = import_zod.z.object({
36
+ id: import_zod.z.string().optional(),
37
+ // Token ID from database (for faster lookup)
38
+ token: import_zod.z.string(),
39
+ // Secret token (lg_xxx format)
40
+ serverUrl: import_zod.z.string().url()
41
+ });
42
+ var HOME_DIR = import_os.default.homedir();
43
+ var CONFIG_DIR = import_path.default.join(HOME_DIR, ".tanuki");
44
+ var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
45
+ async function loadConfig() {
46
+ try {
47
+ const content = await import_promises.default.readFile(CONFIG_FILE, "utf-8");
48
+ const data2 = JSON.parse(content);
49
+ return ConfigSchema.parse(data2);
50
+ } catch (error) {
51
+ return null;
52
+ }
53
+ }
54
+ async function getConfig() {
55
+ const fileConfig = await loadConfig();
56
+ const id = fileConfig?.id || process.env.LEGION_TOKEN_ID || void 0;
57
+ const token = fileConfig?.token || process.env.LEGION_TOKEN_SECRET || process.env.LEGION_TOKEN;
58
+ const serverUrl = fileConfig?.serverUrl || process.env.TANUKI_SERVER_URL || "wss://tanuki.sabw.ru";
59
+ if (!token) {
60
+ throw new Error(
61
+ "Legion token not found. Please set LEGION_TOKEN_SECRET (or LEGION_TOKEN) environment variable or configure ~/.tanuki/config.json"
62
+ );
63
+ }
64
+ const config2 = {
65
+ token,
66
+ serverUrl
67
+ };
68
+ if (id) {
69
+ config2.id = id;
70
+ }
71
+ return config2;
72
+ }
73
+
74
+ // src/util/log.ts
75
+ var import_path3 = __toESM(require("path"));
76
+ var import_promises3 = __toESM(require("fs/promises"));
77
+ var import_fs = require("fs");
78
+
79
+ // src/core/global.ts
80
+ var import_os2 = __toESM(require("os"));
81
+ var import_path2 = __toESM(require("path"));
82
+ var import_promises2 = __toESM(require("fs/promises"));
83
+ var HOME_DIR2 = import_os2.default.homedir();
84
+ var data = import_path2.default.join(HOME_DIR2, ".tanuki", "legion");
85
+ var cache = import_path2.default.join(HOME_DIR2, ".tanuki", "legion", "cache");
86
+ var config = import_path2.default.join(HOME_DIR2, ".tanuki");
87
+ var state = import_path2.default.join(HOME_DIR2, ".tanuki", "legion", "state");
88
+ var Global;
89
+ ((Global2) => {
90
+ Global2.Path = {
91
+ // Allow override via LEGION_TEST_HOME for test isolation
92
+ get home() {
93
+ return process.env.LEGION_TEST_HOME || import_os2.default.homedir();
94
+ },
95
+ data,
96
+ bin: import_path2.default.join(data, "bin"),
97
+ log: import_path2.default.join(data, "log"),
98
+ cache,
99
+ config,
100
+ state
101
+ };
102
+ async function init() {
103
+ await Promise.all([
104
+ import_promises2.default.mkdir(Global2.Path.data, { recursive: true }),
105
+ import_promises2.default.mkdir(Global2.Path.config, { recursive: true }),
106
+ import_promises2.default.mkdir(Global2.Path.state, { recursive: true }),
107
+ import_promises2.default.mkdir(Global2.Path.log, { recursive: true }),
108
+ import_promises2.default.mkdir(Global2.Path.bin, { recursive: true })
109
+ ]);
110
+ }
111
+ Global2.init = init;
112
+ })(Global || (Global = {}));
113
+
114
+ // src/util/log.ts
115
+ var import_zod2 = __toESM(require("zod"));
116
+ var Log;
117
+ ((Log2) => {
118
+ Log2.Level = import_zod2.default.enum(["DEBUG", "INFO", "WARN", "ERROR"]);
119
+ const levelPriority = {
120
+ DEBUG: 0,
121
+ INFO: 1,
122
+ WARN: 2,
123
+ ERROR: 3
124
+ };
125
+ let level = "INFO";
126
+ function shouldLog(input) {
127
+ return levelPriority[input] >= levelPriority[level];
128
+ }
129
+ const loggers = /* @__PURE__ */ new Map();
130
+ Log2.Default = create({ service: "default" });
131
+ let logpath = "";
132
+ function file() {
133
+ return logpath;
134
+ }
135
+ Log2.file = file;
136
+ let write = (msg) => {
137
+ process.stderr.write(msg);
138
+ return msg.length;
139
+ };
140
+ let logStream = null;
141
+ async function init(options) {
142
+ if (options.level) level = options.level;
143
+ await Global.init().catch(() => {
144
+ });
145
+ cleanup(Global.Path.log).catch(() => {
146
+ });
147
+ if (options.print) return;
148
+ logpath = import_path3.default.join(
149
+ Global.Path.log,
150
+ options.dev ? "dev.log" : (/* @__PURE__ */ new Date()).toISOString().split(".")[0].replace(/:/g, "") + ".log"
151
+ );
152
+ try {
153
+ await import_promises3.default.writeFile(logpath, "", { flag: "w" });
154
+ logStream = (0, import_fs.createWriteStream)(logpath, { flags: "a" });
155
+ } catch {
156
+ }
157
+ write = (msg) => {
158
+ if (logStream) {
159
+ const written = logStream.write(msg);
160
+ return written ? msg.length : 0;
161
+ }
162
+ process.stderr.write(msg);
163
+ return msg.length;
164
+ };
165
+ }
166
+ Log2.init = init;
167
+ async function cleanup(dir) {
168
+ try {
169
+ const files = await import_promises3.default.readdir(dir);
170
+ const logFiles = files.filter((f) => /^\d{4}-\d{2}-\d{2}T\d{6}\.log$/.test(f));
171
+ if (logFiles.length <= 5) return;
172
+ const filesToDelete = logFiles.slice(0, -10);
173
+ await Promise.all(filesToDelete.map((file2) => import_promises3.default.unlink(import_path3.default.join(dir, file2)).catch(() => {
174
+ })));
175
+ } catch {
176
+ }
177
+ }
178
+ function formatError(error, depth = 0) {
179
+ const result = error.message;
180
+ return error.cause instanceof Error && depth < 10 ? result + " Caused by: " + formatError(error.cause, depth + 1) : result;
181
+ }
182
+ let last = Date.now();
183
+ function create(tags) {
184
+ tags = tags || {};
185
+ const service = tags["service"];
186
+ if (service && typeof service === "string") {
187
+ const cached = loggers.get(service);
188
+ if (cached) {
189
+ return cached;
190
+ }
191
+ }
192
+ function build(message, extra) {
193
+ const prefix = Object.entries({
194
+ ...tags,
195
+ ...extra
196
+ }).filter(([_, value]) => value !== void 0 && value !== null).map(([key, value]) => {
197
+ const prefix2 = `${key}=`;
198
+ if (value instanceof Error) return prefix2 + formatError(value);
199
+ if (typeof value === "object") return prefix2 + JSON.stringify(value);
200
+ return prefix2 + value;
201
+ }).join(" ");
202
+ const next = /* @__PURE__ */ new Date();
203
+ const diff = next.getTime() - last;
204
+ last = next.getTime();
205
+ return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n";
206
+ }
207
+ const result = {
208
+ debug(message, extra) {
209
+ if (shouldLog("DEBUG")) {
210
+ write("DEBUG " + build(message, extra));
211
+ }
212
+ },
213
+ info(message, extra) {
214
+ if (shouldLog("INFO")) {
215
+ write("INFO " + build(message, extra));
216
+ }
217
+ },
218
+ error(message, extra) {
219
+ if (shouldLog("ERROR")) {
220
+ write("ERROR " + build(message, extra));
221
+ }
222
+ },
223
+ warn(message, extra) {
224
+ if (shouldLog("WARN")) {
225
+ write("WARN " + build(message, extra));
226
+ }
227
+ },
228
+ tag(key, value) {
229
+ if (tags) tags[key] = value;
230
+ return result;
231
+ },
232
+ clone() {
233
+ return Log2.create({ ...tags });
234
+ },
235
+ time(message, extra) {
236
+ const now = Date.now();
237
+ result.info(message, { status: "started", ...extra });
238
+ function stop() {
239
+ result.info(message, {
240
+ status: "completed",
241
+ duration: Date.now() - now,
242
+ ...extra
243
+ });
244
+ }
245
+ return {
246
+ stop,
247
+ [Symbol.dispose]() {
248
+ stop();
249
+ }
250
+ };
251
+ }
252
+ };
253
+ if (service && typeof service === "string") {
254
+ loggers.set(service, result);
255
+ }
256
+ return result;
257
+ }
258
+ Log2.create = create;
259
+ })(Log || (Log = {}));
260
+
261
+ // src/index.ts
262
+ var log = Log.create({ service: "legion" });
263
+ async function main() {
264
+ try {
265
+ await Global.init();
266
+ log.info("\u{1F6E1}\uFE0F Legion v0.1 starting...");
267
+ const config2 = await getConfig();
268
+ log.info("\u{1F517} Connecting to server", { serverUrl: config2.serverUrl });
269
+ const socket = (0, import_socket.io)(config2.serverUrl, {
270
+ auth: {
271
+ id: config2.id,
272
+ // Token ID for faster lookup (optional)
273
+ token: config2.token,
274
+ // Secret token for authentication
275
+ type: "legion"
276
+ },
277
+ transports: ["websocket"],
278
+ reconnection: true,
279
+ reconnectionDelay: 1e3,
280
+ reconnectionDelayMax: 5e3,
281
+ reconnectionAttempts: Infinity
282
+ });
283
+ socket.on("connect", () => {
284
+ log.info("\u2705 Connected to server", { socketId: socket.id });
285
+ const handshakeData = {
286
+ hostname: os3.hostname(),
287
+ platform: os3.platform(),
288
+ release: os3.release(),
289
+ cwd: process.cwd()
290
+ };
291
+ socket.emit("legion:handshake", handshakeData);
292
+ log.debug("\u{1F4E4} Sent handshake", handshakeData);
293
+ });
294
+ socket.on("connect_error", (err) => {
295
+ log.error("\u274C Connection error", {
296
+ message: err.message,
297
+ type: err.type
298
+ });
299
+ if (err.message.includes("auth") || err.message.includes("token")) {
300
+ log.error("\u{1F510} Authentication failed. Please check your token.");
301
+ log.error("\u{1F4A1} Re-authenticate by updating ~/.tanuki/config.json or LEGION_TOKEN env var");
302
+ process.exit(1);
303
+ }
304
+ });
305
+ socket.on("disconnect", (reason) => {
306
+ log.warn("\u26A0\uFE0F Disconnected from server", { reason });
307
+ if (reason === "io server disconnect") {
308
+ log.error("\u{1F6AB} Server disconnected this client. Please check your token.");
309
+ process.exit(1);
310
+ }
311
+ });
312
+ socket.on("server:ping", (data2) => {
313
+ log.debug("\u{1F4E9} Ping from server", data2);
314
+ socket.emit("legion:pong", { ts: Date.now() });
315
+ });
316
+ socket.on("reconnect", (attemptNumber) => {
317
+ log.info("\u{1F504} Reconnected to server", { attempt: attemptNumber });
318
+ });
319
+ socket.on("reconnect_attempt", (attemptNumber) => {
320
+ log.debug("\u{1F504} Reconnection attempt", { attempt: attemptNumber });
321
+ });
322
+ socket.on("reconnect_error", (error) => {
323
+ log.error("\u{1F504} Reconnection error", { message: error.message });
324
+ });
325
+ socket.on("reconnect_failed", () => {
326
+ log.error("\u{1F504} Reconnection failed after all attempts");
327
+ process.exit(1);
328
+ });
329
+ process.stdin.resume();
330
+ const shutdown = () => {
331
+ log.info("\u{1F6D1} Shutting down...");
332
+ socket.disconnect();
333
+ process.exit(0);
334
+ };
335
+ process.on("SIGINT", shutdown);
336
+ process.on("SIGTERM", shutdown);
337
+ process.on("unhandledRejection", (reason) => {
338
+ log.error("Unhandled rejection", { reason });
339
+ });
340
+ process.on("uncaughtException", (error) => {
341
+ log.error("Uncaught exception", { error: error.message, stack: error.stack });
342
+ shutdown();
343
+ });
344
+ } catch (error) {
345
+ if (error instanceof Error) {
346
+ log.error("Failed to start Legion", {
347
+ message: error.message,
348
+ stack: error.stack
349
+ });
350
+ console.error("\n\u274C", error.message);
351
+ console.error("\n\u{1F4A1} Make sure you have configured your token:");
352
+ console.error(" - Set LEGION_TOKEN environment variable, or");
353
+ console.error(" - Create ~/.tanuki/config.json with your token");
354
+ } else {
355
+ log.error("Failed to start Legion", { error });
356
+ console.error("\n\u274C Unknown error occurred");
357
+ }
358
+ process.exit(1);
359
+ }
360
+ }
361
+ main();
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@babylen/legion",
3
+ "version": "0.1.0",
4
+ "description": "Legion agent for connecting devices to Tanuki Cloud",
5
+ "main": "./dist/index.js",
6
+ "keywords": [
7
+ "legion"
8
+ ],
9
+ "author": "",
10
+ "license": "PROPRIETARY",
11
+ "engines": {
12
+ "node": ">=18.0.0"
13
+ },
14
+ "bin": {
15
+ "legion": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "package.json"
21
+ ],
22
+ "scripts": {
23
+ "dev": "bun run --conditions=browser ./src/index.ts",
24
+ "build": "tsup",
25
+ "prepublishOnly": "npm run build",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "bun test"
28
+ },
29
+ "dependencies": {
30
+ "socket.io-client": "^4.7.4",
31
+ "better-sqlite3": "^9.4.0",
32
+ "@opentui/core": "0.1.73",
33
+ "@opentui/solid": "0.1.73",
34
+ "solid-js": "^1.8.15",
35
+ "chokidar": "4.0.3",
36
+ "zod": "^3.22.4",
37
+ "bun-pty": "0.4.4",
38
+ "clipboardy": "4.0.0",
39
+ "diff": "^5.2.0",
40
+ "fuzzysort": "3.1.0",
41
+ "ignore": "7.0.5",
42
+ "jsonc-parser": "3.3.1",
43
+ "minimatch": "10.0.3",
44
+ "open": "10.1.2",
45
+ "opentui-spinner": "0.0.6",
46
+ "strip-ansi": "7.1.2",
47
+ "tree-sitter-bash": "0.25.0",
48
+ "turndown": "7.2.0",
49
+ "ulid": "^2.3.0",
50
+ "web-tree-sitter": "0.25.10",
51
+ "xdg-basedir": "5.1.0",
52
+ "yargs": "18.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@tsconfig/bun": "^1.0.4",
56
+ "@types/better-sqlite3": "^7.6.9",
57
+ "@types/bun": "latest",
58
+ "@types/node": "^25.0.9",
59
+ "@types/yargs": "17.0.33",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.9.3"
62
+ },
63
+ "trustedDependencies": [
64
+ "tree-sitter-bash"
65
+ ]
66
+ }