@meyverick/omnicode 0.0.4 → 0.0.6

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.6",
4
4
  "description": "Ubuntu command-line entrypoint for OpenCode through OmniRoute",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,175 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { commandExists, getDataDir, isProcessRunning, isPidAlive } from "../installer/lib.js";
6
+
7
+ const MAX_OMNI_WAIT = 30;
8
+ const OMNI_CHECK_DELAY = 1000;
9
+
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+
14
+ async function initTool(name, args, logPath) {
15
+ if (!commandExists(name)) {
16
+ console.log(`[omnicode] ${name}: not installed, skipping`);
17
+ return;
18
+ }
19
+ console.log(`[omnicode] ${name}: initializing`);
20
+ let log;
21
+ try {
22
+ log = openSync(logPath, "w");
23
+ const child = spawn(name, args, { stdio: ["ignore", log, log] });
24
+ const result = await Promise.race([
25
+ new Promise((resolve) => {
26
+ child.on("close", resolve);
27
+ child.on("error", () => resolve(null));
28
+ }),
29
+ new Promise((resolve) => {
30
+ setTimeout(() => { child.kill(); resolve(null); }, 30000);
31
+ }),
32
+ ]);
33
+ if (result === null) {
34
+ console.log(`[omnicode] WARNING: ${name} init timed out; continuing. Log: ${logPath}`);
35
+ } else if (result === 0) {
36
+ console.log(`[omnicode] ${name}: ready`);
37
+ } else {
38
+ console.log(`[omnicode] WARNING: ${name} init failed; continuing. Log: ${logPath}`);
39
+ }
40
+ } catch {
41
+ if (log !== undefined) try { closeSync(log); } catch {}
42
+ }
43
+ }
44
+
45
+ async function initTools(dataDir) {
46
+ const graymatterLog = join(dataDir, "graymatter-init.log");
47
+ const openspecLog = join(dataDir, "openspec-init.log");
48
+
49
+ await Promise.all([
50
+ initTool("graymatter", ["init", "--only", "opencode"], graymatterLog),
51
+ initTool("openspec", ["init", "--force", "--tools", "opencode"], openspecLog),
52
+ ]);
53
+ }
54
+
55
+ function startOmniroute(dataDir, logFile, pidFile) {
56
+ if (isProcessRunning("omniroute")) {
57
+ console.log("[omnicode] omniroute already running");
58
+ return null;
59
+ }
60
+
61
+ if (existsSync(pidFile) || existsSync(pidFile)) {
62
+ let pid = null;
63
+ try {
64
+ pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
65
+ } catch {}
66
+ if (pid && isPidAlive(pid)) {
67
+ console.log(`[omnicode] omniroute already running (pid: ${pid})`);
68
+ return null;
69
+ }
70
+ try { unlinkSync(pidFile); } catch {}
71
+ }
72
+
73
+ console.log("[omnicode] starting omniroute...");
74
+ const log = openSync(logFile, "w");
75
+ const child = spawn("omniroute", ["--no-open"], {
76
+ detached: true,
77
+ stdio: ["ignore", log, log],
78
+ });
79
+ child.unref();
80
+ const pid = child.pid;
81
+ writeFileSync(pidFile, String(pid), { mode: 0o600 });
82
+
83
+ return pid;
84
+ }
85
+
86
+ async function waitForOmniroute(pid, logFile) {
87
+ let waited = 0;
88
+ while (waited < MAX_OMNI_WAIT * 1000) {
89
+ await sleep(OMNI_CHECK_DELAY);
90
+ waited += OMNI_CHECK_DELAY;
91
+
92
+ if (!isPidAlive(pid)) {
93
+ console.error(`[omnicode] ERROR: omniroute exited during startup. Log: ${logFile}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ if (isProcessRunning("omniroute")) {
98
+ console.log(`[omnicode] omniroute started (pid: ${pid})`);
99
+ return;
100
+ }
101
+ }
102
+
103
+ console.error(`[omnicode] ERROR: omniroute did not become ready. Log: ${logFile}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ function stopOmnirouteIfIdle(pidFile) {
108
+ if (isProcessRunning("opencode")) {
109
+ return;
110
+ }
111
+
112
+ if (existsSync(pidFile)) {
113
+ let pid = null;
114
+ try {
115
+ pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
116
+ } catch {}
117
+ if (pid && isPidAlive(pid)) {
118
+ console.log(`[omnicode] no opencode left -> stopping omniroute (pid: ${pid})`);
119
+ if (process.platform === "win32") {
120
+ spawn("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore" });
121
+ } else {
122
+ try { process.kill(-pid, "SIGTERM"); } catch {}
123
+ }
124
+ for (let i = 0; i < 10; i++) {
125
+ if (!isPidAlive(pid)) break;
126
+ try { process.kill(pid, 0); } catch { break; }
127
+ }
128
+ }
129
+ try { unlinkSync(pidFile); } catch {}
130
+ }
131
+ }
132
+
133
+ export async function runRuntime(mode) {
134
+ const dataDir = getDataDir();
135
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
136
+
137
+ const logFile = join(dataDir, "omniroute.log");
138
+ const pidFile = join(dataDir, "omniroute.pid");
139
+
140
+ if (!isWindows) {
141
+ process.umask(0o077);
142
+ }
143
+
144
+ const cleanup = () => stopOmnirouteIfIdle(pidFile);
145
+ process.on("exit", cleanup);
146
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
147
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
148
+
149
+ const pid = startOmniroute(dataDir, logFile, pidFile);
150
+
151
+ await Promise.all([
152
+ pid !== null ? waitForOmniroute(pid, logFile) : Promise.resolve(),
153
+ initTools(dataDir),
154
+ ]);
155
+
156
+ if (mode.flag === "-s" && mode.id) {
157
+ console.log(`[omnicode] launching opencode (session: ${mode.id})`);
158
+ await new Promise((resolve) => {
159
+ const child = spawn("opencode", ["-s", mode.id], {
160
+ stdio: "inherit",
161
+ cwd: process.cwd(),
162
+ });
163
+ child.on("close", () => resolve());
164
+ });
165
+ } else {
166
+ console.log("[omnicode] launching opencode (new session)");
167
+ await new Promise((resolve) => {
168
+ const child = spawn("opencode", [], {
169
+ stdio: "inherit",
170
+ cwd: process.cwd(),
171
+ });
172
+ child.on("close", () => resolve());
173
+ });
174
+ }
175
+ }
@@ -1,34 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync, spawn } from "node:child_process";
3
- import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { readFileSync, realpathSync } from "node:fs";
4
3
  import { join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
- import os from "node:os";
7
5
 
8
- import { commandExists } from "../installer/lib.js";
6
+ import { commandExists, getOpencodeDbPath, isProcessRunning } from "../installer/lib.js";
7
+ import { runRuntime } from "./omnicode-runtime.js";
9
8
 
10
9
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
11
- const runtimeScript = join(__dirname, "omnicode-runtime.sh");
12
10
  const packageJsonPath = join(__dirname, "..", "..", "package.json");
13
11
 
14
- const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
12
+ const SESSION_ID_RE = /^(?=.{1,128}$)[a-zA-Z0-9_-]+$/;
13
+
14
+ let DatabaseSync;
15
+ try {
16
+ ({ DatabaseSync } = await import("node:sqlite"));
17
+ } catch {
18
+ DatabaseSync = null;
19
+ }
15
20
 
16
21
  export function printUsage() {
17
22
  console.log(`Usage: omnicode [-s <session_id>] [-c] [--status] [--version]`);
18
23
  }
19
24
 
25
+ let _cachedVersion = null;
26
+
20
27
  export function getVersion() {
28
+ if (_cachedVersion !== null) return _cachedVersion;
21
29
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
22
- return pkg.version;
23
- }
24
-
25
- export function isProcessRunning(name) {
26
- try {
27
- execFileSync("pgrep", ["-x", name], { stdio: "ignore" });
28
- return true;
29
- } catch {
30
- return false;
31
- }
30
+ _cachedVersion = pkg.version;
31
+ return _cachedVersion;
32
32
  }
33
33
 
34
34
  export function getProcessStatus() {
@@ -78,7 +78,7 @@ export function parseArgs(argv) {
78
78
  process.exit(2);
79
79
  }
80
80
 
81
- if (sessionId && !SESSION_ID_RE.test(sessionId)) {
81
+ if (!sessionId || !SESSION_ID_RE.test(sessionId)) {
82
82
  console.error(`[omnicode] ERROR: invalid session ID format`);
83
83
  process.exit(2);
84
84
  }
@@ -87,11 +87,11 @@ export function parseArgs(argv) {
87
87
  }
88
88
 
89
89
  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;
90
+ const dbPath = getOpencodeDbPath();
91
+ if (!dbPath) return null;
92
92
 
93
93
  try {
94
- const { DatabaseSync } = await import("node:sqlite");
94
+ if (!DatabaseSync) return null;
95
95
  const db = new DatabaseSync(dbPath, { readOnly: true });
96
96
  const row = db.prepare(
97
97
  "SELECT id FROM session WHERE directory = ? ORDER BY time_updated DESC LIMIT 1"
@@ -103,21 +103,13 @@ export async function getLatestSessionId(directory = realpathSync(process.cwd())
103
103
  }
104
104
  }
105
105
 
106
- export async function resolveSessionMode(sessionId, continueSession = false, latestSessionId = null) {
106
+ export async function resolveSessionMode(sessionId, latestSessionId = null) {
107
107
  if (sessionId) return { flag: "-s", id: sessionId };
108
108
  if (latestSessionId === null) latestSessionId = await getLatestSessionId();
109
109
  if (latestSessionId) return { flag: "-s", id: latestSessionId };
110
110
  return { flag: null, id: null };
111
111
  }
112
112
 
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
113
  async function main() {
122
114
  const args = parseArgs(process.argv);
123
115
 
@@ -128,23 +120,8 @@ async function main() {
128
120
  process.exit(1);
129
121
  }
130
122
 
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
- });
123
+ const mode = await resolveSessionMode(args.sessionId);
124
+ await runRuntime(mode);
148
125
  }
149
126
 
150
127
  function isMainModule() {
@@ -1,40 +1,55 @@
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();
18
+ export function isProcessRunning(name) {
19
+ try {
20
+ if (isWindows) {
21
+ const extensions = [".exe", ".cmd", ".bat"];
22
+ for (const ext of extensions) {
23
+ const out = execFileSync("tasklist", ["/FI", `IMAGENAME eq ${name}${ext}`, "/NH"], {
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ encoding: "utf8",
26
+ });
27
+ if (out.includes(`${name}${ext}`)) return true;
31
28
  }
32
- });
33
- });
29
+ return false;
30
+ }
31
+ execFileSync("pgrep", ["-x", name], { stdio: "ignore" });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ export function isPidAlive(pid) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ export function getDataDir() {
48
+ return join(os.homedir(), ".local", "share", "omnicode");
34
49
  }
35
50
 
36
- export function getRuntimeDir() {
37
- const dir = join(os.homedir(), ".local", "share", "omnicode");
38
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39
- return dir;
51
+ export function getOpencodeDbPath() {
52
+ const dbPath = join(os.homedir(), ".local", "share", "opencode", "opencode.db");
53
+ if (!existsSync(dbPath)) return null;
54
+ return dbPath;
40
55
  }
@@ -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