@ophan/cli 0.0.1

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.
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createTempDb = createTempDb;
40
+ exports.createMockSupabase = createMockSupabase;
41
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
42
+ const path = __importStar(require("path"));
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const test_utils_1 = require("@ophan/core/test-utils");
46
+ /**
47
+ * Creates a temp directory with a real .ophan/index.db file.
48
+ * Sync functions take rootPath and construct dbPath internally,
49
+ * so we need an actual file on disk.
50
+ */
51
+ function createTempDb(setup) {
52
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ophan-test-"));
53
+ const ophanDir = path.join(rootPath, ".ophan");
54
+ fs.mkdirSync(ophanDir, { recursive: true });
55
+ const dbPath = path.join(ophanDir, "index.db");
56
+ // Create a real file-based DB with the current schema
57
+ const memDb = (0, test_utils_1.createTestDb)();
58
+ // Serialize the in-memory DB to a file
59
+ const db = new better_sqlite3_1.default(dbPath);
60
+ db.exec(memDb.prepare("SELECT sql FROM sqlite_master WHERE type='table'")
61
+ .all()
62
+ .map((row) => row.sql + ";")
63
+ .join("\n"));
64
+ // Run setup callback if provided
65
+ if (setup)
66
+ setup(db);
67
+ db.close();
68
+ memDb.close();
69
+ return {
70
+ dbPath,
71
+ rootPath,
72
+ cleanup: () => {
73
+ try {
74
+ fs.rmSync(rootPath, { recursive: true, force: true });
75
+ }
76
+ catch {
77
+ // best-effort cleanup
78
+ }
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Creates a mock Supabase client that records calls and returns controlled responses.
84
+ *
85
+ * Usage:
86
+ * const mock = createMockSupabase({
87
+ * 'repos.upsert': { data: [{ id: 'repo-1' }], error: null },
88
+ * 'function_analysis.select': { data: [], error: null },
89
+ * })
90
+ * await syncToSupabase(rootPath, mock.client, userId)
91
+ * expect(mock.calls).toContainEqual(expect.objectContaining({ table: 'repos', method: 'upsert' }))
92
+ */
93
+ function createMockSupabase(responses = {}) {
94
+ const calls = [];
95
+ function createChain(table) {
96
+ let primaryMethod = "";
97
+ const resolve = () => {
98
+ const key = `${table}.${primaryMethod}`;
99
+ return responses[key] ?? { data: [], error: null };
100
+ };
101
+ const chain = {};
102
+ const handler = {
103
+ get(_, prop) {
104
+ // Primary operations
105
+ if (["select", "insert", "upsert", "update", "delete"].includes(prop)) {
106
+ return (...args) => {
107
+ primaryMethod = prop;
108
+ calls.push({ table, method: prop, args });
109
+ return new Proxy(chain, handler);
110
+ };
111
+ }
112
+ // Filter/modifier methods — chainable
113
+ if ([
114
+ "eq",
115
+ "in",
116
+ "neq",
117
+ "gt",
118
+ "lt",
119
+ "gte",
120
+ "lte",
121
+ "like",
122
+ "ilike",
123
+ "contains",
124
+ "order",
125
+ "limit",
126
+ "range",
127
+ "or",
128
+ "not",
129
+ "is",
130
+ "head",
131
+ ].includes(prop)) {
132
+ return (...args) => {
133
+ calls.push({ table, method: prop, args });
134
+ return new Proxy(chain, handler);
135
+ };
136
+ }
137
+ // Terminal methods
138
+ if (prop === "single" || prop === "maybeSingle") {
139
+ return (...args) => {
140
+ calls.push({ table, method: prop, args });
141
+ return Promise.resolve(resolve());
142
+ };
143
+ }
144
+ // Thenable — allows `await supabase.from(...).select(...).eq(...)`
145
+ if (prop === "then") {
146
+ const result = resolve();
147
+ return Promise.resolve(result).then.bind(Promise.resolve(result));
148
+ }
149
+ return new Proxy(chain, handler);
150
+ },
151
+ };
152
+ return new Proxy(chain, handler);
153
+ }
154
+ const client = {
155
+ from: (table) => {
156
+ calls.push({ table, method: "from", args: [table] });
157
+ return createChain(table);
158
+ },
159
+ };
160
+ return { client: client, calls };
161
+ }
package/dist/watch.js ADDED
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startWatch = startWatch;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const core_1 = require("@ophan/core");
40
+ function emit(event) {
41
+ process.stdout.write(JSON.stringify(event) + "\n");
42
+ }
43
+ async function startWatch(options) {
44
+ const { rootPath, pullFn, syncFn, json } = options;
45
+ const dbPath = path.join(rootPath, ".ophan", "index.db");
46
+ const lockPath = path.join(rootPath, ".ophan", "watch.lock");
47
+ // Check for existing watcher
48
+ if (fs.existsSync(lockPath)) {
49
+ try {
50
+ const pid = parseInt(fs.readFileSync(lockPath, "utf-8").trim());
51
+ process.kill(pid, 0); // Check if process is alive (signal 0 = no-op)
52
+ if (json)
53
+ emit({ event: "error", message: `Another watcher is running (PID ${pid})` });
54
+ else
55
+ console.error(`Another watcher is already running (PID ${pid}). Kill it or remove .ophan/watch.lock`);
56
+ process.exit(1);
57
+ }
58
+ catch {
59
+ // Process is dead — stale lock file, continue
60
+ }
61
+ }
62
+ // Write lock file
63
+ fs.mkdirSync(path.join(rootPath, ".ophan"), { recursive: true });
64
+ fs.writeFileSync(lockPath, String(process.pid));
65
+ const removeLock = () => {
66
+ try {
67
+ fs.unlinkSync(lockPath);
68
+ }
69
+ catch { }
70
+ };
71
+ process.on("exit", removeLock);
72
+ // Phase 1: Initial full scan (reuses analyzeRepository which opens/closes its own DB)
73
+ if (!json)
74
+ console.log("🔮 Ophan watching...\n");
75
+ if (!json)
76
+ console.log(" Running initial scan...");
77
+ const scanResult = await (0, core_1.analyzeRepository)(rootPath, (current, total, file) => {
78
+ if (!json && process.stdout.isTTY) {
79
+ process.stdout.clearLine(0);
80
+ process.stdout.cursorTo(0);
81
+ process.stdout.write(` [${current}/${total}] ${file}`);
82
+ }
83
+ }, pullFn);
84
+ if (json) {
85
+ emit({
86
+ event: "scan_complete",
87
+ files: scanResult.files,
88
+ analyzed: scanResult.analyzed,
89
+ cached: scanResult.skipped,
90
+ pulled: scanResult.pulled,
91
+ });
92
+ }
93
+ else {
94
+ console.log(`\n Initial scan: ${scanResult.analyzed} analyzed, ${scanResult.skipped} cached` +
95
+ (scanResult.pulled ? ` (${scanResult.pulled} from cloud)` : "") +
96
+ ` across ${scanResult.files} files`);
97
+ }
98
+ // Sync after initial scan if anything was analyzed
99
+ if (syncFn && scanResult.analyzed > 0) {
100
+ await runSync(syncFn, json);
101
+ }
102
+ // Phase 2: Open DB for incremental watching
103
+ const db = (0, core_1.initDb)(dbPath);
104
+ const extSet = new Set((0, core_1.getSupportedExtensions)().map((e) => e.toLowerCase()));
105
+ const DEBOUNCE_MS = 5000;
106
+ const SYNC_DEBOUNCE_MS = 10000;
107
+ // Per-file debounce timers
108
+ const timers = new Map();
109
+ // Sync debounce — batch rapid analyses into one sync
110
+ let syncTimer = null;
111
+ let pendingSync = false;
112
+ function scheduleSyncAfterAnalysis() {
113
+ if (!syncFn)
114
+ return;
115
+ pendingSync = true;
116
+ if (syncTimer)
117
+ clearTimeout(syncTimer);
118
+ syncTimer = setTimeout(async () => {
119
+ syncTimer = null;
120
+ if (pendingSync) {
121
+ pendingSync = false;
122
+ await runSync(syncFn, json);
123
+ }
124
+ }, SYNC_DEBOUNCE_MS);
125
+ }
126
+ // FIFO analysis queue
127
+ let analyzing = false;
128
+ const queue = [];
129
+ async function processQueue() {
130
+ if (analyzing || queue.length === 0)
131
+ return;
132
+ analyzing = true;
133
+ while (queue.length > 0) {
134
+ const file = queue.shift();
135
+ const relPath = path.relative(rootPath, file);
136
+ if (json)
137
+ emit({ event: "analyzing", file: relPath });
138
+ else
139
+ console.log(` Analyzing ${relPath}...`);
140
+ const start = Date.now();
141
+ try {
142
+ const result = await (0, core_1.analyzeFiles)(db, rootPath, [file], { pullFn });
143
+ const duration = Date.now() - start;
144
+ if (json) {
145
+ emit({
146
+ event: "analyzed",
147
+ file: relPath,
148
+ analyzed: result.analyzed,
149
+ cached: result.skipped,
150
+ pulled: result.pulled,
151
+ duration_ms: duration,
152
+ });
153
+ }
154
+ else if (result.analyzed > 0 || result.skipped > 0) {
155
+ console.log(` ✅ ${relPath}: ${result.analyzed} analyzed, ${result.skipped} cached (${duration}ms)`);
156
+ }
157
+ // Schedule sync after analysis
158
+ if (result.analyzed > 0) {
159
+ scheduleSyncAfterAnalysis();
160
+ }
161
+ }
162
+ catch (err) {
163
+ if (json) {
164
+ emit({ event: "error", file: relPath, message: err.message });
165
+ }
166
+ else {
167
+ console.error(` ❌ ${relPath}: ${err.message}`);
168
+ }
169
+ }
170
+ }
171
+ analyzing = false;
172
+ }
173
+ const IGNORE_SEGMENTS = ["node_modules", ".ophan", "__pycache__", ".venv", "venv", ".tox", ".eggs", "dist"];
174
+ function onFileChange(filename) {
175
+ const ext = path.extname(filename).toLowerCase();
176
+ if (!extSet.has(ext))
177
+ return;
178
+ const absPath = path.isAbsolute(filename)
179
+ ? filename
180
+ : path.resolve(rootPath, filename);
181
+ // Skip ignored directories
182
+ if (IGNORE_SEGMENTS.some((seg) => absPath.includes(`/${seg}/`) || absPath.includes(`\\${seg}\\`)))
183
+ return;
184
+ // Skip if file was deleted
185
+ if (!fs.existsSync(absPath))
186
+ return;
187
+ // Reset per-file debounce
188
+ const existing = timers.get(absPath);
189
+ if (existing)
190
+ clearTimeout(existing);
191
+ timers.set(absPath, setTimeout(() => {
192
+ timers.delete(absPath);
193
+ if (!queue.includes(absPath)) {
194
+ queue.push(absPath);
195
+ }
196
+ processQueue();
197
+ }, DEBOUNCE_MS));
198
+ }
199
+ // Start recursive file watcher (macOS + Windows native, Linux may need chokidar)
200
+ const watcher = fs.watch(rootPath, { recursive: true }, (_eventType, filename) => {
201
+ if (!filename)
202
+ return;
203
+ onFileChange(filename);
204
+ });
205
+ watcher.on("error", (err) => {
206
+ if (json)
207
+ emit({ event: "error", message: `Watcher error: ${err.message}` });
208
+ else
209
+ console.error(` Watcher error: ${err.message}`);
210
+ });
211
+ if (!json)
212
+ console.log("\n Watching for changes... (Ctrl+C to stop)\n");
213
+ // Graceful shutdown
214
+ function shutdown() {
215
+ if (!json)
216
+ console.log("\n Shutting down...");
217
+ watcher.close();
218
+ for (const timer of timers.values())
219
+ clearTimeout(timer);
220
+ timers.clear();
221
+ if (syncTimer)
222
+ clearTimeout(syncTimer);
223
+ db.close();
224
+ process.exit(0);
225
+ }
226
+ process.on("SIGINT", shutdown);
227
+ process.on("SIGTERM", shutdown);
228
+ }
229
+ async function runSync(syncFn, json) {
230
+ try {
231
+ const result = await syncFn();
232
+ if (json) {
233
+ emit({ event: "synced", pushed: result.pushed, locations: result.locations });
234
+ }
235
+ else if (result.pushed > 0) {
236
+ console.log(` ☁️ Synced: ${result.pushed} entries pushed`);
237
+ }
238
+ }
239
+ catch (err) {
240
+ if (json) {
241
+ emit({ event: "sync_error", message: err.message });
242
+ }
243
+ else {
244
+ console.error(` ☁️ Sync failed: ${err.message}`);
245
+ }
246
+ }
247
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ophan/cli",
3
+ "version": "0.0.1",
4
+ "files": ["dist"],
5
+ "bin": {
6
+ "ophan": "./dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsx src/index.ts",
11
+ "test": "vitest run"
12
+ },
13
+ "dependencies": {
14
+ "@ophan/core": "workspace:*",
15
+ "@supabase/supabase-js": "^2.49.4",
16
+ "better-sqlite3": "^11.9.1",
17
+ "commander": "^14.0.2",
18
+ "dotenv": "^16.4.7",
19
+ "open": "^10.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/better-sqlite3": "^7.6.13",
23
+ "tsx": "^4.21.0",
24
+ "typescript": "^5.9.3",
25
+ "vitest": "^3.0.5"
26
+ }
27
+ }