@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,133 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * `shogo project pull <projectId>` — clone a project's workspace from
5
+ * Shogo Cloud to a local directory. Optionally `--watch` starts a
6
+ * bidirectional sync: pull initially, then push any local edits back via
7
+ * the {@link CloudSyncWatcher}.
8
+ *
9
+ * This is the easy-button companion to pinning a project to a paired
10
+ * machine:
11
+ *
12
+ * shogo login # one-time pairing
13
+ * shogo project pull <projectId> # clones the staging snapshot locally
14
+ * shogo worker start # auto-routes pinned traffic here
15
+ */
16
+
17
+ import { existsSync, mkdirSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import pc from 'picocolors';
20
+ import { CloudFileTransport, type ProgressEvent, type SyncStats } from '@shogo-ai/sdk/cloud-file-transport';
21
+ import { resolveConfig } from '../lib/config.ts';
22
+ import { ensureProjectsDir, projectDirFor } from '../lib/paths.ts';
23
+ import { CloudSyncWatcher } from '../lib/cloud-sync-watcher.ts';
24
+
25
+ export interface ProjectPullFlags {
26
+ into?: string;
27
+ watch?: boolean;
28
+ include?: string;
29
+ apiKey?: string;
30
+ cloudUrl?: string;
31
+ }
32
+
33
+ export async function runProjectPull(projectId: string, flags: ProjectPullFlags): Promise<void> {
34
+ if (!projectId) throw new Error('projectId is required');
35
+
36
+ const cfg = resolveConfig({
37
+ apiKey: flags.apiKey,
38
+ cloudUrl: flags.cloudUrl,
39
+ });
40
+
41
+ ensureProjectsDir(cfg.projectsDir);
42
+ const into = resolve(flags.into ?? projectDirFor(projectId, cfg.projectsDir));
43
+ if (!existsSync(into)) {
44
+ mkdirSync(into, { recursive: true });
45
+ }
46
+
47
+ const include = flags.include?.split(',').map((s) => s.trim()).filter(Boolean);
48
+
49
+ console.log(pc.bold(`\nshogo project pull ${pc.cyan(projectId)}`));
50
+ console.log(pc.dim(' cloud ') + cfg.cloudUrl);
51
+ console.log(pc.dim(' into ') + into);
52
+ if (include?.length) console.log(pc.dim(' include ') + include.join(', '));
53
+ console.log('');
54
+
55
+ const transport = new CloudFileTransport({
56
+ apiUrl: cfg.cloudUrl,
57
+ apiKey: cfg.apiKey,
58
+ projectId,
59
+ localDir: into,
60
+ include,
61
+ onProgress: makeProgressReporter(),
62
+ });
63
+
64
+ const stats = await transport.downloadAll();
65
+ printSummary('Pull', stats);
66
+
67
+ if (flags.watch) {
68
+ console.log(pc.bold('\nWatching for local changes...'));
69
+ const watcher = new CloudSyncWatcher({
70
+ rootDir: into,
71
+ transport,
72
+ onFlush: ({ uploaded, errors }) => {
73
+ const list = uploaded.length === 1 ? uploaded[0] : `${uploaded.length} files`;
74
+ const errSuffix = errors > 0 ? pc.red(` (${errors} errors)`) : '';
75
+ console.log(pc.dim(` ↑ ${list}${errSuffix}`));
76
+ },
77
+ });
78
+ watcher.start();
79
+
80
+ let shuttingDown = false;
81
+ const shutdown = async (signal: NodeJS.Signals) => {
82
+ if (shuttingDown) return;
83
+ shuttingDown = true;
84
+ console.log(pc.dim(`\nReceived ${signal} — flushing pending uploads...`));
85
+ try {
86
+ await watcher.stop();
87
+ } catch (err: any) {
88
+ console.warn(pc.yellow(`watcher.stop: ${err?.message ?? err}`));
89
+ }
90
+ process.exit(0);
91
+ };
92
+ process.once('SIGINT', () => void shutdown('SIGINT'));
93
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
94
+ process.once('SIGHUP', () => void shutdown('SIGHUP'));
95
+
96
+ // Park the foreground process. Shutdown handler exits when the user terminates.
97
+ await new Promise<void>(() => { /* never resolves */ });
98
+ }
99
+ }
100
+
101
+ function makeProgressReporter(): (e: ProgressEvent) => void {
102
+ return (e) => {
103
+ const verb = e.kind === 'download' ? '↓' : e.kind === 'upload' ? '↑' : e.kind === 'delete' ? '✗' : '·';
104
+ const pct = e.total > 0 ? ` ${e.index + 1}/${e.total}` : '';
105
+ const sizeNote = e.bytes != null ? pc.dim(` (${formatBytes(e.bytes)})`) : '';
106
+ console.log(pc.dim(` ${verb}${pct} ${e.path}${sizeNote}`));
107
+ };
108
+ }
109
+
110
+ function printSummary(label: string, stats: SyncStats): void {
111
+ const ok = stats.errors.length === 0;
112
+ const head = ok ? pc.green(`✓ ${label} complete`) : pc.red(`✗ ${label} completed with errors`);
113
+ console.log(`\n${head}`);
114
+ console.log(pc.dim(' downloaded: ') + stats.downloaded);
115
+ if (stats.uploaded) console.log(pc.dim(' uploaded: ') + stats.uploaded);
116
+ if (stats.deleted) console.log(pc.dim(' deleted: ') + stats.deleted);
117
+ if (stats.skipped) console.log(pc.dim(' skipped: ') + stats.skipped);
118
+ if (!ok) {
119
+ console.log(pc.dim(' errors: ') + stats.errors.length);
120
+ for (const err of stats.errors.slice(0, 5)) {
121
+ console.log(pc.dim(' ') + pc.red(`${err.path}: ${err.message}`));
122
+ }
123
+ if (stats.errors.length > 5) {
124
+ console.log(pc.dim(` ... and ${stats.errors.length - 5} more`));
125
+ }
126
+ }
127
+ }
128
+
129
+ function formatBytes(n: number): string {
130
+ if (n < 1024) return `${n}B`;
131
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
132
+ return `${(n / 1024 / 1024).toFixed(2)}MB`;
133
+ }
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * `shogo project push <projectId>` — upload a local workspace directory
5
+ * back to Shogo Cloud. The inverse of `shogo project pull`.
6
+ *
7
+ * With `--delete-remote`, any file present in the cloud manifest but
8
+ * missing locally is also removed from cloud. This makes `push` behave
9
+ * like a one-shot "mirror local to cloud" rather than a strict additive
10
+ * upload — safer to leave off by default.
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { resolve } from 'node:path';
15
+ import pc from 'picocolors';
16
+ import { CloudFileTransport, type ProgressEvent, type SyncStats } from '@shogo-ai/sdk/cloud-file-transport';
17
+ import { resolveConfig } from '../lib/config.ts';
18
+ import { projectDirFor } from '../lib/paths.ts';
19
+
20
+ export interface ProjectPushFlags {
21
+ from?: string;
22
+ deleteRemote?: boolean;
23
+ include?: string;
24
+ apiKey?: string;
25
+ cloudUrl?: string;
26
+ }
27
+
28
+ export async function runProjectPush(projectId: string, flags: ProjectPushFlags): Promise<void> {
29
+ if (!projectId) throw new Error('projectId is required');
30
+
31
+ const cfg = resolveConfig({
32
+ apiKey: flags.apiKey,
33
+ cloudUrl: flags.cloudUrl,
34
+ });
35
+
36
+ const from = resolve(flags.from ?? projectDirFor(projectId, cfg.projectsDir));
37
+ if (!existsSync(from)) {
38
+ throw new Error(`Source directory does not exist: ${from}`);
39
+ }
40
+
41
+ const include = flags.include?.split(',').map((s) => s.trim()).filter(Boolean);
42
+
43
+ console.log(pc.bold(`\nshogo project push ${pc.cyan(projectId)}`));
44
+ console.log(pc.dim(' cloud ') + cfg.cloudUrl);
45
+ console.log(pc.dim(' from ') + from);
46
+ if (include?.length) console.log(pc.dim(' include ') + include.join(', '));
47
+ if (flags.deleteRemote) console.log(pc.yellow(' --delete-remote: remote files not present locally will be DELETED'));
48
+ console.log('');
49
+
50
+ const transport = new CloudFileTransport({
51
+ apiUrl: cfg.cloudUrl,
52
+ apiKey: cfg.apiKey,
53
+ projectId,
54
+ localDir: from,
55
+ include,
56
+ onProgress: (e: ProgressEvent) => {
57
+ const verb = e.kind === 'upload' ? '↑' : e.kind === 'delete' ? '✗' : '·';
58
+ const pct = e.total > 0 ? ` ${e.index + 1}/${e.total}` : '';
59
+ const sz = e.bytes != null ? pc.dim(` (${formatBytes(e.bytes)})`) : '';
60
+ console.log(pc.dim(` ${verb}${pct} ${e.path}${sz}`));
61
+ },
62
+ });
63
+
64
+ const stats = await transport.uploadAll({ deleteRemote: flags.deleteRemote });
65
+ printSummary('Push', stats);
66
+ }
67
+
68
+ function printSummary(label: string, stats: SyncStats): void {
69
+ const ok = stats.errors.length === 0;
70
+ const head = ok ? pc.green(`✓ ${label} complete`) : pc.red(`✗ ${label} completed with errors`);
71
+ console.log(`\n${head}`);
72
+ console.log(pc.dim(' uploaded: ') + stats.uploaded);
73
+ if (stats.deleted) console.log(pc.dim(' deleted: ') + stats.deleted);
74
+ if (!ok) {
75
+ console.log(pc.dim(' errors: ') + stats.errors.length);
76
+ for (const err of stats.errors.slice(0, 5)) {
77
+ console.log(pc.dim(' ') + pc.red(`${err.path}: ${err.message}`));
78
+ }
79
+ if (stats.errors.length > 5) {
80
+ console.log(pc.dim(` ... and ${stats.errors.length - 5} more`));
81
+ }
82
+ }
83
+ }
84
+
85
+ function formatBytes(n: number): string {
86
+ if (n < 1024) return `${n}B`;
87
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
88
+ return `${(n / 1024 / 1024).toFixed(2)}MB`;
89
+ }
@@ -0,0 +1,104 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * `shogo runtime` subcommands — manage the locally-installed
5
+ * agent-runtime binary that the worker spawns per-project.
6
+ *
7
+ * shogo runtime install [--channel <stable|beta|nightly>] [--version <x>] [--force] [--base-url <url>]
8
+ * shogo runtime version
9
+ * shogo runtime where
10
+ * shogo runtime update [--channel <...>]
11
+ *
12
+ * The runtime is AGPL-3.0-or-later (see packages/agent-runtime). The
13
+ * worker (MIT) installs it as a separate on-disk binary and spawns it
14
+ * as a child process — no library link.
15
+ */
16
+ import pc from 'picocolors';
17
+ import {
18
+ type Channel,
19
+ detectTarget,
20
+ getRuntimePaths,
21
+ installRuntime,
22
+ readInstalledVersion,
23
+ } from '../lib/runtime-install.ts';
24
+ import { resolveRuntime, formatMissingRuntimeError } from '../lib/runtime-resolver.ts';
25
+
26
+ interface InstallFlags {
27
+ channel?: Channel;
28
+ version?: string;
29
+ baseUrl?: string;
30
+ force?: boolean;
31
+ }
32
+
33
+ export async function runRuntimeInstall(flags: InstallFlags = {}): Promise<void> {
34
+ const result = await installRuntime({
35
+ channel: flags.channel,
36
+ version: flags.version,
37
+ baseUrl: flags.baseUrl,
38
+ force: flags.force,
39
+ });
40
+ console.log();
41
+ console.log(pc.green('✓'), `agent-runtime ${pc.bold(result.version)} installed (${result.target})`);
42
+ console.log(` ${pc.dim('path: ')} ${result.binPath}`);
43
+ console.log(` ${pc.dim('source: ')} ${result.source}`);
44
+ console.log(` ${pc.dim('sha256: ')} ${result.sha256}`);
45
+ console.log(` ${pc.dim('channel: ')} ${result.channel}`);
46
+ }
47
+
48
+ export function runRuntimeVersion(): void {
49
+ const installed = readInstalledVersion();
50
+ if (!installed) {
51
+ console.log(pc.yellow('No agent-runtime installed.'));
52
+ console.log(`Run ${pc.cyan('shogo runtime install')} to download the latest.`);
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ console.log(`${pc.bold('agent-runtime')} ${installed.version}`);
57
+ console.log(` ${pc.dim('target: ')} ${installed.target}`);
58
+ console.log(` ${pc.dim('channel: ')} ${installed.channel}`);
59
+ console.log(` ${pc.dim('installed at:')} ${installed.installedAt}`);
60
+ console.log(` ${pc.dim('source: ')} ${installed.source}`);
61
+ }
62
+
63
+ export function runRuntimeWhere(): void {
64
+ const resolved = resolveRuntime();
65
+ const paths = getRuntimePaths();
66
+ if (!resolved) {
67
+ console.log(pc.yellow('agent-runtime binary not found on this machine.'));
68
+ console.log();
69
+ console.log(pc.dim('Default install path:'), paths.runtimeBin);
70
+ console.log();
71
+ console.log(formatMissingRuntimeError());
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ console.log(resolved.path);
76
+ if (process.env.SHOGO_DEBUG || process.env.VERBOSE) {
77
+ console.log(pc.dim(` (resolved via: ${resolved.source})`));
78
+ }
79
+ }
80
+
81
+ interface UpdateFlags {
82
+ channel?: Channel;
83
+ baseUrl?: string;
84
+ }
85
+
86
+ export async function runRuntimeUpdate(flags: UpdateFlags = {}): Promise<void> {
87
+ const installed = readInstalledVersion();
88
+ const targetChannel = flags.channel ?? installed?.channel ?? 'stable';
89
+ if (installed) {
90
+ console.log(
91
+ `${pc.dim('current:')} ${installed.version} (${installed.target}, ${installed.channel})`,
92
+ );
93
+ } else {
94
+ console.log(pc.dim('No existing install — installing fresh...'));
95
+ }
96
+ // installRuntime resolves the latest in-channel version on its own;
97
+ // pass force:true so we always reinstall when the user explicitly
98
+ // ran `update` (matches `npm update` / `brew upgrade` muscle memory).
99
+ await runRuntimeInstall({ channel: targetChannel, baseUrl: flags.baseUrl, force: true });
100
+ }
101
+
102
+ export function getDetectedTarget(): string {
103
+ return detectTarget();
104
+ }
@@ -0,0 +1,290 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * `shogo worker start` — pair this machine with Shogo Cloud.
5
+ *
6
+ * Two execution modes:
7
+ *
8
+ * --foreground Run the tunnel + runtime-manager in this process,
9
+ * log to stdout, exit on SIGINT/SIGTERM. This is the
10
+ * shape used inside the spawned detached child below
11
+ * (so we never duplicate the wire-up code) and the
12
+ * shape `shogo runtime install && shogo worker start --foreground`
13
+ * uses for CI / `systemd --user` setups.
14
+ *
15
+ * (default) Detach: re-spawn `shogo worker start --foreground`
16
+ * as a background process via `spawnWorker`, write
17
+ * the pid file, return immediately. The PID file is
18
+ * what `shogo worker stop / status / logs` poll.
19
+ *
20
+ * Foreground path responsibilities (in order):
21
+ * 1. Resolve config (api key, cloud url, name, worker dir).
22
+ * 2. Apply HTTPS_PROXY into the process env so outbound fetch picks
23
+ * it up via Node/Bun's automatic dispatcher.
24
+ * 3. Locate the AGPL agent-runtime binary on disk; abort with a
25
+ * friendly install hint if missing.
26
+ * 4. Optional --debug preflight (proxy reachability + cloud ping).
27
+ * 5. Construct WorkerRuntimeManager with cloud-routed default spawn
28
+ * config (every per-project runtime gets cloudUrl + apiKey).
29
+ * 6. Construct WorkerTunnel with the runtime manager as its resolver.
30
+ * 7. Install signal handlers (SIGINT / SIGTERM / SIGHUP) that stop
31
+ * the tunnel, stop all per-project runtimes, then exit.
32
+ * 8. Wait forever; the cloud drives traffic in via the tunnel WS.
33
+ */
34
+ import pc from 'picocolors';
35
+ import { existsSync } from 'node:fs';
36
+ import { resolveConfig } from '../lib/config.ts';
37
+ import { spawnWorker } from '../lib/process-manager.ts';
38
+ import { makeChecks, runPreflight } from '../lib/preflight.ts';
39
+ import { resolveProxy, applyProxyToEnv } from '../lib/transport.ts';
40
+ import { resolveRuntime, formatMissingRuntimeError } from '../lib/runtime-resolver.ts';
41
+ import { WorkerRuntimeManager, type ProjectSpawnConfig } from '../lib/runtime-manager.ts';
42
+ import { WorkerTunnel } from '../lib/tunnel.ts';
43
+
44
+ export interface StartFlags {
45
+ name?: string;
46
+ workerDir?: string;
47
+ apiKey?: string;
48
+ cloudUrl?: string;
49
+ port?: string;
50
+ proxy?: string;
51
+ project?: string;
52
+ runtimeBin?: string;
53
+ debug?: boolean;
54
+ foreground?: boolean;
55
+ /** Set to `true` to disable the on-first-request auto-pull of project
56
+ * workspaces from cloud. Auto-pull is ON by default for `cli_worker`
57
+ * workers so a freshly-paired VPS can serve pinned projects without
58
+ * the operator first running `shogo project pull`. */
59
+ noAutoPull?: boolean;
60
+ projectsDir?: string;
61
+ /** Disable the git smart-HTTP sync path even when `git` is on PATH.
62
+ * When set, auto-pull falls back to the CloudFileTransport file-pump.
63
+ * Useful when outbound HTTPS to git pack RPC endpoints is firewalled. */
64
+ noGit?: boolean;
65
+ }
66
+
67
+ export async function runStart(flags: StartFlags): Promise<void> {
68
+ const cfg = resolveConfig({
69
+ name: flags.name,
70
+ workerDir: flags.workerDir,
71
+ apiKey: flags.apiKey,
72
+ cloudUrl: flags.cloudUrl,
73
+ port: flags.port ? parseInt(flags.port, 10) : undefined,
74
+ projectsDir: flags.projectsDir,
75
+ });
76
+
77
+ const proxy = resolveProxy(flags.proxy);
78
+
79
+ if (!flags.foreground) {
80
+ // Detached default — re-launch this same CLI with --foreground in
81
+ // a child process so the user gets their shell back. The actual
82
+ // tunnel + runtime-manager work happens in `runStartForeground()`
83
+ // below in the spawned process.
84
+ return runDetached({ cfg, proxy, flags });
85
+ }
86
+
87
+ // Foreground path: surface a friendly missing-binary error BEFORE
88
+ // we open the tunnel — the binary is always required and it's a
89
+ // much better failure mode to tell the user up front than to fail
90
+ // on the first inbound /agent/* request.
91
+ const resolved = resolveRuntime({ flag: flags.runtimeBin });
92
+ if (!resolved) {
93
+ console.error(pc.red(formatMissingRuntimeError({ flag: flags.runtimeBin })));
94
+ process.exit(1);
95
+ }
96
+
97
+ applyProxyToEnv(process.env, proxy);
98
+
99
+ if (flags.debug) {
100
+ const ok = await runPreflight(
101
+ makeChecks({
102
+ cloudUrl: cfg.cloudUrl,
103
+ apiKey: cfg.apiKey,
104
+ workerDir: cfg.workerDir,
105
+ proxy,
106
+ }),
107
+ );
108
+ if (!ok) process.exit(1);
109
+ }
110
+
111
+ const autoPullEnabled = !flags.noAutoPull;
112
+ const useGit = !flags.noGit;
113
+
114
+ console.log(pc.bold('\nShogo Worker — Starting'));
115
+ console.log(pc.dim(' name ') + cfg.name);
116
+ console.log(pc.dim(' worker-dir ') + cfg.workerDir);
117
+ console.log(pc.dim(' cloud ') + cfg.cloudUrl);
118
+ console.log(pc.dim(' runtime ') + `${resolved.path} ${pc.dim(`(via ${resolved.source})`)}`);
119
+ console.log(pc.dim(' auto-pull ') + (autoPullEnabled ? pc.green('on') + pc.dim(` → ${cfg.projectsDir}`) : pc.yellow('off')));
120
+ console.log(pc.dim(' sync mode ') + (useGit ? pc.green('git') + pc.dim(' (falls back to file transport if git is missing)') : pc.yellow('files-only')));
121
+ if (flags.project) console.log(pc.dim(' project ') + flags.project);
122
+ if (proxy) {
123
+ console.log(pc.dim(' proxy ') + `${proxy.url} ${pc.dim(`(from ${proxy.source})`)}`);
124
+ }
125
+ console.log('');
126
+
127
+ // Derive the AI proxy URL from the cloud URL so the agent-runtime
128
+ // routes LLM calls through Shogo Cloud's proxy instead of requiring
129
+ // direct API keys on the machine. The proxy endpoint lives at
130
+ // <cloudUrl>/api/ai/v1 — the same base the desktop app uses.
131
+ const aiProxyUrl = `${cfg.cloudUrl.replace(/\/+$/, '')}/api/ai/v1`;
132
+
133
+ const defaultSpawnConfig: ProjectSpawnConfig = {
134
+ cloudUrl: cfg.cloudUrl,
135
+ apiKey: cfg.apiKey,
136
+ aiProxyUrl,
137
+ aiProxyToken: cfg.apiKey,
138
+ // No projectDir up front — the runtime manager's `maybeAutoPull`
139
+ // sets PROJECT_DIR per-project to <projectsDir>/<projectId>/ once
140
+ // the clone completes. CWD defaults to that same directory.
141
+ };
142
+
143
+ const runtimeManager = new WorkerRuntimeManager({
144
+ runtimeBin: flags.runtimeBin,
145
+ defaultSpawnConfig,
146
+ autoPull: {
147
+ enabled: autoPullEnabled,
148
+ projectsDir: cfg.projectsDir,
149
+ watch: true,
150
+ useGit,
151
+ },
152
+ });
153
+
154
+ // Eagerly resolve so the cached `resolved` is reused — also exits early
155
+ // if a race deleted the binary between the check above and now.
156
+ if (!runtimeManager.resolveBinary()) {
157
+ console.error(pc.red(formatMissingRuntimeError({ flag: flags.runtimeBin })));
158
+ process.exit(1);
159
+ }
160
+
161
+ const tunnel = new WorkerTunnel({
162
+ apiKey: cfg.apiKey,
163
+ cloudUrl: cfg.cloudUrl,
164
+ name: cfg.name,
165
+ kind: 'cli-worker',
166
+ resolver: runtimeManager,
167
+ onAuthRevoked: (reason) => {
168
+ console.error(pc.red(`✗ Cloud auth revoked: ${reason}`));
169
+ console.error(pc.dim(` Run \`shogo login\` to re-authenticate; this worker will keep polling at the auth-failure backoff until then.`));
170
+ },
171
+ });
172
+
173
+ let shuttingDown = false;
174
+ const shutdown = async (signal: NodeJS.Signals) => {
175
+ if (shuttingDown) return;
176
+ shuttingDown = true;
177
+ console.log(pc.dim(`\nReceived ${signal} — shutting down...`));
178
+ try { tunnel.stop(); } catch { /* already stopped */ }
179
+ try { await runtimeManager.stopAll(); } catch (err: any) {
180
+ console.warn(pc.yellow(`stopAll: ${err?.message ?? err}`));
181
+ }
182
+ process.exit(0);
183
+ };
184
+ process.once('SIGINT', () => void shutdown('SIGINT'));
185
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
186
+ process.once('SIGHUP', () => void shutdown('SIGHUP'));
187
+
188
+ tunnel.start();
189
+ console.log(pc.green('✓ Worker running. Ctrl-C to stop.'));
190
+
191
+ // Pin the foreground process. The shutdown handler above will
192
+ // process.exit() when the user terminates.
193
+ await new Promise<void>(() => { /* never resolves */ });
194
+ }
195
+
196
+ interface DetachedOpts {
197
+ cfg: { apiKey: string; cloudUrl: string; name: string; workerDir: string; port: number };
198
+ proxy: ReturnType<typeof resolveProxy>;
199
+ flags: StartFlags;
200
+ }
201
+
202
+ /**
203
+ * Detach implementation: figure out the right argv to re-invoke this
204
+ * CLI with `worker start --foreground`, spawn it via `spawnWorker`
205
+ * (which writes the PID file + redirects stdio to ~/.shogo/logs/),
206
+ * then exit. The child becomes the long-running worker.
207
+ */
208
+ function runDetached({ cfg, proxy, flags }: DetachedOpts): void {
209
+ const { entry, runner } = resolveSelfEntry();
210
+
211
+ const env: NodeJS.ProcessEnv = applyProxyToEnv(
212
+ {
213
+ ...process.env,
214
+ SHOGO_API_KEY: cfg.apiKey,
215
+ SHOGO_CLOUD_URL: cfg.cloudUrl,
216
+ SHOGO_INSTANCE_NAME: cfg.name,
217
+ SHOGO_WORKER_DIR: cfg.workerDir,
218
+ SHOGO_LOCAL_MODE: 'true',
219
+ PORT: String(cfg.port),
220
+ },
221
+ proxy,
222
+ );
223
+
224
+ // We pass `--foreground` to the spawned child so it takes the
225
+ // foreground branch above. Anything else the user passed is also
226
+ // forwarded so e.g. `--project <id>` survives detachment.
227
+ const argv = buildChildArgv(flags);
228
+
229
+ const { pid } = spawnWorker({
230
+ entry,
231
+ runner,
232
+ env: { ...env, SHOGO_DETACHED_ARGS: argv.join(' ') },
233
+ cwd: cfg.workerDir,
234
+ detach: true,
235
+ inheritStdio: false,
236
+ });
237
+
238
+ console.log(pc.bold('\nShogo Worker — Started'));
239
+ console.log(pc.dim(' pid: ') + pid);
240
+ console.log(pc.dim(' name: ') + cfg.name);
241
+ console.log(pc.dim(' logs: ') + '~/.shogo/logs/worker.log');
242
+ console.log(pc.dim(' stop: ') + 'shogo worker stop');
243
+ }
244
+
245
+ function buildChildArgv(flags: StartFlags): string[] {
246
+ const out: string[] = ['worker', 'start', '--foreground'];
247
+ if (flags.name) out.push('--name', flags.name);
248
+ if (flags.workerDir) out.push('--worker-dir', flags.workerDir);
249
+ if (flags.apiKey) out.push('--api-key', flags.apiKey);
250
+ if (flags.cloudUrl) out.push('--cloud-url', flags.cloudUrl);
251
+ if (flags.port) out.push('--port', flags.port);
252
+ if (flags.proxy) out.push('--proxy', flags.proxy);
253
+ if (flags.project) out.push('--project', flags.project);
254
+ if (flags.runtimeBin) out.push('--runtime-bin', flags.runtimeBin);
255
+ if (flags.debug) out.push('--debug');
256
+ if (flags.noAutoPull) out.push('--no-auto-pull');
257
+ if (flags.projectsDir) out.push('--projects-dir', flags.projectsDir);
258
+ if (flags.noGit) out.push('--no-git');
259
+ return out;
260
+ }
261
+
262
+ /**
263
+ * Find the entry point + runner the detached child should use.
264
+ *
265
+ * Priority:
266
+ * 1. The currently-executing argv[1] if it's an existing file (eg.
267
+ * a globally installed `shogo` bin or `bun src/cli.ts` in the
268
+ * monorepo).
269
+ * 2. The compiled binary at /usr/local/bin/shogo on PATH (best-effort).
270
+ * 3. The bin shim shipped with this package.
271
+ */
272
+ function resolveSelfEntry(): { entry: string; runner: 'bun' | 'node' | 'tsx' } {
273
+ // process.execPath is the bun/node binary; argv[1] is the script.
274
+ const execPath = process.execPath;
275
+ const isBun = /\bbun(?:-[^/\\]*)?$/.test(execPath) || typeof (globalThis as any).Bun !== 'undefined';
276
+ const argvScript = process.argv[1];
277
+ if (argvScript && existsSync(argvScript)) {
278
+ // When spawned via tsx (from the bin shim), process.execPath is still
279
+ // `node` because tsx runs on top of Node. Detect a .ts entry and route
280
+ // through tsx so the detached child can handle TypeScript natively.
281
+ const isTs = /\.ts$/.test(argvScript);
282
+ const runner = isBun ? 'bun' : (isTs ? 'tsx' : 'node');
283
+ return { entry: argvScript, runner };
284
+ }
285
+ // Fallback: the compiled bin shim shipped with the package.
286
+ // Resolved relative to this file via import.meta.url so it works
287
+ // both in monorepo and when published.
288
+ const shim = new URL('../../bin/shogo.mjs', import.meta.url).pathname;
289
+ return { entry: shim, runner: isBun ? 'bun' : 'node' };
290
+ }
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ import pc from 'picocolors';
4
+ import { readPid, isRunning } from '../lib/process-manager.ts';
5
+ import { loadConfig } from '../lib/config.ts';
6
+
7
+ export async function runStatus(): Promise<void> {
8
+ const pid = readPid();
9
+ if (!pid) {
10
+ console.log(pc.yellow('● stopped') + pc.dim(' (no pid file)'));
11
+ return;
12
+ }
13
+ if (!isRunning(pid)) {
14
+ console.log(pc.red('● dead') + pc.dim(` (stale pid ${pid})`));
15
+ return;
16
+ }
17
+ const cfg = loadConfig();
18
+ console.log(pc.green('● running') + pc.dim(` (pid ${pid})`));
19
+ if (cfg.name) console.log(pc.dim(` name: `) + cfg.name);
20
+ if (cfg.cloudUrl) console.log(pc.dim(` cloud: `) + cfg.cloudUrl);
21
+ if (cfg.workerDir) console.log(pc.dim(` dir: `) + cfg.workerDir);
22
+ }
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ import pc from 'picocolors';
4
+ import { stopWorker } from '../lib/process-manager.ts';
5
+
6
+ export async function runStop(): Promise<void> {
7
+ const { killedPid } = stopWorker('SIGTERM');
8
+ if (killedPid === null) {
9
+ console.log(pc.dim('No worker running.'));
10
+ return;
11
+ }
12
+ console.log(pc.green(`✓ Worker stopped (pid=${killedPid}).`));
13
+ }