@shogo-ai/worker 1.0.10

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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +432 -0
  3. package/bin/shogo.mjs +38 -0
  4. package/dist/cli.mjs +64 -0
  5. package/package.json +62 -0
  6. package/src/cli.ts +199 -0
  7. package/src/commands/agent.ts +123 -0
  8. package/src/commands/config.ts +29 -0
  9. package/src/commands/doctor.ts +128 -0
  10. package/src/commands/login.ts +126 -0
  11. package/src/commands/logs.ts +24 -0
  12. package/src/commands/project-checkout.ts +179 -0
  13. package/src/commands/project-pull.ts +133 -0
  14. package/src/commands/project-push.ts +89 -0
  15. package/src/commands/runtime.ts +104 -0
  16. package/src/commands/start.ts +290 -0
  17. package/src/commands/status.ts +22 -0
  18. package/src/commands/stop.ts +13 -0
  19. package/src/lib/__tests__/agent-launch.test.ts +92 -0
  20. package/src/lib/__tests__/cloud-login-coverage.test.ts +210 -0
  21. package/src/lib/__tests__/cloud-login.test.ts +218 -0
  22. package/src/lib/__tests__/cloud-sync-watcher.test.ts +214 -0
  23. package/src/lib/__tests__/config.test.ts +141 -0
  24. package/src/lib/__tests__/db-doctor.test.ts +204 -0
  25. package/src/lib/__tests__/git-cloner.test.ts +382 -0
  26. package/src/lib/__tests__/runtime-manager-auto-pull.test.ts +275 -0
  27. package/src/lib/__tests__/runtime-manager-cloud-sync-gate.test.ts +119 -0
  28. package/src/lib/__tests__/runtime-manager-coverage-gaps.test.ts +768 -0
  29. package/src/lib/__tests__/runtime-manager-describe-rejection.test.ts +42 -0
  30. package/src/lib/__tests__/runtime-manager-git-pull.test.ts +207 -0
  31. package/src/lib/__tests__/runtime-manager-idle-eviction.test.ts +196 -0
  32. package/src/lib/__tests__/runtime-manager-restart-cap.test.ts +310 -0
  33. package/src/lib/__tests__/runtime-manager-tree-sitter-env.test.ts +189 -0
  34. package/src/lib/__tests__/runtime-manager-wait-for-health.test.ts +269 -0
  35. package/src/lib/__tests__/runtime-resolver.test.ts +112 -0
  36. package/src/lib/__tests__/tunnel-coverage.test.ts +1094 -0
  37. package/src/lib/__tests__/tunnel-structured-502.test.ts +101 -0
  38. package/src/lib/__tests__/worker-commands-greenfield.test.ts +463 -0
  39. package/src/lib/__tests__/worker-libs-greenfield.test.ts +355 -0
  40. package/src/lib/__tests__/worker-transport-greenfield.test.ts +176 -0
  41. package/src/lib/api-discovery.ts +36 -0
  42. package/src/lib/cloud-login.ts +320 -0
  43. package/src/lib/cloud-sync-watcher.ts +322 -0
  44. package/src/lib/config.ts +68 -0
  45. package/src/lib/db-doctor.ts +355 -0
  46. package/src/lib/device-id.ts +27 -0
  47. package/src/lib/git-cloner.ts +354 -0
  48. package/src/lib/paths.ts +53 -0
  49. package/src/lib/preflight.ts +158 -0
  50. package/src/lib/process-manager.ts +123 -0
  51. package/src/lib/resource-limits.ts +340 -0
  52. package/src/lib/runtime-install.ts +371 -0
  53. package/src/lib/runtime-manager.ts +1873 -0
  54. package/src/lib/runtime-resolver.ts +136 -0
  55. package/src/lib/transport.ts +202 -0
  56. package/src/lib/tunnel.ts +710 -0
  57. package/src/lib/user-agent.ts +28 -0
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Reads/writes ~/.shogo/config.json. The worker reads this at start,
5
+ * and the `config` / `login` commands write to it.
6
+ */
7
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
8
+ import { hostname } from 'node:os';
9
+ import { CONFIG_FILE, PROJECTS_DIR, ensureHome } from './paths.ts';
10
+
11
+ export interface WorkerConfig {
12
+ apiKey?: string;
13
+ cloudUrl?: string;
14
+ name?: string;
15
+ workerDir?: string;
16
+ port?: number;
17
+ /** Root directory under which pulled project workspaces are stored. */
18
+ projectsDir?: string;
19
+ }
20
+
21
+ const DEFAULTS: { cloudUrl: string; port: number; projectsDir: string } = {
22
+ cloudUrl: 'https://studio.shogo.ai',
23
+ port: 8002,
24
+ projectsDir: PROJECTS_DIR,
25
+ };
26
+
27
+ export function loadConfig(): WorkerConfig {
28
+ if (!existsSync(CONFIG_FILE)) return {};
29
+ try {
30
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
31
+ } catch (err) {
32
+ throw new Error(`Corrupt config at ${CONFIG_FILE}: ${(err as Error).message}`);
33
+ }
34
+ }
35
+
36
+ export function saveConfig(cfg: WorkerConfig): void {
37
+ ensureHome();
38
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
39
+ chmodSync(CONFIG_FILE, 0o600);
40
+ }
41
+
42
+ export function mergeConfig(base: WorkerConfig, override: WorkerConfig): WorkerConfig {
43
+ return { ...base, ...Object.fromEntries(Object.entries(override).filter(([, v]) => v !== undefined)) };
44
+ }
45
+
46
+ export function resolveConfig(override: WorkerConfig = {}): Required<WorkerConfig> & { apiKey: string } {
47
+ const fileCfg = loadConfig();
48
+ const env: WorkerConfig = {
49
+ apiKey: process.env.SHOGO_API_KEY,
50
+ cloudUrl: process.env.SHOGO_CLOUD_URL,
51
+ name: process.env.SHOGO_INSTANCE_NAME,
52
+ workerDir: process.env.SHOGO_WORKER_DIR,
53
+ port: process.env.PORT ? parseInt(process.env.PORT, 10) : undefined,
54
+ projectsDir: process.env.SHOGO_PROJECTS_DIR,
55
+ };
56
+ const merged = mergeConfig(mergeConfig(fileCfg, env), override);
57
+ if (!merged.apiKey) {
58
+ throw new Error('No API key. Run `shogo login` or pass --api-key / set SHOGO_API_KEY.');
59
+ }
60
+ return {
61
+ apiKey: merged.apiKey,
62
+ cloudUrl: merged.cloudUrl ?? DEFAULTS.cloudUrl,
63
+ name: merged.name ?? hostname(),
64
+ workerDir: merged.workerDir ?? process.cwd(),
65
+ port: merged.port ?? DEFAULTS.port,
66
+ projectsDir: merged.projectsDir ?? DEFAULTS.projectsDir,
67
+ };
68
+ }
@@ -0,0 +1,355 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ //
4
+ // Local SQLite migration "doctor" for the Shogo desktop app.
5
+ //
6
+ // The packaged desktop app stores its database at
7
+ // `<userData>/data/shogo.db` and applies schema with
8
+ // `prisma migrate deploy` on every launch. When a migration throws
9
+ // mid-way it leaves a row in `_prisma_migrations` with
10
+ // `finished_at = NULL`, and Prisma's P3009 check then refuses to run
11
+ // ANY further migrations — the app is wedged until that ledger row is
12
+ // cleared. The desktop app surfaces a recovery dialog when this happens
13
+ // on boot (see apps/desktop/src/db-recovery.ts), but a user whose app
14
+ // won't open, or who is being walked through a fix by support, has no
15
+ // way to trigger that repair from a terminal.
16
+ //
17
+ // `shogo doctor` (this module) is that terminal entry point. It performs
18
+ // the SAME safe, dependency-free repair the desktop dialog does:
19
+ //
20
+ // 1. detectFailedMigrations() — find stuck `_prisma_migrations` rows.
21
+ // 2. backupDatabase() — snapshot `shogo.db` (+ -wal/-shm)
22
+ // before touching anything.
23
+ // 3. repairFailedMigrations() — delete the stuck rows (equivalent to
24
+ // `prisma migrate resolve --rolled-back`).
25
+ //
26
+ // It deliberately does NOT re-run `prisma migrate deploy` itself — the
27
+ // CLI doesn't ship the Prisma schema/migration history that lives inside
28
+ // the desktop app bundle. Instead it clears the wedge and tells the user
29
+ // to relaunch Shogo, which re-applies migrations on its next boot.
30
+ //
31
+ // Why shell out to bun rather than link a SQLite driver
32
+ // -----------------------------------------------------
33
+ // The worker CLI runs under Node (npm install) OR Bun (tarball release),
34
+ // and we don't want a native `better-sqlite3` build step. The desktop
35
+ // app already ships a `bun` binary with `bun:sqlite` statically linked,
36
+ // and that's the exact SQLite version Prisma's bun-sqlite adapter uses at
37
+ // runtime — so running `bun -e "<small script>"` against it is both
38
+ // dependency-free and driver-version-matched. This mirrors
39
+ // apps/desktop/src/db-recovery.ts (kept as a separate copy there because
40
+ // the Electron main process is bundled in a different runtime context).
41
+
42
+ import { execFileSync } from 'node:child_process';
43
+ import { existsSync, copyFileSync } from 'node:fs';
44
+ import path from 'node:path';
45
+ import { homedir, platform } from 'node:os';
46
+
47
+ export interface FailedMigration {
48
+ name: string;
49
+ /** Epoch milliseconds the migration was attempted. */
50
+ startedAt: number;
51
+ /** First 600 chars of the Prisma error log row, for display. */
52
+ errorExcerpt: string;
53
+ }
54
+
55
+ /**
56
+ * Run a one-shot bun script against the given DB and parse its stdout
57
+ * as JSON. The script reads the DB path from `process.env.DBP` so we
58
+ * don't have to escape paths (which can contain spaces) through shell
59
+ * quoting. Throws if bun isn't usable or the script bails — we do NOT
60
+ * swallow these, since a broken recovery layer should surface its own
61
+ * defect rather than a misleading "database looks fine".
62
+ */
63
+ function runBunScript<T>(bunPath: string, dbPath: string, script: string): T {
64
+ const out = execFileSync(bunPath, ['-e', script], {
65
+ env: { ...process.env, DBP: dbPath },
66
+ encoding: 'utf-8',
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ timeout: 5000,
69
+ });
70
+ const trimmed = out.trim();
71
+ if (!trimmed) {
72
+ throw new Error(`bun script returned empty output for ${dbPath}`);
73
+ }
74
+ return JSON.parse(trimmed) as T;
75
+ }
76
+
77
+ /**
78
+ * Return `_prisma_migrations` rows that are still failed (`finished_at`
79
+ * NULL) and not yet recovered (`rolled_back_at` NULL). Returns an empty
80
+ * array when the DB doesn't exist, the table hasn't been created, or no
81
+ * failures are present.
82
+ */
83
+ export function detectFailedMigrations(bunPath: string, dbPath: string): FailedMigration[] {
84
+ if (!existsSync(dbPath)) return [];
85
+
86
+ const script = `
87
+ import { Database } from 'bun:sqlite';
88
+ try {
89
+ const db = new Database(process.env.DBP, { readonly: true });
90
+ const hasTable = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='_prisma_migrations'").get();
91
+ if (!hasTable) { console.log('[]'); process.exit(0); }
92
+ const rows = db
93
+ .query("SELECT migration_name as name, started_at as startedAt, substr(coalesce(logs, ''), 1, 600) as errorExcerpt FROM _prisma_migrations WHERE finished_at IS NULL AND rolled_back_at IS NULL ORDER BY started_at")
94
+ .all();
95
+ console.log(JSON.stringify(rows));
96
+ } catch (e) {
97
+ console.error(String(e?.stack || e));
98
+ process.exit(2);
99
+ }
100
+ `;
101
+
102
+ return runBunScript<FailedMigration[]>(bunPath, dbPath, script);
103
+ }
104
+
105
+ /**
106
+ * Snapshot `shogo.db` (plus `-wal`/`-shm` sidecars if present) to a
107
+ * timestamped sibling file. Returns the backup path of the main DB.
108
+ * Uses plain copies (not SQLite's online backup) because the DB is not
109
+ * open at this point and `copyFileSync` works even on a DB SQLite would
110
+ * refuse to open. Throws on any I/O failure — the caller MUST treat that
111
+ * as "do not proceed with repair".
112
+ */
113
+ export function backupDatabase(dbPath: string): string {
114
+ if (!existsSync(dbPath)) {
115
+ throw new Error(`Database does not exist at ${dbPath} — refusing to back up nothing`);
116
+ }
117
+ const dir = path.dirname(dbPath);
118
+ const base = path.basename(dbPath);
119
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
120
+ const backupPath = path.join(dir, `${base}.bak-${stamp}`);
121
+ copyFileSync(dbPath, backupPath);
122
+
123
+ for (const suffix of ['-wal', '-shm']) {
124
+ const sidecar = `${dbPath}${suffix}`;
125
+ if (existsSync(sidecar)) {
126
+ copyFileSync(sidecar, `${backupPath}${suffix}`);
127
+ }
128
+ }
129
+ return backupPath;
130
+ }
131
+
132
+ /**
133
+ * Delete the named failed-migration rows from `_prisma_migrations`. This
134
+ * is the equivalent of `prisma migrate resolve --rolled-back <name>` for
135
+ * each, but without needing the schema-engine binary. Only deletes rows
136
+ * that are actually still failed (defends against a stale name list).
137
+ * Returns the number of rows deleted.
138
+ *
139
+ * The caller is expected to have run `backupDatabase()` first. After this,
140
+ * the next `prisma migrate deploy` (on the desktop app's next launch)
141
+ * re-attempts the migration.
142
+ */
143
+ export function repairFailedMigrations(
144
+ bunPath: string,
145
+ dbPath: string,
146
+ migrationNames: string[],
147
+ ): number {
148
+ if (migrationNames.length === 0) return 0;
149
+ if (!existsSync(dbPath)) {
150
+ throw new Error(`Cannot repair: database does not exist at ${dbPath}`);
151
+ }
152
+
153
+ const namesJson = JSON.stringify(migrationNames);
154
+ const script = `
155
+ import { Database } from 'bun:sqlite';
156
+ const names = ${namesJson};
157
+ if (!Array.isArray(names) || names.some(n => typeof n !== 'string')) {
158
+ console.error('Invalid migration name list');
159
+ process.exit(2);
160
+ }
161
+ try {
162
+ const db = new Database(process.env.DBP, { create: false, readwrite: true });
163
+ const placeholders = names.map(() => '?').join(',');
164
+ const stmt = db.prepare(
165
+ \`DELETE FROM _prisma_migrations WHERE migration_name IN (\${placeholders}) AND finished_at IS NULL AND rolled_back_at IS NULL\`
166
+ );
167
+ const result = stmt.run(...names);
168
+ console.log(JSON.stringify({ deleted: Number(result.changes) }));
169
+ } catch (e) {
170
+ console.error(String(e?.stack || e));
171
+ process.exit(3);
172
+ }
173
+ `;
174
+
175
+ const out = runBunScript<{ deleted: number }>(bunPath, dbPath, script);
176
+ return out.deleted;
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Orchestrator
181
+ // ---------------------------------------------------------------------------
182
+
183
+ export type DoctorStatus = 'healthy' | 'no-database' | 'repaired' | 'failed';
184
+
185
+ export interface DoctorResult {
186
+ status: DoctorStatus;
187
+ /** Migrations found in a failed state before repair. */
188
+ detected: FailedMigration[];
189
+ /** Path of the backup written before repair, if any. */
190
+ backupPath?: string;
191
+ /** Names of migration rows actually cleared. */
192
+ cleared: string[];
193
+ /** Migrations still failed after the repair attempt (should be empty on success). */
194
+ remaining: FailedMigration[];
195
+ /** Human-readable summary of what happened. */
196
+ message: string;
197
+ }
198
+
199
+ export interface DoctorOptions {
200
+ bunPath: string;
201
+ dbPath: string;
202
+ /** Skip the pre-repair backup (default: false). Discouraged. */
203
+ skipBackup?: boolean;
204
+ /** Logger for progress lines (default: no-op). */
205
+ log?: (line: string) => void;
206
+ }
207
+
208
+ /**
209
+ * Run the full safe repair sequence against a local SQLite DB:
210
+ * detect → backup → clear stuck ledger rows → re-detect. Idempotent:
211
+ * a healthy DB is a no-op. Never re-runs `migrate deploy` (that's the
212
+ * desktop app's job on next launch).
213
+ */
214
+ export function runDatabaseDoctor(opts: DoctorOptions): DoctorResult {
215
+ const log = opts.log ?? (() => {});
216
+ const { bunPath, dbPath } = opts;
217
+
218
+ if (!existsSync(dbPath)) {
219
+ return {
220
+ status: 'no-database',
221
+ detected: [],
222
+ cleared: [],
223
+ remaining: [],
224
+ message: `No database found at ${dbPath}. Nothing to repair — launch Shogo once to create it.`,
225
+ };
226
+ }
227
+
228
+ const detected = detectFailedMigrations(bunPath, dbPath);
229
+ if (detected.length === 0) {
230
+ return {
231
+ status: 'healthy',
232
+ detected: [],
233
+ cleared: [],
234
+ remaining: [],
235
+ message: 'No failed migrations detected — the local database looks healthy.',
236
+ };
237
+ }
238
+
239
+ log(`Found ${detected.length} failed migration(s): ${detected.map((m) => m.name).join(', ')}`);
240
+
241
+ let backupPath: string | undefined;
242
+ if (!opts.skipBackup) {
243
+ backupPath = backupDatabase(dbPath);
244
+ log(`Backed up database to ${backupPath}`);
245
+ }
246
+
247
+ const names = detected.map((m) => m.name);
248
+ const deleted = repairFailedMigrations(bunPath, dbPath, names);
249
+ log(`Cleared ${deleted} failed migration row(s).`);
250
+
251
+ const remaining = detectFailedMigrations(bunPath, dbPath);
252
+ const status: DoctorStatus = remaining.length === 0 ? 'repaired' : 'failed';
253
+ const message =
254
+ status === 'repaired'
255
+ ? 'Cleared the failed migration record. Relaunch Shogo to re-apply migrations cleanly.'
256
+ : `Repair incomplete — ${remaining.length} migration(s) still failed: ${remaining
257
+ .map((m) => m.name)
258
+ .join(', ')}.`;
259
+
260
+ return {
261
+ status,
262
+ detected,
263
+ backupPath,
264
+ cleared: names.slice(0, deleted),
265
+ remaining,
266
+ message,
267
+ };
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Path / binary resolution (for the standalone CLI)
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * Resolve the desktop app's per-user data directory, mirroring
276
+ * Electron's `app.getPath('userData')` + the `data/` subdir used by
277
+ * apps/desktop/src/paths.ts. `productName` is "Shogo".
278
+ *
279
+ * macOS: ~/Library/Application Support/Shogo/data
280
+ * Windows: %APPDATA%/Shogo/data (Roaming)
281
+ * Linux: $XDG_CONFIG_HOME/Shogo/data (or ~/.config/Shogo/data)
282
+ */
283
+ export function resolveDesktopDataDir(): string {
284
+ const home = homedir();
285
+ const plat = platform();
286
+ let appData: string;
287
+ if (plat === 'darwin') {
288
+ appData = path.join(home, 'Library', 'Application Support');
289
+ } else if (plat === 'win32') {
290
+ appData = process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming');
291
+ } else {
292
+ appData = process.env.XDG_CONFIG_HOME ?? path.join(home, '.config');
293
+ }
294
+ return path.join(appData, 'Shogo', 'data');
295
+ }
296
+
297
+ /** Default path of the desktop app's local SQLite database. */
298
+ export function resolveDesktopDbPath(): string {
299
+ return path.join(resolveDesktopDataDir(), 'shogo.db');
300
+ }
301
+
302
+ /**
303
+ * Candidate locations of the `bun` binary the installed desktop app
304
+ * ships in its `resources/bun/` directory. Best-effort and
305
+ * platform-specific; missing entries are filtered out by the caller.
306
+ */
307
+ function bundledBunCandidates(): string[] {
308
+ const plat = platform();
309
+ const exe = plat === 'win32' ? 'bun.exe' : 'bun';
310
+ const home = homedir();
311
+ const candidates: string[] = [];
312
+ if (plat === 'darwin') {
313
+ candidates.push(
314
+ path.join('/Applications', 'Shogo.app', 'Contents', 'Resources', 'bun', exe),
315
+ path.join(home, 'Applications', 'Shogo.app', 'Contents', 'Resources', 'bun', exe),
316
+ );
317
+ } else if (plat === 'linux') {
318
+ candidates.push(
319
+ path.join('/opt', 'Shogo', 'resources', 'bun', exe),
320
+ path.join('/usr', 'lib', 'shogo', 'resources', 'bun', exe),
321
+ );
322
+ }
323
+ // Windows installs under a version-stamped Squirrel dir
324
+ // (%LOCALAPPDATA%/shogo/app-<ver>/resources/bun/bun.exe) which we can't
325
+ // resolve without globbing; rely on --bun / PATH there.
326
+ return candidates;
327
+ }
328
+
329
+ /** True if the given binary can be executed and reports a version. */
330
+ function bunIsUsable(bunPath: string): boolean {
331
+ try {
332
+ execFileSync(bunPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
333
+ return true;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Resolve a usable `bun` binary for the repair scripts, in priority order:
341
+ * 1. explicit `override` (the `--bun` flag)
342
+ * 2. the bun currently running this CLI (tarball release)
343
+ * 3. the desktop app's bundled bun
344
+ * 4. `bun` on PATH
345
+ * Returns null if none are usable.
346
+ */
347
+ export function resolveBunBinary(override?: string): string | null {
348
+ if (override) return bunIsUsable(override) ? override : null;
349
+ if (process.versions.bun && process.execPath) return process.execPath;
350
+ for (const candidate of bundledBunCandidates()) {
351
+ if (existsSync(candidate) && bunIsUsable(candidate)) return candidate;
352
+ }
353
+ if (bunIsUsable('bun')) return 'bun';
354
+ return null;
355
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Stable per-machine UUID stored at ~/.shogo/device-id.
5
+ *
6
+ * The cloud uses (workspaceId, deviceId) as a dedupe key — when the
7
+ * same machine re-runs `shogo login`, the previous device-tagged API
8
+ * key is auto-revoked so we don't accumulate "ghost" device rows in
9
+ * the dashboard. Stability across re-logins is what makes this work,
10
+ * so we persist the id at first generation rather than rolling a new
11
+ * one each time.
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { DEVICE_ID_FILE, ensureHome } from './paths.ts';
16
+
17
+ export function getOrCreateDeviceId(): string {
18
+ if (existsSync(DEVICE_ID_FILE)) {
19
+ const value = readFileSync(DEVICE_ID_FILE, 'utf-8').trim();
20
+ if (value) return value;
21
+ }
22
+ ensureHome();
23
+ const id = randomUUID();
24
+ writeFileSync(DEVICE_ID_FILE, id, { mode: 0o600 });
25
+ chmodSync(DEVICE_ID_FILE, 0o600);
26
+ return id;
27
+ }