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 +21 -0
- package/dist/auth.js +166 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +245 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.js +70 -0
- package/dist/onboard.d.ts +6 -0
- package/dist/onboard.js +217 -0
- package/dist/progress.d.ts +18 -0
- package/dist/progress.js +63 -0
- package/package.json +28 -0
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();
|
package/dist/client.d.ts
ADDED
|
@@ -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;
|
package/dist/onboard.js
ADDED
|
@@ -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
|
+
};
|
package/dist/progress.js
ADDED
|
@@ -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
|
+
}
|