@meyverick/omnicode 0.0.4 → 0.0.5

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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # omnicode
2
2
 
3
- The Ubuntu command-line entrypoint for running OpenCode through OmniRoute.
3
+ The cross-platform command-line entrypoint for running OpenCode through OmniRoute.
4
4
 
5
5
  ## What is omnicode?
6
6
 
7
- `omnicode` is a thin wrapper that launches OpenCode through OmniRoute on Ubuntu. It expects you to install the underlying tools yourself, then handles optional GrayMatter and OpenSpec initialization, background OmniRoute lifecycle, and project-local OpenCode sessions.
7
+ `omnicode` is a thin wrapper that launches OpenCode through OmniRoute. It expects you to install the underlying tools yourself, then handles optional GrayMatter and OpenSpec initialization, background OmniRoute lifecycle, and project-local OpenCode sessions. Developed and tested on Ubuntu Linux — cross-platform by design but untested on Windows, macOS, and other Linux distributions.
8
8
 
9
9
  ## Why
10
10
 
@@ -16,7 +16,8 @@ So I wrote this wrapper. It starts OmniRoute, inits GrayMatter and OpenSpec quie
16
16
 
17
17
  ## Features
18
18
 
19
- - Thin npm global command for Ubuntu.
19
+ - Thin npm global command written in Node.js (no bash needed).
20
+ - Cross-platform by design — developed and tested on Ubuntu Linux; untested on Windows, macOS, and other Linux distributions.
20
21
  - Automatic session resume per project using the OpenCode database.
21
22
  - Background OmniRoute lifecycle with cleanup when OpenCode exits.
22
23
  - Quiet GrayMatter and OpenSpec initialization with captured logs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meyverick/omnicode",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Ubuntu command-line entrypoint for OpenCode through OmniRoute",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,192 @@
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import os from "node:os";
5
+
6
+ import { commandExists, getDataDir } from "../installer/lib.js";
7
+
8
+ const isWindows = process.platform === "win32";
9
+ const MAX_OMNI_WAIT = 30;
10
+ const OMNI_CHECK_DELAY = 1000;
11
+
12
+ function isProcessRunning(name) {
13
+ try {
14
+ if (isWindows) {
15
+ const out = execFileSync("tasklist", ["/FI", `IMAGENAME eq ${name}.exe`, "/NH"], {
16
+ stdio: ["ignore", "pipe", "ignore"],
17
+ encoding: "utf8",
18
+ });
19
+ return out.includes(`${name}.exe`);
20
+ }
21
+ execFileSync("pgrep", ["-x", name], { stdio: "ignore" });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function isPidAlive(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function sleep(ms) {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ function initTool(name, args, logPath) {
42
+ return new Promise((resolve) => {
43
+ if (!commandExists(name)) {
44
+ console.log(`[omnicode] ${name}: not installed, skipping`);
45
+ resolve();
46
+ return;
47
+ }
48
+ console.log(`[omnicode] ${name}: initializing`);
49
+ const log = openSync(logPath, "w");
50
+ const child = spawn(name, args, { stdio: ["ignore", log, log] });
51
+ child.on("close", (code) => {
52
+ if (code === 0) {
53
+ console.log(`[omnicode] ${name}: ready`);
54
+ } else {
55
+ console.log(`[omnicode] WARNING: ${name} init failed; continuing. Log: ${logPath}`);
56
+ }
57
+ resolve();
58
+ });
59
+ child.on("error", () => {
60
+ console.log(`[omnicode] WARNING: ${name} init failed; continuing. Log: ${logPath}`);
61
+ resolve();
62
+ });
63
+ });
64
+ }
65
+
66
+ async function initTools(dataDir) {
67
+ const graymatterLog = join(dataDir, "graymatter-init.log");
68
+ const openspecLog = join(dataDir, "openspec-init.log");
69
+
70
+ await Promise.all([
71
+ initTool("graymatter", ["init", "--only", "opencode"], graymatterLog),
72
+ initTool("openspec", ["init", "--force", "--tools", "opencode"], openspecLog),
73
+ ]);
74
+ }
75
+
76
+ function startOmniroute(dataDir, logFile, pidFile) {
77
+ if (isProcessRunning("omniroute")) {
78
+ console.log("[omnicode] omniroute already running");
79
+ return null;
80
+ }
81
+
82
+ if (existsSync(pidFile) || existsSync(pidFile)) {
83
+ let pid = null;
84
+ try {
85
+ pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
86
+ } catch {}
87
+ if (pid && isPidAlive(pid)) {
88
+ console.log(`[omnicode] omniroute already running (pid: ${pid})`);
89
+ return null;
90
+ }
91
+ try { unlinkSync(pidFile); } catch {}
92
+ }
93
+
94
+ console.log("[omnicode] starting omniroute...");
95
+ const log = openSync(logFile, "w");
96
+ const child = spawn("omniroute", ["--no-open"], {
97
+ detached: true,
98
+ stdio: ["ignore", log, log],
99
+ });
100
+ child.unref();
101
+ const pid = child.pid;
102
+ writeFileSync(pidFile, String(pid), { mode: 0o600 });
103
+
104
+ return pid;
105
+ }
106
+
107
+ async function waitForOmniroute(pid, logFile) {
108
+ let waited = 0;
109
+ while (waited < MAX_OMNI_WAIT * 1000) {
110
+ await sleep(OMNI_CHECK_DELAY);
111
+ waited += OMNI_CHECK_DELAY;
112
+
113
+ if (!isPidAlive(pid)) {
114
+ console.error(`[omnicode] ERROR: omniroute exited during startup. Log: ${logFile}`);
115
+ process.exit(1);
116
+ }
117
+
118
+ if (isProcessRunning("omniroute")) {
119
+ console.log(`[omnicode] omniroute started (pid: ${pid})`);
120
+ return;
121
+ }
122
+ }
123
+
124
+ console.error(`[omnicode] ERROR: omniroute did not become ready. Log: ${logFile}`);
125
+ process.exit(1);
126
+ }
127
+
128
+ function stopOmnirouteIfIdle(pidFile) {
129
+ if (isProcessRunning("opencode")) {
130
+ return;
131
+ }
132
+
133
+ if (existsSync(pidFile)) {
134
+ let pid = null;
135
+ try {
136
+ pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
137
+ } catch {}
138
+ if (pid && isPidAlive(pid)) {
139
+ console.log(`[omnicode] no opencode left -> stopping omniroute (pid: ${pid})`);
140
+ try { process.kill(pid, "SIGTERM"); } catch {}
141
+ const deadline = Date.now() + 5000;
142
+ while (isPidAlive(pid) && Date.now() < deadline) {
143
+ try { process.kill(pid, 0); } catch { break; }
144
+ }
145
+ }
146
+ try { unlinkSync(pidFile); } catch {}
147
+ }
148
+ }
149
+
150
+ export async function runRuntime(mode) {
151
+ const dataDir = getDataDir();
152
+ mkdirSync(dataDir, { recursive: true });
153
+
154
+ const logFile = join(dataDir, "omniroute.log");
155
+ const pidFile = join(dataDir, "omniroute.pid");
156
+
157
+ if (!isWindows) {
158
+ process.umask(0o077);
159
+ }
160
+
161
+ const cleanup = () => stopOmnirouteIfIdle(pidFile);
162
+ process.on("exit", cleanup);
163
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
164
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
165
+
166
+ await initTools(dataDir);
167
+
168
+ const pid = startOmniroute(dataDir, logFile, pidFile);
169
+ if (pid !== null) {
170
+ await waitForOmniroute(pid, logFile);
171
+ }
172
+
173
+ if (mode.flag === "-s" && mode.id) {
174
+ console.log(`[omnicode] launching opencode (session: ${mode.id})`);
175
+ await new Promise((resolve) => {
176
+ const child = spawn("opencode", ["-s", mode.id], {
177
+ stdio: "inherit",
178
+ cwd: process.cwd(),
179
+ });
180
+ child.on("close", () => resolve());
181
+ });
182
+ } else {
183
+ console.log("[omnicode] launching opencode (new session)");
184
+ await new Promise((resolve) => {
185
+ const child = spawn("opencode", [], {
186
+ stdio: "inherit",
187
+ cwd: process.cwd(),
188
+ });
189
+ child.on("close", () => resolve());
190
+ });
191
+ }
192
+ }
@@ -1,29 +1,47 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync, spawn } from "node:child_process";
3
- import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { readFileSync, realpathSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import os from "node:os";
7
6
 
8
- import { commandExists } from "../installer/lib.js";
7
+ import { commandExists, getOpencodeDbPath } from "../installer/lib.js";
8
+ import { runRuntime } from "./omnicode-runtime.js";
9
9
 
10
10
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
11
- const runtimeScript = join(__dirname, "omnicode-runtime.sh");
12
11
  const packageJsonPath = join(__dirname, "..", "..", "package.json");
13
12
 
14
13
  const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
14
+ const isWindows = process.platform === "win32";
15
+
16
+ let DatabaseSync;
17
+ try {
18
+ ({ DatabaseSync } = await import("node:sqlite"));
19
+ } catch {
20
+ DatabaseSync = null;
21
+ }
15
22
 
16
23
  export function printUsage() {
17
24
  console.log(`Usage: omnicode [-s <session_id>] [-c] [--status] [--version]`);
18
25
  }
19
26
 
27
+ let _cachedVersion = null;
28
+
20
29
  export function getVersion() {
30
+ if (_cachedVersion !== null) return _cachedVersion;
21
31
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
22
- return pkg.version;
32
+ _cachedVersion = pkg.version;
33
+ return _cachedVersion;
23
34
  }
24
35
 
25
36
  export function isProcessRunning(name) {
26
37
  try {
38
+ if (isWindows) {
39
+ const out = execFileSync("tasklist", ["/FI", `IMAGENAME eq ${name}.exe`, "/NH"], {
40
+ stdio: ["ignore", "pipe", "ignore"],
41
+ encoding: "utf8",
42
+ });
43
+ return out.includes(`${name}.exe`);
44
+ }
27
45
  execFileSync("pgrep", ["-x", name], { stdio: "ignore" });
28
46
  return true;
29
47
  } catch {
@@ -78,7 +96,7 @@ export function parseArgs(argv) {
78
96
  process.exit(2);
79
97
  }
80
98
 
81
- if (sessionId && !SESSION_ID_RE.test(sessionId)) {
99
+ if (!sessionId || !SESSION_ID_RE.test(sessionId)) {
82
100
  console.error(`[omnicode] ERROR: invalid session ID format`);
83
101
  process.exit(2);
84
102
  }
@@ -87,11 +105,11 @@ export function parseArgs(argv) {
87
105
  }
88
106
 
89
107
  export async function getLatestSessionId(directory = realpathSync(process.cwd())) {
90
- const dbPath = join(os.homedir(), ".local", "share", "opencode", "opencode.db");
91
- if (!existsSync(dbPath)) return null;
108
+ const dbPath = getOpencodeDbPath();
109
+ if (!dbPath) return null;
92
110
 
93
111
  try {
94
- const { DatabaseSync } = await import("node:sqlite");
112
+ if (!DatabaseSync) return null;
95
113
  const db = new DatabaseSync(dbPath, { readOnly: true });
96
114
  const row = db.prepare(
97
115
  "SELECT id FROM session WHERE directory = ? ORDER BY time_updated DESC LIMIT 1"
@@ -103,21 +121,13 @@ export async function getLatestSessionId(directory = realpathSync(process.cwd())
103
121
  }
104
122
  }
105
123
 
106
- export async function resolveSessionMode(sessionId, continueSession = false, latestSessionId = null) {
124
+ export async function resolveSessionMode(sessionId, latestSessionId = null) {
107
125
  if (sessionId) return { flag: "-s", id: sessionId };
108
126
  if (latestSessionId === null) latestSessionId = await getLatestSessionId();
109
127
  if (latestSessionId) return { flag: "-s", id: latestSessionId };
110
128
  return { flag: null, id: null };
111
129
  }
112
130
 
113
- export function buildRuntimeArgs(mode) {
114
- const args = [runtimeScript];
115
- if (mode.flag === "-s") {
116
- args.push("-s", mode.id);
117
- }
118
- return args;
119
- }
120
-
121
131
  async function main() {
122
132
  const args = parseArgs(process.argv);
123
133
 
@@ -128,23 +138,8 @@ async function main() {
128
138
  process.exit(1);
129
139
  }
130
140
 
131
- const mode = await resolveSessionMode(args.sessionId, args.continueSession);
132
- const childArgs = buildRuntimeArgs(mode);
133
-
134
- return new Promise((resolve, reject) => {
135
- const child = spawn("bash", childArgs, {
136
- stdio: "inherit",
137
- cwd: process.cwd(),
138
- });
139
- child.on("error", reject);
140
- child.on("close", (code) => {
141
- if (code !== 0) {
142
- process.exit(code);
143
- } else {
144
- resolve();
145
- }
146
- });
147
- });
141
+ const mode = await resolveSessionMode(args.sessionId);
142
+ await runRuntime(mode);
148
143
  }
149
144
 
150
145
  function isMainModule() {
@@ -1,40 +1,26 @@
1
- import { execFileSync, spawn } from "node:child_process";
2
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import os from "node:os";
5
5
 
6
+ const isWindows = process.platform === "win32";
7
+
6
8
  export function commandExists(command) {
9
+ const tool = isWindows ? "where" : "which";
7
10
  try {
8
- execFileSync("which", [command], { stdio: "ignore" });
11
+ execFileSync(tool, [command], { stdio: "ignore" });
9
12
  return true;
10
13
  } catch {
11
14
  return false;
12
15
  }
13
16
  }
14
17
 
15
- export function run(command, args = [], opts = {}) {
16
- const quoted = [command, ...args.map((a) => (a.includes(" ") ? `"${a}"` : a))].join(" ");
17
- console.log(`[omnicode] $ ${quoted}`);
18
- return new Promise((resolve, reject) => {
19
- const child = spawn(command, args, {
20
- stdio: "inherit",
21
- shell: false,
22
- cwd: process.cwd(),
23
- ...opts,
24
- });
25
- child.on("error", reject);
26
- child.on("close", (code) => {
27
- if (code !== 0) {
28
- reject(new Error(`Command failed with exit code ${code}: ${quoted}`));
29
- } else {
30
- resolve();
31
- }
32
- });
33
- });
18
+ export function getDataDir() {
19
+ return join(os.homedir(), ".local", "share", "omnicode");
34
20
  }
35
21
 
36
- export function getRuntimeDir() {
37
- const dir = join(os.homedir(), ".local", "share", "omnicode");
38
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39
- return dir;
22
+ export function getOpencodeDbPath() {
23
+ const dbPath = join(os.homedir(), ".local", "share", "opencode", "opencode.db");
24
+ if (!existsSync(dbPath)) return null;
25
+ return dbPath;
40
26
  }
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SESSION_FLAG="${1:-}"
5
- SESSION_ID="${2:-}"
6
- RUNTIME_DIR="$HOME/.local/share/omnicode"
7
- LOG_FILE="$RUNTIME_DIR/omniroute.log"
8
- GRAYMATTER_LOG="$RUNTIME_DIR/graymatter-init.log"
9
- OPENSPEC_LOG="$RUNTIME_DIR/openspec-init.log"
10
- PID_FILE="$RUNTIME_DIR/omniroute.pid"
11
- MAX_OMNI_WAIT=30
12
- OMNI_CHECK_DELAY=1
13
-
14
- export PATH="$PATH:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
15
-
16
- umask 0077
17
- mkdir -p "$RUNTIME_DIR"
18
-
19
- is_pid_alive() {
20
- local pid="${1:-}"
21
- [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1
22
- }
23
-
24
- is_omniroute_running() {
25
- pgrep -f "omniroute" >/dev/null 2>&1
26
- }
27
-
28
- is_opencode_running() {
29
- pgrep -f "opencode" >/dev/null 2>&1
30
- }
31
-
32
- start_omniroute() {
33
- if is_omniroute_running; then
34
- echo "[omnicode] omniroute already running"
35
- return 0
36
- fi
37
-
38
- echo "[omnicode] starting omniroute..."
39
- : > "$LOG_FILE"
40
- nohup omniroute --no-open >>"$LOG_FILE" 2>&1 &
41
- local pid=$!
42
- if [[ -e "$PID_FILE" ]]; then
43
- echo "[omnicode] ERROR: PID file already exists at $PID_FILE" >&2
44
- exit 1
45
- fi
46
- touch "$PID_FILE"
47
- chmod 600 "$PID_FILE"
48
- printf '%s\n' "$pid" > "$PID_FILE"
49
-
50
- local waited=0
51
- while [[ "$waited" -lt "$MAX_OMNI_WAIT" ]]; do
52
- sleep "$OMNI_CHECK_DELAY"
53
- waited=$((waited + OMNI_CHECK_DELAY))
54
-
55
- if ! is_pid_alive "$pid"; then
56
- echo "[omnicode] ERROR: omniroute exited during startup. Log: $LOG_FILE" >&2
57
- exit 1
58
- fi
59
-
60
- if is_omniroute_running; then
61
- echo "[omnicode] omniroute started (pid: $pid)"
62
- return 0
63
- fi
64
- done
65
-
66
- echo "[omnicode] ERROR: omniroute did not become ready. Log: $LOG_FILE" >&2
67
- exit 1
68
- }
69
-
70
- stop_omniroute_if_idle() {
71
- if is_opencode_running; then
72
- return 0
73
- fi
74
-
75
- if [[ -f "$PID_FILE" ]]; then
76
- local pid
77
- pid="$(cat "$PID_FILE" 2>/dev/null || true)"
78
- if is_pid_alive "$pid"; then
79
- echo "[omnicode] no opencode left -> stopping omniroute (pid: $pid)"
80
- kill "$pid" 2>/dev/null || true
81
- fi
82
- rm -f "$PID_FILE"
83
- fi
84
- }
85
-
86
- cleanup() {
87
- stop_omniroute_if_idle
88
- }
89
- trap cleanup EXIT INT TERM
90
-
91
- if command -v graymatter >/dev/null 2>&1; then
92
- echo "[omnicode] graymatter: initializing"
93
- if graymatter init --only opencode >"$GRAYMATTER_LOG" 2>&1; then
94
- echo "[omnicode] graymatter: ready"
95
- else
96
- echo "[omnicode] WARNING: graymatter init failed; continuing. Log: $GRAYMATTER_LOG"
97
- fi
98
- else
99
- echo "[omnicode] graymatter: not installed, skipping"
100
- fi
101
-
102
- if command -v openspec >/dev/null 2>&1; then
103
- echo "[omnicode] openspec: initializing"
104
- if openspec init --force --tools opencode >"$OPENSPEC_LOG" 2>&1; then
105
- echo "[omnicode] openspec: ready"
106
- else
107
- echo "[omnicode] WARNING: openspec init failed; continuing. Log: $OPENSPEC_LOG"
108
- fi
109
- else
110
- echo "[omnicode] openspec: not installed, skipping"
111
- fi
112
-
113
- start_omniroute
114
-
115
- if [[ "$SESSION_FLAG" == "-s" && -n "$SESSION_ID" ]]; then
116
- echo "[omnicode] launching opencode (session: $SESSION_ID)"
117
- opencode -s "$SESSION_ID"
118
- else
119
- echo "[omnicode] launching opencode (new session)"
120
- opencode
121
- fi