xray-code 0.1.0

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/dist/auth.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * GitHub Device Flow authentication for XRay CLI.
3
+ *
4
+ * Flow:
5
+ * 1. POST /login/device/code → get device_code + user_code
6
+ * 2. User visits github.com/login/device, enters user_code
7
+ * 3. CLI polls /login/oauth/access_token until authorized
8
+ * 4. Token saved to ~/.xray/credentials.json
9
+ */
10
+ export interface XRayCredentials {
11
+ access_token: string;
12
+ token_type: string;
13
+ scope: string;
14
+ refresh_token?: string;
15
+ expires_at?: string;
16
+ github_user?: string;
17
+ created_at: string;
18
+ }
19
+ export declare function loadCredentials(): XRayCredentials | null;
20
+ export declare function login(): Promise<XRayCredentials>;
21
+ export declare function logout(): void;
package/dist/auth.js ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub Device Flow authentication for XRay CLI.
4
+ *
5
+ * Flow:
6
+ * 1. POST /login/device/code → get device_code + user_code
7
+ * 2. User visits github.com/login/device, enters user_code
8
+ * 3. CLI polls /login/oauth/access_token until authorized
9
+ * 4. Token saved to ~/.xray/credentials.json
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.loadCredentials = loadCredentials;
13
+ exports.login = login;
14
+ exports.logout = logout;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const node_os_1 = require("node:os");
18
+ const CLIENT_ID = "Iv23liunw4hec3mnVMwB";
19
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
20
+ const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
21
+ const SCOPES = "repo";
22
+ function credentialsPath() {
23
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), ".xray", "credentials.json");
24
+ }
25
+ function loadCredentials() {
26
+ const path = credentialsPath();
27
+ if (!(0, node_fs_1.existsSync)(path))
28
+ return null;
29
+ try {
30
+ const creds = JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
31
+ // Check if expired
32
+ if (creds.expires_at && new Date(creds.expires_at) < new Date()) {
33
+ return null; // TODO: implement refresh
34
+ }
35
+ return creds;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function saveCredentials(creds) {
42
+ const dir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".xray");
43
+ if (!(0, node_fs_1.existsSync)(dir))
44
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
45
+ (0, node_fs_1.writeFileSync)(credentialsPath(), JSON.stringify(creds, null, 2), { mode: 0o600 });
46
+ }
47
+ async function requestDeviceCode() {
48
+ const res = await fetch(GITHUB_DEVICE_CODE_URL, {
49
+ method: "POST",
50
+ headers: {
51
+ "Accept": "application/json",
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPES }),
55
+ });
56
+ if (!res.ok) {
57
+ throw new Error(`GitHub device code request failed: ${res.status} ${res.statusText}`);
58
+ }
59
+ return res.json();
60
+ }
61
+ async function pollForToken(deviceCode, interval) {
62
+ const pollInterval = Math.max(interval, 5) * 1000; // GitHub minimum 5s
63
+ while (true) {
64
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
65
+ const res = await fetch(GITHUB_TOKEN_URL, {
66
+ method: "POST",
67
+ headers: {
68
+ "Accept": "application/json",
69
+ "Content-Type": "application/json",
70
+ },
71
+ body: JSON.stringify({
72
+ client_id: CLIENT_ID,
73
+ device_code: deviceCode,
74
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
75
+ }),
76
+ });
77
+ const data = await res.json();
78
+ if (data.access_token) {
79
+ return data;
80
+ }
81
+ if (data.error === "authorization_pending") {
82
+ process.stdout.write(".");
83
+ continue;
84
+ }
85
+ if (data.error === "slow_down") {
86
+ // GitHub wants us to slow down, add 5s
87
+ await new Promise(resolve => setTimeout(resolve, 5000));
88
+ continue;
89
+ }
90
+ if (data.error === "expired_token") {
91
+ throw new Error("Device code expired. Please try again.");
92
+ }
93
+ if (data.error === "access_denied") {
94
+ throw new Error("Authorization was denied.");
95
+ }
96
+ throw new Error(`Unexpected error: ${data.error} — ${data.error_description}`);
97
+ }
98
+ }
99
+ async function fetchGitHubUser(token) {
100
+ const res = await fetch("https://api.github.com/user", {
101
+ headers: {
102
+ "Authorization": `Bearer ${token}`,
103
+ "Accept": "application/json",
104
+ },
105
+ });
106
+ if (!res.ok)
107
+ return "unknown";
108
+ const data = await res.json();
109
+ return data.login;
110
+ }
111
+ async function login() {
112
+ // Check existing credentials
113
+ const existing = loadCredentials();
114
+ if (existing) {
115
+ console.log(`Already logged in as @${existing.github_user || "unknown"}`);
116
+ console.log("Use 'xray logout' to sign out first.");
117
+ return existing;
118
+ }
119
+ console.log("\n XRay Code Intelligence — GitHub Login\n");
120
+ // Step 1: Get device code
121
+ const device = await requestDeviceCode();
122
+ // Step 2: Show user instructions
123
+ console.log(` 1. Open: ${device.verification_uri}`);
124
+ console.log(` 2. Enter: ${device.user_code}\n`);
125
+ // Try to open browser automatically
126
+ try {
127
+ const { exec } = await import("node:child_process");
128
+ exec(`open "${device.verification_uri}"`);
129
+ }
130
+ catch {
131
+ // Ignore — user can open manually
132
+ }
133
+ // Step 3: Poll for authorization
134
+ process.stdout.write(" Waiting for authorization");
135
+ const token = await pollForToken(device.device_code, device.interval);
136
+ console.log(" done!\n");
137
+ // Step 4: Fetch user info
138
+ const username = await fetchGitHubUser(token.access_token);
139
+ console.log(` Logged in as @${username}\n`);
140
+ // Step 5: Save credentials
141
+ const creds = {
142
+ access_token: token.access_token,
143
+ token_type: token.token_type || "bearer",
144
+ scope: token.scope || SCOPES,
145
+ refresh_token: token.refresh_token,
146
+ expires_at: token.expires_in
147
+ ? new Date(Date.now() + token.expires_in * 1000).toISOString()
148
+ : undefined,
149
+ github_user: username,
150
+ created_at: new Date().toISOString(),
151
+ };
152
+ saveCredentials(creds);
153
+ console.log(` Credentials saved to ${credentialsPath()}`);
154
+ return creds;
155
+ }
156
+ function logout() {
157
+ const path = credentialsPath();
158
+ if ((0, node_fs_1.existsSync)(path)) {
159
+ const { unlinkSync } = require("node:fs");
160
+ unlinkSync(path);
161
+ console.log("Logged out. Credentials removed.");
162
+ }
163
+ else {
164
+ console.log("Not logged in.");
165
+ }
166
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * xray — Code intelligence CLI.
4
+ * X-ray your codebase with instant cross-references.
5
+ *
6
+ * Usage:
7
+ * xray login Authenticate with GitHub
8
+ * xray logout Remove stored credentials
9
+ * xray init [filter] Set up a repo for code intelligence
10
+ * xray search <query> Fuzzy search for symbols
11
+ * xray who-uses <symbol> Find all usages of a symbol
12
+ * xray impact <symbol> What breaks if I change this?
13
+ * xray symbols-in <file> List symbols defined in a file
14
+ * xray health Check server status
15
+ */
16
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * xray — Code intelligence CLI.
5
+ * X-ray your codebase with instant cross-references.
6
+ *
7
+ * Usage:
8
+ * xray login Authenticate with GitHub
9
+ * xray logout Remove stored credentials
10
+ * xray init [filter] Set up a repo for code intelligence
11
+ * xray search <query> Fuzzy search for symbols
12
+ * xray who-uses <symbol> Find all usages of a symbol
13
+ * xray impact <symbol> What breaks if I change this?
14
+ * xray symbols-in <file> List symbols defined in a file
15
+ * xray health Check server status
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const commander_1 = require("commander");
19
+ const auth_js_1 = require("./auth.js");
20
+ const onboard_js_1 = require("./onboard.js");
21
+ const client_js_1 = require("./client.js");
22
+ const program = new commander_1.Command()
23
+ .name("xray")
24
+ .description("X-ray your codebase — instant cross-references and impact analysis")
25
+ .version("0.1.0");
26
+ // ─── Auth commands ──────────────────────────────────────────────
27
+ program
28
+ .command("login")
29
+ .description("Authenticate with GitHub via device flow")
30
+ .action(async () => {
31
+ try {
32
+ await (0, auth_js_1.login)();
33
+ }
34
+ catch (e) {
35
+ console.error(`Login failed: ${e}`);
36
+ process.exit(1);
37
+ }
38
+ });
39
+ program
40
+ .command("logout")
41
+ .description("Remove stored credentials")
42
+ .action(() => (0, auth_js_1.logout)());
43
+ // ─── Activate ───────────────────────────────────────────────────
44
+ program
45
+ .command("activate <invite-code>")
46
+ .description("Activate XRay with an invite code")
47
+ .action(async (inviteCode) => {
48
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import("node:fs");
49
+ const { join } = await import("node:path");
50
+ const { homedir } = await import("node:os");
51
+ console.log(`\n Validating invite code...`);
52
+ // Redeem invite code via server
53
+ try {
54
+ const res = await fetch("https://xray.proven.dev/api/v1/redeem", {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({ code: inviteCode.trim().toUpperCase() }),
58
+ });
59
+ if (!res.ok) {
60
+ // Fallback: check local invite file (for offline/testing)
61
+ const localPath = join(homedir(), ".xray", "invites.json");
62
+ if (existsSync(localPath)) {
63
+ const invites = JSON.parse(readFileSync(localPath, "utf-8"));
64
+ const entry = invites[inviteCode.trim().toUpperCase()];
65
+ if (entry) {
66
+ const dir = join(homedir(), ".xray");
67
+ if (!existsSync(dir))
68
+ mkdirSync(dir, { recursive: true });
69
+ writeFileSync(join(dir, "config.json"), JSON.stringify({ server: entry.server, apiKey: entry.apiKey }, null, 2), { mode: 0o600 });
70
+ console.log(` XRay activated!`);
71
+ console.log(` Config saved to ~/.xray/config.json\n`);
72
+ const client = new client_js_1.XRayClient(entry.server, entry.apiKey);
73
+ const health = await client.health();
74
+ console.log(` Connected: ${health.symbols.toLocaleString()} symbols, ${health.uses.toLocaleString()} refs`);
75
+ console.log(` You're good to go! Try: xray search <symbol>\n`);
76
+ return;
77
+ }
78
+ }
79
+ console.error(` Invalid invite code. Check your code and try again.\n`);
80
+ process.exit(1);
81
+ }
82
+ const data = await res.json();
83
+ const dir = join(homedir(), ".xray");
84
+ if (!existsSync(dir))
85
+ mkdirSync(dir, { recursive: true });
86
+ writeFileSync(join(dir, "config.json"), JSON.stringify({ server: data.server, apiKey: data.api_key }, null, 2), { mode: 0o600 });
87
+ console.log(` XRay activated!`);
88
+ console.log(` Config saved to ~/.xray/config.json`);
89
+ console.log(` Space: ${data.space}\n`);
90
+ const client = new client_js_1.XRayClient(data.server, data.api_key);
91
+ const health = await client.health();
92
+ console.log(` Connected: ${health.symbols.toLocaleString()} symbols, ${health.uses.toLocaleString()} refs`);
93
+ console.log(` You're good to go! Try: xray search <symbol>\n`);
94
+ }
95
+ catch (e) {
96
+ console.error(` Activation failed: ${e}\n`);
97
+ process.exit(1);
98
+ }
99
+ });
100
+ // ─── Onboarding ─────────────────────────────────────────────────
101
+ program
102
+ .command("init [filter]")
103
+ .description("Set up a repo for XRay code intelligence")
104
+ .action(async (filter) => {
105
+ try {
106
+ await (0, onboard_js_1.onboard)(filter);
107
+ }
108
+ catch (e) {
109
+ console.error(`Setup failed: ${e}`);
110
+ process.exit(1);
111
+ }
112
+ });
113
+ // ─── Query commands ─────────────────────────────────────────────
114
+ function getClient(opts) {
115
+ return new client_js_1.XRayClient(opts.server, opts.key);
116
+ }
117
+ function queryOpts(cmd) {
118
+ return cmd
119
+ .option("--server <url>", "Server URL (env: XRAY_SERVER)")
120
+ .option("--key <key>", "API key (env: XRAY_API_KEY)");
121
+ }
122
+ queryOpts(program
123
+ .command("search <query>")
124
+ .description("Fuzzy search for symbols across the codebase")
125
+ .option("-n, --limit <n>", "Max results", "10")
126
+ .option("--json", "Output as JSON"))
127
+ .action(async (query, opts) => {
128
+ try {
129
+ const client = getClient(opts);
130
+ const res = await client.search(query, parseInt(opts.limit));
131
+ if (opts.json) {
132
+ console.log(JSON.stringify(res, null, 2));
133
+ return;
134
+ }
135
+ if (res.count === 0) {
136
+ console.log(`No symbols matching '${query}'`);
137
+ return;
138
+ }
139
+ console.log(`\n ${res.count} symbols matching '${query}':\n`);
140
+ res.results.forEach((r) => {
141
+ console.log(` ${r.name.padEnd(40)} ${r.file}:${r.line}`);
142
+ });
143
+ console.log("");
144
+ }
145
+ catch (e) {
146
+ console.error(`Search failed: ${e}`);
147
+ process.exit(1);
148
+ }
149
+ });
150
+ queryOpts(program
151
+ .command("who-uses <symbol>")
152
+ .description("Find all usages of a symbol across the codebase")
153
+ .option("-n, --limit <n>", "Max results", "20")
154
+ .option("--json", "Output as JSON"))
155
+ .action(async (symbol, opts) => {
156
+ try {
157
+ const client = getClient(opts);
158
+ const res = await client.whoUses(symbol, parseInt(opts.limit));
159
+ if (opts.json) {
160
+ console.log(JSON.stringify(res, null, 2));
161
+ return;
162
+ }
163
+ if (res.count === 0) {
164
+ console.log(`No usages found for '${symbol}'`);
165
+ return;
166
+ }
167
+ console.log(`\n ${res.count} usages of '${symbol}':\n`);
168
+ res.hits.forEach((h) => {
169
+ console.log(` ${h.source_file}:${h.source_line} (${h.symbol_name})`);
170
+ });
171
+ console.log("");
172
+ }
173
+ catch (e) {
174
+ console.error(`Query failed: ${e}`);
175
+ process.exit(1);
176
+ }
177
+ });
178
+ queryOpts(program
179
+ .command("impact <symbol>")
180
+ .description("What breaks if I change this symbol?")
181
+ .option("--json", "Output as JSON"))
182
+ .action(async (symbol, opts) => {
183
+ try {
184
+ const client = getClient(opts);
185
+ const res = await client.impact(symbol);
186
+ if (opts.json) {
187
+ console.log(JSON.stringify(res, null, 2));
188
+ return;
189
+ }
190
+ console.log(JSON.stringify(res, null, 2));
191
+ }
192
+ catch (e) {
193
+ console.error(`Impact analysis failed: ${e}`);
194
+ process.exit(1);
195
+ }
196
+ });
197
+ queryOpts(program
198
+ .command("symbols-in <file>")
199
+ .description("List all symbols defined in a file")
200
+ .option("--json", "Output as JSON"))
201
+ .action(async (file, opts) => {
202
+ try {
203
+ const client = getClient(opts);
204
+ const res = await client.symbolsIn(file);
205
+ if (opts.json) {
206
+ console.log(JSON.stringify(res, null, 2));
207
+ return;
208
+ }
209
+ if (res.count === 0) {
210
+ console.log(`No symbols found in '${file}'`);
211
+ return;
212
+ }
213
+ console.log(`\n ${res.count} symbols in ${file}:\n`);
214
+ res.symbols.forEach((s) => {
215
+ console.log(` :${String(s.line).padStart(5)} ${s.name} (${s.kind})`);
216
+ });
217
+ console.log("");
218
+ }
219
+ catch (e) {
220
+ console.error(`Query failed: ${e}`);
221
+ process.exit(1);
222
+ }
223
+ });
224
+ queryOpts(program
225
+ .command("health")
226
+ .description("Check XRay server status"))
227
+ .action(async (opts) => {
228
+ try {
229
+ const client = getClient(opts);
230
+ const res = await client.health();
231
+ console.log(`\n XRay Server Status`);
232
+ console.log(` ──────────────────────`);
233
+ console.log(` Status: ${res.status}`);
234
+ console.log(` Symbols: ${res.symbols.toLocaleString()}`);
235
+ console.log(` Refs: ${res.uses.toLocaleString()}`);
236
+ console.log(` Files: ${res.files.toLocaleString()}`);
237
+ console.log(` Stale: ${res.stale ? "YES — rebuild recommended" : "no"}`);
238
+ console.log("");
239
+ }
240
+ catch (e) {
241
+ console.error(`Health check failed: ${e}`);
242
+ process.exit(1);
243
+ }
244
+ });
245
+ program.parse();
@@ -0,0 +1,36 @@
1
+ /**
2
+ * XRay API client — queries the foxref server.
3
+ */
4
+ export declare class XRayClient {
5
+ private server;
6
+ private apiKey;
7
+ constructor(server?: string, apiKey?: string);
8
+ private request;
9
+ health(): Promise<{
10
+ status: string;
11
+ symbols: number;
12
+ uses: number;
13
+ files: number;
14
+ stale: boolean;
15
+ }>;
16
+ search(query: string, limit?: number): Promise<{
17
+ query: string;
18
+ count: number;
19
+ results: Array<{
20
+ name: string;
21
+ file: string;
22
+ line: number;
23
+ }>;
24
+ }>;
25
+ whoUses(symbol: string, limit?: number): Promise<{
26
+ symbol: string;
27
+ count: number;
28
+ hits: unknown[];
29
+ }>;
30
+ impact(symbol: string): Promise<unknown>;
31
+ symbolsIn(file: string): Promise<{
32
+ file: string;
33
+ count: number;
34
+ symbols: unknown[];
35
+ }>;
36
+ }
package/dist/client.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * XRay API client — queries the foxref server.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.XRayClient = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const node_os_1 = require("node:os");
10
+ function loadProjectConfig() {
11
+ // Check .xray/config.json in cwd first, then home
12
+ for (const base of [process.cwd(), (0, node_os_1.homedir)()]) {
13
+ const path = (0, node_path_1.join)(base, ".xray", "config.json");
14
+ if ((0, node_fs_1.existsSync)(path)) {
15
+ try {
16
+ return JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
17
+ }
18
+ catch {
19
+ continue;
20
+ }
21
+ }
22
+ }
23
+ return {};
24
+ }
25
+ class XRayClient {
26
+ server;
27
+ apiKey;
28
+ constructor(server, apiKey) {
29
+ const config = loadProjectConfig();
30
+ this.server = server
31
+ || process.env.XRAY_SERVER
32
+ || config.server
33
+ || "https://xray.proven.dev";
34
+ this.apiKey = apiKey
35
+ || process.env.XRAY_API_KEY
36
+ || config.apiKey
37
+ || "";
38
+ }
39
+ async request(path, params = {}) {
40
+ const url = new URL(`/api/v1${path}`, this.server);
41
+ for (const [k, v] of Object.entries(params)) {
42
+ url.searchParams.set(k, v);
43
+ }
44
+ const headers = { "Accept": "application/json" };
45
+ if (this.apiKey) {
46
+ headers["X-FoxRef-Key"] = this.apiKey;
47
+ }
48
+ const res = await fetch(url.toString(), { headers });
49
+ if (!res.ok) {
50
+ throw new Error(`XRay API error: ${res.status} ${res.statusText}`);
51
+ }
52
+ return res.json();
53
+ }
54
+ async health() {
55
+ return this.request("/health");
56
+ }
57
+ async search(query, limit = 10) {
58
+ return this.request("/search", { q: query, limit: String(limit) });
59
+ }
60
+ async whoUses(symbol, limit = 20) {
61
+ return this.request("/who-uses", { symbol, limit: String(limit) });
62
+ }
63
+ async impact(symbol) {
64
+ return this.request("/impact", { symbol });
65
+ }
66
+ async symbolsIn(file) {
67
+ return this.request("/symbols-in", { file });
68
+ }
69
+ }
70
+ exports.XRayClient = XRayClient;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * XRay onboarding — clone a repo, build the index, provision the endpoint.
3
+ *
4
+ * Called by `xray init` after authentication.
5
+ */
6
+ export declare function onboard(repoFilter?: string): Promise<void>;
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * XRay onboarding — clone a repo, build the index, provision the endpoint.
4
+ *
5
+ * Called by `xray init` after authentication.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.onboard = onboard;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ const node_os_1 = require("node:os");
13
+ const auth_js_1 = require("./auth.js");
14
+ const progress_js_1 = require("./progress.js");
15
+ async function fetchRepos(token) {
16
+ const repos = [];
17
+ let page = 1;
18
+ while (true) {
19
+ const res = await fetch(`https://api.github.com/user/repos?per_page=100&page=${page}&sort=updated&affiliation=owner,collaborator,organization_member`, {
20
+ headers: {
21
+ Authorization: `Bearer ${token}`,
22
+ Accept: "application/json",
23
+ },
24
+ });
25
+ if (!res.ok)
26
+ break;
27
+ const data = (await res.json());
28
+ if (data.length === 0)
29
+ break;
30
+ repos.push(...data);
31
+ page++;
32
+ if (data.length < 100)
33
+ break;
34
+ }
35
+ return repos;
36
+ }
37
+ function promptSelect(items) {
38
+ return new Promise((resolve) => {
39
+ const readline = require("node:readline");
40
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
41
+ rl.question("\n Select repo number: ", (answer) => {
42
+ rl.close();
43
+ const idx = parseInt(answer, 10) - 1;
44
+ resolve(idx >= 0 && idx < items.length ? idx : -1);
45
+ });
46
+ });
47
+ }
48
+ function cloneRepo(url, dest, token) {
49
+ // Inject token into HTTPS URL for private repos
50
+ const authedUrl = url.replace("https://", `https://x-access-token:${token}@`);
51
+ (0, node_child_process_1.execSync)(`git clone --depth 1 "${authedUrl}" "${dest}"`, {
52
+ stdio: ["pipe", "pipe", "pipe"],
53
+ });
54
+ }
55
+ function runCommand(cmd, args, cwd) {
56
+ return new Promise((resolve) => {
57
+ const proc = (0, node_child_process_1.spawn)(cmd, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
58
+ let stdout = "";
59
+ let stderr = "";
60
+ proc.stdout.on("data", (d) => (stdout += d.toString()));
61
+ proc.stderr.on("data", (d) => (stderr += d.toString()));
62
+ proc.on("close", (code) => resolve({ stdout, stderr, code: code ?? 1 }));
63
+ });
64
+ }
65
+ async function onboard(repoFilter) {
66
+ const creds = (0, auth_js_1.loadCredentials)();
67
+ if (!creds) {
68
+ console.error(" Not logged in. Run 'xray login' first.");
69
+ process.exit(1);
70
+ }
71
+ console.log("\n XRay Code Intelligence — Setup\n");
72
+ // ─── Step 1: List & select repo ───────────────────────────────
73
+ const listSpin = (0, progress_js_1.spinner)("Fetching your repositories");
74
+ const repos = await fetchRepos(creds.access_token);
75
+ listSpin.stop(`${repos.length} repos found`);
76
+ if (repos.length === 0) {
77
+ console.error(" No repositories found. Check your GitHub permissions.");
78
+ process.exit(1);
79
+ }
80
+ // Filter if provided
81
+ let filtered = repos;
82
+ if (repoFilter) {
83
+ filtered = repos.filter((r) => r.full_name.toLowerCase().includes(repoFilter.toLowerCase()));
84
+ if (filtered.length === 0) {
85
+ console.error(` No repos matching '${repoFilter}'. Try without a filter.`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ // Show top 20
90
+ const shown = filtered.slice(0, 20);
91
+ console.log("");
92
+ shown.forEach((r, i) => {
93
+ const lang = r.language ? ` (${r.language})` : "";
94
+ const priv = r.private ? " [private]" : "";
95
+ console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}${lang}${priv}`);
96
+ });
97
+ if (filtered.length > 20) {
98
+ console.log(` ... and ${filtered.length - 20} more. Use 'xray init <filter>' to narrow.`);
99
+ }
100
+ const selection = await promptSelect(shown.map((r) => r.full_name));
101
+ if (selection < 0) {
102
+ console.error(" Invalid selection.");
103
+ process.exit(1);
104
+ }
105
+ const repo = shown[selection];
106
+ console.log(`\n Selected: ${repo.full_name}\n`);
107
+ // ─── Step 2: Clone ────────────────────────────────────────────
108
+ const repoName = (0, node_path_1.basename)(repo.full_name);
109
+ const cloneDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Code", repoName);
110
+ if ((0, node_fs_1.existsSync)(cloneDir)) {
111
+ console.log(` Repo already exists at ${cloneDir} — pulling latest...`);
112
+ try {
113
+ (0, node_child_process_1.execSync)("git pull --ff-only", { cwd: cloneDir, stdio: "pipe" });
114
+ }
115
+ catch {
116
+ console.log(" Pull failed, using existing state.");
117
+ }
118
+ }
119
+ else {
120
+ const cloneSpin = (0, progress_js_1.spinner)(`Cloning ${repo.full_name}`);
121
+ try {
122
+ cloneRepo(repo.clone_url, cloneDir, creds.access_token);
123
+ cloneSpin.stop("done");
124
+ }
125
+ catch (e) {
126
+ cloneSpin.stop("FAILED");
127
+ console.error(` Clone failed: ${e}`);
128
+ process.exit(1);
129
+ }
130
+ }
131
+ // ─── Step 3: Build codegraph references ───────────────────────
132
+ const space = repoName.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
133
+ console.log("");
134
+ const cgSpin = (0, progress_js_1.spinner)("Extracting code references (Stage 1 — may take a few minutes)");
135
+ const codegraphBin = (0, node_path_1.join)((0, node_os_1.homedir)(), "Code", "FoxFlow", "crates", "fox-codegraph");
136
+ const cgResult = await runCommand("cargo", ["run", "--", "references", cloneDir, "--space", space, "--write-db"], codegraphBin);
137
+ if (cgResult.code !== 0) {
138
+ cgSpin.stop("FAILED");
139
+ console.error(" Codegraph extraction failed:");
140
+ // Show last few lines of stderr
141
+ const lines = cgResult.stderr.split("\n").filter(Boolean).slice(-5);
142
+ lines.forEach((l) => console.error(` ${l}`));
143
+ process.exit(1);
144
+ }
145
+ // Parse results from output
146
+ const refsMatch = cgResult.stdout.match(/Total references:\s+(\d+)/);
147
+ const symbolsMatch = cgResult.stdout.match(/Symbols queried:\s+(\d+)/);
148
+ cgSpin.stop(`${symbolsMatch?.[1] || "?"} symbols, ${refsMatch?.[1] || "?"} references`);
149
+ // ─── Step 4: Build foxref index ───────────────────────────────
150
+ const foxrefBin = (0, node_path_1.join)((0, node_os_1.homedir)(), "bin", "foxref");
151
+ const buildSpin = (0, progress_js_1.spinner)("Building search index (Stage 2)");
152
+ const buildResult = await runCommand(foxrefBin, ["build", "--space", space], cloneDir);
153
+ if (buildResult.code !== 0) {
154
+ buildSpin.stop("FAILED");
155
+ console.error(" Index build failed:", buildResult.stderr);
156
+ process.exit(1);
157
+ }
158
+ // Parse build output
159
+ const symCount = buildResult.stdout.match(/Symbols:\s+(\d+)/);
160
+ const usesCount = buildResult.stdout.match(/Uses:\s+(\d+)/);
161
+ buildSpin.stop(`${symCount?.[1] || "?"} symbols, ${usesCount?.[1] || "?"} cross-references`);
162
+ // ─── Step 5: Create snapshot ──────────────────────────────────
163
+ const snapSpin = (0, progress_js_1.spinner)("Creating snapshot for incremental rebuilds");
164
+ await runCommand(foxrefBin, ["snapshot", "--repo", ".", "--out", ".fox/foxref/snapshot.json"], cloneDir);
165
+ snapSpin.stop("done");
166
+ // ─── Step 6: Save project config ──────────────────────────────
167
+ const apiKey = (0, node_child_process_1.execSync)("openssl rand -hex 32").toString().trim();
168
+ const webhookSecret = (0, node_child_process_1.execSync)("openssl rand -hex 32").toString().trim();
169
+ const configDir = (0, node_path_1.join)(cloneDir, ".xray");
170
+ if (!(0, node_fs_1.existsSync)(configDir))
171
+ (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
172
+ const projectConfig = {
173
+ tenant: space,
174
+ repo: repo.full_name,
175
+ hostname: `xray.proven.dev`,
176
+ api_key: apiKey,
177
+ webhook_secret: webhookSecret,
178
+ clone_dir: cloneDir,
179
+ space,
180
+ created_at: new Date().toISOString(),
181
+ };
182
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(configDir, "credentials.json"), JSON.stringify(projectConfig, null, 2), { mode: 0o600 });
183
+ // Also save as active project in ~/.xray/
184
+ const homeXray = (0, node_path_1.join)((0, node_os_1.homedir)(), ".xray");
185
+ if (!(0, node_fs_1.existsSync)(homeXray))
186
+ (0, node_fs_1.mkdirSync)(homeXray, { recursive: true });
187
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(homeXray, "config.json"), JSON.stringify({
188
+ server: `https://xray.proven.dev`,
189
+ apiKey,
190
+ space,
191
+ repoDir: cloneDir,
192
+ }, null, 2));
193
+ // ─── Summary ──────────────────────────────────────────────────
194
+ console.log("\n ══════════════════════════════════════════════════");
195
+ console.log(` XRay is ready for ${repo.full_name}!`);
196
+ console.log(" ══════════════════════════════════════════════════\n");
197
+ console.log(` Endpoint: https://xray.proven.dev/api/v1/`);
198
+ console.log(` API Key: ${apiKey}`);
199
+ console.log(` Header: X-FoxRef-Key: ${apiKey}`);
200
+ console.log(` Space: ${space}`);
201
+ console.log(` Index: ${cloneDir}/.fox/foxref/`);
202
+ console.log("");
203
+ console.log(" Quick test:");
204
+ console.log(` xray search <symbol-name>`);
205
+ console.log(` xray who-uses <symbol-name>`);
206
+ console.log(` xray impact <symbol-name>`);
207
+ console.log("");
208
+ console.log(" To set up auto-rebuild on push, add this webhook in GitHub:");
209
+ console.log(` URL: https://xray.proven.dev/api/v1/webhook/push`);
210
+ console.log(` Secret: ${webhookSecret}`);
211
+ console.log(` Events: Push`);
212
+ console.log("");
213
+ // ─── Note: server setup is manual for now ─────────────────────
214
+ console.log(" NOTE: Server provisioning requires admin setup.");
215
+ console.log(` Admin: run 'foxref-add-tenant.sh' with space='${space}' port=<next>`);
216
+ console.log("");
217
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Simple terminal progress bar — zero dependencies.
3
+ */
4
+ export declare class ProgressBar {
5
+ private label;
6
+ private total;
7
+ private current;
8
+ private width;
9
+ private startTime;
10
+ constructor(label: string, total?: number, width?: number);
11
+ update(current: number): void;
12
+ increment(amount?: number): void;
13
+ complete(message?: string): void;
14
+ private render;
15
+ }
16
+ export declare function spinner(label: string): {
17
+ stop: (message?: string) => void;
18
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Simple terminal progress bar — zero dependencies.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ProgressBar = void 0;
7
+ exports.spinner = spinner;
8
+ class ProgressBar {
9
+ label;
10
+ total;
11
+ current;
12
+ width;
13
+ startTime;
14
+ constructor(label, total = 100, width = 30) {
15
+ this.label = label;
16
+ this.total = total;
17
+ this.current = 0;
18
+ this.width = width;
19
+ this.startTime = Date.now();
20
+ }
21
+ update(current) {
22
+ this.current = Math.min(current, this.total);
23
+ this.render();
24
+ }
25
+ increment(amount = 1) {
26
+ this.update(this.current + amount);
27
+ }
28
+ complete(message) {
29
+ this.current = this.total;
30
+ this.render();
31
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
32
+ process.stdout.write(` ${elapsed}s`);
33
+ if (message)
34
+ process.stdout.write(` — ${message}`);
35
+ process.stdout.write("\n");
36
+ }
37
+ render() {
38
+ const pct = this.total > 0 ? this.current / this.total : 0;
39
+ const filled = Math.round(this.width * pct);
40
+ const empty = this.width - filled;
41
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
42
+ const pctStr = `${Math.round(pct * 100)}%`.padStart(4);
43
+ process.stdout.write(`\r ${this.label.padEnd(30)} ${bar} ${pctStr}`);
44
+ }
45
+ }
46
+ exports.ProgressBar = ProgressBar;
47
+ function spinner(label) {
48
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
49
+ let i = 0;
50
+ const interval = setInterval(() => {
51
+ process.stdout.write(`\r ${frames[i % frames.length]} ${label}`);
52
+ i++;
53
+ }, 80);
54
+ return {
55
+ stop(message) {
56
+ clearInterval(interval);
57
+ process.stdout.write(`\r \u2713 ${label}`);
58
+ if (message)
59
+ process.stdout.write(` — ${message}`);
60
+ process.stdout.write("\n");
61
+ },
62
+ };
63
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "xray-code",
3
+ "version": "0.1.0",
4
+ "description": "XRay — code intelligence CLI. X-ray your codebase with instant cross-references.",
5
+ "bin": {
6
+ "xray": "dist/cli.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsx src/cli.ts"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "commander": "^12.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.4.0",
20
+ "tsx": "^4.0.0",
21
+ "@types/node": "^20.0.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "license": "MIT",
27
+ "author": "Proven Dev <hello@proven.dev>"
28
+ }