repowise 0.1.39 → 0.1.40
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/bin/repowise.js +2720 -85
- package/package.json +3 -2
- package/dist/bin/repowise.d.ts +0 -3
- package/dist/bin/repowise.d.ts.map +0 -1
- package/dist/bin/repowise.js.map +0 -1
- package/dist/src/commands/config.d.ts +0 -2
- package/dist/src/commands/config.d.ts.map +0 -1
- package/dist/src/commands/config.js +0 -95
- package/dist/src/commands/config.js.map +0 -1
- package/dist/src/commands/create.d.ts +0 -2
- package/dist/src/commands/create.d.ts.map +0 -1
- package/dist/src/commands/create.js +0 -309
- package/dist/src/commands/create.js.map +0 -1
- package/dist/src/commands/listen.d.ts +0 -5
- package/dist/src/commands/listen.d.ts.map +0 -1
- package/dist/src/commands/listen.js +0 -47
- package/dist/src/commands/listen.js.map +0 -1
- package/dist/src/commands/login.d.ts +0 -5
- package/dist/src/commands/login.d.ts.map +0 -1
- package/dist/src/commands/login.js +0 -58
- package/dist/src/commands/login.js.map +0 -1
- package/dist/src/commands/logout.d.ts +0 -2
- package/dist/src/commands/logout.d.ts.map +0 -1
- package/dist/src/commands/logout.js +0 -12
- package/dist/src/commands/logout.js.map +0 -1
- package/dist/src/commands/start.d.ts +0 -2
- package/dist/src/commands/start.d.ts.map +0 -1
- package/dist/src/commands/start.js +0 -17
- package/dist/src/commands/start.js.map +0 -1
- package/dist/src/commands/status.d.ts +0 -2
- package/dist/src/commands/status.d.ts.map +0 -1
- package/dist/src/commands/status.js +0 -63
- package/dist/src/commands/status.js.map +0 -1
- package/dist/src/commands/stop.d.ts +0 -2
- package/dist/src/commands/stop.d.ts.map +0 -1
- package/dist/src/commands/stop.js +0 -17
- package/dist/src/commands/stop.js.map +0 -1
- package/dist/src/commands/sync.d.ts +0 -2
- package/dist/src/commands/sync.d.ts.map +0 -1
- package/dist/src/commands/sync.js +0 -205
- package/dist/src/commands/sync.js.map +0 -1
- package/dist/src/lib/ai-tools.d.ts +0 -23
- package/dist/src/lib/ai-tools.d.ts.map +0 -1
- package/dist/src/lib/ai-tools.js +0 -193
- package/dist/src/lib/ai-tools.js.map +0 -1
- package/dist/src/lib/api.d.ts +0 -2
- package/dist/src/lib/api.d.ts.map +0 -1
- package/dist/src/lib/api.js +0 -38
- package/dist/src/lib/api.js.map +0 -1
- package/dist/src/lib/auth.d.ts +0 -28
- package/dist/src/lib/auth.d.ts.map +0 -1
- package/dist/src/lib/auth.js +0 -271
- package/dist/src/lib/auth.js.map +0 -1
- package/dist/src/lib/config.d.ts +0 -15
- package/dist/src/lib/config.d.ts.map +0 -1
- package/dist/src/lib/config.js +0 -19
- package/dist/src/lib/config.js.map +0 -1
- package/dist/src/lib/env.d.ts +0 -10
- package/dist/src/lib/env.d.ts.map +0 -1
- package/dist/src/lib/env.js +0 -26
- package/dist/src/lib/env.js.map +0 -1
- package/dist/src/lib/interview-handler.d.ts +0 -2
- package/dist/src/lib/interview-handler.d.ts.map +0 -1
- package/dist/src/lib/interview-handler.js +0 -100
- package/dist/src/lib/interview-handler.js.map +0 -1
- package/dist/src/lib/progress-renderer.d.ts +0 -84
- package/dist/src/lib/progress-renderer.d.ts.map +0 -1
- package/dist/src/lib/progress-renderer.js +0 -388
- package/dist/src/lib/progress-renderer.js.map +0 -1
- package/dist/src/lib/prompts.d.ts +0 -7
- package/dist/src/lib/prompts.d.ts.map +0 -1
- package/dist/src/lib/prompts.js +0 -33
- package/dist/src/lib/prompts.js.map +0 -1
- package/dist/src/lib/welcome.d.ts +0 -6
- package/dist/src/lib/welcome.d.ts.map +0 -1
- package/dist/src/lib/welcome.js +0 -42
- package/dist/src/lib/welcome.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -4
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -2
- package/dist/src/types/index.js.map +0 -1
- package/dist/tsup.config.d.ts +0 -3
- package/dist/tsup.config.d.ts.map +0 -1
- package/dist/tsup.config.js +0 -18
- package/dist/tsup.config.js.map +0 -1
package/dist/bin/repowise.js
CHANGED
|
@@ -1,97 +1,2732 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// bin/repowise.ts
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
|
+
import { dirname as dirname2, join as join15 } from "path";
|
|
8
|
+
|
|
9
|
+
// ../listener/dist/main.js
|
|
10
|
+
import { readFile as readFile4, writeFile as writeFile4, unlink, mkdir as mkdir4 } from "fs/promises";
|
|
11
|
+
import { homedir as homedir4 } from "os";
|
|
12
|
+
import { join as join6 } from "path";
|
|
13
|
+
|
|
14
|
+
// ../listener/dist/lib/config.js
|
|
15
|
+
import { readFile } from "fs/promises";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
var CONFIG_DIR = join(homedir(), ".repowise");
|
|
19
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
20
|
+
var DEFAULT_API_URL = "https://api.repowise.ai";
|
|
21
|
+
async function getListenerConfig() {
|
|
22
|
+
const apiUrl = process.env["REPOWISE_API_URL"] ?? DEFAULT_API_URL;
|
|
23
|
+
try {
|
|
24
|
+
const data = await readFile(CONFIG_PATH, "utf-8");
|
|
25
|
+
const raw = JSON.parse(data);
|
|
26
|
+
const validRepos = (raw.repos ?? []).filter((r) => typeof r === "object" && r !== null && typeof r.repoId === "string" && typeof r.localPath === "string");
|
|
27
|
+
return {
|
|
28
|
+
apiUrl: raw.apiUrl ?? apiUrl,
|
|
29
|
+
repos: validRepos
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { apiUrl, repos: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ../listener/dist/lib/state.js
|
|
37
|
+
import { readFile as readFile2, writeFile, mkdir, chmod } from "fs/promises";
|
|
38
|
+
import { homedir as homedir2 } from "os";
|
|
39
|
+
import { join as join2 } from "path";
|
|
40
|
+
var CONFIG_DIR2 = join2(homedir2(), ".repowise");
|
|
41
|
+
var STATE_PATH = join2(CONFIG_DIR2, "listener-state.json");
|
|
42
|
+
function emptyState() {
|
|
43
|
+
return { repos: {} };
|
|
44
|
+
}
|
|
45
|
+
async function loadState() {
|
|
46
|
+
try {
|
|
47
|
+
const data = await readFile2(STATE_PATH, "utf-8");
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
51
|
+
return emptyState();
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function saveState(state) {
|
|
57
|
+
await mkdir(CONFIG_DIR2, { recursive: true, mode: 448 });
|
|
58
|
+
await writeFile(STATE_PATH, JSON.stringify(state, null, 2));
|
|
59
|
+
await chmod(STATE_PATH, 384);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ../listener/dist/lib/auth.js
|
|
63
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
|
|
64
|
+
import { homedir as homedir3 } from "os";
|
|
65
|
+
import { join as join3 } from "path";
|
|
66
|
+
var CONFIG_DIR3 = join3(homedir3(), ".repowise");
|
|
67
|
+
var CREDENTIALS_PATH = join3(CONFIG_DIR3, "credentials.json");
|
|
68
|
+
function getCognitoConfig() {
|
|
69
|
+
return {
|
|
70
|
+
domain: process.env["REPOWISE_COGNITO_DOMAIN"] ?? "auth-repowise-dev",
|
|
71
|
+
clientId: process.env["REPOWISE_COGNITO_CLIENT_ID"] ?? "",
|
|
72
|
+
region: process.env["REPOWISE_COGNITO_REGION"] ?? "us-east-1"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function getTokenUrl() {
|
|
76
|
+
const { domain, region } = getCognitoConfig();
|
|
77
|
+
return `https://${domain}.auth.${region}.amazoncognito.com/oauth2/token`;
|
|
78
|
+
}
|
|
79
|
+
async function refreshTokens(refreshToken) {
|
|
80
|
+
const response = await fetch(getTokenUrl(), {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
83
|
+
body: new URLSearchParams({
|
|
84
|
+
grant_type: "refresh_token",
|
|
85
|
+
client_id: getCognitoConfig().clientId,
|
|
86
|
+
refresh_token: refreshToken
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
return {
|
|
94
|
+
accessToken: data.access_token,
|
|
95
|
+
refreshToken,
|
|
96
|
+
idToken: data.id_token,
|
|
97
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function getStoredCredentials() {
|
|
101
|
+
try {
|
|
102
|
+
const data = await readFile3(CREDENTIALS_PATH, "utf-8");
|
|
103
|
+
return JSON.parse(data);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function storeCredentials(credentials) {
|
|
112
|
+
await mkdir2(CONFIG_DIR3, { recursive: true, mode: 448 });
|
|
113
|
+
await writeFile2(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2));
|
|
114
|
+
await chmod2(CREDENTIALS_PATH, 384);
|
|
115
|
+
}
|
|
116
|
+
async function getValidCredentials() {
|
|
117
|
+
const creds = await getStoredCredentials();
|
|
118
|
+
if (!creds)
|
|
119
|
+
return null;
|
|
120
|
+
if (Date.now() > creds.expiresAt - 5 * 60 * 1e3) {
|
|
121
|
+
try {
|
|
122
|
+
const refreshed = await refreshTokens(creds.refreshToken);
|
|
123
|
+
await storeCredentials(refreshed);
|
|
124
|
+
return refreshed;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return creds;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ../listener/dist/poll-client.js
|
|
133
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
134
|
+
var AuthError = class extends Error {
|
|
135
|
+
constructor(message) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "AuthError";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var PollClient = class {
|
|
141
|
+
apiUrl;
|
|
142
|
+
constructor(apiUrl) {
|
|
143
|
+
this.apiUrl = apiUrl;
|
|
144
|
+
}
|
|
145
|
+
async poll(repoIds, since) {
|
|
146
|
+
const credentials = await getValidCredentials();
|
|
147
|
+
if (!credentials) {
|
|
148
|
+
throw new AuthError("Not logged in. Run `repowise login` first.");
|
|
149
|
+
}
|
|
150
|
+
const params = new URLSearchParams({
|
|
151
|
+
repoIds: repoIds.join(","),
|
|
152
|
+
since
|
|
153
|
+
});
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const timeoutId = setTimeout(() => controller.abort(), POLL_TIMEOUT_MS);
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`${this.apiUrl}/v1/listeners/poll?${params.toString()}`, {
|
|
158
|
+
headers: {
|
|
159
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
160
|
+
"Content-Type": "application/json"
|
|
161
|
+
},
|
|
162
|
+
signal: controller.signal
|
|
163
|
+
});
|
|
164
|
+
if (response.status === 401) {
|
|
165
|
+
throw new AuthError("Session expired. Run `repowise login` again.");
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Poll request failed: ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
const json = await response.json();
|
|
171
|
+
return json.data;
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ../listener/dist/reconnection.js
|
|
179
|
+
var DEFAULT_CONFIG = {
|
|
180
|
+
initialDelay: 1e3,
|
|
181
|
+
maxDelay: 6e4,
|
|
182
|
+
jitterMax: 1e3
|
|
183
|
+
};
|
|
184
|
+
var BackoffCalculator = class {
|
|
185
|
+
config;
|
|
186
|
+
attempt = 0;
|
|
187
|
+
constructor(config2 = {}) {
|
|
188
|
+
this.config = { ...DEFAULT_CONFIG, ...config2 };
|
|
189
|
+
}
|
|
190
|
+
nextDelay() {
|
|
191
|
+
const baseDelay = Math.min(this.config.initialDelay * Math.pow(2, this.attempt), this.config.maxDelay);
|
|
192
|
+
const jitter = Math.random() * this.config.jitterMax;
|
|
193
|
+
this.attempt++;
|
|
194
|
+
return baseDelay + jitter;
|
|
195
|
+
}
|
|
196
|
+
reset() {
|
|
197
|
+
this.attempt = 0;
|
|
198
|
+
}
|
|
199
|
+
getAttempt() {
|
|
200
|
+
return this.attempt;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// ../listener/dist/notification.js
|
|
205
|
+
import notifier from "node-notifier";
|
|
206
|
+
var TITLE = "RepoWise";
|
|
207
|
+
var notify = notifier.notify.bind(notifier);
|
|
208
|
+
function notifyConnectionLost() {
|
|
209
|
+
try {
|
|
210
|
+
notify({
|
|
211
|
+
title: TITLE,
|
|
212
|
+
message: "Connection lost \u2014 will sync when back online"
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function notifyBackOnline(updateCount) {
|
|
218
|
+
try {
|
|
219
|
+
notify({
|
|
220
|
+
title: TITLE,
|
|
221
|
+
message: `Back online \u2014 ${updateCount} updates synced`
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function notifyContextUpdated(repoId, fileCount) {
|
|
227
|
+
try {
|
|
228
|
+
notify({
|
|
229
|
+
title: TITLE,
|
|
230
|
+
message: `Context updated for ${repoId} \u2014 ${fileCount} files`
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ../listener/dist/context-fetcher.js
|
|
237
|
+
import { execFile } from "child_process";
|
|
238
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
239
|
+
import { join as join5 } from "path";
|
|
240
|
+
import { promisify } from "util";
|
|
241
|
+
|
|
242
|
+
// ../listener/dist/file-writer.js
|
|
243
|
+
import { access } from "fs/promises";
|
|
244
|
+
import { join as join4 } from "path";
|
|
245
|
+
async function verifyContextFolder(localPath) {
|
|
246
|
+
try {
|
|
247
|
+
await access(join4(localPath, "repowise-context"));
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ../listener/dist/context-fetcher.js
|
|
255
|
+
var execFileAsync = promisify(execFile);
|
|
256
|
+
async function fetchContextUpdates(localPath) {
|
|
257
|
+
try {
|
|
258
|
+
const { stdout: beforeSha } = await execFileAsync("git", [
|
|
259
|
+
"-C",
|
|
260
|
+
localPath,
|
|
261
|
+
"rev-parse",
|
|
262
|
+
"HEAD"
|
|
263
|
+
]);
|
|
264
|
+
const before = beforeSha.trim();
|
|
265
|
+
await execFileAsync("git", ["-C", localPath, "pull", "--ff-only"]);
|
|
266
|
+
const { stdout: afterSha } = await execFileAsync("git", ["-C", localPath, "rev-parse", "HEAD"]);
|
|
267
|
+
const after = afterSha.trim();
|
|
268
|
+
if (before === after) {
|
|
269
|
+
return { success: true, updatedFiles: [] };
|
|
270
|
+
}
|
|
271
|
+
const { stdout } = await execFileAsync("git", [
|
|
272
|
+
"-C",
|
|
273
|
+
localPath,
|
|
274
|
+
"diff",
|
|
275
|
+
"--name-only",
|
|
276
|
+
before,
|
|
277
|
+
after,
|
|
278
|
+
"--",
|
|
279
|
+
"repowise-context/"
|
|
280
|
+
]);
|
|
281
|
+
const updatedFiles = stdout.trim().split("\n").filter(Boolean);
|
|
282
|
+
const contextExists = await verifyContextFolder(localPath);
|
|
283
|
+
if (!contextExists && updatedFiles.length > 0) {
|
|
284
|
+
console.warn(`Warning: repowise-context/ folder not found in ${localPath} after pull`);
|
|
285
|
+
}
|
|
286
|
+
return { success: true, updatedFiles };
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
289
|
+
console.error(`Git pull failed for ${localPath}: ${message}`);
|
|
290
|
+
return { success: false, updatedFiles: [] };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function fetchContextFromServer(repoId, localPath, apiUrl, authToken) {
|
|
294
|
+
try {
|
|
295
|
+
const headers = { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json" };
|
|
296
|
+
const listRes = await fetch(`${apiUrl}/v1/repos/${repoId}/context`, { headers });
|
|
297
|
+
if (!listRes.ok) {
|
|
298
|
+
console.error(`Context list failed (${listRes.status}) for repo ${repoId}`);
|
|
299
|
+
return { success: false, updatedFiles: [] };
|
|
300
|
+
}
|
|
301
|
+
const listData = await listRes.json();
|
|
302
|
+
const files = listData.data?.files ?? [];
|
|
303
|
+
if (files.length === 0) {
|
|
304
|
+
return { success: true, updatedFiles: [] };
|
|
305
|
+
}
|
|
306
|
+
const contextDir = join5(localPath, "repowise-context");
|
|
307
|
+
await mkdir3(contextDir, { recursive: true });
|
|
308
|
+
const updatedFiles = [];
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
const urlRes = await fetch(`${apiUrl}/v1/repos/${repoId}/context/${file.fileName}`, {
|
|
311
|
+
headers
|
|
312
|
+
});
|
|
313
|
+
if (!urlRes.ok)
|
|
314
|
+
continue;
|
|
315
|
+
const urlData = await urlRes.json();
|
|
316
|
+
const presignedUrl = urlData.data?.url;
|
|
317
|
+
if (!presignedUrl)
|
|
318
|
+
continue;
|
|
319
|
+
const contentRes = await fetch(presignedUrl);
|
|
320
|
+
if (!contentRes.ok)
|
|
321
|
+
continue;
|
|
322
|
+
const content = await contentRes.text();
|
|
323
|
+
await writeFile3(join5(contextDir, file.fileName), content, "utf-8");
|
|
324
|
+
updatedFiles.push(file.fileName);
|
|
325
|
+
}
|
|
326
|
+
return { success: true, updatedFiles };
|
|
327
|
+
} catch (err) {
|
|
328
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
329
|
+
console.error(`Server context fetch failed for ${repoId}: ${message}`);
|
|
330
|
+
return { success: false, updatedFiles: [] };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ../listener/dist/main.js
|
|
335
|
+
var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
|
|
336
|
+
var PID_PATH = join6(homedir4(), ".repowise", "listener.pid");
|
|
337
|
+
var running = false;
|
|
338
|
+
var sleepResolve = null;
|
|
339
|
+
async function writePidFile() {
|
|
340
|
+
await mkdir4(join6(homedir4(), ".repowise"), { recursive: true });
|
|
341
|
+
await writeFile4(PID_PATH, String(process.pid));
|
|
342
|
+
}
|
|
343
|
+
async function removePidFile() {
|
|
344
|
+
try {
|
|
345
|
+
await unlink(PID_PATH);
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function handleStalePid() {
|
|
350
|
+
try {
|
|
351
|
+
const content = await readFile4(PID_PATH, "utf-8");
|
|
352
|
+
const pid = parseInt(content.trim(), 10);
|
|
353
|
+
if (!Number.isNaN(pid) && pid !== process.pid) {
|
|
354
|
+
try {
|
|
355
|
+
process.kill(pid, 0);
|
|
356
|
+
console.error(`Listener already running (PID: ${pid}). Stop it first with \`repowise stop\`.`);
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
return true;
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
function stop() {
|
|
367
|
+
running = false;
|
|
368
|
+
if (sleepResolve) {
|
|
369
|
+
sleepResolve();
|
|
370
|
+
sleepResolve = null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function sleep(ms) {
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
sleepResolve = resolve;
|
|
376
|
+
const timer = setTimeout(() => {
|
|
377
|
+
sleepResolve = null;
|
|
378
|
+
resolve();
|
|
379
|
+
}, ms);
|
|
380
|
+
if (typeof timer === "object" && "unref" in timer)
|
|
381
|
+
timer.unref();
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async function processNotifications(notifications, state, repoLocalPaths, apiUrl, authToken) {
|
|
385
|
+
let updateCount = 0;
|
|
386
|
+
for (const notif of notifications) {
|
|
387
|
+
if (notif.type === "sync.completed") {
|
|
388
|
+
const localPath = repoLocalPaths.get(notif.repoId);
|
|
389
|
+
if (localPath) {
|
|
390
|
+
let result;
|
|
391
|
+
if (apiUrl && authToken) {
|
|
392
|
+
result = await fetchContextFromServer(notif.repoId, localPath, apiUrl, authToken);
|
|
393
|
+
} else {
|
|
394
|
+
result = await fetchContextUpdates(localPath);
|
|
395
|
+
}
|
|
396
|
+
if (result.success) {
|
|
397
|
+
updateCount++;
|
|
398
|
+
notifyContextUpdated(notif.repoId, result.updatedFiles.length);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
state.repos[notif.repoId] = {
|
|
403
|
+
lastSyncTimestamp: notif.createdAt,
|
|
404
|
+
lastSyncCommitSha: notif.commitSha
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return updateCount;
|
|
408
|
+
}
|
|
409
|
+
async function handleCatchUp(offlineState, pollClient, repoIds, state, repoLocalPaths, apiUrl, authToken) {
|
|
410
|
+
if (!offlineState.offlineSince)
|
|
411
|
+
return;
|
|
412
|
+
const offlineDuration = Date.now() - new Date(offlineState.offlineSince).getTime();
|
|
413
|
+
if (offlineDuration >= TWENTY_FOUR_HOURS_MS) {
|
|
414
|
+
let syncCount = 0;
|
|
415
|
+
for (const [repoId, localPath] of repoLocalPaths) {
|
|
416
|
+
let result;
|
|
417
|
+
if (apiUrl && authToken) {
|
|
418
|
+
result = await fetchContextFromServer(repoId, localPath, apiUrl, authToken);
|
|
419
|
+
} else {
|
|
420
|
+
result = await fetchContextUpdates(localPath);
|
|
421
|
+
}
|
|
422
|
+
if (result.success)
|
|
423
|
+
syncCount++;
|
|
424
|
+
}
|
|
425
|
+
notifyBackOnline(syncCount);
|
|
426
|
+
} else {
|
|
427
|
+
const sinceTimestamp = offlineState.offlineSince;
|
|
428
|
+
const response = await pollClient.poll(repoIds, sinceTimestamp);
|
|
429
|
+
const updateCount = await processNotifications(response.notifications, state, repoLocalPaths, apiUrl, authToken);
|
|
430
|
+
await saveState(state);
|
|
431
|
+
notifyBackOnline(updateCount);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function startListener() {
|
|
435
|
+
running = true;
|
|
436
|
+
const alreadyRunning = await handleStalePid();
|
|
437
|
+
if (alreadyRunning)
|
|
438
|
+
return;
|
|
439
|
+
await writePidFile();
|
|
440
|
+
const config2 = await getListenerConfig();
|
|
441
|
+
if (config2.repos.length === 0) {
|
|
442
|
+
console.error("No repos configured. Add repos to ~/.repowise/config.json");
|
|
443
|
+
process.exitCode = 1;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const credentials = await getValidCredentials();
|
|
447
|
+
if (!credentials) {
|
|
448
|
+
console.error("Not logged in. Run `repowise login` first.");
|
|
449
|
+
process.exitCode = 1;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const state = await loadState();
|
|
453
|
+
const pollClient = new PollClient(config2.apiUrl);
|
|
454
|
+
const backoff = new BackoffCalculator();
|
|
455
|
+
const repoIds = config2.repos.map((r) => r.repoId);
|
|
456
|
+
const repoLocalPaths = new Map(config2.repos.map((r) => [r.repoId, r.localPath]));
|
|
457
|
+
const offlineState = {
|
|
458
|
+
isOffline: false,
|
|
459
|
+
offlineSince: null,
|
|
460
|
+
attemptCount: 0
|
|
461
|
+
};
|
|
462
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
463
|
+
for (const repoId of repoIds) {
|
|
464
|
+
if (!state.repos[repoId]) {
|
|
465
|
+
state.repos[repoId] = { lastSyncTimestamp: now, lastSyncCommitSha: null };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let pollIntervalMs = 5e3;
|
|
469
|
+
console.log(`RepoWise Listener started \u2014 watching ${repoIds.length} repo(s)`);
|
|
470
|
+
const shutdown = async () => {
|
|
471
|
+
console.log("Shutting down...");
|
|
472
|
+
stop();
|
|
473
|
+
await saveState(state);
|
|
474
|
+
await removePidFile();
|
|
475
|
+
};
|
|
476
|
+
process.on("SIGTERM", () => void shutdown());
|
|
477
|
+
process.on("SIGINT", () => void shutdown());
|
|
478
|
+
while (running) {
|
|
479
|
+
try {
|
|
480
|
+
const sinceTimestamp = repoIds.reduce((earliest, id) => {
|
|
481
|
+
const ts = state.repos[id]?.lastSyncTimestamp ?? now;
|
|
482
|
+
return ts < earliest ? ts : earliest;
|
|
483
|
+
}, now);
|
|
484
|
+
const response = await pollClient.poll(repoIds, sinceTimestamp);
|
|
485
|
+
const freshCredentials = await getValidCredentials();
|
|
486
|
+
const authToken = freshCredentials?.idToken;
|
|
487
|
+
if (offlineState.isOffline) {
|
|
488
|
+
await handleCatchUp(offlineState, pollClient, repoIds, state, repoLocalPaths, config2.apiUrl, authToken);
|
|
489
|
+
offlineState.isOffline = false;
|
|
490
|
+
offlineState.offlineSince = null;
|
|
491
|
+
offlineState.attemptCount = 0;
|
|
492
|
+
backoff.reset();
|
|
493
|
+
} else if (response.notifications.length > 0) {
|
|
494
|
+
await processNotifications(response.notifications, state, repoLocalPaths, config2.apiUrl, authToken);
|
|
495
|
+
await saveState(state);
|
|
496
|
+
}
|
|
497
|
+
pollIntervalMs = response.pollIntervalMs;
|
|
498
|
+
await sleep(pollIntervalMs);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (!running)
|
|
501
|
+
break;
|
|
502
|
+
if (err instanceof AuthError) {
|
|
503
|
+
console.error(err.message);
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (!offlineState.isOffline) {
|
|
508
|
+
offlineState.isOffline = true;
|
|
509
|
+
offlineState.offlineSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
510
|
+
offlineState.attemptCount = 0;
|
|
511
|
+
notifyConnectionLost();
|
|
512
|
+
}
|
|
513
|
+
offlineState.attemptCount++;
|
|
514
|
+
const delay = backoff.nextDelay();
|
|
515
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
516
|
+
console.error(`Poll failed (attempt ${offlineState.attemptCount}): ${message}. Retrying in ${Math.round(delay / 1e3)}s`);
|
|
517
|
+
await sleep(delay);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
await removePidFile();
|
|
521
|
+
}
|
|
522
|
+
var isDirectRun = process.argv[1]?.endsWith("main.js") || process.argv[1]?.endsWith("main.ts");
|
|
523
|
+
if (isDirectRun) {
|
|
524
|
+
startListener().catch((err) => {
|
|
525
|
+
console.error("Listener fatal error:", err);
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// bin/repowise.ts
|
|
531
|
+
import { Command } from "commander";
|
|
532
|
+
|
|
533
|
+
// src/lib/env.ts
|
|
534
|
+
var staging = false;
|
|
535
|
+
function setStagingMode(value) {
|
|
536
|
+
staging = value;
|
|
537
|
+
}
|
|
538
|
+
var PRODUCTION = {
|
|
539
|
+
apiUrl: "https://api.repowise.ai",
|
|
540
|
+
cognitoDomain: "auth.repowise.ai",
|
|
541
|
+
cognitoClientId: "",
|
|
542
|
+
// TODO: set after production Cognito deploy
|
|
543
|
+
cognitoRegion: "us-east-1",
|
|
544
|
+
customDomain: true
|
|
545
|
+
};
|
|
546
|
+
var STAGING = {
|
|
547
|
+
apiUrl: "https://staging-api.repowise.ai",
|
|
548
|
+
cognitoDomain: "auth-staging.repowise.ai",
|
|
549
|
+
cognitoClientId: "7h0l0dhjcb1v5erer0gaclv0q6",
|
|
550
|
+
cognitoRegion: "us-east-1",
|
|
551
|
+
customDomain: true
|
|
552
|
+
};
|
|
553
|
+
function getEnvConfig() {
|
|
554
|
+
return staging ? STAGING : PRODUCTION;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/lib/welcome.ts
|
|
558
|
+
import chalk from "chalk";
|
|
559
|
+
|
|
560
|
+
// src/lib/config.ts
|
|
561
|
+
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
562
|
+
import { homedir as homedir5 } from "os";
|
|
563
|
+
import { join as join7 } from "path";
|
|
564
|
+
var CONFIG_DIR4 = join7(homedir5(), ".repowise");
|
|
565
|
+
var CONFIG_PATH2 = join7(CONFIG_DIR4, "config.json");
|
|
566
|
+
async function getConfig() {
|
|
567
|
+
try {
|
|
568
|
+
const data = await readFile5(CONFIG_PATH2, "utf-8");
|
|
569
|
+
return JSON.parse(data);
|
|
570
|
+
} catch {
|
|
571
|
+
return {};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function saveConfig(config2) {
|
|
575
|
+
await mkdir5(CONFIG_DIR4, { recursive: true });
|
|
576
|
+
await writeFile5(CONFIG_PATH2, JSON.stringify(config2, null, 2));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/lib/welcome.ts
|
|
580
|
+
var W = 41;
|
|
581
|
+
function row(styled, visible) {
|
|
582
|
+
return chalk.cyan(" \u2502") + styled + " ".repeat(W - visible) + chalk.cyan("\u2502");
|
|
583
|
+
}
|
|
584
|
+
async function showWelcome(currentVersion) {
|
|
585
|
+
try {
|
|
586
|
+
const config2 = await getConfig();
|
|
587
|
+
if (config2.lastSeenVersion === currentVersion) return;
|
|
588
|
+
const isUpgrade = !!config2.lastSeenVersion;
|
|
589
|
+
const tag = isUpgrade ? "updated" : "installed";
|
|
590
|
+
const titleText = `RepoWise v${currentVersion}`;
|
|
591
|
+
const titleStyled = " " + chalk.bold(titleText) + chalk.green(` \u2713 ${tag}`);
|
|
592
|
+
const titleVisible = 3 + titleText.length + 3 + tag.length;
|
|
593
|
+
const border = "\u2500".repeat(W);
|
|
594
|
+
console.log("");
|
|
595
|
+
console.log(chalk.cyan(` \u256D${border}\u256E`));
|
|
596
|
+
console.log(row("", 0));
|
|
597
|
+
console.log(row(titleStyled, titleVisible));
|
|
598
|
+
console.log(row("", 0));
|
|
599
|
+
console.log(row(" " + chalk.dim("Get started:"), 15));
|
|
600
|
+
console.log(row(" $ " + chalk.bold("repowise create"), 22));
|
|
601
|
+
console.log(row("", 0));
|
|
602
|
+
console.log(row(" " + chalk.dim("Thank you for using RepoWise!"), 32));
|
|
603
|
+
console.log(row(" " + chalk.dim("https://repowise.ai"), 22));
|
|
604
|
+
console.log(row("", 0));
|
|
605
|
+
console.log(chalk.cyan(` \u2570${border}\u256F`));
|
|
606
|
+
console.log("");
|
|
607
|
+
await saveConfig({ ...config2, lastSeenVersion: currentVersion });
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/commands/create.ts
|
|
613
|
+
import { execSync } from "child_process";
|
|
614
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
615
|
+
import { join as join12 } from "path";
|
|
616
|
+
|
|
617
|
+
// ../listener/dist/process-manager.js
|
|
618
|
+
import { spawn } from "child_process";
|
|
619
|
+
import { openSync, closeSync } from "fs";
|
|
620
|
+
import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir6, unlink as unlink2 } from "fs/promises";
|
|
621
|
+
import { homedir as homedir6 } from "os";
|
|
622
|
+
import { join as join8 } from "path";
|
|
623
|
+
import { createRequire } from "module";
|
|
624
|
+
import { fileURLToPath } from "url";
|
|
625
|
+
var REPOWISE_DIR = join8(homedir6(), ".repowise");
|
|
626
|
+
var PID_PATH2 = join8(REPOWISE_DIR, "listener.pid");
|
|
627
|
+
var LOG_DIR = join8(REPOWISE_DIR, "logs");
|
|
628
|
+
function resolveListenerCommand() {
|
|
629
|
+
try {
|
|
630
|
+
const require2 = createRequire(import.meta.url);
|
|
631
|
+
const mainPath = require2.resolve("@repowise/listener/main");
|
|
632
|
+
return { script: mainPath, args: [] };
|
|
633
|
+
} catch {
|
|
634
|
+
const bundlePath = fileURLToPath(import.meta.url);
|
|
635
|
+
return { script: bundlePath, args: ["__listener"] };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function readPid() {
|
|
639
|
+
try {
|
|
640
|
+
const content = await readFile6(PID_PATH2, "utf-8");
|
|
641
|
+
const pid = parseInt(content.trim(), 10);
|
|
642
|
+
return Number.isNaN(pid) ? null : pid;
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (err.code === "ENOENT")
|
|
645
|
+
return null;
|
|
646
|
+
throw err;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function isAlive(pid) {
|
|
650
|
+
try {
|
|
651
|
+
process.kill(pid, 0);
|
|
652
|
+
return true;
|
|
653
|
+
} catch {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function startBackground() {
|
|
658
|
+
await mkdir6(LOG_DIR, { recursive: true });
|
|
659
|
+
const cmd = resolveListenerCommand();
|
|
660
|
+
const stdoutFd = openSync(join8(LOG_DIR, "listener-stdout.log"), "a");
|
|
661
|
+
const stderrFd = openSync(join8(LOG_DIR, "listener-stderr.log"), "a");
|
|
662
|
+
const child = spawn(process.execPath, [cmd.script, ...cmd.args], {
|
|
663
|
+
detached: true,
|
|
664
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
665
|
+
cwd: homedir6(),
|
|
666
|
+
env: { ...process.env }
|
|
667
|
+
});
|
|
668
|
+
child.unref();
|
|
669
|
+
closeSync(stdoutFd);
|
|
670
|
+
closeSync(stderrFd);
|
|
671
|
+
const pid = child.pid;
|
|
672
|
+
if (!pid)
|
|
673
|
+
throw new Error("Failed to spawn listener process");
|
|
674
|
+
await writeFile6(PID_PATH2, String(pid));
|
|
675
|
+
return pid;
|
|
676
|
+
}
|
|
677
|
+
async function stopProcess() {
|
|
678
|
+
const pid = await readPid();
|
|
679
|
+
if (pid === null)
|
|
680
|
+
return;
|
|
681
|
+
if (!isAlive(pid)) {
|
|
682
|
+
await removePidFile2();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
process.kill(pid, "SIGTERM");
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
const deadline = Date.now() + 5e3;
|
|
690
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
691
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
692
|
+
}
|
|
693
|
+
if (isAlive(pid)) {
|
|
694
|
+
try {
|
|
695
|
+
process.kill(pid, "SIGKILL");
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
await removePidFile2();
|
|
700
|
+
}
|
|
701
|
+
async function isRunning() {
|
|
702
|
+
const pid = await readPid();
|
|
703
|
+
if (pid === null)
|
|
704
|
+
return false;
|
|
705
|
+
return isAlive(pid);
|
|
706
|
+
}
|
|
707
|
+
async function getStatus() {
|
|
708
|
+
const pid = await readPid();
|
|
709
|
+
if (pid === null)
|
|
710
|
+
return { running: false, pid: null };
|
|
711
|
+
const alive = isAlive(pid);
|
|
712
|
+
return { running: alive, pid: alive ? pid : null };
|
|
713
|
+
}
|
|
714
|
+
async function removePidFile2() {
|
|
715
|
+
try {
|
|
716
|
+
await unlink2(PID_PATH2);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ../listener/dist/service-installer.js
|
|
722
|
+
import { execFile as execFile2 } from "child_process";
|
|
723
|
+
import { writeFile as writeFile7, mkdir as mkdir7, unlink as unlink3 } from "fs/promises";
|
|
724
|
+
import { homedir as homedir7 } from "os";
|
|
725
|
+
import { join as join9 } from "path";
|
|
726
|
+
import { createRequire as createRequire2 } from "module";
|
|
727
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
728
|
+
function resolveListenerCommand2() {
|
|
729
|
+
try {
|
|
730
|
+
const require2 = createRequire2(import.meta.url);
|
|
731
|
+
const mainPath = require2.resolve("@repowise/listener/main");
|
|
732
|
+
return { script: mainPath, args: [] };
|
|
733
|
+
} catch {
|
|
734
|
+
const bundlePath = fileURLToPath2(import.meta.url);
|
|
735
|
+
return { script: bundlePath, args: ["__listener"] };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function exec(cmd, args) {
|
|
739
|
+
return new Promise((resolve, reject) => {
|
|
740
|
+
execFile2(cmd, args, (err, stdout) => {
|
|
741
|
+
if (err) {
|
|
742
|
+
reject(err);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
resolve(String(stdout ?? ""));
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
var PLIST_LABEL = "com.repowise.listener";
|
|
750
|
+
function plistPath() {
|
|
751
|
+
return join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
752
|
+
}
|
|
753
|
+
function logDir() {
|
|
754
|
+
return join9(homedir7(), ".repowise", "logs");
|
|
755
|
+
}
|
|
756
|
+
function buildPlist() {
|
|
757
|
+
const cmd = resolveListenerCommand2();
|
|
758
|
+
const logs = logDir();
|
|
759
|
+
const programArgs = [process.execPath, cmd.script, ...cmd.args].map((a) => ` <string>${a}</string>`).join("\n");
|
|
760
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
761
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
762
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
763
|
+
<plist version="1.0">
|
|
764
|
+
<dict>
|
|
765
|
+
<key>Label</key>
|
|
766
|
+
<string>${PLIST_LABEL}</string>
|
|
767
|
+
<key>ProgramArguments</key>
|
|
768
|
+
<array>
|
|
769
|
+
${programArgs}
|
|
770
|
+
</array>
|
|
771
|
+
<key>RunAtLoad</key>
|
|
772
|
+
<true/>
|
|
773
|
+
<key>KeepAlive</key>
|
|
774
|
+
<true/>
|
|
775
|
+
<key>StandardOutPath</key>
|
|
776
|
+
<string>${join9(logs, "listener-stdout.log")}</string>
|
|
777
|
+
<key>StandardErrorPath</key>
|
|
778
|
+
<string>${join9(logs, "listener-stderr.log")}</string>
|
|
779
|
+
<key>ProcessType</key>
|
|
780
|
+
<string>Background</string>
|
|
781
|
+
</dict>
|
|
782
|
+
</plist>`;
|
|
783
|
+
}
|
|
784
|
+
async function darwinInstall() {
|
|
785
|
+
await mkdir7(logDir(), { recursive: true });
|
|
786
|
+
await mkdir7(join9(homedir7(), "Library", "LaunchAgents"), { recursive: true });
|
|
787
|
+
try {
|
|
788
|
+
await exec("launchctl", ["unload", plistPath()]);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
await writeFile7(plistPath(), buildPlist());
|
|
792
|
+
await exec("launchctl", ["load", plistPath()]);
|
|
793
|
+
}
|
|
794
|
+
async function darwinUninstall() {
|
|
795
|
+
try {
|
|
796
|
+
await exec("launchctl", ["unload", plistPath()]);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
await unlink3(plistPath());
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async function darwinIsInstalled() {
|
|
805
|
+
try {
|
|
806
|
+
const stdout = await exec("launchctl", ["list"]);
|
|
807
|
+
return stdout.includes(PLIST_LABEL);
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var SYSTEMD_SERVICE = "repowise-listener";
|
|
813
|
+
function unitPath() {
|
|
814
|
+
return join9(homedir7(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
|
|
815
|
+
}
|
|
816
|
+
function buildUnit() {
|
|
817
|
+
const cmd = resolveListenerCommand2();
|
|
818
|
+
const execStart = [process.execPath, cmd.script, ...cmd.args].join(" ");
|
|
819
|
+
const logs = logDir();
|
|
820
|
+
return `[Unit]
|
|
821
|
+
Description=RepoWise Listener
|
|
822
|
+
After=network-online.target
|
|
823
|
+
Wants=network-online.target
|
|
824
|
+
|
|
825
|
+
[Service]
|
|
826
|
+
Type=simple
|
|
827
|
+
ExecStart=${execStart}
|
|
828
|
+
Restart=on-failure
|
|
829
|
+
RestartSec=10
|
|
830
|
+
StandardOutput=append:${join9(logs, "listener-stdout.log")}
|
|
831
|
+
StandardError=append:${join9(logs, "listener-stderr.log")}
|
|
832
|
+
|
|
833
|
+
[Install]
|
|
834
|
+
WantedBy=default.target`;
|
|
835
|
+
}
|
|
836
|
+
async function linuxInstall() {
|
|
837
|
+
await mkdir7(logDir(), { recursive: true });
|
|
838
|
+
await mkdir7(join9(homedir7(), ".config", "systemd", "user"), { recursive: true });
|
|
839
|
+
await writeFile7(unitPath(), buildUnit());
|
|
840
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
841
|
+
await exec("systemctl", ["--user", "enable", SYSTEMD_SERVICE]);
|
|
842
|
+
await exec("systemctl", ["--user", "start", SYSTEMD_SERVICE]);
|
|
843
|
+
}
|
|
844
|
+
async function linuxUninstall() {
|
|
845
|
+
try {
|
|
846
|
+
await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
await exec("systemctl", ["--user", "disable", SYSTEMD_SERVICE]);
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
await unlink3(unitPath());
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function linuxIsInstalled() {
|
|
863
|
+
try {
|
|
864
|
+
const stdout = await exec("systemctl", ["--user", "is-enabled", SYSTEMD_SERVICE]);
|
|
865
|
+
return stdout.trim() === "enabled";
|
|
866
|
+
} catch {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
var TASK_NAME = "RepoWise Listener";
|
|
871
|
+
async function win32Install() {
|
|
872
|
+
await mkdir7(logDir(), { recursive: true });
|
|
873
|
+
const cmd = resolveListenerCommand2();
|
|
874
|
+
const taskCmd = [process.execPath, cmd.script, ...cmd.args].map((a) => `"${a}"`).join(" ");
|
|
875
|
+
await exec("schtasks", [
|
|
876
|
+
"/create",
|
|
877
|
+
"/tn",
|
|
878
|
+
TASK_NAME,
|
|
879
|
+
"/tr",
|
|
880
|
+
taskCmd,
|
|
881
|
+
"/sc",
|
|
882
|
+
"onlogon",
|
|
883
|
+
"/ru",
|
|
884
|
+
process.env.USERNAME ?? "",
|
|
885
|
+
"/f"
|
|
886
|
+
]);
|
|
887
|
+
await exec("schtasks", ["/run", "/tn", TASK_NAME]);
|
|
888
|
+
}
|
|
889
|
+
async function win32Uninstall() {
|
|
890
|
+
try {
|
|
891
|
+
await exec("schtasks", ["/end", "/tn", TASK_NAME]);
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
await exec("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function win32IsInstalled() {
|
|
900
|
+
try {
|
|
901
|
+
await exec("schtasks", ["/query", "/tn", TASK_NAME]);
|
|
902
|
+
return true;
|
|
903
|
+
} catch {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function install() {
|
|
908
|
+
switch (process.platform) {
|
|
909
|
+
case "darwin":
|
|
910
|
+
await darwinInstall();
|
|
911
|
+
break;
|
|
912
|
+
case "linux":
|
|
913
|
+
await linuxInstall();
|
|
914
|
+
break;
|
|
915
|
+
case "win32":
|
|
916
|
+
await win32Install();
|
|
917
|
+
break;
|
|
918
|
+
default:
|
|
919
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function uninstall() {
|
|
923
|
+
switch (process.platform) {
|
|
924
|
+
case "darwin":
|
|
925
|
+
await darwinUninstall();
|
|
926
|
+
break;
|
|
927
|
+
case "linux":
|
|
928
|
+
await linuxUninstall();
|
|
929
|
+
break;
|
|
930
|
+
case "win32":
|
|
931
|
+
await win32Uninstall();
|
|
932
|
+
break;
|
|
933
|
+
default:
|
|
934
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function isInstalled() {
|
|
938
|
+
switch (process.platform) {
|
|
939
|
+
case "darwin":
|
|
940
|
+
return darwinIsInstalled();
|
|
941
|
+
case "linux":
|
|
942
|
+
return linuxIsInstalled();
|
|
943
|
+
case "win32":
|
|
944
|
+
return win32IsInstalled();
|
|
945
|
+
default:
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/commands/create.ts
|
|
951
|
+
import chalk5 from "chalk";
|
|
952
|
+
import ora from "ora";
|
|
953
|
+
|
|
954
|
+
// src/lib/auth.ts
|
|
955
|
+
import { createHash, randomBytes } from "crypto";
|
|
956
|
+
import { readFile as readFile7, writeFile as writeFile8, mkdir as mkdir8, chmod as chmod3, unlink as unlink4 } from "fs/promises";
|
|
957
|
+
import http from "http";
|
|
958
|
+
import { homedir as homedir8 } from "os";
|
|
959
|
+
import { join as join10 } from "path";
|
|
960
|
+
var CONFIG_DIR5 = join10(homedir8(), ".repowise");
|
|
961
|
+
var CREDENTIALS_PATH2 = join10(CONFIG_DIR5, "credentials.json");
|
|
962
|
+
var CLI_CALLBACK_PORT = 19876;
|
|
963
|
+
var CALLBACK_TIMEOUT_MS = 12e4;
|
|
964
|
+
function getCognitoConfig2() {
|
|
965
|
+
const env = getEnvConfig();
|
|
966
|
+
return {
|
|
967
|
+
domain: process.env["REPOWISE_COGNITO_DOMAIN"] ?? env.cognitoDomain,
|
|
968
|
+
clientId: process.env["REPOWISE_COGNITO_CLIENT_ID"] ?? env.cognitoClientId,
|
|
969
|
+
region: process.env["REPOWISE_COGNITO_REGION"] ?? env.cognitoRegion,
|
|
970
|
+
customDomain: env.customDomain
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function getCognitoBaseUrl() {
|
|
974
|
+
const { domain, region, customDomain } = getCognitoConfig2();
|
|
975
|
+
return customDomain ? `https://${domain}` : `https://${domain}.auth.${region}.amazoncognito.com`;
|
|
976
|
+
}
|
|
977
|
+
function generateCodeVerifier() {
|
|
978
|
+
return randomBytes(32).toString("base64url");
|
|
979
|
+
}
|
|
980
|
+
function generateCodeChallenge(verifier) {
|
|
981
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
982
|
+
}
|
|
983
|
+
function generateState() {
|
|
984
|
+
return randomBytes(32).toString("hex");
|
|
985
|
+
}
|
|
986
|
+
function getAuthorizeUrl(codeChallenge, state) {
|
|
987
|
+
const { clientId } = getCognitoConfig2();
|
|
988
|
+
if (!clientId) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
"Missing REPOWISE_COGNITO_CLIENT_ID environment variable. Configure it before running login."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const params = new URLSearchParams({
|
|
994
|
+
response_type: "code",
|
|
995
|
+
client_id: clientId,
|
|
996
|
+
redirect_uri: `http://localhost:${CLI_CALLBACK_PORT}/callback`,
|
|
997
|
+
code_challenge: codeChallenge,
|
|
998
|
+
code_challenge_method: "S256",
|
|
999
|
+
scope: "openid email profile",
|
|
1000
|
+
state
|
|
1001
|
+
});
|
|
1002
|
+
return `${getCognitoBaseUrl()}/oauth2/authorize?${params.toString()}`;
|
|
1003
|
+
}
|
|
1004
|
+
function getTokenUrl2() {
|
|
1005
|
+
return `${getCognitoBaseUrl()}/oauth2/token`;
|
|
1006
|
+
}
|
|
1007
|
+
function startCallbackServer() {
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
const server = http.createServer((req, res) => {
|
|
1010
|
+
const url = new URL(req.url, `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
1011
|
+
if (url.pathname !== "/callback") {
|
|
1012
|
+
res.writeHead(404);
|
|
1013
|
+
res.end();
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const code = url.searchParams.get("code");
|
|
1017
|
+
const state = url.searchParams.get("state");
|
|
1018
|
+
const error = url.searchParams.get("error");
|
|
1019
|
+
if (error) {
|
|
1020
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1021
|
+
res.end(
|
|
1022
|
+
callbackPage(
|
|
1023
|
+
"Authentication Failed",
|
|
1024
|
+
"Something went wrong. Please close this tab and try again.",
|
|
1025
|
+
true
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
server.close();
|
|
1029
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (!code || !state) {
|
|
1033
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1034
|
+
res.end(
|
|
1035
|
+
callbackPage(
|
|
1036
|
+
"Missing Parameters",
|
|
1037
|
+
"The callback was missing required data. Please close this tab and try again.",
|
|
1038
|
+
true
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
server.close();
|
|
1042
|
+
reject(new Error("Missing code or state in callback"));
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1046
|
+
res.end(
|
|
1047
|
+
callbackPage(
|
|
1048
|
+
"Authentication Successful",
|
|
1049
|
+
"You can close this tab and return to the terminal.",
|
|
1050
|
+
false
|
|
1051
|
+
)
|
|
1052
|
+
);
|
|
1053
|
+
server.close();
|
|
1054
|
+
resolve({ code, state });
|
|
1055
|
+
});
|
|
1056
|
+
server.listen(CLI_CALLBACK_PORT, "127.0.0.1");
|
|
1057
|
+
server.on("error", (err) => {
|
|
1058
|
+
if (err.code === "EADDRINUSE") {
|
|
1059
|
+
reject(
|
|
1060
|
+
new Error(
|
|
1061
|
+
`Port ${CLI_CALLBACK_PORT} is already in use. Close the conflicting process and try again.`
|
|
1062
|
+
)
|
|
1063
|
+
);
|
|
1064
|
+
} else {
|
|
1065
|
+
reject(err);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
const timeout = setTimeout(() => {
|
|
1069
|
+
server.close();
|
|
1070
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
1071
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
1072
|
+
server.on("close", () => clearTimeout(timeout));
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
1076
|
+
const response = await fetch(getTokenUrl2(), {
|
|
1077
|
+
method: "POST",
|
|
1078
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1079
|
+
body: new URLSearchParams({
|
|
1080
|
+
grant_type: "authorization_code",
|
|
1081
|
+
client_id: getCognitoConfig2().clientId,
|
|
1082
|
+
redirect_uri: `http://localhost:${CLI_CALLBACK_PORT}/callback`,
|
|
1083
|
+
code,
|
|
1084
|
+
code_verifier: codeVerifier
|
|
1085
|
+
})
|
|
1086
|
+
});
|
|
1087
|
+
if (!response.ok) {
|
|
1088
|
+
const text = await response.text();
|
|
1089
|
+
throw new Error(`Token exchange failed: ${response.status} ${text}`);
|
|
1090
|
+
}
|
|
1091
|
+
const data = await response.json();
|
|
1092
|
+
return {
|
|
1093
|
+
accessToken: data.access_token,
|
|
1094
|
+
refreshToken: data.refresh_token,
|
|
1095
|
+
idToken: data.id_token,
|
|
1096
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
async function refreshTokens2(refreshToken) {
|
|
1100
|
+
const response = await fetch(getTokenUrl2(), {
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1103
|
+
body: new URLSearchParams({
|
|
1104
|
+
grant_type: "refresh_token",
|
|
1105
|
+
client_id: getCognitoConfig2().clientId,
|
|
1106
|
+
refresh_token: refreshToken
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
if (!response.ok) {
|
|
1110
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
1111
|
+
}
|
|
1112
|
+
const data = await response.json();
|
|
1113
|
+
return {
|
|
1114
|
+
accessToken: data.access_token,
|
|
1115
|
+
refreshToken,
|
|
1116
|
+
// Cognito does not return a new refresh token
|
|
1117
|
+
idToken: data.id_token,
|
|
1118
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
async function getStoredCredentials2() {
|
|
1122
|
+
try {
|
|
1123
|
+
const data = await readFile7(CREDENTIALS_PATH2, "utf-8");
|
|
1124
|
+
return JSON.parse(data);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
throw err;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function storeCredentials2(credentials) {
|
|
1133
|
+
await mkdir8(CONFIG_DIR5, { recursive: true, mode: 448 });
|
|
1134
|
+
await writeFile8(CREDENTIALS_PATH2, JSON.stringify(credentials, null, 2));
|
|
1135
|
+
await chmod3(CREDENTIALS_PATH2, 384);
|
|
1136
|
+
}
|
|
1137
|
+
async function clearCredentials() {
|
|
1138
|
+
try {
|
|
1139
|
+
await unlink4(CREDENTIALS_PATH2);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
if (err.code !== "ENOENT") throw err;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async function getValidCredentials2() {
|
|
1145
|
+
const creds = await getStoredCredentials2();
|
|
1146
|
+
if (!creds) return null;
|
|
1147
|
+
if (Date.now() > creds.expiresAt - 5 * 60 * 1e3) {
|
|
1148
|
+
try {
|
|
1149
|
+
const refreshed = await refreshTokens2(creds.refreshToken);
|
|
1150
|
+
await storeCredentials2(refreshed);
|
|
1151
|
+
return refreshed;
|
|
1152
|
+
} catch {
|
|
1153
|
+
await clearCredentials();
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return creds;
|
|
1158
|
+
}
|
|
1159
|
+
async function performLogin() {
|
|
1160
|
+
const codeVerifier = generateCodeVerifier();
|
|
1161
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
1162
|
+
const state = generateState();
|
|
1163
|
+
const authorizeUrl = getAuthorizeUrl(codeChallenge, state);
|
|
1164
|
+
const callbackPromise = startCallbackServer();
|
|
1165
|
+
try {
|
|
1166
|
+
const open = (await import("open")).default;
|
|
1167
|
+
await open(authorizeUrl);
|
|
1168
|
+
} catch {
|
|
1169
|
+
console.log(`
|
|
1170
|
+
Open this URL in your browser to authenticate:
|
|
1171
|
+
`);
|
|
1172
|
+
console.log(authorizeUrl);
|
|
1173
|
+
}
|
|
1174
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
1175
|
+
if (returnedState !== state) {
|
|
1176
|
+
throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");
|
|
1177
|
+
}
|
|
1178
|
+
const credentials = await exchangeCodeForTokens(code, codeVerifier);
|
|
1179
|
+
await storeCredentials2(credentials);
|
|
1180
|
+
return credentials;
|
|
1181
|
+
}
|
|
1182
|
+
function callbackPage(title, message, isError) {
|
|
1183
|
+
const icon = isError ? '<svg width="48" height="48" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="#ef4444" stroke-width="2"/><path stroke="#ef4444" stroke-width="2" stroke-linecap="round" d="M15 9l-6 6M9 9l6 6"/></svg>' : '<svg width="48" height="48" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="#10b981" stroke-width="2"/><path stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M8 12l3 3 5-5"/></svg>';
|
|
1184
|
+
return `<!DOCTYPE html>
|
|
1185
|
+
<html lang="en">
|
|
1186
|
+
<head>
|
|
1187
|
+
<meta charset="UTF-8">
|
|
1188
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1189
|
+
<title>${title} \u2014 RepoWise</title>
|
|
1190
|
+
<link rel="icon" href="https://staging.repowise.ai/favicon.svg" type="image/svg+xml">
|
|
1191
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
1192
|
+
<style>
|
|
1193
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1194
|
+
body { font-family: 'Inter', system-ui, sans-serif; background: #0a0b14; color: #e4e4e7; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
1195
|
+
.card { text-align: center; max-width: 440px; padding: 48px 40px; }
|
|
1196
|
+
.logo { margin-bottom: 32px; }
|
|
1197
|
+
.logo svg { height: 48px; width: auto; }
|
|
1198
|
+
.icon { margin-bottom: 20px; }
|
|
1199
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; color: ${isError ? "#ef4444" : "#e4e4e7"}; }
|
|
1200
|
+
p { font-size: 15px; color: #a1a1aa; line-height: 1.5; }
|
|
1201
|
+
</style>
|
|
1202
|
+
</head>
|
|
1203
|
+
<body>
|
|
1204
|
+
<div class="card">
|
|
1205
|
+
<div class="logo">
|
|
1206
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 50" height="48">
|
|
1207
|
+
<text x="0" y="38" font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="36" fill="#e4e4e7">Repo<tspan fill="#6c5ce7">Wise</tspan></text>
|
|
1208
|
+
</svg>
|
|
1209
|
+
</div>
|
|
1210
|
+
<div class="icon">${icon}</div>
|
|
1211
|
+
<h1>${title}</h1>
|
|
1212
|
+
<p>${message}</p>
|
|
1213
|
+
</div>
|
|
1214
|
+
</body>
|
|
1215
|
+
</html>`;
|
|
1216
|
+
}
|
|
1217
|
+
function decodeIdToken(idToken) {
|
|
1218
|
+
try {
|
|
1219
|
+
const parts = idToken.split(".");
|
|
1220
|
+
if (parts.length < 2) return { email: "unknown" };
|
|
1221
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
1222
|
+
return { email: payload.email ?? "unknown", tenantId: payload["custom:tenant_id"] };
|
|
1223
|
+
} catch {
|
|
1224
|
+
return { email: "unknown" };
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/lib/api.ts
|
|
1229
|
+
function getApiUrl() {
|
|
1230
|
+
return process.env["REPOWISE_API_URL"] ?? getEnvConfig().apiUrl;
|
|
1231
|
+
}
|
|
1232
|
+
async function apiRequest(path, options) {
|
|
1233
|
+
const credentials = await getValidCredentials2();
|
|
1234
|
+
if (!credentials) {
|
|
1235
|
+
throw new Error("Not logged in. Run `repowise login` first.");
|
|
1236
|
+
}
|
|
1237
|
+
const response = await fetch(`${getApiUrl()}${path}`, {
|
|
1238
|
+
...options,
|
|
1239
|
+
headers: {
|
|
1240
|
+
"Content-Type": "application/json",
|
|
1241
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1242
|
+
...options?.headers
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
if (response.status === 401) {
|
|
1246
|
+
await clearCredentials();
|
|
1247
|
+
throw new Error("Session expired. Run `repowise login` again.");
|
|
1248
|
+
}
|
|
1249
|
+
if (!response.ok) {
|
|
1250
|
+
let message = `Request failed with status ${response.status}`;
|
|
1251
|
+
try {
|
|
1252
|
+
const body = await response.json();
|
|
1253
|
+
if (body.error?.message) message = body.error.message;
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
throw new Error(message);
|
|
1257
|
+
}
|
|
1258
|
+
const json = await response.json();
|
|
1259
|
+
return json.data;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/lib/prompts.ts
|
|
1263
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
1264
|
+
import chalk2 from "chalk";
|
|
1265
|
+
async function selectAiTools() {
|
|
1266
|
+
const choices = [
|
|
1267
|
+
{ name: "Cursor", value: "cursor" },
|
|
1268
|
+
{ name: "Claude Code", value: "claude-code" },
|
|
1269
|
+
{ name: "GitHub Copilot", value: "copilot" },
|
|
1270
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
1271
|
+
{ name: "Cline", value: "cline" },
|
|
1272
|
+
{ name: "Codex", value: "codex" },
|
|
1273
|
+
{ name: "Roo Code", value: "roo-code" },
|
|
1274
|
+
{ name: "Other (manual setup)", value: "other" }
|
|
1275
|
+
];
|
|
1276
|
+
while (true) {
|
|
1277
|
+
console.log(chalk2.dim(" Use Space to select, Enter to continue.\n"));
|
|
1278
|
+
const selected = await checkbox({
|
|
1279
|
+
message: chalk2.bold("Which AI tools do you use?"),
|
|
1280
|
+
choices
|
|
1281
|
+
});
|
|
1282
|
+
if (selected.length === 0) {
|
|
1283
|
+
const goBack = await confirm({
|
|
1284
|
+
message: "No tools selected. Go back and choose?",
|
|
1285
|
+
default: true
|
|
1286
|
+
});
|
|
1287
|
+
if (goBack) continue;
|
|
1288
|
+
}
|
|
1289
|
+
const hasOther = selected.includes("other");
|
|
1290
|
+
const tools = selected.filter((s) => s !== "other");
|
|
1291
|
+
return { tools, hasOther };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/lib/ai-tools.ts
|
|
1296
|
+
import { readFile as readFile8, writeFile as writeFile9, mkdir as mkdir9, readdir } from "fs/promises";
|
|
1297
|
+
import { join as join11, dirname } from "path";
|
|
1298
|
+
var AI_TOOL_CONFIG = {
|
|
1299
|
+
cursor: {
|
|
1300
|
+
label: "Cursor",
|
|
1301
|
+
fileName: ".cursorrules",
|
|
1302
|
+
filePath: ".cursorrules",
|
|
1303
|
+
markerStart: "# --- repowise-start ---",
|
|
1304
|
+
markerEnd: "# --- repowise-end ---",
|
|
1305
|
+
format: "plain-text"
|
|
1306
|
+
},
|
|
1307
|
+
"claude-code": {
|
|
1308
|
+
label: "Claude Code",
|
|
1309
|
+
fileName: "CLAUDE.md",
|
|
1310
|
+
filePath: "CLAUDE.md",
|
|
1311
|
+
markerStart: "<!-- repowise-start -->",
|
|
1312
|
+
markerEnd: "<!-- repowise-end -->",
|
|
1313
|
+
format: "markdown"
|
|
1314
|
+
},
|
|
1315
|
+
copilot: {
|
|
1316
|
+
label: "GitHub Copilot",
|
|
1317
|
+
fileName: "copilot-instructions.md",
|
|
1318
|
+
filePath: ".github/copilot-instructions.md",
|
|
1319
|
+
markerStart: "<!-- repowise-start -->",
|
|
1320
|
+
markerEnd: "<!-- repowise-end -->",
|
|
1321
|
+
format: "markdown"
|
|
1322
|
+
},
|
|
1323
|
+
windsurf: {
|
|
1324
|
+
label: "Windsurf",
|
|
1325
|
+
fileName: ".windsurfrules",
|
|
1326
|
+
filePath: ".windsurfrules",
|
|
1327
|
+
markerStart: "# --- repowise-start ---",
|
|
1328
|
+
markerEnd: "# --- repowise-end ---",
|
|
1329
|
+
format: "plain-text"
|
|
1330
|
+
},
|
|
1331
|
+
cline: {
|
|
1332
|
+
label: "Cline",
|
|
1333
|
+
fileName: ".clinerules",
|
|
1334
|
+
filePath: ".clinerules",
|
|
1335
|
+
markerStart: "# --- repowise-start ---",
|
|
1336
|
+
markerEnd: "# --- repowise-end ---",
|
|
1337
|
+
format: "plain-text"
|
|
1338
|
+
},
|
|
1339
|
+
codex: {
|
|
1340
|
+
label: "Codex",
|
|
1341
|
+
fileName: "AGENTS.md",
|
|
1342
|
+
filePath: "AGENTS.md",
|
|
1343
|
+
markerStart: "<!-- repowise-start -->",
|
|
1344
|
+
markerEnd: "<!-- repowise-end -->",
|
|
1345
|
+
format: "markdown"
|
|
1346
|
+
},
|
|
1347
|
+
"roo-code": {
|
|
1348
|
+
label: "Roo Code",
|
|
1349
|
+
fileName: "rules.md",
|
|
1350
|
+
filePath: ".roo/rules.md",
|
|
1351
|
+
markerStart: "<!-- repowise-start -->",
|
|
1352
|
+
markerEnd: "<!-- repowise-end -->",
|
|
1353
|
+
format: "markdown"
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
var SUPPORTED_TOOLS = Object.keys(AI_TOOL_CONFIG);
|
|
1357
|
+
function sanitizeRepoName(name) {
|
|
1358
|
+
return name.replace(/[<>[\]`()|\\]/g, "");
|
|
1359
|
+
}
|
|
1360
|
+
function fileDescriptionFromName(fileName) {
|
|
1361
|
+
return fileName.replace(/\.md$/, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1362
|
+
}
|
|
1363
|
+
function generateReference(tool, repoName, contextFolder, contextFiles) {
|
|
1364
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
1365
|
+
const safeName = sanitizeRepoName(repoName);
|
|
1366
|
+
const fileLines = contextFiles.map((f) => {
|
|
1367
|
+
const desc = fileDescriptionFromName(f.fileName);
|
|
1368
|
+
const isOverview = f.fileName === "project-overview.md";
|
|
1369
|
+
return { path: f.relativePath, desc: isOverview ? `${desc} (full index of all files)` : desc };
|
|
1370
|
+
});
|
|
1371
|
+
const hasFiles = fileLines.length > 0;
|
|
1372
|
+
if (config2.format === "markdown") {
|
|
1373
|
+
const lines2 = [
|
|
1374
|
+
config2.markerStart,
|
|
1375
|
+
"",
|
|
1376
|
+
`## Project Context \u2014 ${safeName}`,
|
|
1377
|
+
"",
|
|
1378
|
+
`This repository has AI-optimized context files generated by RepoWise.`,
|
|
1379
|
+
`Before making changes, read the relevant context files in \`${contextFolder}/\` to understand the project's architecture, coding patterns, conventions, and domain knowledge.`,
|
|
1380
|
+
"",
|
|
1381
|
+
`**Start here:** \`${contextFolder}/project-overview.md\` \u2014 the routing document that describes every context file and when to read it.`,
|
|
1382
|
+
""
|
|
1383
|
+
];
|
|
1384
|
+
if (hasFiles) {
|
|
1385
|
+
lines2.push(
|
|
1386
|
+
`**Core context files:**`,
|
|
1387
|
+
"",
|
|
1388
|
+
...fileLines.map((f) => `- \`${f.path}\` \u2014 ${f.desc}`),
|
|
1389
|
+
"",
|
|
1390
|
+
`> Additional context files may exist beyond this list. Check \`project-overview.md\` for the complete index.`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
lines2.push("", config2.markerEnd);
|
|
1394
|
+
return lines2.join("\n");
|
|
1395
|
+
}
|
|
1396
|
+
const lines = [
|
|
1397
|
+
config2.markerStart,
|
|
1398
|
+
`# Project Context \u2014 ${safeName}`,
|
|
1399
|
+
"#",
|
|
1400
|
+
`# This repository has AI-optimized context files generated by RepoWise.`,
|
|
1401
|
+
`# Before making changes, read the relevant context files in ${contextFolder}/`,
|
|
1402
|
+
`# to understand the project's architecture, coding patterns, conventions, and domain knowledge.`,
|
|
1403
|
+
"#",
|
|
1404
|
+
`# Start here: ${contextFolder}/project-overview.md`,
|
|
1405
|
+
`# The routing document that describes every context file and when to read it.`
|
|
1406
|
+
];
|
|
1407
|
+
if (hasFiles) {
|
|
1408
|
+
lines.push(
|
|
1409
|
+
"#",
|
|
1410
|
+
`# Core context files:`,
|
|
1411
|
+
...fileLines.map((f) => `# ${f.path} \u2014 ${f.desc}`),
|
|
1412
|
+
"#",
|
|
1413
|
+
"# Additional context files may exist beyond this list.",
|
|
1414
|
+
"# Check project-overview.md for the complete index."
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
lines.push(config2.markerEnd);
|
|
1418
|
+
return lines.join("\n");
|
|
1419
|
+
}
|
|
1420
|
+
async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
|
|
1421
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
1422
|
+
const fullPath = join11(repoRoot, config2.filePath);
|
|
1423
|
+
const dir = dirname(fullPath);
|
|
1424
|
+
if (dir !== repoRoot) {
|
|
1425
|
+
await mkdir9(dir, { recursive: true });
|
|
1426
|
+
}
|
|
1427
|
+
const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles);
|
|
1428
|
+
let existing = "";
|
|
1429
|
+
let created = true;
|
|
1430
|
+
try {
|
|
1431
|
+
existing = await readFile8(fullPath, "utf-8");
|
|
1432
|
+
created = false;
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
if (err.code !== "ENOENT") throw err;
|
|
1435
|
+
}
|
|
1436
|
+
const startIdx = existing.indexOf(config2.markerStart);
|
|
1437
|
+
const endIdx = existing.indexOf(config2.markerEnd);
|
|
1438
|
+
let content;
|
|
1439
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
1440
|
+
const before = existing.slice(0, startIdx);
|
|
1441
|
+
const after = existing.slice(endIdx + config2.markerEnd.length);
|
|
1442
|
+
content = before + referenceBlock + after;
|
|
1443
|
+
} else {
|
|
1444
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
1445
|
+
content = existing + separator + referenceBlock + "\n";
|
|
1446
|
+
}
|
|
1447
|
+
await writeFile9(fullPath, content, "utf-8");
|
|
1448
|
+
return { created };
|
|
1449
|
+
}
|
|
1450
|
+
async function scanLocalContextFiles(repoRoot, contextFolder) {
|
|
1451
|
+
const folderPath = join11(repoRoot, contextFolder);
|
|
1452
|
+
try {
|
|
1453
|
+
const entries = await readdir(folderPath, { withFileTypes: true });
|
|
1454
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({
|
|
1455
|
+
fileName: e.name,
|
|
1456
|
+
relativePath: `${contextFolder}/${e.name}`
|
|
1457
|
+
})).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
if (err.code === "ENOENT") return [];
|
|
1460
|
+
throw err;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/lib/interview-handler.ts
|
|
1465
|
+
import chalk3 from "chalk";
|
|
1466
|
+
import { input } from "@inquirer/prompts";
|
|
1467
|
+
var INTERVIEW_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1468
|
+
var MAX_QUESTIONS = 10;
|
|
1469
|
+
var questionCounter = 0;
|
|
1470
|
+
async function handleInterview(syncId, questionId, questionText, questionContext, estimatedQuestions) {
|
|
1471
|
+
questionCounter++;
|
|
1472
|
+
if (questionCounter === 1) {
|
|
1473
|
+
console.log("");
|
|
1474
|
+
console.log(chalk3.cyan.bold(" \u2500\u2500 Interview \u2500\u2500"));
|
|
1475
|
+
console.log(chalk3.dim(" Help us understand your project better. Answer a few short"));
|
|
1476
|
+
console.log(
|
|
1477
|
+
chalk3.dim(
|
|
1478
|
+
` questions so we can generate more relevant context files (up to ${MAX_QUESTIONS}).`
|
|
1479
|
+
)
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
const total = Math.min(estimatedQuestions ?? MAX_QUESTIONS, MAX_QUESTIONS);
|
|
1483
|
+
console.log("");
|
|
1484
|
+
console.log(chalk3.cyan.bold(` Question ${questionCounter}/${total}`));
|
|
1485
|
+
if (questionContext) {
|
|
1486
|
+
console.log(chalk3.dim(` ${questionContext}`));
|
|
1487
|
+
}
|
|
1488
|
+
console.log(` ${questionText}`);
|
|
1489
|
+
console.log(chalk3.dim(' (Enter to skip \xB7 "done" to finish early)'));
|
|
1490
|
+
let answer;
|
|
1491
|
+
try {
|
|
1492
|
+
answer = await Promise.race([
|
|
1493
|
+
input({
|
|
1494
|
+
message: chalk3.cyan(">"),
|
|
1495
|
+
theme: { prefix: " " }
|
|
1496
|
+
}),
|
|
1497
|
+
new Promise(
|
|
1498
|
+
(_, reject) => setTimeout(() => reject(new Error("INTERVIEW_TIMEOUT")), INTERVIEW_TIMEOUT_MS)
|
|
1499
|
+
)
|
|
1500
|
+
]);
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
if (err instanceof Error && err.message === "INTERVIEW_TIMEOUT") {
|
|
1503
|
+
console.log(chalk3.yellow(" Timed out \u2014 auto-skipping this question."));
|
|
1504
|
+
answer = "skip";
|
|
1505
|
+
} else {
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const trimmed = answer.trim();
|
|
1510
|
+
let action;
|
|
1511
|
+
let answerText = "";
|
|
1512
|
+
if (trimmed.toLowerCase() === "done") {
|
|
1513
|
+
action = "done";
|
|
1514
|
+
} else if (trimmed === "" || trimmed.toLowerCase() === "skip") {
|
|
1515
|
+
action = "skip";
|
|
1516
|
+
} else {
|
|
1517
|
+
action = "answer";
|
|
1518
|
+
answerText = trimmed;
|
|
1519
|
+
}
|
|
1520
|
+
if (questionCounter >= MAX_QUESTIONS && action !== "done") {
|
|
1521
|
+
action = "done";
|
|
1522
|
+
console.log(chalk3.green(" Thanks for your answers! Wrapping up the interview."));
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
await apiRequest(`/v1/sync/${syncId}/answer`, {
|
|
1526
|
+
method: "POST",
|
|
1527
|
+
body: JSON.stringify({ questionId, answerText, action })
|
|
1528
|
+
});
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1531
|
+
if (message.includes("not awaiting input") || message.includes("expired")) {
|
|
1532
|
+
console.log(chalk3.dim(" Pipeline has already moved on \u2014 continuing."));
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1537
|
+
await apiRequest(`/v1/sync/${syncId}/answer`, {
|
|
1538
|
+
method: "POST",
|
|
1539
|
+
body: JSON.stringify({ questionId, answerText, action })
|
|
1540
|
+
});
|
|
1541
|
+
} catch {
|
|
1542
|
+
console.log(chalk3.yellow(" Could not submit answer \u2014 pipeline will continue."));
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (action === "done") {
|
|
1547
|
+
console.log(chalk3.dim(" Interview ended early."));
|
|
1548
|
+
} else if (action === "skip") {
|
|
1549
|
+
console.log(chalk3.dim(" Skipped."));
|
|
1550
|
+
} else {
|
|
1551
|
+
console.log(chalk3.dim(" Answer recorded."));
|
|
1552
|
+
}
|
|
1553
|
+
console.log("");
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/lib/progress-renderer.ts
|
|
1557
|
+
import chalk4 from "chalk";
|
|
1558
|
+
var CORE_FILES = /* @__PURE__ */ new Set([
|
|
1559
|
+
"project-overview.md",
|
|
1560
|
+
"architecture.md",
|
|
1561
|
+
"data-models.md",
|
|
1562
|
+
"api-contracts.md",
|
|
1563
|
+
"coding-patterns.md"
|
|
1564
|
+
]);
|
|
1565
|
+
var FILE_DESCRIPTIONS = {
|
|
1566
|
+
// Core
|
|
1567
|
+
"project-overview.md": "Project overview & file index",
|
|
1568
|
+
"architecture.md": "System design & components",
|
|
1569
|
+
"data-models.md": "Schemas, entities & relationships",
|
|
1570
|
+
"api-contracts.md": "API endpoints & contracts",
|
|
1571
|
+
"coding-patterns.md": "Code conventions & patterns",
|
|
1572
|
+
// Tailored
|
|
1573
|
+
"domain-knowledge.md": "Business domain & terminology",
|
|
1574
|
+
"testing-strategy.md": "Test frameworks & coverage",
|
|
1575
|
+
"deployment-workflows.md": "CI/CD & release process",
|
|
1576
|
+
"state-management.md": "State & caching patterns",
|
|
1577
|
+
"performance-optimization.md": "Performance & optimization",
|
|
1578
|
+
"accessibility-patterns.md": "Accessibility & ARIA patterns",
|
|
1579
|
+
"tech-stack.md": "Technology inventory & versions",
|
|
1580
|
+
"tribal-knowledge.md": "Team knowledge & conventions",
|
|
1581
|
+
"development-setup.md": "Dev environment setup",
|
|
1582
|
+
"ui-patterns.md": "UI components & design system",
|
|
1583
|
+
"ux-patterns.md": "UX interactions & feedback",
|
|
1584
|
+
"user-flows.md": "User journeys & navigation",
|
|
1585
|
+
"security-patterns.md": "Auth & security patterns",
|
|
1586
|
+
"error-handling.md": "Error handling & recovery",
|
|
1587
|
+
"integration-patterns.md": "External integrations & APIs",
|
|
1588
|
+
"configuration.md": "Config & environment settings"
|
|
1589
|
+
};
|
|
1590
|
+
var ALL_PERSONAS = ["pm", "architect", "dev", "analyst", "tea", "ux", "sm", "techWriter"];
|
|
1591
|
+
var PERSONA_LABELS = {
|
|
1592
|
+
pm: "Product Manager",
|
|
1593
|
+
architect: "Architect",
|
|
1594
|
+
dev: "Developer",
|
|
1595
|
+
analyst: "Business Analyst",
|
|
1596
|
+
tea: "Test Architect",
|
|
1597
|
+
ux: "UX Designer",
|
|
1598
|
+
sm: "Scrum Master",
|
|
1599
|
+
techWriter: "Tech Writer"
|
|
1600
|
+
};
|
|
1601
|
+
function computeOverallProgress(syncResult) {
|
|
1602
|
+
const stepNumber = syncResult.stepNumber ?? 1;
|
|
1603
|
+
const totalSteps = syncResult.totalSteps ?? 6;
|
|
1604
|
+
const stepPct = syncResult.progressPercentage ?? 0;
|
|
1605
|
+
return Math.min(100, Math.round(((stepNumber - 1) * 100 + stepPct) / totalSteps));
|
|
1606
|
+
}
|
|
1607
|
+
var ProgressRenderer = class {
|
|
1608
|
+
privacyShieldShown = false;
|
|
1609
|
+
discoveryShown = false;
|
|
1610
|
+
scanSummaryShown = false;
|
|
1611
|
+
validationShown = false;
|
|
1612
|
+
lastValidationSnapshot = "";
|
|
1613
|
+
lastValidationLineCount = 0;
|
|
1614
|
+
pushShown = false;
|
|
1615
|
+
fileStatusHeaderShown = false;
|
|
1616
|
+
lastFileStatusSnapshot = "";
|
|
1617
|
+
lastFileStatusLineCount = 0;
|
|
1618
|
+
renderPrivacyShield(enabled, spinner) {
|
|
1619
|
+
if (this.privacyShieldShown) return;
|
|
1620
|
+
this.privacyShieldShown = true;
|
|
1621
|
+
spinner.stop();
|
|
1622
|
+
console.log("");
|
|
1623
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 Privacy Shield \u2500\u2500"));
|
|
1624
|
+
if (enabled) {
|
|
1625
|
+
console.log(` ${chalk4.green("\u2713")} Privacy Shield active`);
|
|
1626
|
+
console.log(` ${chalk4.green("\u2713")} Private connection established`);
|
|
1627
|
+
} else {
|
|
1628
|
+
console.log(` ${chalk4.yellow("\u2139")} Privacy Shield not in current plan`);
|
|
1629
|
+
console.log(chalk4.dim(" Shield your data from the open internet."));
|
|
1630
|
+
}
|
|
1631
|
+
console.log("");
|
|
1632
|
+
spinner.start();
|
|
1633
|
+
}
|
|
1634
|
+
renderDiscovery(result, spinner) {
|
|
1635
|
+
if (this.discoveryShown) return;
|
|
1636
|
+
this.discoveryShown = true;
|
|
1637
|
+
spinner.stop();
|
|
1638
|
+
console.log("");
|
|
1639
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 Repository Discovery \u2500\u2500"));
|
|
1640
|
+
if (result.languages.length > 0) {
|
|
1641
|
+
const langs = result.languages.slice(0, 5).map((l) => `${l.name} (${Math.round(l.percentage)}%)`).join(", ");
|
|
1642
|
+
console.log(` ${chalk4.dim("Languages:")} ${langs}`);
|
|
1643
|
+
}
|
|
1644
|
+
if (result.frameworks.length > 0) {
|
|
1645
|
+
console.log(
|
|
1646
|
+
` ${chalk4.dim("Frameworks:")} ${result.frameworks.map((f) => f.name).join(", ")}`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
console.log(
|
|
1650
|
+
` ${chalk4.dim("Structure:")} ${result.structureType} ${chalk4.dim(`(${result.fileCount} files)`)}`
|
|
1651
|
+
);
|
|
1652
|
+
if (result.existingDocs.length > 0) {
|
|
1653
|
+
console.log(` ${chalk4.dim("Existing docs:")} ${result.existingDocs.join(", ")}`);
|
|
1654
|
+
}
|
|
1655
|
+
if (result.fileTree && result.fileTree.length > 0) {
|
|
1656
|
+
this.renderTree(result.fileTree);
|
|
1657
|
+
}
|
|
1658
|
+
console.log("");
|
|
1659
|
+
spinner.start();
|
|
1660
|
+
}
|
|
1661
|
+
renderTree(entries) {
|
|
1662
|
+
console.log("");
|
|
1663
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 Project Structure \u2500\u2500"));
|
|
1664
|
+
const root = { name: "", type: "tree", children: /* @__PURE__ */ new Map() };
|
|
1665
|
+
for (const entry of entries) {
|
|
1666
|
+
const parts = entry.path.split("/");
|
|
1667
|
+
let current = root;
|
|
1668
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1669
|
+
const part = parts[i];
|
|
1670
|
+
if (!current.children.has(part)) {
|
|
1671
|
+
const isLast = i === parts.length - 1;
|
|
1672
|
+
current.children.set(part, {
|
|
1673
|
+
name: part,
|
|
1674
|
+
type: isLast ? entry.type : "tree",
|
|
1675
|
+
children: /* @__PURE__ */ new Map()
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
current = current.children.get(part);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const printNode = (node, prefix, isLast) => {
|
|
1682
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1683
|
+
const display = node.type === "tree" ? chalk4.bold.dim(`${node.name}/`) : node.name;
|
|
1684
|
+
console.log(` ${prefix}${connector}${display}`);
|
|
1685
|
+
const sorted = [...node.children.values()].sort((a, b) => {
|
|
1686
|
+
if (a.type === "tree" && b.type !== "tree") return -1;
|
|
1687
|
+
if (a.type !== "tree" && b.type === "tree") return 1;
|
|
1688
|
+
return a.name.localeCompare(b.name);
|
|
1689
|
+
});
|
|
1690
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1691
|
+
sorted.forEach((child, idx) => {
|
|
1692
|
+
printNode(child, childPrefix, idx === sorted.length - 1);
|
|
1693
|
+
});
|
|
1694
|
+
};
|
|
1695
|
+
const topLevel = [...root.children.values()].sort((a, b) => {
|
|
1696
|
+
if (a.type === "tree" && b.type !== "tree") return -1;
|
|
1697
|
+
if (a.type !== "tree" && b.type === "tree") return 1;
|
|
1698
|
+
return a.name.localeCompare(b.name);
|
|
1699
|
+
});
|
|
1700
|
+
topLevel.forEach((child, idx) => {
|
|
1701
|
+
printNode(child, "", idx === topLevel.length - 1);
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
renderScanSummary(summary, spinner) {
|
|
1705
|
+
if (this.scanSummaryShown) return;
|
|
1706
|
+
this.scanSummaryShown = true;
|
|
1707
|
+
spinner.stop();
|
|
1708
|
+
console.log(
|
|
1709
|
+
chalk4.dim(
|
|
1710
|
+
` Scan complete: ${summary.totalFiles} files, ${summary.totalFunctions} functions, ${summary.totalClasses} classes, ${summary.totalEndpoints} endpoints`
|
|
1711
|
+
)
|
|
1712
|
+
);
|
|
1713
|
+
console.log("");
|
|
1714
|
+
spinner.start();
|
|
1715
|
+
}
|
|
1716
|
+
renderValidation(progress, spinner) {
|
|
1717
|
+
const resultMap = new Map(progress.personaResults.map((r) => [r.persona, r.score]));
|
|
1718
|
+
const isComplete = progress.status === "complete";
|
|
1719
|
+
if (isComplete && this.validationShown) return;
|
|
1720
|
+
const snapshot = `${progress.round}:${progress.status}:${progress.personaResults.map((r) => `${r.persona}:${r.score}`).join(",")}`;
|
|
1721
|
+
if (snapshot === this.lastValidationSnapshot) return;
|
|
1722
|
+
this.lastValidationSnapshot = snapshot;
|
|
1723
|
+
if (isComplete) this.validationShown = true;
|
|
1724
|
+
spinner.stop();
|
|
1725
|
+
if (this.lastValidationLineCount > 0) {
|
|
1726
|
+
process.stdout.write(`\x1B[${this.lastValidationLineCount}A`);
|
|
1727
|
+
}
|
|
1728
|
+
const lines = [];
|
|
1729
|
+
const title = isComplete ? "Validation Results" : "Validation";
|
|
1730
|
+
lines.push(chalk4.cyan.bold(` \u2500\u2500 ${title} \u2500\u2500`));
|
|
1731
|
+
if (!isComplete) {
|
|
1732
|
+
lines.push(
|
|
1733
|
+
chalk4.dim(
|
|
1734
|
+
` ${ALL_PERSONAS.length} AI reviewers checking context quality \u2014 issues are auto-fixed.`
|
|
1735
|
+
)
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
const passCount = progress.personaResults.filter((r) => r.score === "PASS").length;
|
|
1739
|
+
if (isComplete) {
|
|
1740
|
+
const roundInfo = progress.round > 1 ? ` (${progress.round} rounds)` : "";
|
|
1741
|
+
lines.push(chalk4.dim(` ${passCount}/${ALL_PERSONAS.length} PASS${roundInfo}`));
|
|
1742
|
+
} else if (progress.personaResults.length > 0) {
|
|
1743
|
+
const statusSuffix = progress.status === "regenerating" ? chalk4.dim(" \u2014 improving files based on feedback") : "";
|
|
1744
|
+
lines.push(
|
|
1745
|
+
` Round ${progress.round}/${progress.maxRounds}: ${passCount}/${ALL_PERSONAS.length} passed${statusSuffix}`
|
|
1746
|
+
);
|
|
1747
|
+
} else {
|
|
1748
|
+
lines.push(chalk4.dim(` Round ${progress.round}/${progress.maxRounds}: validating...`));
|
|
1749
|
+
}
|
|
1750
|
+
for (const persona of ALL_PERSONAS) {
|
|
1751
|
+
const label = PERSONA_LABELS[persona] ?? persona;
|
|
1752
|
+
const score = resultMap.get(persona);
|
|
1753
|
+
if (score) {
|
|
1754
|
+
const icon = score === "PASS" ? chalk4.green("\u2713") : chalk4.red("\u2717");
|
|
1755
|
+
const scoreColor = score === "PASS" ? chalk4.green : score === "PARTIAL" ? chalk4.yellow : chalk4.red;
|
|
1756
|
+
const fixingSuffix = progress.status === "regenerating" && score !== "PASS" ? chalk4.dim(" \u2192 fixing...") : "";
|
|
1757
|
+
lines.push(` ${icon} ${label}: ${scoreColor(score)}${fixingSuffix}`);
|
|
1758
|
+
} else {
|
|
1759
|
+
lines.push(` ${chalk4.dim("\u25CB")} ${chalk4.dim(label)}`);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (isComplete && passCount < ALL_PERSONAS.length) {
|
|
1763
|
+
lines.push(chalk4.yellow(" \u26A0 Continuing with best-effort context"));
|
|
1764
|
+
}
|
|
1765
|
+
lines.push("");
|
|
1766
|
+
for (const line of lines) {
|
|
1767
|
+
process.stdout.write(`\x1B[2K${line}
|
|
1768
|
+
`);
|
|
1769
|
+
}
|
|
1770
|
+
for (let i = lines.length; i < this.lastValidationLineCount; i++) {
|
|
1771
|
+
process.stdout.write("\x1B[2K\n");
|
|
1772
|
+
}
|
|
1773
|
+
this.lastValidationLineCount = lines.length;
|
|
1774
|
+
spinner.start();
|
|
1775
|
+
}
|
|
1776
|
+
renderFileStatuses(fileStatuses, spinner) {
|
|
1777
|
+
const snapshot = fileStatuses.map((f) => `${f.fileName}:${f.status}`).join(",");
|
|
1778
|
+
if (snapshot === this.lastFileStatusSnapshot) return;
|
|
1779
|
+
this.lastFileStatusSnapshot = snapshot;
|
|
1780
|
+
const completedCount = fileStatuses.filter((f) => f.status === "completed").length;
|
|
1781
|
+
const totalCount = fileStatuses.length;
|
|
1782
|
+
spinner.stop();
|
|
1783
|
+
if (!this.fileStatusHeaderShown) {
|
|
1784
|
+
this.fileStatusHeaderShown = true;
|
|
1785
|
+
console.log("");
|
|
1786
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 RepoWise Context Generation \u2500\u2500"));
|
|
1787
|
+
console.log(chalk4.dim(" Building AI-optimized context files from your codebase."));
|
|
1788
|
+
}
|
|
1789
|
+
if (this.lastFileStatusLineCount > 0) {
|
|
1790
|
+
process.stdout.write(`\x1B[${this.lastFileStatusLineCount}A`);
|
|
1791
|
+
}
|
|
1792
|
+
const coreFiles = [];
|
|
1793
|
+
const tailoredFiles = [];
|
|
1794
|
+
for (const file of fileStatuses) {
|
|
1795
|
+
const baseName = file.fileName.split("/").pop() ?? file.fileName;
|
|
1796
|
+
if (CORE_FILES.has(baseName)) {
|
|
1797
|
+
coreFiles.push(file);
|
|
1798
|
+
} else {
|
|
1799
|
+
tailoredFiles.push(file);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
const maxCoreLen = coreFiles.reduce((m, f) => Math.max(m, f.fileName.length), 0);
|
|
1803
|
+
const maxTailoredLen = tailoredFiles.reduce((m, f) => Math.max(m, f.fileName.length), 0);
|
|
1804
|
+
const formatFileLine = (file, padLen) => {
|
|
1805
|
+
const baseName = file.fileName.split("/").pop() ?? file.fileName;
|
|
1806
|
+
const desc = FILE_DESCRIPTIONS[baseName] ?? baseName.replace(/\.md$/, "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1807
|
+
const padded = file.fileName.padEnd(padLen);
|
|
1808
|
+
switch (file.status) {
|
|
1809
|
+
case "completed":
|
|
1810
|
+
return ` ${chalk4.green("\u2713")} ${padded} ${chalk4.dim(`\u2014 ${desc}`)}`;
|
|
1811
|
+
case "generating":
|
|
1812
|
+
return ` ${chalk4.cyan("\u27F3")} ${padded} ${chalk4.dim(`\u2014 ${desc}`)}`;
|
|
1813
|
+
case "failed":
|
|
1814
|
+
return ` ${chalk4.red("\u2717")} ${padded} ${chalk4.dim(`\u2014 ${desc}`)}`;
|
|
1815
|
+
case "pending":
|
|
1816
|
+
return ` ${chalk4.dim("\u25CB")} ${chalk4.dim(`${padded} \u2014 ${desc}`)}`;
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
const lines = [];
|
|
1820
|
+
lines.push(chalk4.dim(` Generated ${completedCount}/${totalCount} files`));
|
|
1821
|
+
lines.push("");
|
|
1822
|
+
lines.push(` ${chalk4.bold("Core")}`);
|
|
1823
|
+
for (const file of coreFiles) {
|
|
1824
|
+
lines.push(formatFileLine(file, maxCoreLen));
|
|
1825
|
+
}
|
|
1826
|
+
if (tailoredFiles.length > 0) {
|
|
1827
|
+
lines.push("");
|
|
1828
|
+
lines.push(` ${chalk4.bold("Tailored")}`);
|
|
1829
|
+
for (const file of tailoredFiles) {
|
|
1830
|
+
lines.push(formatFileLine(file, maxTailoredLen));
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
lines.push("");
|
|
1834
|
+
for (const line of lines) {
|
|
1835
|
+
process.stdout.write(`\x1B[2K${line}
|
|
1836
|
+
`);
|
|
1837
|
+
}
|
|
1838
|
+
for (let i = lines.length; i < this.lastFileStatusLineCount; i++) {
|
|
1839
|
+
process.stdout.write("\x1B[2K\n");
|
|
1840
|
+
}
|
|
1841
|
+
this.lastFileStatusLineCount = lines.length;
|
|
1842
|
+
spinner.start();
|
|
1843
|
+
}
|
|
1844
|
+
renderPush(spinner) {
|
|
1845
|
+
if (this.pushShown) return;
|
|
1846
|
+
this.pushShown = true;
|
|
1847
|
+
spinner.stop();
|
|
1848
|
+
console.log("");
|
|
1849
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 Saving Context \u2500\u2500"));
|
|
1850
|
+
console.log(` ${chalk4.dim("Encrypting and saving context files to RepoWise servers...")}`);
|
|
1851
|
+
console.log("");
|
|
1852
|
+
spinner.start();
|
|
1853
|
+
}
|
|
1854
|
+
getSpinnerText(syncResult) {
|
|
1855
|
+
const stepLabel = syncResult.stepLabel ?? syncResult.currentStep ?? "Processing";
|
|
1856
|
+
const overallPct = computeOverallProgress(syncResult);
|
|
1857
|
+
let progressText = stepLabel;
|
|
1858
|
+
if (syncResult.scanProgress && !syncResult.scanProgress.summary) {
|
|
1859
|
+
progressText = `Scanning batch ${syncResult.scanProgress.currentBatch}/${syncResult.scanProgress.totalBatches}`;
|
|
1860
|
+
} else if (syncResult.generationProgress) {
|
|
1861
|
+
const gp = syncResult.generationProgress;
|
|
1862
|
+
if (gp.fileStatuses && gp.fileStatuses.length > 0) {
|
|
1863
|
+
const completed = gp.fileStatuses.filter((f) => f.status === "completed").length;
|
|
1864
|
+
const total = gp.fileStatuses.length;
|
|
1865
|
+
const generating = gp.fileStatuses.find((f) => f.status === "generating");
|
|
1866
|
+
if (completed === total) {
|
|
1867
|
+
progressText = stepLabel;
|
|
1868
|
+
} else if (generating) {
|
|
1869
|
+
progressText = `Generating ${generating.fileName} (${completed}/${total})`;
|
|
1870
|
+
} else {
|
|
1871
|
+
progressText = `Generating (${completed}/${total})`;
|
|
1872
|
+
}
|
|
1873
|
+
} else {
|
|
1874
|
+
progressText = `Generating ${gp.currentFileName} (${gp.currentFile}/${gp.totalFiles})`;
|
|
1875
|
+
}
|
|
1876
|
+
} else if (syncResult.validationProgress && syncResult.validationProgress.status !== "complete") {
|
|
1877
|
+
const vp = syncResult.validationProgress;
|
|
1878
|
+
if (vp.status === "regenerating") {
|
|
1879
|
+
progressText = `Improving files based on feedback (round ${vp.round})`;
|
|
1880
|
+
} else {
|
|
1881
|
+
progressText = `Validation round ${vp.round}/${vp.maxRounds}`;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return `${progressText}... ${chalk4.dim(`(${overallPct}%)`)}`;
|
|
1885
|
+
}
|
|
1886
|
+
update(syncResult, spinner) {
|
|
1887
|
+
if (syncResult.privacyShieldEnabled !== void 0) {
|
|
1888
|
+
this.renderPrivacyShield(syncResult.privacyShieldEnabled, spinner);
|
|
1889
|
+
}
|
|
1890
|
+
if (syncResult.discoveryResult) {
|
|
1891
|
+
this.renderDiscovery(syncResult.discoveryResult, spinner);
|
|
1892
|
+
}
|
|
1893
|
+
if (syncResult.scanProgress?.summary && syncResult.scanProgress.summary.totalFiles > 0) {
|
|
1894
|
+
this.renderScanSummary(syncResult.scanProgress.summary, spinner);
|
|
1895
|
+
}
|
|
1896
|
+
if (syncResult.generationProgress?.fileStatuses && syncResult.generationProgress.fileStatuses.length > 0) {
|
|
1897
|
+
this.renderFileStatuses(syncResult.generationProgress.fileStatuses, spinner);
|
|
1898
|
+
}
|
|
1899
|
+
if (syncResult.validationProgress) {
|
|
1900
|
+
this.renderValidation(syncResult.validationProgress, spinner);
|
|
1901
|
+
}
|
|
1902
|
+
if (syncResult.currentStep === "push-context") {
|
|
1903
|
+
this.renderPush(spinner);
|
|
1904
|
+
}
|
|
1905
|
+
spinner.text = this.getSpinnerText(syncResult);
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
// src/commands/create.ts
|
|
1910
|
+
function detectRepoRoot() {
|
|
1911
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
1912
|
+
}
|
|
1913
|
+
function detectRepoName(repoRoot) {
|
|
1914
|
+
try {
|
|
1915
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
1916
|
+
encoding: "utf-8",
|
|
1917
|
+
cwd: repoRoot
|
|
1918
|
+
}).trim();
|
|
1919
|
+
const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
|
|
1920
|
+
if (match?.[1]) return match[1];
|
|
1921
|
+
} catch {
|
|
1922
|
+
}
|
|
1923
|
+
return repoRoot.split("/").pop() ?? "unknown";
|
|
1924
|
+
}
|
|
1925
|
+
function formatElapsed(ms) {
|
|
1926
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
1927
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
1928
|
+
const seconds = totalSeconds % 60;
|
|
1929
|
+
if (minutes === 0) return `${seconds}s`;
|
|
1930
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
1931
|
+
}
|
|
1932
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1933
|
+
var MAX_POLL_ATTEMPTS = 600;
|
|
1934
|
+
var DEFAULT_CONTEXT_FOLDER = "repowise-context";
|
|
1935
|
+
async function create() {
|
|
1936
|
+
const startTime = Date.now();
|
|
1937
|
+
const spinner = ora("Checking authentication...").start();
|
|
1938
|
+
try {
|
|
1939
|
+
let credentials = await getValidCredentials2();
|
|
1940
|
+
if (!credentials) {
|
|
1941
|
+
spinner.info(chalk5.yellow("Not logged in. Opening browser to authenticate..."));
|
|
1942
|
+
credentials = await performLogin();
|
|
1943
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
1944
|
+
spinner.succeed(chalk5.green(`Authenticated as ${chalk5.bold(email)}`));
|
|
1945
|
+
} else {
|
|
1946
|
+
spinner.succeed("Authenticated");
|
|
1947
|
+
}
|
|
1948
|
+
let repoId;
|
|
1949
|
+
let repoName;
|
|
1950
|
+
let repoRoot;
|
|
1951
|
+
spinner.start("Checking for pending repository...");
|
|
1952
|
+
try {
|
|
1953
|
+
const pending = await apiRequest("/v1/onboarding/pending");
|
|
1954
|
+
if (pending?.repoId) {
|
|
1955
|
+
repoId = pending.repoId;
|
|
1956
|
+
repoName = pending.repoName;
|
|
1957
|
+
spinner.succeed(`Found pending repository: ${chalk5.bold(repoName)}`);
|
|
1958
|
+
apiRequest("/v1/onboarding/pending", { method: "DELETE" }).catch(() => {
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
if (!repoId) {
|
|
1964
|
+
spinner.text = "Detecting repository...";
|
|
1965
|
+
try {
|
|
1966
|
+
repoRoot = detectRepoRoot();
|
|
1967
|
+
repoName = detectRepoName(repoRoot);
|
|
1968
|
+
spinner.succeed(`Repository: ${chalk5.bold(repoName)}`);
|
|
1969
|
+
} catch {
|
|
1970
|
+
spinner.fail(
|
|
1971
|
+
chalk5.red(
|
|
1972
|
+
"Not in a git repository. Run this command from your repo directory, or select a repo on the dashboard first."
|
|
1973
|
+
)
|
|
1974
|
+
);
|
|
1975
|
+
process.exitCode = 1;
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
const repos = await apiRequest("/v1/repos");
|
|
1980
|
+
const match = repos.find((r) => r.name === repoName || r.fullName.endsWith(`/${repoName}`));
|
|
1981
|
+
if (match) {
|
|
1982
|
+
repoId = match.repoId;
|
|
1983
|
+
}
|
|
1984
|
+
} catch {
|
|
1985
|
+
}
|
|
1986
|
+
} else {
|
|
1987
|
+
try {
|
|
1988
|
+
repoRoot = detectRepoRoot();
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (!repoId) {
|
|
1993
|
+
spinner.fail(
|
|
1994
|
+
chalk5.red(
|
|
1995
|
+
"Could not find this repository in your RepoWise account. Connect it on the dashboard first."
|
|
1996
|
+
)
|
|
1997
|
+
);
|
|
1998
|
+
process.exitCode = 1;
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const { tools, hasOther } = await selectAiTools();
|
|
2002
|
+
if (hasOther) {
|
|
2003
|
+
console.log(
|
|
2004
|
+
chalk5.cyan(
|
|
2005
|
+
"\nFor AI tools not listed, context files still work with any tool that reads the filesystem.\nRequest support for your tool at: https://dashboard.repowise.ai/support/ai-tools"
|
|
2006
|
+
)
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
if (tools.length === 0 && !hasOther) {
|
|
2010
|
+
console.log(
|
|
2011
|
+
chalk5.yellow(
|
|
2012
|
+
"\nNo AI tools selected. You can configure them later with `repowise config`."
|
|
2013
|
+
)
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
const contextStorage = "server";
|
|
2017
|
+
spinner.start("Starting context generation pipeline...");
|
|
2018
|
+
let syncId;
|
|
2019
|
+
try {
|
|
2020
|
+
const triggerResult = await apiRequest(`/v1/repos/${repoId}/sync`, {
|
|
2021
|
+
method: "POST",
|
|
2022
|
+
body: JSON.stringify({ scanType: "full", contextStorage })
|
|
2023
|
+
});
|
|
2024
|
+
syncId = triggerResult.syncId;
|
|
2025
|
+
} catch (triggerErr) {
|
|
2026
|
+
const msg = triggerErr instanceof Error ? triggerErr.message : "";
|
|
2027
|
+
if (!msg.toLowerCase().includes("already running")) {
|
|
2028
|
+
throw triggerErr;
|
|
2029
|
+
}
|
|
2030
|
+
spinner.text = "Resuming existing pipeline...";
|
|
2031
|
+
const syncs = await apiRequest(
|
|
2032
|
+
`/v1/repos/${repoId}/syncs?limit=1`
|
|
2033
|
+
);
|
|
2034
|
+
const active = syncs.items.find(
|
|
2035
|
+
(s) => s.status === "in_progress" || s.status === "awaiting_input"
|
|
2036
|
+
);
|
|
2037
|
+
if (!active) {
|
|
2038
|
+
throw new Error("Could not find active sync to resume. Please try again.");
|
|
2039
|
+
}
|
|
2040
|
+
syncId = active.syncId;
|
|
2041
|
+
spinner.info(chalk5.cyan("Resuming existing pipeline..."));
|
|
2042
|
+
spinner.start();
|
|
2043
|
+
}
|
|
2044
|
+
let pollAttempts = 0;
|
|
2045
|
+
const progressRenderer = new ProgressRenderer();
|
|
2046
|
+
while (true) {
|
|
2047
|
+
if (++pollAttempts > MAX_POLL_ATTEMPTS) {
|
|
2048
|
+
spinner.fail(chalk5.red("Pipeline timed out. Check dashboard for status."));
|
|
2049
|
+
process.exitCode = 1;
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
2053
|
+
const syncResult = await apiRequest(`/v1/sync/${syncId}/status`);
|
|
2054
|
+
progressRenderer.update(syncResult, spinner);
|
|
2055
|
+
if (syncResult.status === "awaiting_input" && syncResult.questionId && syncResult.questionText) {
|
|
2056
|
+
spinner.stop();
|
|
2057
|
+
await handleInterview(
|
|
2058
|
+
syncId,
|
|
2059
|
+
syncResult.questionId,
|
|
2060
|
+
syncResult.questionText,
|
|
2061
|
+
syncResult.questionContext ?? void 0,
|
|
2062
|
+
syncResult.discoveryResult?.estimatedInterviewQuestions
|
|
2063
|
+
);
|
|
2064
|
+
spinner.start("Resuming pipeline...");
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (syncResult.status === "completed") {
|
|
2068
|
+
const generatedFiles = syncResult.filesGenerated ?? [];
|
|
2069
|
+
const fileCount = generatedFiles.length;
|
|
2070
|
+
if (fileCount > 0) {
|
|
2071
|
+
const coreCount = generatedFiles.filter(
|
|
2072
|
+
(f) => CORE_FILES.has(f.split("/").pop() ?? f)
|
|
2073
|
+
).length;
|
|
2074
|
+
const tailoredCount = fileCount - coreCount;
|
|
2075
|
+
spinner.succeed(
|
|
2076
|
+
`Context generation complete \u2014 ${coreCount} core + ${tailoredCount} tailored files`
|
|
2077
|
+
);
|
|
2078
|
+
} else {
|
|
2079
|
+
spinner.warn(chalk5.yellow("Pipeline completed but no context files were generated."));
|
|
2080
|
+
console.log(
|
|
2081
|
+
chalk5.yellow(
|
|
2082
|
+
" This may be due to AI throttling or a parsing issue. Try running `repowise create` again."
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
2088
|
+
if (syncResult.status === "failed") {
|
|
2089
|
+
spinner.fail(chalk5.red(`Pipeline failed: ${syncResult.error ?? "Unknown error"}`));
|
|
2090
|
+
process.exitCode = 1;
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (repoRoot) {
|
|
2095
|
+
spinner.start("Downloading context files from server...");
|
|
2096
|
+
try {
|
|
2097
|
+
const listResult = await apiRequest(`/v1/repos/${repoId}/context`);
|
|
2098
|
+
const files = listResult.data?.files ?? listResult.files ?? [];
|
|
2099
|
+
if (files.length > 0) {
|
|
2100
|
+
const contextDir = join12(repoRoot, DEFAULT_CONTEXT_FOLDER);
|
|
2101
|
+
mkdirSync(contextDir, { recursive: true });
|
|
2102
|
+
let downloadedCount = 0;
|
|
2103
|
+
let failedCount = 0;
|
|
2104
|
+
for (const file of files) {
|
|
2105
|
+
if (file.fileName.includes("..") || file.fileName.includes("/")) {
|
|
2106
|
+
failedCount++;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const urlResult = await apiRequest(`/v1/repos/${repoId}/context/${file.fileName}`);
|
|
2110
|
+
const presignedUrl = urlResult.data?.url ?? urlResult.url;
|
|
2111
|
+
const response = await fetch(presignedUrl);
|
|
2112
|
+
if (response.ok) {
|
|
2113
|
+
const content = await response.text();
|
|
2114
|
+
writeFileSync(join12(contextDir, file.fileName), content, "utf-8");
|
|
2115
|
+
downloadedCount++;
|
|
2116
|
+
} else {
|
|
2117
|
+
failedCount++;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (failedCount > 0) {
|
|
2121
|
+
spinner.warn(
|
|
2122
|
+
`Downloaded ${downloadedCount}/${files.length} files to ./${DEFAULT_CONTEXT_FOLDER}/ (${failedCount} failed)`
|
|
2123
|
+
);
|
|
2124
|
+
} else {
|
|
2125
|
+
spinner.succeed(`Context files downloaded to ./${DEFAULT_CONTEXT_FOLDER}/`);
|
|
2126
|
+
}
|
|
2127
|
+
} else {
|
|
2128
|
+
spinner.warn("No context files found on server");
|
|
2129
|
+
}
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
2132
|
+
spinner.warn(
|
|
2133
|
+
chalk5.yellow(
|
|
2134
|
+
`Cannot reach RepoWise servers to download context: ${msg}
|
|
2135
|
+
Files are stored on our servers (not in git). Retry when online.`
|
|
2136
|
+
)
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
const contextFolder = DEFAULT_CONTEXT_FOLDER;
|
|
2141
|
+
let contextFiles = [];
|
|
2142
|
+
if (repoRoot) {
|
|
2143
|
+
contextFiles = await scanLocalContextFiles(repoRoot, contextFolder);
|
|
2144
|
+
}
|
|
2145
|
+
if (contextFiles.length === 0) {
|
|
2146
|
+
console.log(
|
|
2147
|
+
chalk5.yellow(
|
|
2148
|
+
` No context files found in ${contextFolder}/. Try re-running \`repowise create\`.`
|
|
2149
|
+
)
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
if (tools.length > 0 && repoRoot) {
|
|
2153
|
+
spinner.start("Configuring AI tools...");
|
|
2154
|
+
const results = [];
|
|
2155
|
+
for (const tool of tools) {
|
|
2156
|
+
const { created: wasCreated } = await updateToolConfig(
|
|
2157
|
+
repoRoot,
|
|
2158
|
+
tool,
|
|
2159
|
+
repoName,
|
|
2160
|
+
contextFolder,
|
|
2161
|
+
contextFiles
|
|
2162
|
+
);
|
|
2163
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
2164
|
+
const action = wasCreated ? "Created" : "Updated";
|
|
2165
|
+
results.push(` ${action} ${config2.filePath}`);
|
|
2166
|
+
}
|
|
2167
|
+
spinner.succeed("AI tools configured");
|
|
2168
|
+
console.log(chalk5.dim(results.join("\n")));
|
|
2169
|
+
}
|
|
2170
|
+
const existingConfig = await getConfig();
|
|
2171
|
+
const existingRepos = existingConfig.repos ?? [];
|
|
2172
|
+
const updatedRepos = existingRepos.filter((r) => r.repoId !== repoId);
|
|
2173
|
+
if (repoRoot) {
|
|
2174
|
+
updatedRepos.push({ repoId, localPath: repoRoot });
|
|
2175
|
+
}
|
|
2176
|
+
await saveConfig({
|
|
2177
|
+
...existingConfig,
|
|
2178
|
+
aiTools: tools,
|
|
2179
|
+
contextFolder,
|
|
2180
|
+
repos: updatedRepos
|
|
2181
|
+
});
|
|
2182
|
+
let listenerRunning = false;
|
|
2183
|
+
try {
|
|
2184
|
+
await install();
|
|
2185
|
+
await startBackground();
|
|
2186
|
+
listenerRunning = true;
|
|
2187
|
+
} catch {
|
|
2188
|
+
console.log(
|
|
2189
|
+
chalk5.yellow(
|
|
2190
|
+
"Warning: Could not start listener automatically. Run the following to enable it:"
|
|
2191
|
+
)
|
|
2192
|
+
);
|
|
2193
|
+
console.log(chalk5.yellow(` $ repowise listen --install`));
|
|
2194
|
+
}
|
|
2195
|
+
const elapsed = formatElapsed(Date.now() - startTime);
|
|
2196
|
+
console.log("");
|
|
2197
|
+
console.log(chalk5.green.bold(" All done! Setup complete!"));
|
|
2198
|
+
console.log(
|
|
2199
|
+
chalk5.green(
|
|
2200
|
+
` Your AI tools now have access to project context for ${chalk5.bold(repoName)}.`
|
|
2201
|
+
)
|
|
2202
|
+
);
|
|
2203
|
+
if (listenerRunning) {
|
|
2204
|
+
console.log("");
|
|
2205
|
+
console.log(chalk5.cyan(" The RepoWise listener is running in the background \u2014"));
|
|
2206
|
+
console.log(chalk5.cyan(" your context will stay in sync automatically."));
|
|
2207
|
+
console.log(chalk5.cyan(" Go back to coding, we've got it from here!"));
|
|
2208
|
+
}
|
|
2209
|
+
console.log("");
|
|
2210
|
+
console.log(
|
|
2211
|
+
chalk5.cyan(
|
|
2212
|
+
' Head back to the dashboard and click "Complete Onboarding" to explore your RepoWise dashboard!'
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
console.log(chalk5.dim(`
|
|
2216
|
+
Total time: ${elapsed}`));
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
const message = err instanceof Error ? err.message : "Create failed";
|
|
2219
|
+
spinner.fail(chalk5.red(message));
|
|
2220
|
+
process.exitCode = 1;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/commands/login.ts
|
|
2225
|
+
import chalk6 from "chalk";
|
|
2226
|
+
import ora2 from "ora";
|
|
2227
|
+
async function login(options = {}) {
|
|
2228
|
+
const spinner = ora2("Preparing login...").start();
|
|
2229
|
+
try {
|
|
2230
|
+
const codeVerifier = generateCodeVerifier();
|
|
2231
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2232
|
+
const state = generateState();
|
|
2233
|
+
const authorizeUrl = getAuthorizeUrl(codeChallenge, state);
|
|
2234
|
+
const callbackPromise = startCallbackServer();
|
|
2235
|
+
if (options.browser === false) {
|
|
2236
|
+
spinner.stop();
|
|
2237
|
+
console.log(`
|
|
2238
|
+
Open this URL in your browser to authenticate:
|
|
2239
|
+
`);
|
|
2240
|
+
console.log(chalk6.cyan(authorizeUrl));
|
|
2241
|
+
console.log(`
|
|
2242
|
+
Waiting for authentication...`);
|
|
2243
|
+
} else {
|
|
2244
|
+
spinner.text = "Opening browser for authentication...";
|
|
2245
|
+
try {
|
|
2246
|
+
const open = (await import("open")).default;
|
|
2247
|
+
await open(authorizeUrl);
|
|
2248
|
+
spinner.text = "Waiting for authentication in browser...";
|
|
2249
|
+
} catch {
|
|
2250
|
+
spinner.stop();
|
|
2251
|
+
console.log(`
|
|
2252
|
+
Could not open browser automatically. Open this URL:
|
|
2253
|
+
`);
|
|
2254
|
+
console.log(chalk6.cyan(authorizeUrl));
|
|
2255
|
+
console.log(`
|
|
2256
|
+
Waiting for authentication...`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
2260
|
+
if (returnedState !== state) {
|
|
2261
|
+
throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");
|
|
2262
|
+
}
|
|
2263
|
+
spinner.start("Exchanging authorization code...");
|
|
2264
|
+
const credentials = await exchangeCodeForTokens(code, codeVerifier);
|
|
2265
|
+
await storeCredentials2(credentials);
|
|
2266
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
2267
|
+
spinner.succeed(chalk6.green(`Logged in as ${chalk6.bold(email)}`));
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
const message = err instanceof Error ? err.message : "Login failed";
|
|
2270
|
+
spinner.fail(chalk6.red(message));
|
|
2271
|
+
process.exitCode = 1;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/commands/logout.ts
|
|
2276
|
+
import chalk7 from "chalk";
|
|
2277
|
+
async function logout() {
|
|
2278
|
+
const creds = await getStoredCredentials2();
|
|
2279
|
+
if (!creds) {
|
|
2280
|
+
console.log(chalk7.yellow("Not logged in."));
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
await clearCredentials();
|
|
2284
|
+
console.log(chalk7.green("Logged out successfully."));
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/commands/status.ts
|
|
2288
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2289
|
+
import { homedir as homedir9 } from "os";
|
|
2290
|
+
import { join as join13 } from "path";
|
|
2291
|
+
var STATE_PATH2 = join13(homedir9(), ".repowise", "listener-state.json");
|
|
2292
|
+
async function status() {
|
|
2293
|
+
let state = null;
|
|
2294
|
+
try {
|
|
2295
|
+
const data = await readFile9(STATE_PATH2, "utf-8");
|
|
2296
|
+
state = JSON.parse(data);
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
let processRunning = false;
|
|
2300
|
+
let pid = null;
|
|
2301
|
+
try {
|
|
2302
|
+
const processStatus = await getStatus();
|
|
2303
|
+
processRunning = processStatus.running;
|
|
2304
|
+
pid = processStatus.pid;
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
let serviceInstalled = false;
|
|
2308
|
+
try {
|
|
2309
|
+
serviceInstalled = await isInstalled();
|
|
2310
|
+
} catch {
|
|
2311
|
+
}
|
|
2312
|
+
console.log("RepoWise Status");
|
|
2313
|
+
console.log("===============");
|
|
2314
|
+
if (processRunning) {
|
|
2315
|
+
console.log(`Listener: running (PID: ${pid})`);
|
|
2316
|
+
} else {
|
|
2317
|
+
console.log("Listener: stopped");
|
|
2318
|
+
}
|
|
2319
|
+
if (serviceInstalled) {
|
|
2320
|
+
console.log("Auto-start: enabled");
|
|
2321
|
+
} else {
|
|
2322
|
+
console.log("Auto-start: disabled");
|
|
2323
|
+
}
|
|
2324
|
+
console.log("");
|
|
2325
|
+
if (!state || Object.keys(state.repos).length === 0) {
|
|
2326
|
+
console.log("No sync history. Run `repowise listen` to start syncing.");
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
console.log("Watched Repos:");
|
|
2330
|
+
for (const [repoId, repoState] of Object.entries(state.repos)) {
|
|
2331
|
+
const syncTime = repoState.lastSyncTimestamp ? new Date(repoState.lastSyncTimestamp).toLocaleString() : "never";
|
|
2332
|
+
const commit = repoState.lastSyncCommitSha ? repoState.lastSyncCommitSha.slice(0, 7) : "none";
|
|
2333
|
+
console.log(` ${repoId}: last sync ${syncTime} (commit: ${commit})`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/commands/sync.ts
|
|
2338
|
+
import { execSync as execSync2 } from "child_process";
|
|
2339
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2340
|
+
import { join as join14 } from "path";
|
|
2341
|
+
import chalk8 from "chalk";
|
|
2342
|
+
import ora3 from "ora";
|
|
2343
|
+
var POLL_INTERVAL_MS2 = 3e3;
|
|
2344
|
+
var MAX_POLL_ATTEMPTS2 = 600;
|
|
2345
|
+
var DEFAULT_CONTEXT_FOLDER2 = "repowise-context";
|
|
2346
|
+
function detectRepoRoot2() {
|
|
2347
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
2348
|
+
}
|
|
2349
|
+
function detectRepoName2(repoRoot) {
|
|
2350
|
+
try {
|
|
2351
|
+
const remoteUrl = execSync2("git remote get-url origin", {
|
|
2352
|
+
encoding: "utf-8",
|
|
2353
|
+
cwd: repoRoot
|
|
2354
|
+
}).trim();
|
|
2355
|
+
const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
|
|
2356
|
+
if (match?.[1]) return match[1];
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
return repoRoot.split("/").pop() ?? "unknown";
|
|
2360
|
+
}
|
|
2361
|
+
function formatElapsed2(ms) {
|
|
2362
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
2363
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
2364
|
+
const seconds = totalSeconds % 60;
|
|
2365
|
+
if (minutes === 0) return `${seconds}s`;
|
|
2366
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
2367
|
+
}
|
|
2368
|
+
async function sync() {
|
|
2369
|
+
const startTime = Date.now();
|
|
2370
|
+
const spinner = ora3("Checking authentication...").start();
|
|
2371
|
+
try {
|
|
2372
|
+
let credentials = await getValidCredentials2();
|
|
2373
|
+
if (!credentials) {
|
|
2374
|
+
spinner.info(chalk8.yellow("Not logged in. Opening browser to authenticate..."));
|
|
2375
|
+
credentials = await performLogin();
|
|
2376
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
2377
|
+
spinner.succeed(chalk8.green(`Authenticated as ${chalk8.bold(email)}`));
|
|
2378
|
+
} else {
|
|
2379
|
+
spinner.succeed("Authenticated");
|
|
2380
|
+
}
|
|
2381
|
+
let repoRoot;
|
|
2382
|
+
let repoName;
|
|
2383
|
+
spinner.start("Detecting repository...");
|
|
2384
|
+
try {
|
|
2385
|
+
repoRoot = detectRepoRoot2();
|
|
2386
|
+
repoName = detectRepoName2(repoRoot);
|
|
2387
|
+
spinner.succeed(`Repository: ${chalk8.bold(repoName)}`);
|
|
2388
|
+
} catch {
|
|
2389
|
+
spinner.fail(
|
|
2390
|
+
chalk8.red("Not in a git repository. Run this command from your repo directory.")
|
|
2391
|
+
);
|
|
2392
|
+
process.exitCode = 1;
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
let repoId;
|
|
2396
|
+
spinner.start("Resolving repository...");
|
|
2397
|
+
try {
|
|
2398
|
+
const repos = await apiRequest("/v1/repos");
|
|
2399
|
+
const match = repos.find((r) => r.name === repoName || r.fullName.endsWith(`/${repoName}`));
|
|
2400
|
+
if (match) {
|
|
2401
|
+
repoId = match.repoId;
|
|
2402
|
+
}
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
2405
|
+
if (!repoId) {
|
|
2406
|
+
spinner.fail(
|
|
2407
|
+
chalk8.red(
|
|
2408
|
+
"Could not find this repository in your RepoWise account. Run `repowise create` first."
|
|
2409
|
+
)
|
|
2410
|
+
);
|
|
2411
|
+
process.exitCode = 1;
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
spinner.succeed("Repository resolved");
|
|
2415
|
+
spinner.start("Triggering incremental sync...");
|
|
2416
|
+
let syncId;
|
|
2417
|
+
try {
|
|
2418
|
+
const triggerResult = await apiRequest(`/v1/repos/${repoId}/sync`, {
|
|
2419
|
+
method: "POST",
|
|
2420
|
+
body: JSON.stringify({ scanType: "incremental" })
|
|
2421
|
+
});
|
|
2422
|
+
syncId = triggerResult.syncId;
|
|
2423
|
+
} catch (triggerErr) {
|
|
2424
|
+
const msg = triggerErr instanceof Error ? triggerErr.message : "";
|
|
2425
|
+
if (!msg.toLowerCase().includes("already running")) {
|
|
2426
|
+
throw triggerErr;
|
|
2427
|
+
}
|
|
2428
|
+
spinner.text = "Resuming existing sync...";
|
|
2429
|
+
const syncs = await apiRequest(
|
|
2430
|
+
`/v1/repos/${repoId}/syncs?limit=1`
|
|
2431
|
+
);
|
|
2432
|
+
const active = syncs.items.find(
|
|
2433
|
+
(s) => s.status === "in_progress" || s.status === "awaiting_input"
|
|
2434
|
+
);
|
|
2435
|
+
if (!active) {
|
|
2436
|
+
throw new Error("Could not find active sync to resume. Please try again.");
|
|
2437
|
+
}
|
|
2438
|
+
syncId = active.syncId;
|
|
2439
|
+
spinner.info(chalk8.cyan("Resuming existing sync..."));
|
|
2440
|
+
spinner.start();
|
|
2441
|
+
}
|
|
2442
|
+
let pollAttempts = 0;
|
|
2443
|
+
const progressRenderer = new ProgressRenderer();
|
|
2444
|
+
while (true) {
|
|
2445
|
+
if (++pollAttempts > MAX_POLL_ATTEMPTS2) {
|
|
2446
|
+
spinner.fail(chalk8.red("Sync timed out. Check dashboard for status."));
|
|
2447
|
+
process.exitCode = 1;
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
|
|
2451
|
+
const syncResult = await apiRequest(`/v1/sync/${syncId}/status`);
|
|
2452
|
+
progressRenderer.update(syncResult, spinner);
|
|
2453
|
+
if (syncResult.status === "awaiting_input" && syncResult.questionId && syncResult.questionText) {
|
|
2454
|
+
spinner.stop();
|
|
2455
|
+
await handleInterview(
|
|
2456
|
+
syncId,
|
|
2457
|
+
syncResult.questionId,
|
|
2458
|
+
syncResult.questionText,
|
|
2459
|
+
syncResult.questionContext ?? void 0,
|
|
2460
|
+
syncResult.discoveryResult?.estimatedInterviewQuestions
|
|
2461
|
+
);
|
|
2462
|
+
spinner.start("Resuming sync...");
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (syncResult.status === "completed") {
|
|
2466
|
+
const generatedFiles = syncResult.filesGenerated ?? [];
|
|
2467
|
+
const fileCount = generatedFiles.length;
|
|
2468
|
+
if (fileCount > 0) {
|
|
2469
|
+
spinner.succeed(chalk8.green(`Incremental update complete \u2014 ${fileCount} files updated`));
|
|
2470
|
+
} else {
|
|
2471
|
+
spinner.succeed(chalk8.green("Incremental sync complete \u2014 no files needed updating"));
|
|
2472
|
+
}
|
|
2473
|
+
break;
|
|
2474
|
+
}
|
|
2475
|
+
if (syncResult.status === "failed") {
|
|
2476
|
+
spinner.fail(chalk8.red(`Sync failed: ${syncResult.error ?? "Unknown error"}`));
|
|
2477
|
+
process.exitCode = 1;
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
spinner.start("Downloading context files from server...");
|
|
2482
|
+
try {
|
|
2483
|
+
const listResult = await apiRequest(`/v1/repos/${repoId}/context`);
|
|
2484
|
+
const files = listResult.data?.files ?? listResult.files ?? [];
|
|
2485
|
+
if (files.length > 0) {
|
|
2486
|
+
const contextDir = join14(repoRoot, DEFAULT_CONTEXT_FOLDER2);
|
|
2487
|
+
mkdirSync2(contextDir, { recursive: true });
|
|
2488
|
+
let downloadedCount = 0;
|
|
2489
|
+
let failedCount = 0;
|
|
2490
|
+
for (const file of files) {
|
|
2491
|
+
if (file.fileName.includes("..") || file.fileName.includes("/")) {
|
|
2492
|
+
failedCount++;
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const urlResult = await apiRequest(`/v1/repos/${repoId}/context/${file.fileName}`);
|
|
2496
|
+
const presignedUrl = urlResult.data?.url ?? urlResult.url;
|
|
2497
|
+
const response = await fetch(presignedUrl);
|
|
2498
|
+
if (response.ok) {
|
|
2499
|
+
const content = await response.text();
|
|
2500
|
+
writeFileSync2(join14(contextDir, file.fileName), content, "utf-8");
|
|
2501
|
+
downloadedCount++;
|
|
2502
|
+
} else {
|
|
2503
|
+
failedCount++;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
if (failedCount > 0) {
|
|
2507
|
+
spinner.warn(
|
|
2508
|
+
`Downloaded ${downloadedCount}/${files.length} files to ./${DEFAULT_CONTEXT_FOLDER2}/ (${failedCount} failed)`
|
|
2509
|
+
);
|
|
2510
|
+
} else {
|
|
2511
|
+
spinner.succeed(`Context files downloaded to ./${DEFAULT_CONTEXT_FOLDER2}/`);
|
|
2512
|
+
}
|
|
2513
|
+
} else {
|
|
2514
|
+
spinner.info("No context files found on server");
|
|
2515
|
+
}
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
2518
|
+
spinner.warn(chalk8.yellow(`Could not download context files: ${msg}
|
|
2519
|
+
Retry when online.`));
|
|
2520
|
+
}
|
|
2521
|
+
const elapsed = formatElapsed2(Date.now() - startTime);
|
|
2522
|
+
console.log(chalk8.dim(`
|
|
2523
|
+
Total time: ${elapsed}`));
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
const message = err instanceof Error ? err.message : "Sync failed";
|
|
2526
|
+
spinner.fail(chalk8.red(message));
|
|
2527
|
+
process.exitCode = 1;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// src/commands/listen.ts
|
|
2532
|
+
async function listen(options) {
|
|
2533
|
+
if (options.install) {
|
|
2534
|
+
try {
|
|
2535
|
+
await install();
|
|
2536
|
+
console.log("Auto-start service installed. The listener will start on boot.");
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2539
|
+
console.error(`Failed to install auto-start service: ${message}`);
|
|
2540
|
+
console.error("You can still run the listener manually with `repowise listen`.");
|
|
2541
|
+
process.exitCode = 1;
|
|
2542
|
+
}
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
if (options.uninstall) {
|
|
2546
|
+
try {
|
|
2547
|
+
await uninstall();
|
|
2548
|
+
console.log("Auto-start service removed.");
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2551
|
+
console.error(`Failed to uninstall auto-start service: ${message}`);
|
|
2552
|
+
process.exitCode = 1;
|
|
2553
|
+
}
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const credentials = await getValidCredentials2();
|
|
2557
|
+
if (!credentials) {
|
|
2558
|
+
console.error("Not logged in. Run `repowise login` first.");
|
|
2559
|
+
process.exitCode = 1;
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
console.log("Starting RepoWise listener...");
|
|
2563
|
+
try {
|
|
2564
|
+
await startListener();
|
|
2565
|
+
} catch {
|
|
2566
|
+
console.error("Failed to start listener.");
|
|
2567
|
+
process.exitCode = 1;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// src/commands/start.ts
|
|
2572
|
+
async function start() {
|
|
2573
|
+
try {
|
|
2574
|
+
if (await isRunning()) {
|
|
2575
|
+
console.log("Listener is already running.");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
const pid = await startBackground();
|
|
2579
|
+
console.log(`Listener started (PID: ${pid}).`);
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2582
|
+
console.error(`Failed to start listener: ${message}`);
|
|
2583
|
+
process.exitCode = 1;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/commands/stop.ts
|
|
2588
|
+
async function stop2() {
|
|
2589
|
+
try {
|
|
2590
|
+
if (!await isRunning()) {
|
|
2591
|
+
console.log("Listener is not running.");
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
await stopProcess();
|
|
2595
|
+
console.log("Listener stopped.");
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2598
|
+
console.error(`Failed to stop listener: ${message}`);
|
|
2599
|
+
process.exitCode = 1;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/commands/config.ts
|
|
2604
|
+
import chalk9 from "chalk";
|
|
2605
|
+
import ora4 from "ora";
|
|
2606
|
+
import { select } from "@inquirer/prompts";
|
|
2607
|
+
async function config() {
|
|
2608
|
+
const spinner = ora4("Checking authentication...").start();
|
|
2609
|
+
try {
|
|
2610
|
+
let credentials = await getValidCredentials2();
|
|
2611
|
+
if (!credentials) {
|
|
2612
|
+
spinner.info(chalk9.yellow("Not logged in. Opening browser to authenticate..."));
|
|
2613
|
+
credentials = await performLogin();
|
|
2614
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
2615
|
+
spinner.succeed(chalk9.green(`Authenticated as ${chalk9.bold(email)}`));
|
|
2616
|
+
} else {
|
|
2617
|
+
spinner.succeed("Authenticated");
|
|
2618
|
+
}
|
|
2619
|
+
spinner.start("Loading repositories...");
|
|
2620
|
+
const repos = await apiRequest("/v1/repos");
|
|
2621
|
+
spinner.stop();
|
|
2622
|
+
if (repos.length === 0) {
|
|
2623
|
+
console.log(chalk9.yellow("No repositories connected. Run `repowise create` first."));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const repoId = await select({
|
|
2627
|
+
message: "Select a repository to configure",
|
|
2628
|
+
choices: repos.map((r) => ({
|
|
2629
|
+
name: r.fullName,
|
|
2630
|
+
value: r.repoId
|
|
2631
|
+
}))
|
|
2632
|
+
});
|
|
2633
|
+
const repo = repos.find((r) => r.repoId === repoId);
|
|
2634
|
+
const currentDelivery = repo.deliveryMode ?? "direct";
|
|
2635
|
+
const currentBranch = repo.monitoredBranch ?? repo.defaultBranch;
|
|
2636
|
+
console.log("");
|
|
2637
|
+
console.log(chalk9.bold(`Settings for ${repo.fullName}`));
|
|
2638
|
+
console.log(chalk9.dim(` Context storage: RepoWise servers`));
|
|
2639
|
+
console.log(chalk9.dim(` Delivery mode: ${currentDelivery}`));
|
|
2640
|
+
console.log(chalk9.dim(` Monitored branch: ${currentBranch}`));
|
|
2641
|
+
console.log("");
|
|
2642
|
+
const setting = await select({
|
|
2643
|
+
message: "What would you like to change?",
|
|
2644
|
+
choices: [
|
|
2645
|
+
{ name: `Delivery mode (current: ${currentDelivery})`, value: "deliveryMode" },
|
|
2646
|
+
{ name: `Monitored branch (current: ${currentBranch})`, value: "monitoredBranch" }
|
|
2647
|
+
]
|
|
2648
|
+
});
|
|
2649
|
+
const patch = {};
|
|
2650
|
+
if (setting === "deliveryMode") {
|
|
2651
|
+
const newMode = await select({
|
|
2652
|
+
message: "Delivery mode",
|
|
2653
|
+
choices: [
|
|
2654
|
+
{ name: "Direct push to branch", value: "direct" },
|
|
2655
|
+
{ name: "Create pull request", value: "pr" }
|
|
2656
|
+
],
|
|
2657
|
+
default: currentDelivery
|
|
2658
|
+
});
|
|
2659
|
+
if (newMode === currentDelivery) {
|
|
2660
|
+
console.log(chalk9.dim("No change."));
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
patch.deliveryMode = newMode;
|
|
2664
|
+
} else if (setting === "monitoredBranch") {
|
|
2665
|
+
const { input: input2 } = await import("@inquirer/prompts");
|
|
2666
|
+
const newBranch = await input2({
|
|
2667
|
+
message: "Monitored branch",
|
|
2668
|
+
default: currentBranch
|
|
2669
|
+
});
|
|
2670
|
+
if (newBranch === currentBranch) {
|
|
2671
|
+
console.log(chalk9.dim("No change."));
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
patch.monitoredBranch = newBranch;
|
|
2675
|
+
}
|
|
2676
|
+
spinner.start("Saving...");
|
|
2677
|
+
await apiRequest(`/v1/repos/${repoId}`, {
|
|
2678
|
+
method: "PATCH",
|
|
2679
|
+
body: JSON.stringify(patch)
|
|
2680
|
+
});
|
|
2681
|
+
spinner.succeed(chalk9.green("Setting updated"));
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
spinner.stop();
|
|
2684
|
+
const message = err instanceof Error ? err.message : "Config failed";
|
|
2685
|
+
console.error(chalk9.red(message));
|
|
2686
|
+
process.exitCode = 1;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// bin/repowise.ts
|
|
2691
|
+
var __filename = fileURLToPath3(import.meta.url);
|
|
2692
|
+
var __dirname = dirname2(__filename);
|
|
2693
|
+
var pkg = JSON.parse(readFileSync(join15(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
2694
|
+
var program = new Command();
|
|
2695
|
+
program.name("repowise").description("AI-optimized codebase context generator").version(pkg.version).option("--staging", "Use the staging environment").hook("preAction", async () => {
|
|
2696
|
+
if (program.opts()["staging"]) {
|
|
2697
|
+
setStagingMode(true);
|
|
2698
|
+
}
|
|
2699
|
+
await showWelcome(pkg.version);
|
|
22
2700
|
});
|
|
23
|
-
program
|
|
24
|
-
|
|
25
|
-
.description('Create context for a repository')
|
|
26
|
-
.action(async () => {
|
|
27
|
-
const { create } = await import('../src/commands/create.js');
|
|
28
|
-
await create();
|
|
2701
|
+
program.command("create").description("Create context for a repository").action(async () => {
|
|
2702
|
+
await create();
|
|
29
2703
|
});
|
|
30
|
-
program
|
|
31
|
-
|
|
32
|
-
.description('Authenticate with RepoWise')
|
|
33
|
-
.option('--no-browser', 'Print login URL instead of opening browser')
|
|
34
|
-
.action(async (options) => {
|
|
35
|
-
const { login } = await import('../src/commands/login.js');
|
|
36
|
-
await login(options);
|
|
2704
|
+
program.command("login").description("Authenticate with RepoWise").option("--no-browser", "Print login URL instead of opening browser").action(async (options) => {
|
|
2705
|
+
await login(options);
|
|
37
2706
|
});
|
|
38
|
-
program
|
|
39
|
-
|
|
40
|
-
.description('Sign out of RepoWise')
|
|
41
|
-
.action(async () => {
|
|
42
|
-
const { logout } = await import('../src/commands/logout.js');
|
|
43
|
-
await logout();
|
|
2707
|
+
program.command("logout").description("Sign out of RepoWise").action(async () => {
|
|
2708
|
+
await logout();
|
|
44
2709
|
});
|
|
45
|
-
program
|
|
46
|
-
|
|
47
|
-
.description('Show current status')
|
|
48
|
-
.action(async () => {
|
|
49
|
-
const { status } = await import('../src/commands/status.js');
|
|
50
|
-
await status();
|
|
2710
|
+
program.command("status").description("Show current status").action(async () => {
|
|
2711
|
+
await status();
|
|
51
2712
|
});
|
|
52
|
-
program
|
|
53
|
-
|
|
54
|
-
.description('Trigger a manual sync')
|
|
55
|
-
.action(async () => {
|
|
56
|
-
const { sync } = await import('../src/commands/sync.js');
|
|
57
|
-
await sync();
|
|
2713
|
+
program.command("sync").description("Trigger a manual sync").action(async () => {
|
|
2714
|
+
await sync();
|
|
58
2715
|
});
|
|
59
|
-
program
|
|
60
|
-
|
|
61
|
-
.description('Start the context listener')
|
|
62
|
-
.option('--install', 'Install auto-start service')
|
|
63
|
-
.option('--uninstall', 'Remove auto-start service')
|
|
64
|
-
.action(async (options) => {
|
|
65
|
-
const { listen } = await import('../src/commands/listen.js');
|
|
66
|
-
await listen(options);
|
|
2716
|
+
program.command("listen").description("Start the context listener").option("--install", "Install auto-start service").option("--uninstall", "Remove auto-start service").action(async (options) => {
|
|
2717
|
+
await listen(options);
|
|
67
2718
|
});
|
|
68
|
-
program
|
|
69
|
-
|
|
70
|
-
.description('Start the listener as a background process')
|
|
71
|
-
.action(async () => {
|
|
72
|
-
const { start } = await import('../src/commands/start.js');
|
|
73
|
-
await start();
|
|
2719
|
+
program.command("start").description("Start the listener as a background process").action(async () => {
|
|
2720
|
+
await start();
|
|
74
2721
|
});
|
|
75
|
-
program
|
|
76
|
-
|
|
77
|
-
.description('Stop the running listener process')
|
|
78
|
-
.action(async () => {
|
|
79
|
-
const { stop } = await import('../src/commands/stop.js');
|
|
80
|
-
await stop();
|
|
2722
|
+
program.command("stop").description("Stop the running listener process").action(async () => {
|
|
2723
|
+
await stop2();
|
|
81
2724
|
});
|
|
82
|
-
program
|
|
83
|
-
|
|
84
|
-
.description('Manage configuration')
|
|
85
|
-
.action(async () => {
|
|
86
|
-
const { config } = await import('../src/commands/config.js');
|
|
87
|
-
await config();
|
|
2725
|
+
program.command("config").description("Manage configuration").action(async () => {
|
|
2726
|
+
await config();
|
|
88
2727
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
program.parse();
|
|
2728
|
+
if (process.argv[2] === "__listener") {
|
|
2729
|
+
await startListener();
|
|
2730
|
+
} else {
|
|
2731
|
+
program.parse();
|
|
96
2732
|
}
|
|
97
|
-
//# sourceMappingURL=repowise.js.map
|