repowisestage 0.0.1-staging.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/repowise.js +4070 -0
- package/package.json +50 -0
|
@@ -0,0 +1,4070 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/repowise.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// ../listener/dist/main.js
|
|
10
|
+
import { writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
|
|
11
|
+
import { join as join10 } from "path";
|
|
12
|
+
import lockfile2 from "proper-lockfile";
|
|
13
|
+
|
|
14
|
+
// ../../packages/shared/src/config-dir.ts
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
function getConfigDir() {
|
|
18
|
+
const isStaging = true ? false : false;
|
|
19
|
+
return join(homedir(), isStaging ? ".repowise-staging" : ".repowise");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../../packages/shared/src/constants/tiers.ts
|
|
23
|
+
var SUBSCRIPTION_TIERS = {
|
|
24
|
+
STARTER: "starter",
|
|
25
|
+
PRO: "pro",
|
|
26
|
+
TEAM: "team"
|
|
27
|
+
};
|
|
28
|
+
var TIER_LIMITS = {
|
|
29
|
+
[SUBSCRIPTION_TIERS.STARTER]: {
|
|
30
|
+
maxRepos: 1,
|
|
31
|
+
maxSeats: 1,
|
|
32
|
+
maxSyncsPerMonth: 10,
|
|
33
|
+
maxConcurrentSyncs: 1,
|
|
34
|
+
bedrockEndpoint: "public"
|
|
35
|
+
},
|
|
36
|
+
[SUBSCRIPTION_TIERS.PRO]: {
|
|
37
|
+
maxRepos: 3,
|
|
38
|
+
maxSeats: 1,
|
|
39
|
+
maxSyncsPerMonth: 30,
|
|
40
|
+
maxConcurrentSyncs: 2,
|
|
41
|
+
bedrockEndpoint: "public"
|
|
42
|
+
},
|
|
43
|
+
[SUBSCRIPTION_TIERS.TEAM]: {
|
|
44
|
+
maxRepos: 5,
|
|
45
|
+
// per seat
|
|
46
|
+
maxSeats: 25,
|
|
47
|
+
maxSyncsPerMonth: 50,
|
|
48
|
+
maxConcurrentSyncs: 10,
|
|
49
|
+
bedrockEndpoint: "privatelink"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ../../packages/shared/src/constants/roles.ts
|
|
54
|
+
var ROLES = {
|
|
55
|
+
OWNER: "owner",
|
|
56
|
+
ADMIN: "admin",
|
|
57
|
+
REPO_MANAGER: "repo_manager",
|
|
58
|
+
MEMBER: "member"
|
|
59
|
+
};
|
|
60
|
+
var PERMISSIONS = {
|
|
61
|
+
[ROLES.OWNER]: {
|
|
62
|
+
connectRepos: true,
|
|
63
|
+
viewSync: true,
|
|
64
|
+
triggerRetry: true,
|
|
65
|
+
configWatcher: true,
|
|
66
|
+
inviteMembers: true,
|
|
67
|
+
manageBilling: true,
|
|
68
|
+
configAiTools: true,
|
|
69
|
+
manageTeam: true
|
|
70
|
+
},
|
|
71
|
+
[ROLES.ADMIN]: {
|
|
72
|
+
connectRepos: true,
|
|
73
|
+
viewSync: true,
|
|
74
|
+
triggerRetry: true,
|
|
75
|
+
configWatcher: true,
|
|
76
|
+
inviteMembers: true,
|
|
77
|
+
manageBilling: false,
|
|
78
|
+
configAiTools: true,
|
|
79
|
+
manageTeam: true
|
|
80
|
+
},
|
|
81
|
+
[ROLES.REPO_MANAGER]: {
|
|
82
|
+
connectRepos: true,
|
|
83
|
+
viewSync: true,
|
|
84
|
+
triggerRetry: true,
|
|
85
|
+
configWatcher: true,
|
|
86
|
+
inviteMembers: false,
|
|
87
|
+
manageBilling: false,
|
|
88
|
+
configAiTools: true,
|
|
89
|
+
manageTeam: false
|
|
90
|
+
},
|
|
91
|
+
[ROLES.MEMBER]: {
|
|
92
|
+
connectRepos: false,
|
|
93
|
+
viewSync: true,
|
|
94
|
+
triggerRetry: true,
|
|
95
|
+
configWatcher: false,
|
|
96
|
+
inviteMembers: false,
|
|
97
|
+
manageBilling: false,
|
|
98
|
+
configAiTools: true,
|
|
99
|
+
manageTeam: false
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var ROLE_PRIORITY = [
|
|
103
|
+
ROLES.OWNER,
|
|
104
|
+
ROLES.ADMIN,
|
|
105
|
+
ROLES.REPO_MANAGER,
|
|
106
|
+
ROLES.MEMBER
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// ../listener/dist/lib/config.js
|
|
110
|
+
import { readFile, writeFile, rename, unlink, mkdir, chmod } from "fs/promises";
|
|
111
|
+
import { join as join2 } from "path";
|
|
112
|
+
import lockfile from "proper-lockfile";
|
|
113
|
+
var DEFAULT_API_URL = false ? "https://staging-api.repowise.ai" : "https://api.repowise.ai";
|
|
114
|
+
async function getListenerConfig() {
|
|
115
|
+
const configDir = getConfigDir();
|
|
116
|
+
const configPath = join2(configDir, "config.json");
|
|
117
|
+
const apiUrl = process.env["REPOWISE_API_URL"] ?? DEFAULT_API_URL;
|
|
118
|
+
try {
|
|
119
|
+
const data = await readFile(configPath, "utf-8");
|
|
120
|
+
const raw = JSON.parse(data);
|
|
121
|
+
const validRepos = (raw.repos ?? []).filter((r) => typeof r === "object" && r !== null && typeof r.repoId === "string" && typeof r.localPath === "string");
|
|
122
|
+
return {
|
|
123
|
+
defaultApiUrl: raw.apiUrl ?? apiUrl,
|
|
124
|
+
repos: validRepos,
|
|
125
|
+
autoDiscoverRepos: raw.autoDiscoverRepos ?? false,
|
|
126
|
+
noAutoUpdate: raw.noAutoUpdate ?? false
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return { defaultApiUrl: apiUrl, repos: [], autoDiscoverRepos: false, noAutoUpdate: false };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function saveListenerConfig(config2) {
|
|
133
|
+
const configDir = getConfigDir();
|
|
134
|
+
const configPath = join2(configDir, "config.json");
|
|
135
|
+
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
136
|
+
try {
|
|
137
|
+
await writeFile(configPath, "", { flag: "a" });
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
let release = null;
|
|
141
|
+
try {
|
|
142
|
+
release = await lockfile.lock(configPath, { stale: 1e4, retries: 3, realpath: false });
|
|
143
|
+
let raw = {};
|
|
144
|
+
try {
|
|
145
|
+
const data = await readFile(configPath, "utf-8");
|
|
146
|
+
raw = JSON.parse(data);
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
const output = {
|
|
150
|
+
...raw,
|
|
151
|
+
apiUrl: config2.defaultApiUrl,
|
|
152
|
+
repos: config2.repos
|
|
153
|
+
};
|
|
154
|
+
const tmpPath = configPath + ".tmp";
|
|
155
|
+
try {
|
|
156
|
+
await writeFile(tmpPath, JSON.stringify(output, null, 2));
|
|
157
|
+
await chmod(tmpPath, 384);
|
|
158
|
+
await rename(tmpPath, configPath);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
try {
|
|
161
|
+
await unlink(tmpPath);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
if (release) {
|
|
168
|
+
try {
|
|
169
|
+
await release();
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ../listener/dist/lib/state.js
|
|
177
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
178
|
+
import { join as join3 } from "path";
|
|
179
|
+
function emptyState() {
|
|
180
|
+
return { repos: {} };
|
|
181
|
+
}
|
|
182
|
+
async function loadState() {
|
|
183
|
+
const statePath = join3(getConfigDir(), "listener-state.json");
|
|
184
|
+
try {
|
|
185
|
+
const data = await readFile2(statePath, "utf-8");
|
|
186
|
+
return JSON.parse(data);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
189
|
+
return emptyState();
|
|
190
|
+
}
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function saveState(state) {
|
|
195
|
+
const configDir = getConfigDir();
|
|
196
|
+
const statePath = join3(configDir, "listener-state.json");
|
|
197
|
+
await mkdir2(configDir, { recursive: true, mode: 448 });
|
|
198
|
+
const tmpPath = statePath + ".tmp";
|
|
199
|
+
try {
|
|
200
|
+
await writeFile2(tmpPath, JSON.stringify(state, null, 2));
|
|
201
|
+
await chmod2(tmpPath, 384);
|
|
202
|
+
await rename2(tmpPath, statePath);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
try {
|
|
205
|
+
await unlink2(tmpPath);
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ../listener/dist/lib/reconcile.js
|
|
213
|
+
import { statSync, readdirSync } from "fs";
|
|
214
|
+
import { basename, dirname, join as join4 } from "path";
|
|
215
|
+
function reconcileRepos(configRepos, activeRepos, state, apiUrl, options) {
|
|
216
|
+
if (activeRepos.length === 0) {
|
|
217
|
+
return { updated: false, repos: configRepos, changes: [], addedRepos: [] };
|
|
218
|
+
}
|
|
219
|
+
const changes = [];
|
|
220
|
+
let updated = false;
|
|
221
|
+
const addedRepos = [];
|
|
222
|
+
const usedPaths = new Set(configRepos.map((r) => r.localPath));
|
|
223
|
+
const updatedRepos = configRepos.map((repo) => {
|
|
224
|
+
if (repo.apiUrl && repo.apiUrl !== apiUrl) {
|
|
225
|
+
return repo;
|
|
226
|
+
}
|
|
227
|
+
if (activeRepos.some((ar) => ar.repoId === repo.repoId)) {
|
|
228
|
+
if (!repo.platform || !repo.externalId) {
|
|
229
|
+
const match = activeRepos.find((ar) => ar.repoId === repo.repoId);
|
|
230
|
+
if (match) {
|
|
231
|
+
changes.push(`Backfilled platform+externalId for ${match.fullName} (${repo.repoId})`);
|
|
232
|
+
updated = true;
|
|
233
|
+
return {
|
|
234
|
+
...repo,
|
|
235
|
+
platform: match.platform,
|
|
236
|
+
externalId: match.externalId
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return repo;
|
|
241
|
+
}
|
|
242
|
+
if (repo.platform && repo.externalId) {
|
|
243
|
+
const match = activeRepos.find((ar) => ar.platform === repo.platform && ar.externalId === repo.externalId);
|
|
244
|
+
if (match) {
|
|
245
|
+
const oldId = repo.repoId;
|
|
246
|
+
changes.push(`Updated repoId for ${match.fullName}: ${oldId} \u2192 ${match.repoId}`);
|
|
247
|
+
migrateState(state, oldId, match.repoId);
|
|
248
|
+
updated = true;
|
|
249
|
+
return {
|
|
250
|
+
...repo,
|
|
251
|
+
repoId: match.repoId,
|
|
252
|
+
platform: match.platform,
|
|
253
|
+
externalId: match.externalId
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const dirName = basename(repo.localPath);
|
|
258
|
+
const parentDir = basename(dirname(repo.localPath));
|
|
259
|
+
const fullPathName = `${parentDir}/${dirName}`;
|
|
260
|
+
let matches = activeRepos.filter((ar) => ar.fullName === fullPathName);
|
|
261
|
+
if (matches.length === 0) {
|
|
262
|
+
matches = activeRepos.filter((ar) => ar.name === dirName);
|
|
263
|
+
}
|
|
264
|
+
if (matches.length === 1) {
|
|
265
|
+
const match = matches[0];
|
|
266
|
+
if (match.repoId !== repo.repoId) {
|
|
267
|
+
const oldId = repo.repoId;
|
|
268
|
+
changes.push(`Updated repoId for ${match.fullName} (name match): ${oldId} \u2192 ${match.repoId}`);
|
|
269
|
+
migrateState(state, oldId, match.repoId);
|
|
270
|
+
updated = true;
|
|
271
|
+
return {
|
|
272
|
+
...repo,
|
|
273
|
+
repoId: match.repoId,
|
|
274
|
+
platform: match.platform,
|
|
275
|
+
externalId: match.externalId
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (!repo.platform || !repo.externalId) {
|
|
279
|
+
changes.push(`Backfilled platform+externalId for ${match.fullName} (name match)`);
|
|
280
|
+
updated = true;
|
|
281
|
+
return {
|
|
282
|
+
...repo,
|
|
283
|
+
platform: match.platform,
|
|
284
|
+
externalId: match.externalId
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
} else if (matches.length > 1) {
|
|
288
|
+
console.warn(`Ambiguous name match for ${repo.localPath} \u2014 ${matches.length} repos match "${dirName}". Skipping auto-update. Add platform+externalId to config or re-run \`repowise create\`.`);
|
|
289
|
+
}
|
|
290
|
+
return repo;
|
|
291
|
+
});
|
|
292
|
+
if (options?.autoDiscover) {
|
|
293
|
+
const configuredRepoIds = new Set(updatedRepos.map((r) => r.repoId));
|
|
294
|
+
const unmatchedActiveRepos = activeRepos.filter((ar) => !configuredRepoIds.has(ar.repoId));
|
|
295
|
+
if (unmatchedActiveRepos.length > 0) {
|
|
296
|
+
const parentDirs = /* @__PURE__ */ new Set();
|
|
297
|
+
for (const repo of updatedRepos) {
|
|
298
|
+
parentDirs.add(dirname(repo.localPath));
|
|
299
|
+
}
|
|
300
|
+
for (const activeRepo of unmatchedActiveRepos) {
|
|
301
|
+
const found = findLocalRepo(activeRepo, parentDirs, usedPaths);
|
|
302
|
+
if (found) {
|
|
303
|
+
const newRepo = {
|
|
304
|
+
repoId: activeRepo.repoId,
|
|
305
|
+
localPath: found,
|
|
306
|
+
apiUrl,
|
|
307
|
+
platform: activeRepo.platform,
|
|
308
|
+
externalId: activeRepo.externalId
|
|
309
|
+
};
|
|
310
|
+
updatedRepos.push(newRepo);
|
|
311
|
+
addedRepos.push(newRepo);
|
|
312
|
+
usedPaths.add(found);
|
|
313
|
+
configuredRepoIds.add(activeRepo.repoId);
|
|
314
|
+
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
315
|
+
state.repos[activeRepo.repoId] = {
|
|
316
|
+
...state.repos[activeRepo.repoId],
|
|
317
|
+
lastSyncTimestamp: twentyFourHoursAgo,
|
|
318
|
+
lastSyncCommitSha: null
|
|
319
|
+
};
|
|
320
|
+
changes.push(`[self-heal] Auto-added ${activeRepo.fullName} from ${found}`);
|
|
321
|
+
updated = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { updated, repos: updatedRepos, changes, addedRepos };
|
|
327
|
+
}
|
|
328
|
+
function findLocalRepo(activeRepo, parentDirs, usedPaths) {
|
|
329
|
+
const nameParts = activeRepo.fullName.split("/");
|
|
330
|
+
const repoName = nameParts[nameParts.length - 1];
|
|
331
|
+
for (const parentDir of parentDirs) {
|
|
332
|
+
const candidate = join4(parentDir, repoName);
|
|
333
|
+
if (!usedPaths.has(candidate) && hasContextFolder(candidate)) {
|
|
334
|
+
return candidate;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function hasContextFolder(dirPath) {
|
|
340
|
+
try {
|
|
341
|
+
const contextPath = join4(dirPath, "repowise-context");
|
|
342
|
+
const s = statSync(contextPath);
|
|
343
|
+
if (!s.isDirectory())
|
|
344
|
+
return false;
|
|
345
|
+
const entries = readdirSync(contextPath);
|
|
346
|
+
return entries.length > 0;
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function migrateState(state, oldId, newId) {
|
|
352
|
+
const oldState = state.repos[oldId];
|
|
353
|
+
if (oldState) {
|
|
354
|
+
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
355
|
+
state.repos[newId] = {
|
|
356
|
+
...state.repos[newId],
|
|
357
|
+
lastSyncTimestamp: twentyFourHoursAgo,
|
|
358
|
+
lastSyncCommitSha: null
|
|
359
|
+
};
|
|
360
|
+
delete state.repos[oldId];
|
|
361
|
+
} else {
|
|
362
|
+
state.repos[newId] = {
|
|
363
|
+
...state.repos[newId],
|
|
364
|
+
lastSyncTimestamp: new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString(),
|
|
365
|
+
lastSyncCommitSha: null
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ../listener/dist/lib/auth.js
|
|
371
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
|
|
372
|
+
import { join as join5 } from "path";
|
|
373
|
+
function getTokenUrl(creds) {
|
|
374
|
+
const cognito = creds?.cognito;
|
|
375
|
+
const domain = process.env["REPOWISE_COGNITO_DOMAIN"] ?? cognito?.domain ?? "auth-repowise-dev";
|
|
376
|
+
const region = process.env["REPOWISE_COGNITO_REGION"] ?? cognito?.region ?? "us-east-1";
|
|
377
|
+
const customDomain = cognito?.customDomain ?? false;
|
|
378
|
+
return customDomain ? `https://${domain}/oauth2/token` : `https://${domain}.auth.${region}.amazoncognito.com/oauth2/token`;
|
|
379
|
+
}
|
|
380
|
+
function getClientId(creds) {
|
|
381
|
+
return process.env["REPOWISE_COGNITO_CLIENT_ID"] ?? creds?.cognito?.clientId ?? "";
|
|
382
|
+
}
|
|
383
|
+
async function refreshTokens(refreshToken, creds) {
|
|
384
|
+
const clientId = getClientId(creds);
|
|
385
|
+
if (!clientId) {
|
|
386
|
+
throw new Error("No Cognito client ID available. Run `repowise login` first.");
|
|
387
|
+
}
|
|
388
|
+
const response = await fetch(getTokenUrl(creds), {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
391
|
+
body: new URLSearchParams({
|
|
392
|
+
grant_type: "refresh_token",
|
|
393
|
+
client_id: clientId,
|
|
394
|
+
refresh_token: refreshToken
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
399
|
+
}
|
|
400
|
+
const data = await response.json();
|
|
401
|
+
return {
|
|
402
|
+
accessToken: data.access_token,
|
|
403
|
+
refreshToken,
|
|
404
|
+
idToken: data.id_token,
|
|
405
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
406
|
+
cognito: creds.cognito
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function getStoredCredentials() {
|
|
410
|
+
try {
|
|
411
|
+
const credPath = join5(getConfigDir(), "credentials.json");
|
|
412
|
+
const data = await readFile3(credPath, "utf-8");
|
|
413
|
+
return JSON.parse(data);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function storeCredentials(credentials) {
|
|
422
|
+
const dir = getConfigDir();
|
|
423
|
+
const credPath = join5(dir, "credentials.json");
|
|
424
|
+
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
425
|
+
await writeFile3(credPath, JSON.stringify(credentials, null, 2));
|
|
426
|
+
await chmod3(credPath, 384);
|
|
427
|
+
}
|
|
428
|
+
async function getValidCredentials(options) {
|
|
429
|
+
const creds = await getStoredCredentials();
|
|
430
|
+
if (!creds)
|
|
431
|
+
return null;
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
const needsRefresh = options?.forceRefresh || now > creds.expiresAt - 5 * 60 * 1e3;
|
|
434
|
+
if (needsRefresh) {
|
|
435
|
+
try {
|
|
436
|
+
const refreshed = await refreshTokens(creds.refreshToken, creds);
|
|
437
|
+
await storeCredentials(refreshed);
|
|
438
|
+
return refreshed;
|
|
439
|
+
} catch (err) {
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
console.warn(`Token refresh failed: ${message}`);
|
|
442
|
+
if (now < creds.expiresAt) {
|
|
443
|
+
return creds;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return creds;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ../listener/dist/poll-client.js
|
|
452
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
453
|
+
var AuthError = class extends Error {
|
|
454
|
+
constructor(message) {
|
|
455
|
+
super(message);
|
|
456
|
+
this.name = "AuthError";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var PollClient = class {
|
|
460
|
+
apiUrl;
|
|
461
|
+
constructor(apiUrl) {
|
|
462
|
+
this.apiUrl = apiUrl;
|
|
463
|
+
}
|
|
464
|
+
async poll(repoIds, since, options) {
|
|
465
|
+
const credentials = await getValidCredentials();
|
|
466
|
+
if (!credentials) {
|
|
467
|
+
throw new AuthError("Not logged in. Run `repowise login` first.");
|
|
468
|
+
}
|
|
469
|
+
const params = new URLSearchParams({
|
|
470
|
+
repoIds: repoIds.join(","),
|
|
471
|
+
since
|
|
472
|
+
});
|
|
473
|
+
if (options?.includeActiveRepos) {
|
|
474
|
+
params.set("includeActiveRepos", "true");
|
|
475
|
+
}
|
|
476
|
+
const url = `${this.apiUrl}/v1/listeners/poll?${params.toString()}`;
|
|
477
|
+
const result = await this.fetchWithAuth(url, credentials.accessToken);
|
|
478
|
+
if (result.status === 401 || result.status === 403) {
|
|
479
|
+
const refreshed = await getValidCredentials({ forceRefresh: true });
|
|
480
|
+
if (!refreshed) {
|
|
481
|
+
throw new AuthError("Session expired. Run `repowise login` again.");
|
|
482
|
+
}
|
|
483
|
+
const retry = await this.fetchWithAuth(url, refreshed.accessToken);
|
|
484
|
+
if (retry.status === 401 || retry.status === 403) {
|
|
485
|
+
throw new AuthError("Session expired. Run `repowise login` again.");
|
|
486
|
+
}
|
|
487
|
+
if (!retry.ok) {
|
|
488
|
+
throw new Error(`Poll request failed: ${retry.status}`);
|
|
489
|
+
}
|
|
490
|
+
const json2 = await retry.json();
|
|
491
|
+
return json2.data;
|
|
492
|
+
}
|
|
493
|
+
if (!result.ok) {
|
|
494
|
+
throw new Error(`Poll request failed: ${result.status}`);
|
|
495
|
+
}
|
|
496
|
+
const json = await result.json();
|
|
497
|
+
return json.data;
|
|
498
|
+
}
|
|
499
|
+
async fetchWithAuth(url, accessToken) {
|
|
500
|
+
const controller = new AbortController();
|
|
501
|
+
const timeoutId = setTimeout(() => controller.abort(), POLL_TIMEOUT_MS);
|
|
502
|
+
try {
|
|
503
|
+
return await fetch(url, {
|
|
504
|
+
headers: {
|
|
505
|
+
Authorization: `Bearer ${accessToken}`,
|
|
506
|
+
"Content-Type": "application/json"
|
|
507
|
+
},
|
|
508
|
+
signal: controller.signal
|
|
509
|
+
});
|
|
510
|
+
} finally {
|
|
511
|
+
clearTimeout(timeoutId);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// ../listener/dist/reconnection.js
|
|
517
|
+
var DEFAULT_CONFIG = {
|
|
518
|
+
initialDelay: 1e3,
|
|
519
|
+
maxDelay: 6e4,
|
|
520
|
+
jitterMax: 1e3
|
|
521
|
+
};
|
|
522
|
+
var BackoffCalculator = class {
|
|
523
|
+
config;
|
|
524
|
+
attempt = 0;
|
|
525
|
+
constructor(config2 = {}) {
|
|
526
|
+
this.config = { ...DEFAULT_CONFIG, ...config2 };
|
|
527
|
+
}
|
|
528
|
+
nextDelay() {
|
|
529
|
+
const baseDelay = Math.min(this.config.initialDelay * Math.pow(2, this.attempt), this.config.maxDelay);
|
|
530
|
+
const jitter = Math.random() * this.config.jitterMax;
|
|
531
|
+
this.attempt++;
|
|
532
|
+
return baseDelay + jitter;
|
|
533
|
+
}
|
|
534
|
+
reset() {
|
|
535
|
+
this.attempt = 0;
|
|
536
|
+
}
|
|
537
|
+
getAttempt() {
|
|
538
|
+
return this.attempt;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// ../listener/dist/notification.js
|
|
543
|
+
import notifier from "node-notifier";
|
|
544
|
+
var TITLE = "RepoWise";
|
|
545
|
+
var notify = notifier.notify.bind(notifier);
|
|
546
|
+
function notifyConnectionLost() {
|
|
547
|
+
try {
|
|
548
|
+
notify({
|
|
549
|
+
title: TITLE,
|
|
550
|
+
message: "Connection lost \u2014 will sync when back online"
|
|
551
|
+
});
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function notifyBackOnline(updateCount) {
|
|
556
|
+
try {
|
|
557
|
+
notify({
|
|
558
|
+
title: TITLE,
|
|
559
|
+
message: `Back online \u2014 ${updateCount} updates synced`
|
|
560
|
+
});
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function notifyContextUpdated(repoId, fileCount) {
|
|
565
|
+
try {
|
|
566
|
+
notify({
|
|
567
|
+
title: TITLE,
|
|
568
|
+
message: `Context updated for ${repoId} \u2014 ${fileCount} files`
|
|
569
|
+
});
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ../listener/dist/context-fetcher.js
|
|
575
|
+
import { execFile } from "child_process";
|
|
576
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
577
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
578
|
+
import { promisify } from "util";
|
|
579
|
+
|
|
580
|
+
// ../listener/dist/file-writer.js
|
|
581
|
+
import { access } from "fs/promises";
|
|
582
|
+
import { join as join6 } from "path";
|
|
583
|
+
async function verifyContextFolder(localPath) {
|
|
584
|
+
try {
|
|
585
|
+
await access(join6(localPath, "repowise-context"));
|
|
586
|
+
return true;
|
|
587
|
+
} catch {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ../listener/dist/context-fetcher.js
|
|
593
|
+
var execFileAsync = promisify(execFile);
|
|
594
|
+
async function fetchContextUpdates(localPath) {
|
|
595
|
+
try {
|
|
596
|
+
const { stdout: beforeSha } = await execFileAsync("git", [
|
|
597
|
+
"-C",
|
|
598
|
+
localPath,
|
|
599
|
+
"rev-parse",
|
|
600
|
+
"HEAD"
|
|
601
|
+
]);
|
|
602
|
+
const before = beforeSha.trim();
|
|
603
|
+
await execFileAsync("git", ["-C", localPath, "pull", "--ff-only"]);
|
|
604
|
+
const { stdout: afterSha } = await execFileAsync("git", ["-C", localPath, "rev-parse", "HEAD"]);
|
|
605
|
+
const after = afterSha.trim();
|
|
606
|
+
if (before === after) {
|
|
607
|
+
return { success: true, updatedFiles: [] };
|
|
608
|
+
}
|
|
609
|
+
const { stdout } = await execFileAsync("git", [
|
|
610
|
+
"-C",
|
|
611
|
+
localPath,
|
|
612
|
+
"diff",
|
|
613
|
+
"--name-only",
|
|
614
|
+
before,
|
|
615
|
+
after,
|
|
616
|
+
"--",
|
|
617
|
+
"repowise-context/"
|
|
618
|
+
]);
|
|
619
|
+
const updatedFiles = stdout.trim().split("\n").filter(Boolean);
|
|
620
|
+
const contextExists = await verifyContextFolder(localPath);
|
|
621
|
+
if (!contextExists && updatedFiles.length > 0) {
|
|
622
|
+
console.warn(`Warning: repowise-context/ folder not found in ${localPath} after pull`);
|
|
623
|
+
}
|
|
624
|
+
return { success: true, updatedFiles };
|
|
625
|
+
} catch (err) {
|
|
626
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
627
|
+
console.error(`Git pull failed for ${localPath}: ${message}`);
|
|
628
|
+
return { success: false, updatedFiles: [] };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function delay(ms) {
|
|
632
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
633
|
+
}
|
|
634
|
+
async function fetchContextFromServer(repoId, localPath, apiUrl) {
|
|
635
|
+
try {
|
|
636
|
+
const credentials = await getValidCredentials();
|
|
637
|
+
if (!credentials) {
|
|
638
|
+
console.error(`Context fetch for ${repoId}: no valid credentials`);
|
|
639
|
+
return { success: false, updatedFiles: [] };
|
|
640
|
+
}
|
|
641
|
+
const headers = {
|
|
642
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
643
|
+
"Content-Type": "application/json"
|
|
644
|
+
};
|
|
645
|
+
let listRes;
|
|
646
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
647
|
+
try {
|
|
648
|
+
listRes = await fetch(`${apiUrl}/v1/repos/${repoId}/context`, { headers });
|
|
649
|
+
if (listRes.status === 401 || listRes.status === 403) {
|
|
650
|
+
console.error(`Context list auth error (${listRes.status}) for repo ${repoId}`);
|
|
651
|
+
return { success: false, updatedFiles: [] };
|
|
652
|
+
}
|
|
653
|
+
if (listRes.ok)
|
|
654
|
+
break;
|
|
655
|
+
console.error(`Context list failed (${listRes.status}) for repo ${repoId}, attempt ${attempt}/3`);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
658
|
+
console.error(`Context list network error for repo ${repoId}, attempt ${attempt}/3: ${msg}`);
|
|
659
|
+
}
|
|
660
|
+
if (attempt < 3)
|
|
661
|
+
await delay(2e3);
|
|
662
|
+
}
|
|
663
|
+
if (!listRes || !listRes.ok) {
|
|
664
|
+
console.error(`Context list failed after 3 attempts for repo ${repoId}`);
|
|
665
|
+
return { success: false, updatedFiles: [] };
|
|
666
|
+
}
|
|
667
|
+
const listData = await listRes.json();
|
|
668
|
+
const files = listData.data?.files ?? [];
|
|
669
|
+
console.log(`Context fetch for ${repoId}: ${files.length} file(s) available`);
|
|
670
|
+
if (files.length === 0) {
|
|
671
|
+
return { success: true, updatedFiles: [] };
|
|
672
|
+
}
|
|
673
|
+
const contextDir = join7(localPath, "repowise-context");
|
|
674
|
+
await mkdir4(contextDir, { recursive: true });
|
|
675
|
+
const updatedFiles = [];
|
|
676
|
+
for (const file of files) {
|
|
677
|
+
if (file.fileName.includes(".."))
|
|
678
|
+
continue;
|
|
679
|
+
const urlRes = await fetch(`${apiUrl}/v1/repos/${repoId}/context/files/${file.fileName}`, {
|
|
680
|
+
headers
|
|
681
|
+
});
|
|
682
|
+
if (!urlRes.ok) {
|
|
683
|
+
console.error(`Context fetch for ${repoId}: failed to get URL for ${file.fileName} (${urlRes.status})`);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const urlData = await urlRes.json();
|
|
687
|
+
const presignedUrl = urlData.data?.url;
|
|
688
|
+
if (!presignedUrl) {
|
|
689
|
+
console.error(`Context fetch for ${repoId}: no presigned URL returned for ${file.fileName}`);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const contentRes = await fetch(presignedUrl);
|
|
693
|
+
if (!contentRes.ok) {
|
|
694
|
+
console.error(`Context fetch for ${repoId}: download failed for ${file.fileName} (${contentRes.status})`);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const content = await contentRes.text();
|
|
698
|
+
const filePath = join7(contextDir, file.fileName);
|
|
699
|
+
await mkdir4(dirname2(filePath), { recursive: true });
|
|
700
|
+
await writeFile4(filePath, content, "utf-8");
|
|
701
|
+
updatedFiles.push(file.fileName);
|
|
702
|
+
}
|
|
703
|
+
console.log(`Context fetch for ${repoId}: downloaded ${updatedFiles.length}/${files.length} file(s)`);
|
|
704
|
+
return { success: true, updatedFiles };
|
|
705
|
+
} catch (err) {
|
|
706
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
707
|
+
console.error(`Server context fetch failed for ${repoId}: ${message}`);
|
|
708
|
+
return { success: false, updatedFiles: [] };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ../listener/dist/process-manager.js
|
|
713
|
+
import { spawn } from "child_process";
|
|
714
|
+
import { openSync, closeSync } from "fs";
|
|
715
|
+
import { readFile as readFile4, writeFile as writeFile5, mkdir as mkdir5, unlink as unlink3 } from "fs/promises";
|
|
716
|
+
import { homedir as homedir2 } from "os";
|
|
717
|
+
import { join as join8 } from "path";
|
|
718
|
+
import { createRequire } from "module";
|
|
719
|
+
import { fileURLToPath } from "url";
|
|
720
|
+
function repowiseDir() {
|
|
721
|
+
return getConfigDir();
|
|
722
|
+
}
|
|
723
|
+
function pidPath() {
|
|
724
|
+
return join8(repowiseDir(), "listener.pid");
|
|
725
|
+
}
|
|
726
|
+
function logDirPath() {
|
|
727
|
+
return join8(repowiseDir(), "logs");
|
|
728
|
+
}
|
|
729
|
+
function resolveListenerCommand() {
|
|
730
|
+
try {
|
|
731
|
+
const require2 = createRequire(import.meta.url);
|
|
732
|
+
const mainPath = require2.resolve("@repowise/listener/main");
|
|
733
|
+
return { script: mainPath, args: [] };
|
|
734
|
+
} catch {
|
|
735
|
+
const bundlePath = fileURLToPath(import.meta.url);
|
|
736
|
+
return { script: bundlePath, args: ["__listener"] };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function readPid() {
|
|
740
|
+
try {
|
|
741
|
+
const content = await readFile4(pidPath(), "utf-8");
|
|
742
|
+
const pid = parseInt(content.trim(), 10);
|
|
743
|
+
return Number.isNaN(pid) ? null : pid;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (err.code === "ENOENT")
|
|
746
|
+
return null;
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function isAlive(pid) {
|
|
751
|
+
try {
|
|
752
|
+
process.kill(pid, 0);
|
|
753
|
+
return true;
|
|
754
|
+
} catch {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function startBackground() {
|
|
759
|
+
if (await isRunning()) {
|
|
760
|
+
const pid2 = await readPid();
|
|
761
|
+
if (pid2)
|
|
762
|
+
return pid2;
|
|
763
|
+
}
|
|
764
|
+
const logDir2 = logDirPath();
|
|
765
|
+
await mkdir5(logDir2, { recursive: true });
|
|
766
|
+
const cmd = resolveListenerCommand();
|
|
767
|
+
const stdoutFd = openSync(join8(logDir2, "listener-stdout.log"), "a");
|
|
768
|
+
const stderrFd = openSync(join8(logDir2, "listener-stderr.log"), "a");
|
|
769
|
+
const child = spawn(process.execPath, [cmd.script, ...cmd.args], {
|
|
770
|
+
detached: true,
|
|
771
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
772
|
+
cwd: homedir2(),
|
|
773
|
+
env: { ...process.env }
|
|
774
|
+
});
|
|
775
|
+
child.unref();
|
|
776
|
+
closeSync(stdoutFd);
|
|
777
|
+
closeSync(stderrFd);
|
|
778
|
+
const pid = child.pid;
|
|
779
|
+
if (!pid)
|
|
780
|
+
throw new Error("Failed to spawn listener process");
|
|
781
|
+
await writeFile5(pidPath(), String(pid));
|
|
782
|
+
return pid;
|
|
783
|
+
}
|
|
784
|
+
async function stopProcess() {
|
|
785
|
+
const pid = await readPid();
|
|
786
|
+
if (pid === null)
|
|
787
|
+
return;
|
|
788
|
+
if (!isAlive(pid)) {
|
|
789
|
+
await removePidFile();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
process.kill(pid, "SIGTERM");
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
const deadline = Date.now() + 5e3;
|
|
797
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
798
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
799
|
+
}
|
|
800
|
+
if (isAlive(pid)) {
|
|
801
|
+
try {
|
|
802
|
+
process.kill(pid, "SIGKILL");
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
await removePidFile();
|
|
807
|
+
}
|
|
808
|
+
async function isRunning() {
|
|
809
|
+
const pid = await readPid();
|
|
810
|
+
if (pid === null)
|
|
811
|
+
return false;
|
|
812
|
+
return isAlive(pid);
|
|
813
|
+
}
|
|
814
|
+
async function getStatus() {
|
|
815
|
+
const pid = await readPid();
|
|
816
|
+
if (pid === null)
|
|
817
|
+
return { running: false, pid: null };
|
|
818
|
+
const alive = isAlive(pid);
|
|
819
|
+
return { running: alive, pid: alive ? pid : null };
|
|
820
|
+
}
|
|
821
|
+
async function removePidFile() {
|
|
822
|
+
try {
|
|
823
|
+
await unlink3(pidPath());
|
|
824
|
+
} catch {
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ../listener/dist/lib/auto-updater.js
|
|
829
|
+
import { execFile as execFile2 } from "child_process";
|
|
830
|
+
import { access as access2, constants } from "fs/promises";
|
|
831
|
+
import { join as join9 } from "path";
|
|
832
|
+
import { promisify as promisify2 } from "util";
|
|
833
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
834
|
+
function getPackageName() {
|
|
835
|
+
return true ? "repowise" : "repowise";
|
|
836
|
+
}
|
|
837
|
+
function getNpmDistTag() {
|
|
838
|
+
return true ? "latest" : "latest";
|
|
839
|
+
}
|
|
840
|
+
async function checkForUpdate(state) {
|
|
841
|
+
if (process.env["CI"] === "true")
|
|
842
|
+
return false;
|
|
843
|
+
const packageName = getPackageName();
|
|
844
|
+
const distTag = getNpmDistTag();
|
|
845
|
+
try {
|
|
846
|
+
const { stdout: prefix } = await execFileAsync2("npm", ["prefix", "-g"], { timeout: 1e4 });
|
|
847
|
+
const npmDir = join9(prefix.trim(), "lib", "node_modules");
|
|
848
|
+
const checkDir = process.platform === "win32" ? prefix.trim() : npmDir;
|
|
849
|
+
await access2(checkDir, constants.W_OK);
|
|
850
|
+
} catch {
|
|
851
|
+
console.log("[auto-update] npm global prefix not writable \u2014 skipping update check");
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
let distTags;
|
|
855
|
+
try {
|
|
856
|
+
const controller = new AbortController();
|
|
857
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
858
|
+
const res = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, {
|
|
859
|
+
signal: controller.signal
|
|
860
|
+
});
|
|
861
|
+
clearTimeout(timeout);
|
|
862
|
+
if (!res.ok) {
|
|
863
|
+
console.warn(`[auto-update] npm registry returned ${res.status}`);
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
distTags = await res.json();
|
|
867
|
+
} catch (err) {
|
|
868
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
869
|
+
console.warn(`[auto-update] Failed to fetch dist-tags: ${msg}`);
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const latestVersion = distTags[distTag];
|
|
873
|
+
if (!latestVersion) {
|
|
874
|
+
console.warn(`[auto-update] No version found for dist-tag "${distTag}"`);
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
let currentVersion;
|
|
878
|
+
try {
|
|
879
|
+
const { stdout } = await execFileAsync2(packageName, ["--version"], { timeout: 1e4 });
|
|
880
|
+
currentVersion = stdout.trim();
|
|
881
|
+
} catch {
|
|
882
|
+
console.warn("[auto-update] Could not determine current version");
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
if (!isNewer(latestVersion, currentVersion)) {
|
|
886
|
+
console.log(`[auto-update] Current version ${currentVersion} is up to date`);
|
|
887
|
+
state.lastUpdateCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
console.log(`[auto-update] Updating ${packageName} from ${currentVersion} to ${latestVersion}...`);
|
|
891
|
+
try {
|
|
892
|
+
await execFileAsync2("npm", ["install", "-g", `${packageName}@${latestVersion}`], {
|
|
893
|
+
timeout: 6e4
|
|
894
|
+
});
|
|
895
|
+
console.log(`[auto-update] Successfully updated to ${latestVersion}`);
|
|
896
|
+
state.lastUpdateCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
897
|
+
return true;
|
|
898
|
+
} catch (err) {
|
|
899
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
900
|
+
console.error(`[auto-update] Installation failed: ${msg}`);
|
|
901
|
+
state.lastUpdateCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function isNewer(a, b) {
|
|
906
|
+
const parseVer = (v) => {
|
|
907
|
+
const [main, pre] = v.split("-", 2);
|
|
908
|
+
const parts = main.split(".").map(Number);
|
|
909
|
+
return { parts, pre: pre ?? "" };
|
|
910
|
+
};
|
|
911
|
+
const va = parseVer(a);
|
|
912
|
+
const vb = parseVer(b);
|
|
913
|
+
for (let i = 0; i < Math.max(va.parts.length, vb.parts.length); i++) {
|
|
914
|
+
const na = va.parts[i] ?? 0;
|
|
915
|
+
const nb = vb.parts[i] ?? 0;
|
|
916
|
+
if (na > nb)
|
|
917
|
+
return true;
|
|
918
|
+
if (na < nb)
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
if (vb.pre && !va.pre)
|
|
922
|
+
return true;
|
|
923
|
+
if (va.pre && vb.pre)
|
|
924
|
+
return va.pre > vb.pre;
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ../listener/dist/main.js
|
|
929
|
+
var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
|
|
930
|
+
var STALE_CHECK_COOLDOWN_MS = 60 * 60 * 1e3;
|
|
931
|
+
var AUTO_UPDATE_EVERY_N_CYCLES = 4320;
|
|
932
|
+
var CRASH_LOOP_WINDOW_MS = 3e4;
|
|
933
|
+
var CRASH_LOOP_THRESHOLD = 3;
|
|
934
|
+
var running = false;
|
|
935
|
+
var sleepResolve = null;
|
|
936
|
+
var releaseLock = null;
|
|
937
|
+
function stop() {
|
|
938
|
+
running = false;
|
|
939
|
+
if (sleepResolve) {
|
|
940
|
+
sleepResolve();
|
|
941
|
+
sleepResolve = null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function sleep(ms) {
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
sleepResolve = resolve;
|
|
947
|
+
setTimeout(() => {
|
|
948
|
+
sleepResolve = null;
|
|
949
|
+
resolve();
|
|
950
|
+
}, ms);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function decodeEmailFromIdToken(idToken) {
|
|
954
|
+
try {
|
|
955
|
+
const payload = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString());
|
|
956
|
+
return typeof payload.email === "string" ? payload.email : null;
|
|
957
|
+
} catch {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function processNotifications(notifications, state, repoLocalPaths, apiUrl) {
|
|
962
|
+
let updateCount = 0;
|
|
963
|
+
for (const notif of notifications) {
|
|
964
|
+
const repoState = state.repos[notif.repoId];
|
|
965
|
+
if (repoState && notif.createdAt <= repoState.lastSyncTimestamp) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (notif.type === "sync.completed") {
|
|
969
|
+
if (!notif.commitSha) {
|
|
970
|
+
console.warn(`sync.completed for ${notif.repoId} has no commitSha`);
|
|
971
|
+
}
|
|
972
|
+
const localPath = repoLocalPaths.get(notif.repoId);
|
|
973
|
+
if (localPath) {
|
|
974
|
+
let result;
|
|
975
|
+
if (apiUrl) {
|
|
976
|
+
result = await fetchContextFromServer(notif.repoId, localPath, apiUrl);
|
|
977
|
+
} else {
|
|
978
|
+
result = await fetchContextUpdates(localPath);
|
|
979
|
+
}
|
|
980
|
+
if (result.success) {
|
|
981
|
+
updateCount++;
|
|
982
|
+
notifyContextUpdated(notif.repoId, result.updatedFiles.length);
|
|
983
|
+
state.repos[notif.repoId] = {
|
|
984
|
+
...state.repos[notif.repoId],
|
|
985
|
+
lastSyncTimestamp: notif.createdAt,
|
|
986
|
+
lastSyncCommitSha: notif.commitSha
|
|
987
|
+
};
|
|
988
|
+
} else {
|
|
989
|
+
console.error(`Download failed for repo ${notif.repoId} \u2014 will retry on next poll`);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
state.repos[notif.repoId] = {
|
|
993
|
+
...state.repos[notif.repoId],
|
|
994
|
+
lastSyncTimestamp: notif.createdAt,
|
|
995
|
+
lastSyncCommitSha: notif.commitSha
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
state.repos[notif.repoId] = {
|
|
1000
|
+
...state.repos[notif.repoId],
|
|
1001
|
+
lastSyncTimestamp: notif.createdAt,
|
|
1002
|
+
lastSyncCommitSha: notif.commitSha
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return updateCount;
|
|
1007
|
+
}
|
|
1008
|
+
async function handleCatchUp(offlineState, pollClient, repoIds, state, repoLocalPaths, apiUrl) {
|
|
1009
|
+
if (!offlineState.offlineSince)
|
|
1010
|
+
return;
|
|
1011
|
+
const offlineDuration = Date.now() - new Date(offlineState.offlineSince).getTime();
|
|
1012
|
+
if (offlineDuration >= TWENTY_FOUR_HOURS_MS) {
|
|
1013
|
+
let syncCount = 0;
|
|
1014
|
+
for (const [repoId, localPath] of repoLocalPaths) {
|
|
1015
|
+
let result;
|
|
1016
|
+
if (apiUrl) {
|
|
1017
|
+
result = await fetchContextFromServer(repoId, localPath, apiUrl);
|
|
1018
|
+
} else {
|
|
1019
|
+
result = await fetchContextUpdates(localPath);
|
|
1020
|
+
}
|
|
1021
|
+
if (result.success) {
|
|
1022
|
+
syncCount++;
|
|
1023
|
+
state.repos[repoId] = {
|
|
1024
|
+
...state.repos[repoId],
|
|
1025
|
+
lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1026
|
+
lastSyncCommitSha: null
|
|
1027
|
+
// unknown from catch-up, but timestamp prevents re-download
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
await saveState(state);
|
|
1032
|
+
notifyBackOnline(syncCount);
|
|
1033
|
+
} else {
|
|
1034
|
+
const sinceTimestamp = offlineState.offlineSince;
|
|
1035
|
+
const response = await pollClient.poll(repoIds, sinceTimestamp);
|
|
1036
|
+
const updateCount = await processNotifications(response.notifications, state, repoLocalPaths, apiUrl);
|
|
1037
|
+
await saveState(state);
|
|
1038
|
+
notifyBackOnline(updateCount);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async function checkStaleContext(repos, state, groups) {
|
|
1042
|
+
const now = Date.now();
|
|
1043
|
+
let dirty = false;
|
|
1044
|
+
for (const repo of repos) {
|
|
1045
|
+
const repoState = state.repos[repo.repoId];
|
|
1046
|
+
if (!repoState)
|
|
1047
|
+
continue;
|
|
1048
|
+
if (repoState.lastContextCheckAt) {
|
|
1049
|
+
const lastCheck = new Date(repoState.lastContextCheckAt).getTime();
|
|
1050
|
+
if (now - lastCheck < STALE_CHECK_COOLDOWN_MS)
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const apiUrl = repo.apiUrl;
|
|
1054
|
+
const group = groups.find((g) => g.apiUrl === apiUrl);
|
|
1055
|
+
if (group?.offline.isOffline)
|
|
1056
|
+
continue;
|
|
1057
|
+
const { statSync: statSync2, readdirSync: readdirSync2 } = await import("fs");
|
|
1058
|
+
const contextPath = join10(repo.localPath, "repowise-context");
|
|
1059
|
+
let isMissingOrEmpty = false;
|
|
1060
|
+
try {
|
|
1061
|
+
const s = statSync2(contextPath);
|
|
1062
|
+
if (!s.isDirectory()) {
|
|
1063
|
+
isMissingOrEmpty = true;
|
|
1064
|
+
} else {
|
|
1065
|
+
const entries = readdirSync2(contextPath);
|
|
1066
|
+
isMissingOrEmpty = entries.length === 0;
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
isMissingOrEmpty = true;
|
|
1070
|
+
}
|
|
1071
|
+
state.repos[repo.repoId] = {
|
|
1072
|
+
...state.repos[repo.repoId],
|
|
1073
|
+
lastContextCheckAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1074
|
+
};
|
|
1075
|
+
dirty = true;
|
|
1076
|
+
if (isMissingOrEmpty && apiUrl) {
|
|
1077
|
+
console.log(`[self-heal] Context missing/empty for ${repo.repoId}, re-downloading...`);
|
|
1078
|
+
const result = await fetchContextFromServer(repo.repoId, repo.localPath, apiUrl);
|
|
1079
|
+
if (result.success && result.updatedFiles.length > 0) {
|
|
1080
|
+
console.log(`[self-heal] Re-downloaded ${result.updatedFiles.length} files for ${repo.repoId}`);
|
|
1081
|
+
notifyContextUpdated(repo.repoId, result.updatedFiles.length);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return dirty;
|
|
1086
|
+
}
|
|
1087
|
+
async function startListener() {
|
|
1088
|
+
running = true;
|
|
1089
|
+
const configDir = getConfigDir();
|
|
1090
|
+
await mkdir6(configDir, { recursive: true });
|
|
1091
|
+
const lockPath = join10(configDir, "listener.lock");
|
|
1092
|
+
await writeFile6(lockPath, "", { flag: "a" });
|
|
1093
|
+
let lockIsHeld = false;
|
|
1094
|
+
try {
|
|
1095
|
+
lockIsHeld = await lockfile2.check(lockPath, { stale: 3e4, realpath: false });
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
if (!lockIsHeld) {
|
|
1099
|
+
const existingPid = await readPid();
|
|
1100
|
+
if (existingPid && existingPid !== process.pid && isAlive(existingPid)) {
|
|
1101
|
+
try {
|
|
1102
|
+
process.kill(existingPid, "SIGTERM");
|
|
1103
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
releaseLock = await lockfile2.lock(lockPath, { stale: 3e4, realpath: false });
|
|
1110
|
+
} catch {
|
|
1111
|
+
console.error(`Listener already running. Stop it first with \`${true ? "repowise" : "repowise"} stop\`.`);
|
|
1112
|
+
process.exitCode = 1;
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const releaseLockAndExit = async () => {
|
|
1116
|
+
if (releaseLock) {
|
|
1117
|
+
try {
|
|
1118
|
+
await releaseLock();
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
releaseLock = null;
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
let config2;
|
|
1125
|
+
try {
|
|
1126
|
+
config2 = await getListenerConfig();
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.error("Failed to load config:", err);
|
|
1129
|
+
await releaseLockAndExit();
|
|
1130
|
+
process.exitCode = 1;
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (config2.repos.length === 0 && !config2.autoDiscoverRepos) {
|
|
1134
|
+
console.error(`No repos configured. Add repos to ${join10(configDir, "config.json")}`);
|
|
1135
|
+
await releaseLockAndExit();
|
|
1136
|
+
process.exitCode = 1;
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const credentials = await getValidCredentials();
|
|
1140
|
+
if (!credentials) {
|
|
1141
|
+
console.error("Not logged in. Run `repowise login` first.");
|
|
1142
|
+
await releaseLockAndExit();
|
|
1143
|
+
process.exitCode = 1;
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
let state;
|
|
1147
|
+
try {
|
|
1148
|
+
state = await loadState();
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.error("Failed to load state:", err);
|
|
1151
|
+
await releaseLockAndExit();
|
|
1152
|
+
process.exitCode = 1;
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1156
|
+
const nowMs = Date.now();
|
|
1157
|
+
if (state.lastBootAt) {
|
|
1158
|
+
const lastBootMs = new Date(state.lastBootAt).getTime();
|
|
1159
|
+
if (nowMs - lastBootMs < CRASH_LOOP_WINDOW_MS) {
|
|
1160
|
+
state.crashCount = (state.crashCount ?? 0) + 1;
|
|
1161
|
+
} else {
|
|
1162
|
+
state.crashCount = 0;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
state.lastBootAt = nowIso;
|
|
1166
|
+
const crashLoopDetected = (state.crashCount ?? 0) >= CRASH_LOOP_THRESHOLD;
|
|
1167
|
+
if (crashLoopDetected) {
|
|
1168
|
+
console.warn(`[self-heal] Crash-loop detected (${state.crashCount} rapid restarts). Auto-update disabled until stable.`);
|
|
1169
|
+
}
|
|
1170
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
1171
|
+
for (const repo of config2.repos) {
|
|
1172
|
+
const apiUrl = repo.apiUrl ?? config2.defaultApiUrl;
|
|
1173
|
+
let group = groupMap.get(apiUrl);
|
|
1174
|
+
if (!group) {
|
|
1175
|
+
group = {
|
|
1176
|
+
apiUrl,
|
|
1177
|
+
pollClient: new PollClient(apiUrl),
|
|
1178
|
+
backoff: new BackoffCalculator(),
|
|
1179
|
+
repoIds: [],
|
|
1180
|
+
repoLocalPaths: /* @__PURE__ */ new Map(),
|
|
1181
|
+
offline: { isOffline: false, offlineSince: null, attemptCount: 0, nextRetryAt: 0 }
|
|
1182
|
+
};
|
|
1183
|
+
groupMap.set(apiUrl, group);
|
|
1184
|
+
}
|
|
1185
|
+
group.repoIds.push(repo.repoId);
|
|
1186
|
+
group.repoLocalPaths.set(repo.repoId, repo.localPath);
|
|
1187
|
+
}
|
|
1188
|
+
const groups = Array.from(groupMap.values());
|
|
1189
|
+
const allRepoIds = config2.repos.map((r) => r.repoId);
|
|
1190
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1191
|
+
for (const repoId of allRepoIds) {
|
|
1192
|
+
if (!state.repos[repoId]) {
|
|
1193
|
+
state.repos[repoId] = { lastSyncTimestamp: now, lastSyncCommitSha: null };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
await saveState(state);
|
|
1197
|
+
let pollIntervalMs = 5e3;
|
|
1198
|
+
let pollCycleCount = 0;
|
|
1199
|
+
const RECONCILE_EVERY_N_CYCLES = 60;
|
|
1200
|
+
const origLog = console.log.bind(console);
|
|
1201
|
+
const origError = console.error.bind(console);
|
|
1202
|
+
const origWarn = console.warn.bind(console);
|
|
1203
|
+
const ts = () => {
|
|
1204
|
+
const d = /* @__PURE__ */ new Date();
|
|
1205
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1206
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1207
|
+
};
|
|
1208
|
+
console.log = (...args) => origLog(`[${ts()}]`, ...args);
|
|
1209
|
+
console.error = (...args) => origError(`[${ts()}]`, ...args);
|
|
1210
|
+
console.warn = (...args) => origWarn(`[${ts()}]`, ...args);
|
|
1211
|
+
console.log(`RepoWise Listener started \u2014 watching ${allRepoIds.length} repo(s)`);
|
|
1212
|
+
const shutdown = async () => {
|
|
1213
|
+
console.log("Shutting down...");
|
|
1214
|
+
stop();
|
|
1215
|
+
await saveState(state);
|
|
1216
|
+
if (releaseLock) {
|
|
1217
|
+
try {
|
|
1218
|
+
await releaseLock();
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
releaseLock = null;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
process.on("SIGTERM", () => {
|
|
1225
|
+
shutdown().catch((err) => {
|
|
1226
|
+
console.error("Shutdown error:", err);
|
|
1227
|
+
}).finally(() => process.exit(0));
|
|
1228
|
+
});
|
|
1229
|
+
process.on("SIGINT", () => {
|
|
1230
|
+
shutdown().catch((err) => {
|
|
1231
|
+
console.error("Shutdown error:", err);
|
|
1232
|
+
}).finally(() => process.exit(0));
|
|
1233
|
+
});
|
|
1234
|
+
while (running) {
|
|
1235
|
+
let anyAuthError = null;
|
|
1236
|
+
let authErrorGroup = null;
|
|
1237
|
+
let minPollInterval = pollIntervalMs;
|
|
1238
|
+
let connectionLostNotified = false;
|
|
1239
|
+
pollCycleCount++;
|
|
1240
|
+
const shouldReconcile = pollCycleCount % RECONCILE_EVERY_N_CYCLES === 0;
|
|
1241
|
+
if (!crashLoopDetected && !config2.noAutoUpdate && pollCycleCount % AUTO_UPDATE_EVERY_N_CYCLES === 0) {
|
|
1242
|
+
try {
|
|
1243
|
+
const updated = await checkForUpdate(state);
|
|
1244
|
+
if (updated) {
|
|
1245
|
+
await saveState(state);
|
|
1246
|
+
if (releaseLock) {
|
|
1247
|
+
try {
|
|
1248
|
+
await releaseLock();
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
releaseLock = null;
|
|
1252
|
+
}
|
|
1253
|
+
console.log("Auto-update complete \u2014 exiting for restart");
|
|
1254
|
+
process.exit(0);
|
|
1255
|
+
}
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1258
|
+
console.error(`Auto-update check failed: ${msg}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
for (const group of groups) {
|
|
1262
|
+
if (!running)
|
|
1263
|
+
break;
|
|
1264
|
+
if (group.offline.isOffline && Date.now() < group.offline.nextRetryAt) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
const sinceTimestamp = group.repoIds.reduce((earliest, id) => {
|
|
1269
|
+
const ts2 = state.repos[id]?.lastSyncTimestamp ?? now;
|
|
1270
|
+
return ts2 < earliest ? ts2 : earliest;
|
|
1271
|
+
}, now);
|
|
1272
|
+
const response = await group.pollClient.poll(group.repoIds, sinceTimestamp, {
|
|
1273
|
+
includeActiveRepos: shouldReconcile
|
|
1274
|
+
});
|
|
1275
|
+
if (response.activeRepos && response.activeRepos.length > 0) {
|
|
1276
|
+
try {
|
|
1277
|
+
const result = reconcileRepos(config2.repos, response.activeRepos, state, group.apiUrl, {
|
|
1278
|
+
autoDiscover: config2.autoDiscoverRepos
|
|
1279
|
+
});
|
|
1280
|
+
if (result.updated) {
|
|
1281
|
+
config2.repos = result.repos;
|
|
1282
|
+
await saveListenerConfig(config2);
|
|
1283
|
+
await saveState(state);
|
|
1284
|
+
group.repoIds = result.repos.filter((r) => (r.apiUrl ?? config2.defaultApiUrl) === group.apiUrl).map((r) => r.repoId);
|
|
1285
|
+
group.repoLocalPaths.clear();
|
|
1286
|
+
for (const r of result.repos) {
|
|
1287
|
+
if ((r.apiUrl ?? config2.defaultApiUrl) === group.apiUrl) {
|
|
1288
|
+
group.repoLocalPaths.set(r.repoId, r.localPath);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
for (const change of result.changes) {
|
|
1292
|
+
console.log(`[reconcile] ${change}`);
|
|
1293
|
+
}
|
|
1294
|
+
if (result.addedRepos.length > 0) {
|
|
1295
|
+
for (const addedRepo of result.addedRepos) {
|
|
1296
|
+
const apiUrl = addedRepo.apiUrl ?? config2.defaultApiUrl;
|
|
1297
|
+
try {
|
|
1298
|
+
const fetchResult = await fetchContextFromServer(addedRepo.repoId, addedRepo.localPath, apiUrl);
|
|
1299
|
+
if (fetchResult.success) {
|
|
1300
|
+
console.log(`[self-heal] Downloaded ${fetchResult.updatedFiles.length} context files for ${addedRepo.repoId}`);
|
|
1301
|
+
}
|
|
1302
|
+
} catch (fetchErr) {
|
|
1303
|
+
console.warn(`[self-heal] Context download failed for ${addedRepo.repoId}:`, fetchErr instanceof Error ? fetchErr.message : String(fetchErr));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
} catch (reconcileErr) {
|
|
1309
|
+
console.warn("Repo reconciliation failed \u2014 will retry next cycle", reconcileErr instanceof Error ? reconcileErr.message : String(reconcileErr));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (group.offline.isOffline) {
|
|
1313
|
+
await handleCatchUp(group.offline, group.pollClient, group.repoIds, state, group.repoLocalPaths, group.apiUrl);
|
|
1314
|
+
group.offline.isOffline = false;
|
|
1315
|
+
group.offline.offlineSince = null;
|
|
1316
|
+
group.offline.attemptCount = 0;
|
|
1317
|
+
group.offline.nextRetryAt = 0;
|
|
1318
|
+
group.backoff.reset();
|
|
1319
|
+
} else if (response.notifications.length > 0) {
|
|
1320
|
+
await processNotifications(response.notifications, state, group.repoLocalPaths, group.apiUrl);
|
|
1321
|
+
await saveState(state);
|
|
1322
|
+
}
|
|
1323
|
+
minPollInterval = Math.min(minPollInterval, response.pollIntervalMs);
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
if (!running)
|
|
1326
|
+
break;
|
|
1327
|
+
if (err instanceof AuthError) {
|
|
1328
|
+
anyAuthError = err;
|
|
1329
|
+
authErrorGroup = group;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
if (!group.offline.isOffline) {
|
|
1333
|
+
group.offline.isOffline = true;
|
|
1334
|
+
group.offline.offlineSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
1335
|
+
group.offline.attemptCount = 0;
|
|
1336
|
+
if (!connectionLostNotified) {
|
|
1337
|
+
notifyConnectionLost();
|
|
1338
|
+
connectionLostNotified = true;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
group.offline.attemptCount++;
|
|
1342
|
+
const delay2 = group.backoff.nextDelay();
|
|
1343
|
+
group.offline.nextRetryAt = Date.now() + delay2;
|
|
1344
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1345
|
+
console.error(`Poll failed for ${group.apiUrl} (attempt ${group.offline.attemptCount}): ${message}. Retrying in ${Math.round(delay2 / 1e3)}s`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (shouldReconcile) {
|
|
1349
|
+
try {
|
|
1350
|
+
const staleDirty = await checkStaleContext(config2.repos, state, groups);
|
|
1351
|
+
if (staleDirty) {
|
|
1352
|
+
await saveState(state);
|
|
1353
|
+
}
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
console.warn("[self-heal] Stale context check failed:", err instanceof Error ? err.message : String(err));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (anyAuthError) {
|
|
1359
|
+
let recovered = false;
|
|
1360
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1361
|
+
console.warn(`Auth error \u2014 retrying credentials (attempt ${attempt}/3)...`);
|
|
1362
|
+
await sleep(5e3);
|
|
1363
|
+
if (!running)
|
|
1364
|
+
break;
|
|
1365
|
+
const fresh = await getValidCredentials({ forceRefresh: true });
|
|
1366
|
+
if (fresh) {
|
|
1367
|
+
console.log("Credentials recovered \u2014 resuming listener");
|
|
1368
|
+
recovered = true;
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (recovered)
|
|
1373
|
+
continue;
|
|
1374
|
+
console.error("Session expired after 3 retries. Sending notification email and waiting for re-authentication...");
|
|
1375
|
+
const creds = await getStoredCredentials();
|
|
1376
|
+
const email = creds?.idToken ? decodeEmailFromIdToken(creds.idToken) : null;
|
|
1377
|
+
if (email && authErrorGroup) {
|
|
1378
|
+
try {
|
|
1379
|
+
await fetch(`${authErrorGroup.apiUrl}/v1/public/notify-auth-expired`, {
|
|
1380
|
+
method: "POST",
|
|
1381
|
+
headers: { "Content-Type": "application/json" },
|
|
1382
|
+
body: JSON.stringify({ email })
|
|
1383
|
+
});
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
while (running) {
|
|
1388
|
+
await sleep(5 * 60 * 1e3);
|
|
1389
|
+
if (!running)
|
|
1390
|
+
break;
|
|
1391
|
+
const fresh = await getValidCredentials({ forceRefresh: true });
|
|
1392
|
+
if (fresh) {
|
|
1393
|
+
console.log("Re-authenticated \u2014 resuming listener");
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
pollIntervalMs = minPollInterval;
|
|
1400
|
+
await sleep(pollIntervalMs);
|
|
1401
|
+
}
|
|
1402
|
+
if (releaseLock) {
|
|
1403
|
+
try {
|
|
1404
|
+
await releaseLock();
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
releaseLock = null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
var isDirectRun = process.argv[1]?.endsWith("main.js") || process.argv[1]?.endsWith("main.ts");
|
|
1411
|
+
if (isDirectRun) {
|
|
1412
|
+
startListener().catch((err) => {
|
|
1413
|
+
console.error("Listener fatal error:", err);
|
|
1414
|
+
process.exitCode = 1;
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/lib/env.ts
|
|
1419
|
+
import { homedir as homedir3 } from "os";
|
|
1420
|
+
import { join as join11 } from "path";
|
|
1421
|
+
var IS_STAGING = true ? false : false;
|
|
1422
|
+
var PRODUCTION = {
|
|
1423
|
+
apiUrl: "https://api.repowise.ai",
|
|
1424
|
+
cognitoDomain: "auth.repowise.ai",
|
|
1425
|
+
cognitoClientId: "50so1fkmjbqt1ufnsmbhsjbtst",
|
|
1426
|
+
cognitoRegion: "us-east-1",
|
|
1427
|
+
customDomain: true
|
|
1428
|
+
};
|
|
1429
|
+
var STAGING = {
|
|
1430
|
+
apiUrl: "https://staging-api.repowise.ai",
|
|
1431
|
+
cognitoDomain: "auth-staging.repowise.ai",
|
|
1432
|
+
cognitoClientId: "7h0l0dhjcb1v5erer0gaclv0q6",
|
|
1433
|
+
cognitoRegion: "us-east-1",
|
|
1434
|
+
customDomain: true
|
|
1435
|
+
};
|
|
1436
|
+
function getEnvConfig() {
|
|
1437
|
+
return IS_STAGING ? STAGING : PRODUCTION;
|
|
1438
|
+
}
|
|
1439
|
+
function getConfigDir2() {
|
|
1440
|
+
return join11(homedir3(), IS_STAGING ? ".repowise-staging" : ".repowise");
|
|
1441
|
+
}
|
|
1442
|
+
function getPackageName2() {
|
|
1443
|
+
return true ? "repowise" : "repowise";
|
|
1444
|
+
}
|
|
1445
|
+
function getNpmDistTag2() {
|
|
1446
|
+
return true ? "latest" : "latest";
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/lib/welcome.ts
|
|
1450
|
+
import chalk2 from "chalk";
|
|
1451
|
+
|
|
1452
|
+
// src/lib/config.ts
|
|
1453
|
+
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir7, rename as rename3, unlink as unlink4 } from "fs/promises";
|
|
1454
|
+
import { join as join12 } from "path";
|
|
1455
|
+
import lockfile3 from "proper-lockfile";
|
|
1456
|
+
async function getConfig() {
|
|
1457
|
+
try {
|
|
1458
|
+
const data = await readFile5(join12(getConfigDir2(), "config.json"), "utf-8");
|
|
1459
|
+
return JSON.parse(data);
|
|
1460
|
+
} catch {
|
|
1461
|
+
return {};
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function saveConfig(config2) {
|
|
1465
|
+
const dir = getConfigDir2();
|
|
1466
|
+
const path = join12(dir, "config.json");
|
|
1467
|
+
await mkdir7(dir, { recursive: true });
|
|
1468
|
+
const tmpPath = path + ".tmp";
|
|
1469
|
+
try {
|
|
1470
|
+
await writeFile7(tmpPath, JSON.stringify(config2, null, 2));
|
|
1471
|
+
await rename3(tmpPath, path);
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
try {
|
|
1474
|
+
await unlink4(tmpPath);
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
throw err;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async function mergeAndSaveConfig(updates) {
|
|
1481
|
+
const dir = getConfigDir2();
|
|
1482
|
+
const path = join12(dir, "config.json");
|
|
1483
|
+
await mkdir7(dir, { recursive: true });
|
|
1484
|
+
try {
|
|
1485
|
+
await writeFile7(path, "", { flag: "a" });
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
let release = null;
|
|
1489
|
+
try {
|
|
1490
|
+
release = await lockfile3.lock(path, { stale: 1e4, retries: 3, realpath: false });
|
|
1491
|
+
let raw = {};
|
|
1492
|
+
try {
|
|
1493
|
+
raw = JSON.parse(await readFile5(path, "utf-8"));
|
|
1494
|
+
} catch {
|
|
1495
|
+
}
|
|
1496
|
+
const merged = { ...raw, ...updates };
|
|
1497
|
+
if (updates.repos) {
|
|
1498
|
+
const existingRepos = Array.isArray(raw["repos"]) ? raw["repos"] : [];
|
|
1499
|
+
const updatedRepoIds = new Set(updates.repos.map((r) => r.repoId));
|
|
1500
|
+
merged.repos = [
|
|
1501
|
+
...existingRepos.filter((r) => !updatedRepoIds.has(r.repoId)),
|
|
1502
|
+
...updates.repos
|
|
1503
|
+
];
|
|
1504
|
+
}
|
|
1505
|
+
await saveConfig(merged);
|
|
1506
|
+
} finally {
|
|
1507
|
+
if (release) {
|
|
1508
|
+
try {
|
|
1509
|
+
await release();
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/lib/update-check.ts
|
|
1517
|
+
import chalk from "chalk";
|
|
1518
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1519
|
+
async function checkForCliUpdate(currentVersion) {
|
|
1520
|
+
try {
|
|
1521
|
+
if (process.env["CI"] === "true" || process.env["REPOWISE_NO_UPDATE_CHECK"] === "1") {
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
const config2 = await getConfig();
|
|
1525
|
+
if (config2.noUpdateCheck) return false;
|
|
1526
|
+
if (config2.lastUpdateCheck) {
|
|
1527
|
+
const lastCheck = new Date(config2.lastUpdateCheck).getTime();
|
|
1528
|
+
if (Date.now() - lastCheck < ONE_DAY_MS) return false;
|
|
1529
|
+
}
|
|
1530
|
+
const packageName = getPackageName2();
|
|
1531
|
+
const distTag = getNpmDistTag2();
|
|
1532
|
+
const controller = new AbortController();
|
|
1533
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1534
|
+
let distTags;
|
|
1535
|
+
try {
|
|
1536
|
+
const res = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, {
|
|
1537
|
+
signal: controller.signal
|
|
1538
|
+
});
|
|
1539
|
+
clearTimeout(timeout);
|
|
1540
|
+
if (!res.ok) return false;
|
|
1541
|
+
distTags = await res.json();
|
|
1542
|
+
} catch {
|
|
1543
|
+
clearTimeout(timeout);
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
const latestVersion = distTags[distTag];
|
|
1547
|
+
if (!latestVersion) return false;
|
|
1548
|
+
await mergeAndSaveConfig({ lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1549
|
+
if (!isNewer2(latestVersion, currentVersion)) return false;
|
|
1550
|
+
console.log("");
|
|
1551
|
+
console.log(
|
|
1552
|
+
chalk.yellow(
|
|
1553
|
+
` Update available: ${chalk.dim(currentVersion)} ${chalk.yellow("\u2192")} ${chalk.green(latestVersion)}`
|
|
1554
|
+
)
|
|
1555
|
+
);
|
|
1556
|
+
console.log(chalk.yellow(` Run: ${chalk.bold(`npm install -g ${packageName}`)}`));
|
|
1557
|
+
console.log("");
|
|
1558
|
+
return true;
|
|
1559
|
+
} catch {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function isNewer2(a, b) {
|
|
1564
|
+
const parseVer = (v) => {
|
|
1565
|
+
const [main, pre] = v.split("-", 2);
|
|
1566
|
+
const parts = main.split(".").map(Number);
|
|
1567
|
+
return { parts, pre: pre ?? "" };
|
|
1568
|
+
};
|
|
1569
|
+
const va = parseVer(a);
|
|
1570
|
+
const vb = parseVer(b);
|
|
1571
|
+
for (let i = 0; i < Math.max(va.parts.length, vb.parts.length); i++) {
|
|
1572
|
+
const na = va.parts[i] ?? 0;
|
|
1573
|
+
const nb = vb.parts[i] ?? 0;
|
|
1574
|
+
if (na > nb) return true;
|
|
1575
|
+
if (na < nb) return false;
|
|
1576
|
+
}
|
|
1577
|
+
if (vb.pre && !va.pre) return true;
|
|
1578
|
+
if (va.pre && vb.pre) return va.pre > vb.pre;
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/lib/welcome.ts
|
|
1583
|
+
var W = 41;
|
|
1584
|
+
function row(styled, visible) {
|
|
1585
|
+
return chalk2.cyan(" \u2502") + styled + " ".repeat(W - visible) + chalk2.cyan("\u2502");
|
|
1586
|
+
}
|
|
1587
|
+
async function showWelcome(currentVersion) {
|
|
1588
|
+
try {
|
|
1589
|
+
const config2 = await getConfig();
|
|
1590
|
+
if (config2.lastSeenVersion === currentVersion) {
|
|
1591
|
+
await checkForCliUpdate(currentVersion);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const isUpgrade = !!config2.lastSeenVersion;
|
|
1595
|
+
const tag = isUpgrade ? "updated" : "installed";
|
|
1596
|
+
const titleText = `RepoWise v${currentVersion}`;
|
|
1597
|
+
const titleStyled = " " + chalk2.bold(titleText) + chalk2.green(` \u2713 ${tag}`);
|
|
1598
|
+
const titleVisible = 3 + titleText.length + 3 + tag.length;
|
|
1599
|
+
const border = "\u2500".repeat(W);
|
|
1600
|
+
console.log("");
|
|
1601
|
+
console.log(chalk2.cyan(` \u256D${border}\u256E`));
|
|
1602
|
+
console.log(row("", 0));
|
|
1603
|
+
console.log(row(titleStyled, titleVisible));
|
|
1604
|
+
console.log(row("", 0));
|
|
1605
|
+
console.log(row(" " + chalk2.dim("Get started:"), 15));
|
|
1606
|
+
console.log(row(" $ " + chalk2.bold("repowise create"), 22));
|
|
1607
|
+
console.log(row("", 0));
|
|
1608
|
+
console.log(row(" " + chalk2.dim("Thank you for using RepoWise!"), 32));
|
|
1609
|
+
console.log(row(" " + chalk2.dim("https://repowise.ai"), 22));
|
|
1610
|
+
console.log(row("", 0));
|
|
1611
|
+
console.log(chalk2.cyan(` \u2570${border}\u256F`));
|
|
1612
|
+
console.log("");
|
|
1613
|
+
await mergeAndSaveConfig({ lastSeenVersion: currentVersion });
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// src/commands/create.ts
|
|
1619
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1620
|
+
import { dirname as dirname4, join as join17 } from "path";
|
|
1621
|
+
|
|
1622
|
+
// ../listener/dist/service-installer.js
|
|
1623
|
+
import { execFile as execFile3 } from "child_process";
|
|
1624
|
+
import { writeFile as writeFile8, mkdir as mkdir8, unlink as unlink5 } from "fs/promises";
|
|
1625
|
+
import { homedir as homedir4 } from "os";
|
|
1626
|
+
import { join as join13 } from "path";
|
|
1627
|
+
import { createRequire as createRequire2 } from "module";
|
|
1628
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1629
|
+
var IS_STAGING2 = true ? false : false;
|
|
1630
|
+
function resolveListenerCommand2() {
|
|
1631
|
+
try {
|
|
1632
|
+
const require2 = createRequire2(import.meta.url);
|
|
1633
|
+
const mainPath = require2.resolve("@repowise/listener/main");
|
|
1634
|
+
return { script: mainPath, args: [] };
|
|
1635
|
+
} catch {
|
|
1636
|
+
const bundlePath = fileURLToPath2(import.meta.url);
|
|
1637
|
+
return { script: bundlePath, args: ["__listener"] };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function exec(cmd, args) {
|
|
1641
|
+
return new Promise((resolve, reject) => {
|
|
1642
|
+
execFile3(cmd, args, (err, stdout) => {
|
|
1643
|
+
if (err) {
|
|
1644
|
+
reject(err);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
resolve(String(stdout ?? ""));
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
var PLIST_LABEL = IS_STAGING2 ? "com.repowise-staging.listener" : "com.repowise.listener";
|
|
1652
|
+
function plistPath() {
|
|
1653
|
+
return join13(homedir4(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
1654
|
+
}
|
|
1655
|
+
function logDir() {
|
|
1656
|
+
return join13(getConfigDir(), "logs");
|
|
1657
|
+
}
|
|
1658
|
+
function buildPlist() {
|
|
1659
|
+
const cmd = resolveListenerCommand2();
|
|
1660
|
+
const logs = logDir();
|
|
1661
|
+
const programArgs = [process.execPath, cmd.script, ...cmd.args].map((a) => ` <string>${a}</string>`).join("\n");
|
|
1662
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1663
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
1664
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1665
|
+
<plist version="1.0">
|
|
1666
|
+
<dict>
|
|
1667
|
+
<key>Label</key>
|
|
1668
|
+
<string>${PLIST_LABEL}</string>
|
|
1669
|
+
<key>ProgramArguments</key>
|
|
1670
|
+
<array>
|
|
1671
|
+
${programArgs}
|
|
1672
|
+
</array>
|
|
1673
|
+
<key>RunAtLoad</key>
|
|
1674
|
+
<true/>
|
|
1675
|
+
<key>KeepAlive</key>
|
|
1676
|
+
<true/>
|
|
1677
|
+
<key>StandardOutPath</key>
|
|
1678
|
+
<string>${join13(logs, "listener-stdout.log")}</string>
|
|
1679
|
+
<key>StandardErrorPath</key>
|
|
1680
|
+
<string>${join13(logs, "listener-stderr.log")}</string>
|
|
1681
|
+
<key>ProcessType</key>
|
|
1682
|
+
<string>Background</string>
|
|
1683
|
+
</dict>
|
|
1684
|
+
</plist>`;
|
|
1685
|
+
}
|
|
1686
|
+
async function darwinInstall() {
|
|
1687
|
+
await mkdir8(logDir(), { recursive: true });
|
|
1688
|
+
await mkdir8(join13(homedir4(), "Library", "LaunchAgents"), { recursive: true });
|
|
1689
|
+
try {
|
|
1690
|
+
await exec("launchctl", ["unload", plistPath()]);
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
await writeFile8(plistPath(), buildPlist());
|
|
1694
|
+
await exec("launchctl", ["load", plistPath()]);
|
|
1695
|
+
}
|
|
1696
|
+
async function darwinUninstall() {
|
|
1697
|
+
try {
|
|
1698
|
+
await exec("launchctl", ["unload", plistPath()]);
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
await unlink5(plistPath());
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function darwinIsInstalled() {
|
|
1707
|
+
try {
|
|
1708
|
+
const stdout = await exec("launchctl", ["list"]);
|
|
1709
|
+
return stdout.includes(PLIST_LABEL);
|
|
1710
|
+
} catch {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
var SYSTEMD_SERVICE = IS_STAGING2 ? "repowise-staging-listener" : "repowise-listener";
|
|
1715
|
+
function unitPath() {
|
|
1716
|
+
return join13(homedir4(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
|
|
1717
|
+
}
|
|
1718
|
+
function buildUnit() {
|
|
1719
|
+
const cmd = resolveListenerCommand2();
|
|
1720
|
+
const execStart = [process.execPath, cmd.script, ...cmd.args].join(" ");
|
|
1721
|
+
const logs = logDir();
|
|
1722
|
+
return `[Unit]
|
|
1723
|
+
Description=RepoWise Listener
|
|
1724
|
+
After=network-online.target
|
|
1725
|
+
Wants=network-online.target
|
|
1726
|
+
|
|
1727
|
+
[Service]
|
|
1728
|
+
Type=simple
|
|
1729
|
+
ExecStart=${execStart}
|
|
1730
|
+
Restart=always
|
|
1731
|
+
RestartSec=10
|
|
1732
|
+
StandardOutput=append:${join13(logs, "listener-stdout.log")}
|
|
1733
|
+
StandardError=append:${join13(logs, "listener-stderr.log")}
|
|
1734
|
+
|
|
1735
|
+
[Install]
|
|
1736
|
+
WantedBy=default.target`;
|
|
1737
|
+
}
|
|
1738
|
+
async function linuxInstall() {
|
|
1739
|
+
await mkdir8(logDir(), { recursive: true });
|
|
1740
|
+
await mkdir8(join13(homedir4(), ".config", "systemd", "user"), { recursive: true });
|
|
1741
|
+
await writeFile8(unitPath(), buildUnit());
|
|
1742
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
1743
|
+
await exec("systemctl", ["--user", "enable", SYSTEMD_SERVICE]);
|
|
1744
|
+
await exec("systemctl", ["--user", "start", SYSTEMD_SERVICE]);
|
|
1745
|
+
}
|
|
1746
|
+
async function linuxUninstall() {
|
|
1747
|
+
try {
|
|
1748
|
+
await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
|
|
1749
|
+
} catch {
|
|
1750
|
+
}
|
|
1751
|
+
try {
|
|
1752
|
+
await exec("systemctl", ["--user", "disable", SYSTEMD_SERVICE]);
|
|
1753
|
+
} catch {
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
await unlink5(unitPath());
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
try {
|
|
1760
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function linuxIsInstalled() {
|
|
1765
|
+
try {
|
|
1766
|
+
const stdout = await exec("systemctl", ["--user", "is-enabled", SYSTEMD_SERVICE]);
|
|
1767
|
+
return stdout.trim() === "enabled";
|
|
1768
|
+
} catch {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
var TASK_NAME = IS_STAGING2 ? "RepoWise Staging Listener" : "RepoWise Listener";
|
|
1773
|
+
async function win32Install() {
|
|
1774
|
+
await mkdir8(logDir(), { recursive: true });
|
|
1775
|
+
const cmd = resolveListenerCommand2();
|
|
1776
|
+
const taskCmd = [process.execPath, cmd.script, ...cmd.args].map((a) => `"${a}"`).join(" ");
|
|
1777
|
+
await exec("schtasks", [
|
|
1778
|
+
"/create",
|
|
1779
|
+
"/tn",
|
|
1780
|
+
TASK_NAME,
|
|
1781
|
+
"/tr",
|
|
1782
|
+
taskCmd,
|
|
1783
|
+
"/sc",
|
|
1784
|
+
"onlogon",
|
|
1785
|
+
"/ru",
|
|
1786
|
+
process.env.USERNAME ?? "",
|
|
1787
|
+
"/f"
|
|
1788
|
+
]);
|
|
1789
|
+
await exec("schtasks", ["/run", "/tn", TASK_NAME]);
|
|
1790
|
+
}
|
|
1791
|
+
async function win32Uninstall() {
|
|
1792
|
+
try {
|
|
1793
|
+
await exec("schtasks", ["/end", "/tn", TASK_NAME]);
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
await exec("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]);
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function win32IsInstalled() {
|
|
1802
|
+
try {
|
|
1803
|
+
await exec("schtasks", ["/query", "/tn", TASK_NAME]);
|
|
1804
|
+
return true;
|
|
1805
|
+
} catch {
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async function darwinStop() {
|
|
1810
|
+
try {
|
|
1811
|
+
await exec("launchctl", ["unload", plistPath()]);
|
|
1812
|
+
} catch {
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async function linuxStop() {
|
|
1816
|
+
await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
|
|
1817
|
+
}
|
|
1818
|
+
async function win32Stop() {
|
|
1819
|
+
await exec("schtasks", ["/end", "/tn", TASK_NAME]);
|
|
1820
|
+
}
|
|
1821
|
+
async function stopService() {
|
|
1822
|
+
switch (process.platform) {
|
|
1823
|
+
case "darwin":
|
|
1824
|
+
await darwinStop();
|
|
1825
|
+
break;
|
|
1826
|
+
case "linux":
|
|
1827
|
+
await linuxStop();
|
|
1828
|
+
break;
|
|
1829
|
+
case "win32":
|
|
1830
|
+
await win32Stop();
|
|
1831
|
+
break;
|
|
1832
|
+
default:
|
|
1833
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
async function install() {
|
|
1837
|
+
switch (process.platform) {
|
|
1838
|
+
case "darwin":
|
|
1839
|
+
await darwinInstall();
|
|
1840
|
+
break;
|
|
1841
|
+
case "linux":
|
|
1842
|
+
await linuxInstall();
|
|
1843
|
+
break;
|
|
1844
|
+
case "win32":
|
|
1845
|
+
await win32Install();
|
|
1846
|
+
break;
|
|
1847
|
+
default:
|
|
1848
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
async function uninstall() {
|
|
1852
|
+
switch (process.platform) {
|
|
1853
|
+
case "darwin":
|
|
1854
|
+
await darwinUninstall();
|
|
1855
|
+
break;
|
|
1856
|
+
case "linux":
|
|
1857
|
+
await linuxUninstall();
|
|
1858
|
+
break;
|
|
1859
|
+
case "win32":
|
|
1860
|
+
await win32Uninstall();
|
|
1861
|
+
break;
|
|
1862
|
+
default:
|
|
1863
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async function isInstalled() {
|
|
1867
|
+
switch (process.platform) {
|
|
1868
|
+
case "darwin":
|
|
1869
|
+
return darwinIsInstalled();
|
|
1870
|
+
case "linux":
|
|
1871
|
+
return linuxIsInstalled();
|
|
1872
|
+
case "win32":
|
|
1873
|
+
return win32IsInstalled();
|
|
1874
|
+
default:
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/commands/create.ts
|
|
1880
|
+
import chalk6 from "chalk";
|
|
1881
|
+
import ora from "ora";
|
|
1882
|
+
|
|
1883
|
+
// src/lib/auth.ts
|
|
1884
|
+
import { createHash, randomBytes } from "crypto";
|
|
1885
|
+
import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir9, chmod as chmod4, unlink as unlink6 } from "fs/promises";
|
|
1886
|
+
import http from "http";
|
|
1887
|
+
import { join as join14 } from "path";
|
|
1888
|
+
var CLI_CALLBACK_PORT = 19876;
|
|
1889
|
+
var CALLBACK_TIMEOUT_MS = 12e4;
|
|
1890
|
+
function getCognitoConfigForStorage() {
|
|
1891
|
+
const { domain, clientId, region, customDomain } = getCognitoConfig();
|
|
1892
|
+
return { domain, clientId, region, customDomain };
|
|
1893
|
+
}
|
|
1894
|
+
function getCognitoConfig() {
|
|
1895
|
+
const env = getEnvConfig();
|
|
1896
|
+
return {
|
|
1897
|
+
domain: process.env["REPOWISE_COGNITO_DOMAIN"] ?? env.cognitoDomain,
|
|
1898
|
+
clientId: process.env["REPOWISE_COGNITO_CLIENT_ID"] ?? env.cognitoClientId,
|
|
1899
|
+
region: process.env["REPOWISE_COGNITO_REGION"] ?? env.cognitoRegion,
|
|
1900
|
+
customDomain: env.customDomain
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function getCognitoBaseUrl() {
|
|
1904
|
+
const { domain, region, customDomain } = getCognitoConfig();
|
|
1905
|
+
return customDomain ? `https://${domain}` : `https://${domain}.auth.${region}.amazoncognito.com`;
|
|
1906
|
+
}
|
|
1907
|
+
function generateCodeVerifier() {
|
|
1908
|
+
return randomBytes(32).toString("base64url");
|
|
1909
|
+
}
|
|
1910
|
+
function generateCodeChallenge(verifier) {
|
|
1911
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
1912
|
+
}
|
|
1913
|
+
function generateState() {
|
|
1914
|
+
return randomBytes(32).toString("hex");
|
|
1915
|
+
}
|
|
1916
|
+
function getAuthorizeUrl(codeChallenge, state) {
|
|
1917
|
+
const { clientId } = getCognitoConfig();
|
|
1918
|
+
if (!clientId) {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
"Missing REPOWISE_COGNITO_CLIENT_ID environment variable. Configure it before running login."
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const params = new URLSearchParams({
|
|
1924
|
+
response_type: "code",
|
|
1925
|
+
client_id: clientId,
|
|
1926
|
+
redirect_uri: `http://localhost:${CLI_CALLBACK_PORT}/callback`,
|
|
1927
|
+
code_challenge: codeChallenge,
|
|
1928
|
+
code_challenge_method: "S256",
|
|
1929
|
+
scope: "openid email profile",
|
|
1930
|
+
state
|
|
1931
|
+
});
|
|
1932
|
+
return `${getCognitoBaseUrl()}/oauth2/authorize?${params.toString()}`;
|
|
1933
|
+
}
|
|
1934
|
+
function getTokenUrl2() {
|
|
1935
|
+
return `${getCognitoBaseUrl()}/oauth2/token`;
|
|
1936
|
+
}
|
|
1937
|
+
function startCallbackServer() {
|
|
1938
|
+
return new Promise((resolve, reject) => {
|
|
1939
|
+
const server = http.createServer((req, res) => {
|
|
1940
|
+
const url = new URL(req.url, `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
1941
|
+
if (url.pathname !== "/callback") {
|
|
1942
|
+
res.writeHead(404);
|
|
1943
|
+
res.end();
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const code = url.searchParams.get("code");
|
|
1947
|
+
const state = url.searchParams.get("state");
|
|
1948
|
+
const error = url.searchParams.get("error");
|
|
1949
|
+
if (error) {
|
|
1950
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1951
|
+
res.end(
|
|
1952
|
+
callbackPage(
|
|
1953
|
+
"Authentication Failed",
|
|
1954
|
+
"Something went wrong. Please close this tab and try again.",
|
|
1955
|
+
true
|
|
1956
|
+
)
|
|
1957
|
+
);
|
|
1958
|
+
server.close();
|
|
1959
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (!code || !state) {
|
|
1963
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1964
|
+
res.end(
|
|
1965
|
+
callbackPage(
|
|
1966
|
+
"Missing Parameters",
|
|
1967
|
+
"The callback was missing required data. Please close this tab and try again.",
|
|
1968
|
+
true
|
|
1969
|
+
)
|
|
1970
|
+
);
|
|
1971
|
+
server.close();
|
|
1972
|
+
reject(new Error("Missing code or state in callback"));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1976
|
+
res.end(
|
|
1977
|
+
callbackPage(
|
|
1978
|
+
"Authentication Successful",
|
|
1979
|
+
"You can close this tab and return to the terminal.",
|
|
1980
|
+
false
|
|
1981
|
+
)
|
|
1982
|
+
);
|
|
1983
|
+
server.close();
|
|
1984
|
+
resolve({ code, state });
|
|
1985
|
+
});
|
|
1986
|
+
server.listen(CLI_CALLBACK_PORT, "127.0.0.1");
|
|
1987
|
+
server.on("error", (err) => {
|
|
1988
|
+
if (err.code === "EADDRINUSE") {
|
|
1989
|
+
reject(
|
|
1990
|
+
new Error(
|
|
1991
|
+
`Port ${CLI_CALLBACK_PORT} is already in use. Close the conflicting process and try again.`
|
|
1992
|
+
)
|
|
1993
|
+
);
|
|
1994
|
+
} else {
|
|
1995
|
+
reject(err);
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
const timeout = setTimeout(() => {
|
|
1999
|
+
server.close();
|
|
2000
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
2001
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
2002
|
+
server.on("close", () => clearTimeout(timeout));
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
2006
|
+
const response = await fetch(getTokenUrl2(), {
|
|
2007
|
+
method: "POST",
|
|
2008
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2009
|
+
body: new URLSearchParams({
|
|
2010
|
+
grant_type: "authorization_code",
|
|
2011
|
+
client_id: getCognitoConfig().clientId,
|
|
2012
|
+
redirect_uri: `http://localhost:${CLI_CALLBACK_PORT}/callback`,
|
|
2013
|
+
code,
|
|
2014
|
+
code_verifier: codeVerifier
|
|
2015
|
+
})
|
|
2016
|
+
});
|
|
2017
|
+
if (!response.ok) {
|
|
2018
|
+
const text = await response.text();
|
|
2019
|
+
throw new Error(`Token exchange failed: ${response.status} ${text}`);
|
|
2020
|
+
}
|
|
2021
|
+
const data = await response.json();
|
|
2022
|
+
return {
|
|
2023
|
+
accessToken: data.access_token,
|
|
2024
|
+
refreshToken: data.refresh_token,
|
|
2025
|
+
idToken: data.id_token,
|
|
2026
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
async function refreshTokens2(refreshToken) {
|
|
2030
|
+
const response = await fetch(getTokenUrl2(), {
|
|
2031
|
+
method: "POST",
|
|
2032
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2033
|
+
body: new URLSearchParams({
|
|
2034
|
+
grant_type: "refresh_token",
|
|
2035
|
+
client_id: getCognitoConfig().clientId,
|
|
2036
|
+
refresh_token: refreshToken
|
|
2037
|
+
})
|
|
2038
|
+
});
|
|
2039
|
+
if (!response.ok) {
|
|
2040
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
2041
|
+
}
|
|
2042
|
+
const data = await response.json();
|
|
2043
|
+
return {
|
|
2044
|
+
accessToken: data.access_token,
|
|
2045
|
+
refreshToken,
|
|
2046
|
+
// Cognito does not return a new refresh token
|
|
2047
|
+
idToken: data.id_token,
|
|
2048
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
async function getStoredCredentials2() {
|
|
2052
|
+
try {
|
|
2053
|
+
const credPath = join14(getConfigDir2(), "credentials.json");
|
|
2054
|
+
const data = await readFile6(credPath, "utf-8");
|
|
2055
|
+
return JSON.parse(data);
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
throw err;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
async function storeCredentials2(credentials) {
|
|
2064
|
+
const dir = getConfigDir2();
|
|
2065
|
+
const credPath = join14(dir, "credentials.json");
|
|
2066
|
+
await mkdir9(dir, { recursive: true, mode: 448 });
|
|
2067
|
+
await writeFile9(credPath, JSON.stringify(credentials, null, 2));
|
|
2068
|
+
await chmod4(credPath, 384);
|
|
2069
|
+
}
|
|
2070
|
+
async function clearCredentials() {
|
|
2071
|
+
try {
|
|
2072
|
+
await unlink6(join14(getConfigDir2(), "credentials.json"));
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
if (err.code !== "ENOENT") throw err;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
async function getValidCredentials2(opts) {
|
|
2078
|
+
const creds = await getStoredCredentials2();
|
|
2079
|
+
if (!creds) return null;
|
|
2080
|
+
const needsRefresh = opts?.forceRefresh || Date.now() > creds.expiresAt - 5 * 60 * 1e3;
|
|
2081
|
+
if (needsRefresh) {
|
|
2082
|
+
try {
|
|
2083
|
+
const refreshed = await refreshTokens2(creds.refreshToken);
|
|
2084
|
+
refreshed.cognito = creds.cognito;
|
|
2085
|
+
await storeCredentials2(refreshed);
|
|
2086
|
+
return refreshed;
|
|
2087
|
+
} catch {
|
|
2088
|
+
if (Date.now() < creds.expiresAt) {
|
|
2089
|
+
return creds;
|
|
2090
|
+
}
|
|
2091
|
+
await clearCredentials();
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return creds;
|
|
2096
|
+
}
|
|
2097
|
+
async function performLogin() {
|
|
2098
|
+
const codeVerifier = generateCodeVerifier();
|
|
2099
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2100
|
+
const state = generateState();
|
|
2101
|
+
const authorizeUrl = getAuthorizeUrl(codeChallenge, state);
|
|
2102
|
+
const callbackPromise = startCallbackServer();
|
|
2103
|
+
try {
|
|
2104
|
+
const open = (await import("open")).default;
|
|
2105
|
+
await open(authorizeUrl);
|
|
2106
|
+
} catch {
|
|
2107
|
+
console.log(`
|
|
2108
|
+
Open this URL in your browser to authenticate:
|
|
2109
|
+
`);
|
|
2110
|
+
console.log(authorizeUrl);
|
|
2111
|
+
}
|
|
2112
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
2113
|
+
if (returnedState !== state) {
|
|
2114
|
+
throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");
|
|
2115
|
+
}
|
|
2116
|
+
const credentials = await exchangeCodeForTokens(code, codeVerifier);
|
|
2117
|
+
const { domain, clientId, region, customDomain } = getCognitoConfig();
|
|
2118
|
+
credentials.cognito = { domain, clientId, region, customDomain };
|
|
2119
|
+
await storeCredentials2(credentials);
|
|
2120
|
+
return credentials;
|
|
2121
|
+
}
|
|
2122
|
+
function callbackPage(title, message, isError) {
|
|
2123
|
+
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>';
|
|
2124
|
+
return `<!DOCTYPE html>
|
|
2125
|
+
<html lang="en">
|
|
2126
|
+
<head>
|
|
2127
|
+
<meta charset="UTF-8">
|
|
2128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2129
|
+
<title>${title} \u2014 RepoWise</title>
|
|
2130
|
+
<link rel="icon" href="https://staging.repowise.ai/favicon.svg" type="image/svg+xml">
|
|
2131
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
2132
|
+
<style>
|
|
2133
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2134
|
+
body { font-family: 'Inter', system-ui, sans-serif; background: #0a0b14; color: #e4e4e7; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
2135
|
+
.card { text-align: center; max-width: 440px; padding: 48px 40px; }
|
|
2136
|
+
.logo { margin-bottom: 32px; }
|
|
2137
|
+
.logo svg { height: 48px; width: auto; }
|
|
2138
|
+
.icon { margin-bottom: 20px; }
|
|
2139
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; color: ${isError ? "#ef4444" : "#e4e4e7"}; }
|
|
2140
|
+
p { font-size: 15px; color: #a1a1aa; line-height: 1.5; }
|
|
2141
|
+
</style>
|
|
2142
|
+
</head>
|
|
2143
|
+
<body>
|
|
2144
|
+
<div class="card">
|
|
2145
|
+
<div class="logo">
|
|
2146
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 50" height="48">
|
|
2147
|
+
<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>
|
|
2148
|
+
</svg>
|
|
2149
|
+
</div>
|
|
2150
|
+
<div class="icon">${icon}</div>
|
|
2151
|
+
<h1>${title}</h1>
|
|
2152
|
+
<p>${message}</p>
|
|
2153
|
+
</div>
|
|
2154
|
+
</body>
|
|
2155
|
+
</html>`;
|
|
2156
|
+
}
|
|
2157
|
+
function decodeIdToken(idToken) {
|
|
2158
|
+
try {
|
|
2159
|
+
const parts = idToken.split(".");
|
|
2160
|
+
if (parts.length < 2) return { email: "unknown" };
|
|
2161
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
2162
|
+
return { email: payload.email ?? "unknown", tenantId: payload["custom:tenant_id"] };
|
|
2163
|
+
} catch {
|
|
2164
|
+
return { email: "unknown" };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/lib/api.ts
|
|
2169
|
+
function getApiUrl() {
|
|
2170
|
+
return process.env["REPOWISE_API_URL"] ?? getEnvConfig().apiUrl;
|
|
2171
|
+
}
|
|
2172
|
+
var BILLING_URL = "https://app.repowise.ai/billing";
|
|
2173
|
+
async function apiRequest(path, options) {
|
|
2174
|
+
const credentials = await getValidCredentials2();
|
|
2175
|
+
if (!credentials) {
|
|
2176
|
+
throw new Error("Not logged in. Run `repowise login` first.");
|
|
2177
|
+
}
|
|
2178
|
+
let response = await fetch(`${getApiUrl()}${path}`, {
|
|
2179
|
+
...options,
|
|
2180
|
+
headers: {
|
|
2181
|
+
"Content-Type": "application/json",
|
|
2182
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2183
|
+
...options?.headers
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
if (response.status === 401) {
|
|
2187
|
+
const refreshed = await getValidCredentials2({ forceRefresh: true });
|
|
2188
|
+
if (refreshed) {
|
|
2189
|
+
response = await fetch(`${getApiUrl()}${path}`, {
|
|
2190
|
+
...options,
|
|
2191
|
+
headers: {
|
|
2192
|
+
"Content-Type": "application/json",
|
|
2193
|
+
Authorization: `Bearer ${refreshed.accessToken}`,
|
|
2194
|
+
...options?.headers
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
if (!refreshed || response.status === 401) {
|
|
2199
|
+
await clearCredentials();
|
|
2200
|
+
throw new Error("Session expired. Run `repowise login` again.");
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (response.status === 402) {
|
|
2204
|
+
let message = "Your subscription is inactive.";
|
|
2205
|
+
try {
|
|
2206
|
+
const body = await response.json();
|
|
2207
|
+
if (body.error?.message) message = body.error.message;
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
throw new Error(`${message} Visit ${BILLING_URL} to update your subscription.`);
|
|
2211
|
+
}
|
|
2212
|
+
if (!response.ok) {
|
|
2213
|
+
let message = `Request failed with status ${response.status}`;
|
|
2214
|
+
let code = "";
|
|
2215
|
+
try {
|
|
2216
|
+
const body = await response.json();
|
|
2217
|
+
if (body.error?.message) message = body.error.message;
|
|
2218
|
+
if (body.error?.code) code = body.error.code;
|
|
2219
|
+
} catch {
|
|
2220
|
+
}
|
|
2221
|
+
if (code === "BILLING_LIMIT_EXCEEDED" || code === "SYNC_LIMIT_EXCEEDED" || code === "BILLING_SUBSCRIPTION_INACTIVE") {
|
|
2222
|
+
throw new Error(`${message} Visit ${BILLING_URL} to manage your plan.`);
|
|
2223
|
+
}
|
|
2224
|
+
throw new Error(message);
|
|
2225
|
+
}
|
|
2226
|
+
const json = await response.json();
|
|
2227
|
+
return json.data;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/lib/prompts.ts
|
|
2231
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
2232
|
+
import chalk3 from "chalk";
|
|
2233
|
+
async function selectAiTools() {
|
|
2234
|
+
const choices = [
|
|
2235
|
+
{ name: "Cursor", value: "cursor" },
|
|
2236
|
+
{ name: "Claude Code", value: "claude-code" },
|
|
2237
|
+
{ name: "GitHub Copilot", value: "copilot" },
|
|
2238
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
2239
|
+
{ name: "Cline", value: "cline" },
|
|
2240
|
+
{ name: "Codex", value: "codex" },
|
|
2241
|
+
{ name: "Roo Code", value: "roo-code" },
|
|
2242
|
+
{ name: "Other (manual setup)", value: "other" }
|
|
2243
|
+
];
|
|
2244
|
+
while (true) {
|
|
2245
|
+
const selected = await checkbox({
|
|
2246
|
+
message: chalk3.bold("Which AI tools do you use?") + chalk3.dim(" (Space to select, Enter to continue)"),
|
|
2247
|
+
choices
|
|
2248
|
+
});
|
|
2249
|
+
if (selected.length === 0) {
|
|
2250
|
+
const goBack = await confirm({
|
|
2251
|
+
message: "No tools selected. Go back and choose?",
|
|
2252
|
+
default: true
|
|
2253
|
+
});
|
|
2254
|
+
if (goBack) continue;
|
|
2255
|
+
}
|
|
2256
|
+
const hasOther = selected.includes("other");
|
|
2257
|
+
const tools = selected.filter((s) => s !== "other");
|
|
2258
|
+
return { tools, hasOther };
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// src/lib/ai-tools.ts
|
|
2263
|
+
import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir10, readdir } from "fs/promises";
|
|
2264
|
+
import { join as join15, dirname as dirname3 } from "path";
|
|
2265
|
+
var AI_TOOL_CONFIG = {
|
|
2266
|
+
cursor: {
|
|
2267
|
+
label: "Cursor",
|
|
2268
|
+
fileName: ".cursorrules",
|
|
2269
|
+
filePath: ".cursorrules",
|
|
2270
|
+
markerStart: "# --- repowise-start ---",
|
|
2271
|
+
markerEnd: "# --- repowise-end ---",
|
|
2272
|
+
format: "plain-text"
|
|
2273
|
+
},
|
|
2274
|
+
"claude-code": {
|
|
2275
|
+
label: "Claude Code",
|
|
2276
|
+
fileName: "CLAUDE.md",
|
|
2277
|
+
filePath: "CLAUDE.md",
|
|
2278
|
+
markerStart: "<!-- repowise-start -->",
|
|
2279
|
+
markerEnd: "<!-- repowise-end -->",
|
|
2280
|
+
format: "markdown"
|
|
2281
|
+
},
|
|
2282
|
+
copilot: {
|
|
2283
|
+
label: "GitHub Copilot",
|
|
2284
|
+
fileName: "copilot-instructions.md",
|
|
2285
|
+
filePath: ".github/copilot-instructions.md",
|
|
2286
|
+
markerStart: "<!-- repowise-start -->",
|
|
2287
|
+
markerEnd: "<!-- repowise-end -->",
|
|
2288
|
+
format: "markdown"
|
|
2289
|
+
},
|
|
2290
|
+
windsurf: {
|
|
2291
|
+
label: "Windsurf",
|
|
2292
|
+
fileName: ".windsurfrules",
|
|
2293
|
+
filePath: ".windsurfrules",
|
|
2294
|
+
markerStart: "# --- repowise-start ---",
|
|
2295
|
+
markerEnd: "# --- repowise-end ---",
|
|
2296
|
+
format: "plain-text"
|
|
2297
|
+
},
|
|
2298
|
+
cline: {
|
|
2299
|
+
label: "Cline",
|
|
2300
|
+
fileName: ".clinerules",
|
|
2301
|
+
filePath: ".clinerules",
|
|
2302
|
+
markerStart: "# --- repowise-start ---",
|
|
2303
|
+
markerEnd: "# --- repowise-end ---",
|
|
2304
|
+
format: "plain-text"
|
|
2305
|
+
},
|
|
2306
|
+
codex: {
|
|
2307
|
+
label: "Codex",
|
|
2308
|
+
fileName: "AGENTS.md",
|
|
2309
|
+
filePath: "AGENTS.md",
|
|
2310
|
+
markerStart: "<!-- repowise-start -->",
|
|
2311
|
+
markerEnd: "<!-- repowise-end -->",
|
|
2312
|
+
format: "markdown"
|
|
2313
|
+
},
|
|
2314
|
+
"roo-code": {
|
|
2315
|
+
label: "Roo Code",
|
|
2316
|
+
fileName: "rules.md",
|
|
2317
|
+
filePath: ".roo/rules.md",
|
|
2318
|
+
markerStart: "<!-- repowise-start -->",
|
|
2319
|
+
markerEnd: "<!-- repowise-end -->",
|
|
2320
|
+
format: "markdown"
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
var SUPPORTED_TOOLS = Object.keys(AI_TOOL_CONFIG);
|
|
2324
|
+
function sanitizeRepoName(name) {
|
|
2325
|
+
return name.replace(/[<>[\]`()|\\]/g, "");
|
|
2326
|
+
}
|
|
2327
|
+
function fileDescriptionFromName(fileName) {
|
|
2328
|
+
return fileName.replace(/\.md$/, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2329
|
+
}
|
|
2330
|
+
function generateReference(tool, repoName, contextFolder, contextFiles) {
|
|
2331
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
2332
|
+
const safeName = sanitizeRepoName(repoName);
|
|
2333
|
+
const fileLines = contextFiles.map((f) => {
|
|
2334
|
+
const baseName = f.fileName.split("/").pop() ?? f.fileName;
|
|
2335
|
+
const desc = fileDescriptionFromName(baseName);
|
|
2336
|
+
const isOverview = baseName === "project-overview.md";
|
|
2337
|
+
return { path: f.relativePath, desc: isOverview ? `${desc} (full index of all files)` : desc };
|
|
2338
|
+
});
|
|
2339
|
+
const hasFiles = fileLines.length > 0;
|
|
2340
|
+
if (config2.format === "markdown") {
|
|
2341
|
+
const lines2 = [
|
|
2342
|
+
config2.markerStart,
|
|
2343
|
+
"",
|
|
2344
|
+
`## Project Context \u2014 ${safeName}`,
|
|
2345
|
+
"",
|
|
2346
|
+
`This repository has AI-optimized context files generated by RepoWise.`,
|
|
2347
|
+
`**IMPORTANT: Before answering questions about the codebase or making any changes, ALWAYS check the \`${contextFolder}/\` folder first.** These files contain pre-analyzed architecture, patterns, API contracts, and domain knowledge that will answer most questions without needing to search the codebase.`,
|
|
2348
|
+
"",
|
|
2349
|
+
`**Start here:** \`${contextFolder}/project-overview.md\` \u2014 the routing document that maps every context file to its domain. Read it first to find which context file has the answer you need.`,
|
|
2350
|
+
""
|
|
2351
|
+
];
|
|
2352
|
+
if (hasFiles) {
|
|
2353
|
+
lines2.push(
|
|
2354
|
+
`**Core context files:**`,
|
|
2355
|
+
"",
|
|
2356
|
+
...fileLines.map((f) => `- \`${f.path}\` \u2014 ${f.desc}`),
|
|
2357
|
+
"",
|
|
2358
|
+
`> Additional context files may exist beyond this list. Check \`project-overview.md\` for the complete index.`
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
lines2.push("", config2.markerEnd);
|
|
2362
|
+
return lines2.join("\n");
|
|
2363
|
+
}
|
|
2364
|
+
const lines = [
|
|
2365
|
+
config2.markerStart,
|
|
2366
|
+
`# Project Context \u2014 ${safeName}`,
|
|
2367
|
+
"#",
|
|
2368
|
+
`# This repository has AI-optimized context files generated by RepoWise.`,
|
|
2369
|
+
`# IMPORTANT: Before answering questions about the codebase or making any changes,`,
|
|
2370
|
+
`# ALWAYS check the ${contextFolder}/ folder first. These files contain pre-analyzed`,
|
|
2371
|
+
`# architecture, patterns, API contracts, and domain knowledge that will answer`,
|
|
2372
|
+
`# most questions without needing to search the codebase.`,
|
|
2373
|
+
"#",
|
|
2374
|
+
`# Start here: ${contextFolder}/project-overview.md`,
|
|
2375
|
+
`# The routing document that maps every context file to its domain.`,
|
|
2376
|
+
`# Read it first to find which context file has the answer you need.`
|
|
2377
|
+
];
|
|
2378
|
+
if (hasFiles) {
|
|
2379
|
+
lines.push(
|
|
2380
|
+
"#",
|
|
2381
|
+
`# Core context files:`,
|
|
2382
|
+
...fileLines.map((f) => `# ${f.path} \u2014 ${f.desc}`),
|
|
2383
|
+
"#",
|
|
2384
|
+
"# Additional context files may exist beyond this list.",
|
|
2385
|
+
"# Check project-overview.md for the complete index."
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
lines.push(config2.markerEnd);
|
|
2389
|
+
return lines.join("\n");
|
|
2390
|
+
}
|
|
2391
|
+
async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
|
|
2392
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
2393
|
+
const fullPath = join15(repoRoot, config2.filePath);
|
|
2394
|
+
const dir = dirname3(fullPath);
|
|
2395
|
+
if (dir !== repoRoot) {
|
|
2396
|
+
await mkdir10(dir, { recursive: true });
|
|
2397
|
+
}
|
|
2398
|
+
const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles);
|
|
2399
|
+
let existing = "";
|
|
2400
|
+
let created = true;
|
|
2401
|
+
try {
|
|
2402
|
+
existing = await readFile7(fullPath, "utf-8");
|
|
2403
|
+
created = false;
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
if (err.code !== "ENOENT") throw err;
|
|
2406
|
+
}
|
|
2407
|
+
const startIdx = existing.indexOf(config2.markerStart);
|
|
2408
|
+
const endIdx = existing.indexOf(config2.markerEnd);
|
|
2409
|
+
let content;
|
|
2410
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
2411
|
+
const before = existing.slice(0, startIdx);
|
|
2412
|
+
const after = existing.slice(endIdx + config2.markerEnd.length);
|
|
2413
|
+
content = before + referenceBlock + after;
|
|
2414
|
+
} else {
|
|
2415
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
2416
|
+
content = existing + separator + referenceBlock + "\n";
|
|
2417
|
+
}
|
|
2418
|
+
await writeFile10(fullPath, content, "utf-8");
|
|
2419
|
+
return { created };
|
|
2420
|
+
}
|
|
2421
|
+
async function scanLocalContextFiles(repoRoot, contextFolder) {
|
|
2422
|
+
const folderPath = join15(repoRoot, contextFolder);
|
|
2423
|
+
try {
|
|
2424
|
+
const entries = await readdir(folderPath, { withFileTypes: true, recursive: true });
|
|
2425
|
+
const results = [];
|
|
2426
|
+
for (const entry of entries) {
|
|
2427
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2428
|
+
const parentDir = entry.parentPath ?? folderPath;
|
|
2429
|
+
const fullPath = join15(parentDir, entry.name);
|
|
2430
|
+
const relFromContext = fullPath.slice(folderPath.length + 1);
|
|
2431
|
+
results.push({
|
|
2432
|
+
fileName: relFromContext,
|
|
2433
|
+
relativePath: `${contextFolder}/${relFromContext}`
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
return results.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
if (err.code === "ENOENT") return [];
|
|
2439
|
+
throw err;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/lib/gitignore.ts
|
|
2444
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2445
|
+
import { join as join16 } from "path";
|
|
2446
|
+
function ensureGitignore(repoRoot, entry) {
|
|
2447
|
+
const gitignorePath = join16(repoRoot, ".gitignore");
|
|
2448
|
+
if (existsSync(gitignorePath)) {
|
|
2449
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
2450
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
2451
|
+
if (lines.includes(entry) || lines.includes(entry + "/")) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const separator = content.endsWith("\n") ? "" : "\n";
|
|
2455
|
+
writeFileSync(gitignorePath, content + separator + entry + "\n", "utf-8");
|
|
2456
|
+
} else {
|
|
2457
|
+
writeFileSync(gitignorePath, entry + "\n", "utf-8");
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/lib/git.ts
|
|
2462
|
+
import { execSync } from "child_process";
|
|
2463
|
+
function detectRepoRoot() {
|
|
2464
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
2465
|
+
}
|
|
2466
|
+
function detectRepoName(repoRoot) {
|
|
2467
|
+
try {
|
|
2468
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
2469
|
+
encoding: "utf-8",
|
|
2470
|
+
cwd: repoRoot
|
|
2471
|
+
}).trim();
|
|
2472
|
+
const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
|
|
2473
|
+
if (match?.[1]) return match[1];
|
|
2474
|
+
} catch {
|
|
2475
|
+
}
|
|
2476
|
+
return repoRoot.split("/").pop() ?? "unknown";
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// src/lib/interview-handler.ts
|
|
2480
|
+
import chalk4 from "chalk";
|
|
2481
|
+
import { input } from "@inquirer/prompts";
|
|
2482
|
+
var INTERVIEW_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2483
|
+
var MAX_QUESTIONS = 10;
|
|
2484
|
+
var questionCounter = 0;
|
|
2485
|
+
async function handleInterview(syncId, questionId, questionText, questionContext, estimatedQuestions) {
|
|
2486
|
+
questionCounter++;
|
|
2487
|
+
if (questionCounter === 1) {
|
|
2488
|
+
console.log("");
|
|
2489
|
+
console.log(chalk4.cyan.bold(" \u2500\u2500 Interview \u2500\u2500"));
|
|
2490
|
+
console.log(chalk4.dim(" Help us understand your project better. Answer a few short"));
|
|
2491
|
+
console.log(
|
|
2492
|
+
chalk4.dim(
|
|
2493
|
+
` questions so we can generate more relevant context files (up to ${MAX_QUESTIONS}).`
|
|
2494
|
+
)
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
const total = Math.min(estimatedQuestions ?? MAX_QUESTIONS, MAX_QUESTIONS);
|
|
2498
|
+
console.log("");
|
|
2499
|
+
console.log(chalk4.cyan.bold(` Question ${questionCounter}/${total}`));
|
|
2500
|
+
if (questionContext) {
|
|
2501
|
+
console.log(chalk4.dim(` ${questionContext}`));
|
|
2502
|
+
}
|
|
2503
|
+
console.log(` ${questionText}`);
|
|
2504
|
+
console.log(chalk4.dim(' (Enter to skip \xB7 "done" to finish early)'));
|
|
2505
|
+
let answer;
|
|
2506
|
+
try {
|
|
2507
|
+
answer = await Promise.race([
|
|
2508
|
+
input({
|
|
2509
|
+
message: chalk4.cyan(">"),
|
|
2510
|
+
theme: { prefix: " " }
|
|
2511
|
+
}),
|
|
2512
|
+
new Promise(
|
|
2513
|
+
(_, reject) => setTimeout(() => reject(new Error("INTERVIEW_TIMEOUT")), INTERVIEW_TIMEOUT_MS)
|
|
2514
|
+
)
|
|
2515
|
+
]);
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
if (err instanceof Error && err.message === "INTERVIEW_TIMEOUT") {
|
|
2518
|
+
console.log(chalk4.yellow(" Timed out \u2014 auto-skipping this question."));
|
|
2519
|
+
answer = "skip";
|
|
2520
|
+
} else {
|
|
2521
|
+
throw err;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const trimmed = answer.trim();
|
|
2525
|
+
let action;
|
|
2526
|
+
let answerText = "";
|
|
2527
|
+
if (trimmed.toLowerCase() === "done") {
|
|
2528
|
+
action = "done";
|
|
2529
|
+
} else if (trimmed === "" || trimmed.toLowerCase() === "skip") {
|
|
2530
|
+
action = "skip";
|
|
2531
|
+
} else {
|
|
2532
|
+
action = "answer";
|
|
2533
|
+
answerText = trimmed;
|
|
2534
|
+
}
|
|
2535
|
+
if (questionCounter >= MAX_QUESTIONS && action !== "done") {
|
|
2536
|
+
action = "done";
|
|
2537
|
+
console.log(chalk4.green(" Thanks for your answers! Wrapping up the interview."));
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
await apiRequest(`/v1/sync/${syncId}/answer`, {
|
|
2541
|
+
method: "POST",
|
|
2542
|
+
body: JSON.stringify({ questionId, answerText, action })
|
|
2543
|
+
});
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2546
|
+
if (message.includes("not awaiting input") || message.includes("expired")) {
|
|
2547
|
+
console.log(chalk4.dim(" Pipeline has already moved on \u2014 continuing."));
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
try {
|
|
2551
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2552
|
+
await apiRequest(`/v1/sync/${syncId}/answer`, {
|
|
2553
|
+
method: "POST",
|
|
2554
|
+
body: JSON.stringify({ questionId, answerText, action })
|
|
2555
|
+
});
|
|
2556
|
+
} catch {
|
|
2557
|
+
console.log(chalk4.yellow(" Could not submit answer \u2014 pipeline will continue."));
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (action === "done") {
|
|
2562
|
+
console.log(chalk4.dim(" Interview ended early."));
|
|
2563
|
+
} else if (action === "skip") {
|
|
2564
|
+
console.log(chalk4.dim(" Skipped."));
|
|
2565
|
+
} else {
|
|
2566
|
+
console.log(chalk4.dim(" Answer recorded."));
|
|
2567
|
+
}
|
|
2568
|
+
console.log("");
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// src/lib/progress-renderer.ts
|
|
2572
|
+
import chalk5 from "chalk";
|
|
2573
|
+
var CORE_FILES = /* @__PURE__ */ new Set([
|
|
2574
|
+
"project-overview.md",
|
|
2575
|
+
"architecture.md",
|
|
2576
|
+
"data-models.md",
|
|
2577
|
+
"api-contracts.md",
|
|
2578
|
+
"coding-patterns.md"
|
|
2579
|
+
]);
|
|
2580
|
+
var ALL_PERSONAS = ["pm", "architect", "dev", "analyst", "tea", "ux", "sm", "techWriter"];
|
|
2581
|
+
var PERSONA_LABELS = {
|
|
2582
|
+
pm: "Product Manager",
|
|
2583
|
+
architect: "Architect",
|
|
2584
|
+
dev: "Developer",
|
|
2585
|
+
analyst: "Business Analyst",
|
|
2586
|
+
tea: "Test Architect",
|
|
2587
|
+
ux: "UX Designer",
|
|
2588
|
+
sm: "Scrum Master",
|
|
2589
|
+
techWriter: "Tech Writer"
|
|
2590
|
+
};
|
|
2591
|
+
function computeOverallProgress(syncResult) {
|
|
2592
|
+
const stepNumber = syncResult.stepNumber ?? 1;
|
|
2593
|
+
const totalSteps = syncResult.totalSteps ?? 7;
|
|
2594
|
+
const stepPct = syncResult.progressPercentage ?? 0;
|
|
2595
|
+
return Math.min(100, Math.round(((stepNumber - 1) * 100 + stepPct) / totalSteps));
|
|
2596
|
+
}
|
|
2597
|
+
var ProgressRenderer = class {
|
|
2598
|
+
privacyShieldShown = false;
|
|
2599
|
+
discoveryShown = false;
|
|
2600
|
+
scanHeaderShown = false;
|
|
2601
|
+
scanSummaryShown = false;
|
|
2602
|
+
generationHeaderShown = false;
|
|
2603
|
+
coreSubtitleShown = false;
|
|
2604
|
+
coreCompleteShown = false;
|
|
2605
|
+
tailoredSubtitleShown = false;
|
|
2606
|
+
tailoredCompleteShown = false;
|
|
2607
|
+
validationHeaderShown = false;
|
|
2608
|
+
validationShown = false;
|
|
2609
|
+
pushShown = false;
|
|
2610
|
+
renderPrivacyShield(enabled, spinner) {
|
|
2611
|
+
if (this.privacyShieldShown) return;
|
|
2612
|
+
this.privacyShieldShown = true;
|
|
2613
|
+
spinner.stop();
|
|
2614
|
+
console.log("");
|
|
2615
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Privacy Shield \u2500\u2500"));
|
|
2616
|
+
if (enabled) {
|
|
2617
|
+
console.log(` ${chalk5.green("\u2713")} Privacy Shield active`);
|
|
2618
|
+
console.log(` ${chalk5.green("\u2713")} Private connection established`);
|
|
2619
|
+
} else {
|
|
2620
|
+
console.log(` ${chalk5.yellow("\u2139")} Privacy Shield not in current plan`);
|
|
2621
|
+
console.log(chalk5.dim(" Shield your data from the open internet."));
|
|
2622
|
+
}
|
|
2623
|
+
console.log("");
|
|
2624
|
+
spinner.start();
|
|
2625
|
+
}
|
|
2626
|
+
renderDiscovery(result, spinner) {
|
|
2627
|
+
if (this.discoveryShown) return;
|
|
2628
|
+
this.discoveryShown = true;
|
|
2629
|
+
spinner.stop();
|
|
2630
|
+
console.log("");
|
|
2631
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Repository Discovery \u2500\u2500"));
|
|
2632
|
+
if (result.languages.length > 0) {
|
|
2633
|
+
const langs = result.languages.slice(0, 5).map((l) => `${l.name} (${Math.round(l.percentage)}%)`).join(", ");
|
|
2634
|
+
console.log(` ${chalk5.dim("Languages:")} ${langs}`);
|
|
2635
|
+
}
|
|
2636
|
+
if (result.frameworks.length > 0) {
|
|
2637
|
+
console.log(
|
|
2638
|
+
` ${chalk5.dim("Frameworks:")} ${result.frameworks.map((f) => f.name).join(", ")}`
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
console.log(
|
|
2642
|
+
` ${chalk5.dim("Structure:")} ${result.structureType} ${chalk5.dim(`(${result.fileCount} files)`)}`
|
|
2643
|
+
);
|
|
2644
|
+
if (result.existingDocs.length > 0) {
|
|
2645
|
+
console.log(` ${chalk5.dim("Existing docs:")} ${result.existingDocs.join(", ")}`);
|
|
2646
|
+
}
|
|
2647
|
+
if (result.fileTree && result.fileTree.length > 0) {
|
|
2648
|
+
this.renderTree(result.fileTree);
|
|
2649
|
+
}
|
|
2650
|
+
console.log("");
|
|
2651
|
+
spinner.start();
|
|
2652
|
+
}
|
|
2653
|
+
renderTree(entries) {
|
|
2654
|
+
console.log("");
|
|
2655
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Project Structure \u2500\u2500"));
|
|
2656
|
+
const root = { name: "", type: "tree", children: /* @__PURE__ */ new Map() };
|
|
2657
|
+
for (const entry of entries) {
|
|
2658
|
+
const parts = entry.path.split("/");
|
|
2659
|
+
let current = root;
|
|
2660
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2661
|
+
const part = parts[i];
|
|
2662
|
+
if (!current.children.has(part)) {
|
|
2663
|
+
const isLast = i === parts.length - 1;
|
|
2664
|
+
current.children.set(part, {
|
|
2665
|
+
name: part,
|
|
2666
|
+
type: isLast ? entry.type : "tree",
|
|
2667
|
+
children: /* @__PURE__ */ new Map()
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
current = current.children.get(part);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const printNode = (node, prefix, isLast) => {
|
|
2674
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2675
|
+
const display = node.type === "tree" ? chalk5.bold.dim(`${node.name}/`) : node.name;
|
|
2676
|
+
console.log(` ${prefix}${connector}${display}`);
|
|
2677
|
+
const sorted = [...node.children.values()].sort((a, b) => {
|
|
2678
|
+
if (a.type === "tree" && b.type !== "tree") return -1;
|
|
2679
|
+
if (a.type !== "tree" && b.type === "tree") return 1;
|
|
2680
|
+
return a.name.localeCompare(b.name);
|
|
2681
|
+
});
|
|
2682
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
2683
|
+
sorted.forEach((child, idx) => {
|
|
2684
|
+
printNode(child, childPrefix, idx === sorted.length - 1);
|
|
2685
|
+
});
|
|
2686
|
+
};
|
|
2687
|
+
const topLevel = [...root.children.values()].sort((a, b) => {
|
|
2688
|
+
if (a.type === "tree" && b.type !== "tree") return -1;
|
|
2689
|
+
if (a.type !== "tree" && b.type === "tree") return 1;
|
|
2690
|
+
return a.name.localeCompare(b.name);
|
|
2691
|
+
});
|
|
2692
|
+
topLevel.forEach((child, idx) => {
|
|
2693
|
+
printNode(child, "", idx === topLevel.length - 1);
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
renderScanProgress(progress, spinner) {
|
|
2697
|
+
if (!this.scanHeaderShown) {
|
|
2698
|
+
this.scanHeaderShown = true;
|
|
2699
|
+
spinner.stop();
|
|
2700
|
+
console.log("");
|
|
2701
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Code Analysis \u2500\u2500"));
|
|
2702
|
+
console.log(chalk5.dim(" Analyzing your codebase structure, functions, and relationships."));
|
|
2703
|
+
spinner.start();
|
|
2704
|
+
}
|
|
2705
|
+
if (progress.summary && !this.scanSummaryShown) {
|
|
2706
|
+
this.scanSummaryShown = true;
|
|
2707
|
+
const s = progress.summary;
|
|
2708
|
+
spinner.stop();
|
|
2709
|
+
console.log(
|
|
2710
|
+
` ${chalk5.green("\u2713")} ${s.totalFiles} files, ${s.totalFunctions} functions, ${s.totalClasses} classes, ${s.totalEndpoints} entry points`
|
|
2711
|
+
);
|
|
2712
|
+
console.log("");
|
|
2713
|
+
spinner.start();
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
renderFileStatuses(fileStatuses, spinner) {
|
|
2717
|
+
if (!this.generationHeaderShown) {
|
|
2718
|
+
this.generationHeaderShown = true;
|
|
2719
|
+
spinner.stop();
|
|
2720
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Context Generation \u2500\u2500"));
|
|
2721
|
+
console.log(
|
|
2722
|
+
chalk5.cyan(
|
|
2723
|
+
" \u2615 This takes a few minutes \u2014 grab a coffee, we'll handle the rest!"
|
|
2724
|
+
)
|
|
2725
|
+
);
|
|
2726
|
+
console.log("");
|
|
2727
|
+
spinner.start();
|
|
2728
|
+
}
|
|
2729
|
+
const coreFiles = [];
|
|
2730
|
+
const tailoredFiles = [];
|
|
2731
|
+
for (const file of fileStatuses) {
|
|
2732
|
+
const baseName = file.fileName.split("/").pop() ?? file.fileName;
|
|
2733
|
+
if (CORE_FILES.has(baseName)) {
|
|
2734
|
+
coreFiles.push(file);
|
|
2735
|
+
} else {
|
|
2736
|
+
tailoredFiles.push(file);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const coreCompleted = coreFiles.filter((f) => f.status === "completed").length;
|
|
2740
|
+
const coreTotal = coreFiles.length;
|
|
2741
|
+
const coreAllDone = coreCompleted === coreTotal && coreTotal > 0;
|
|
2742
|
+
if (coreTotal > 0 && !this.coreSubtitleShown) {
|
|
2743
|
+
this.coreSubtitleShown = true;
|
|
2744
|
+
spinner.stop();
|
|
2745
|
+
console.log(chalk5.dim(" Generating core context files:"));
|
|
2746
|
+
spinner.start();
|
|
2747
|
+
}
|
|
2748
|
+
if (coreAllDone && !this.coreCompleteShown) {
|
|
2749
|
+
this.coreCompleteShown = true;
|
|
2750
|
+
spinner.stop();
|
|
2751
|
+
console.log(` Core ${chalk5.green("\u2713")} ${coreCompleted}/${coreTotal} completed`);
|
|
2752
|
+
console.log("");
|
|
2753
|
+
spinner.start();
|
|
2754
|
+
}
|
|
2755
|
+
const tailoredCompleted = tailoredFiles.filter((f) => f.status === "completed").length;
|
|
2756
|
+
const tailoredTotal = tailoredFiles.length;
|
|
2757
|
+
const tailoredAllDone = tailoredCompleted === tailoredTotal && tailoredTotal > 0;
|
|
2758
|
+
if (tailoredTotal > 0 && coreAllDone && !this.tailoredSubtitleShown) {
|
|
2759
|
+
this.tailoredSubtitleShown = true;
|
|
2760
|
+
spinner.stop();
|
|
2761
|
+
console.log(chalk5.dim(" Generating tailored context files:"));
|
|
2762
|
+
spinner.start();
|
|
2763
|
+
}
|
|
2764
|
+
if (tailoredAllDone && !this.tailoredCompleteShown) {
|
|
2765
|
+
this.tailoredCompleteShown = true;
|
|
2766
|
+
spinner.stop();
|
|
2767
|
+
console.log(
|
|
2768
|
+
` Tailored ${chalk5.green("\u2713")} ${tailoredCompleted}/${tailoredTotal} completed`
|
|
2769
|
+
);
|
|
2770
|
+
console.log("");
|
|
2771
|
+
spinner.start();
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
renderValidation(progress, spinner) {
|
|
2775
|
+
const isComplete = progress.status === "complete";
|
|
2776
|
+
if (isComplete && this.validationShown) return;
|
|
2777
|
+
if (!this.validationHeaderShown) {
|
|
2778
|
+
this.validationHeaderShown = true;
|
|
2779
|
+
spinner.stop();
|
|
2780
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Context Validation \u2500\u2500"));
|
|
2781
|
+
console.log(
|
|
2782
|
+
chalk5.dim(
|
|
2783
|
+
` ${ALL_PERSONAS.length} AI reviewers checking context quality \u2014 issues are auto-fixed.`
|
|
2784
|
+
)
|
|
2785
|
+
);
|
|
2786
|
+
spinner.start();
|
|
2787
|
+
}
|
|
2788
|
+
if (isComplete) {
|
|
2789
|
+
this.validationShown = true;
|
|
2790
|
+
spinner.stop();
|
|
2791
|
+
const passCount = progress.personaResults.filter((r) => r.score === "PASS").length;
|
|
2792
|
+
const roundInfo = progress.round > 1 ? ` (${progress.round} rounds)` : "";
|
|
2793
|
+
console.log(
|
|
2794
|
+
` ${chalk5.green("\u2713")} ${passCount}/${ALL_PERSONAS.length} PASS${roundInfo}`
|
|
2795
|
+
);
|
|
2796
|
+
const resultMap = new Map(progress.personaResults.map((r) => [r.persona, r.score]));
|
|
2797
|
+
for (const persona of ALL_PERSONAS) {
|
|
2798
|
+
const label = PERSONA_LABELS[persona] ?? persona;
|
|
2799
|
+
const score = resultMap.get(persona);
|
|
2800
|
+
if (score) {
|
|
2801
|
+
const icon = score === "PASS" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
2802
|
+
const scoreColor = score === "PASS" ? chalk5.green : score === "PARTIAL" ? chalk5.yellow : chalk5.red;
|
|
2803
|
+
console.log(` ${icon} ${label}: ${scoreColor(score)}`);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
if (passCount < ALL_PERSONAS.length) {
|
|
2807
|
+
console.log(chalk5.yellow(" \u26A0 Continuing with best-effort context"));
|
|
2808
|
+
}
|
|
2809
|
+
console.log("");
|
|
2810
|
+
spinner.start();
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
renderPush(spinner) {
|
|
2814
|
+
if (this.pushShown) return;
|
|
2815
|
+
this.pushShown = true;
|
|
2816
|
+
spinner.stop();
|
|
2817
|
+
console.log(chalk5.cyan.bold(" \u2500\u2500 Saving Context \u2500\u2500"));
|
|
2818
|
+
console.log(` ${chalk5.dim("Encrypting and saving context files to RepoWise servers...")}`);
|
|
2819
|
+
console.log("");
|
|
2820
|
+
spinner.start();
|
|
2821
|
+
}
|
|
2822
|
+
/** No-op kept for backward compat with create.ts/sync.ts finalize() calls */
|
|
2823
|
+
finalize() {
|
|
2824
|
+
}
|
|
2825
|
+
getSpinnerText(syncResult) {
|
|
2826
|
+
const overallPct = computeOverallProgress(syncResult);
|
|
2827
|
+
const stepLabel = syncResult.stepLabel ?? syncResult.currentStep ?? "Processing";
|
|
2828
|
+
if (syncResult.scanProgress && !syncResult.scanProgress.summary) {
|
|
2829
|
+
const sp = syncResult.scanProgress;
|
|
2830
|
+
const pct = sp.totalBatches > 0 ? Math.round(sp.currentBatch / sp.totalBatches * 100) : 0;
|
|
2831
|
+
return `Scanning batch ${sp.currentBatch}/${sp.totalBatches} ${chalk5.dim(`(${pct}%)`)}`;
|
|
2832
|
+
}
|
|
2833
|
+
if (syncResult.validationProgress && syncResult.validationProgress.status !== "complete") {
|
|
2834
|
+
const vp = syncResult.validationProgress;
|
|
2835
|
+
const passCount = vp.personaResults.filter((r) => r.score === "PASS").length;
|
|
2836
|
+
if (vp.status === "regenerating") {
|
|
2837
|
+
return `Improving files based on feedback (round ${vp.round}) ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2838
|
+
}
|
|
2839
|
+
if (vp.personaResults.length > 0) {
|
|
2840
|
+
return `Round ${vp.round}/${vp.maxRounds}: ${passCount}/${ALL_PERSONAS.length} passed ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2841
|
+
}
|
|
2842
|
+
return `Round ${vp.round}/${vp.maxRounds}: validating... ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2843
|
+
}
|
|
2844
|
+
if (syncResult.generationProgress) {
|
|
2845
|
+
const gp = syncResult.generationProgress;
|
|
2846
|
+
if (gp.fileStatuses && gp.fileStatuses.length > 0) {
|
|
2847
|
+
const generating = gp.fileStatuses.find((f) => f.status === "generating");
|
|
2848
|
+
if (generating) {
|
|
2849
|
+
const genBaseName = generating.fileName.split("/").pop() ?? generating.fileName;
|
|
2850
|
+
const isCore = CORE_FILES.has(genBaseName);
|
|
2851
|
+
const sectionFiles = gp.fileStatuses.filter((f) => {
|
|
2852
|
+
const bn = f.fileName.split("/").pop() ?? f.fileName;
|
|
2853
|
+
return isCore ? CORE_FILES.has(bn) : !CORE_FILES.has(bn);
|
|
2854
|
+
});
|
|
2855
|
+
const sectionCompleted = sectionFiles.filter((f) => f.status === "completed").length;
|
|
2856
|
+
return `${generating.fileName} (${sectionCompleted}/${sectionFiles.length}) ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2857
|
+
}
|
|
2858
|
+
const allDone = gp.fileStatuses.every((f) => f.status === "completed");
|
|
2859
|
+
if (allDone) {
|
|
2860
|
+
return `${stepLabel}... ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return `${stepLabel}... ${chalk5.dim(`(${overallPct}%)`)}`;
|
|
2865
|
+
}
|
|
2866
|
+
update(syncResult, spinner) {
|
|
2867
|
+
if (syncResult.privacyShieldEnabled !== void 0) {
|
|
2868
|
+
this.renderPrivacyShield(syncResult.privacyShieldEnabled, spinner);
|
|
2869
|
+
}
|
|
2870
|
+
if (syncResult.discoveryResult) {
|
|
2871
|
+
this.renderDiscovery(syncResult.discoveryResult, spinner);
|
|
2872
|
+
}
|
|
2873
|
+
if (syncResult.scanProgress) {
|
|
2874
|
+
this.renderScanProgress(syncResult.scanProgress, spinner);
|
|
2875
|
+
}
|
|
2876
|
+
if (syncResult.generationProgress?.fileStatuses && syncResult.generationProgress.fileStatuses.length > 0) {
|
|
2877
|
+
this.renderFileStatuses(syncResult.generationProgress.fileStatuses, spinner);
|
|
2878
|
+
}
|
|
2879
|
+
if (syncResult.validationProgress) {
|
|
2880
|
+
this.renderValidation(syncResult.validationProgress, spinner);
|
|
2881
|
+
}
|
|
2882
|
+
if (syncResult.currentStep === "push-context") {
|
|
2883
|
+
this.renderPush(spinner);
|
|
2884
|
+
}
|
|
2885
|
+
spinner.text = this.getSpinnerText(syncResult);
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
// src/commands/create.ts
|
|
2890
|
+
function formatElapsed(ms) {
|
|
2891
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
2892
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
2893
|
+
const seconds = totalSeconds % 60;
|
|
2894
|
+
if (minutes === 0) return `${seconds}s`;
|
|
2895
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
2896
|
+
}
|
|
2897
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
2898
|
+
var MAX_POLL_ATTEMPTS = 7200;
|
|
2899
|
+
var DEFAULT_CONTEXT_FOLDER = "repowise-context";
|
|
2900
|
+
async function create() {
|
|
2901
|
+
const startTime = Date.now();
|
|
2902
|
+
const spinner = ora("Checking authentication...").start();
|
|
2903
|
+
try {
|
|
2904
|
+
let credentials = await getValidCredentials2();
|
|
2905
|
+
if (!credentials) {
|
|
2906
|
+
spinner.info(chalk6.yellow("Not logged in. Opening browser to authenticate..."));
|
|
2907
|
+
credentials = await performLogin();
|
|
2908
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
2909
|
+
spinner.succeed(chalk6.green(`Authenticated as ${chalk6.bold(email)}`));
|
|
2910
|
+
} else {
|
|
2911
|
+
spinner.succeed("Authenticated");
|
|
2912
|
+
}
|
|
2913
|
+
let repoId;
|
|
2914
|
+
let repoName;
|
|
2915
|
+
let repoRoot;
|
|
2916
|
+
let repoPlatform;
|
|
2917
|
+
let repoExternalId;
|
|
2918
|
+
spinner.start("Checking for pending repository...");
|
|
2919
|
+
try {
|
|
2920
|
+
const pending = await apiRequest("/v1/onboarding/pending");
|
|
2921
|
+
if (pending?.repoId) {
|
|
2922
|
+
repoId = pending.repoId;
|
|
2923
|
+
repoName = pending.repoName;
|
|
2924
|
+
spinner.succeed(`Found pending repository: ${chalk6.bold(repoName)}`);
|
|
2925
|
+
apiRequest("/v1/onboarding/pending", { method: "DELETE" }).catch(() => {
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
} catch {
|
|
2929
|
+
}
|
|
2930
|
+
if (!repoId) {
|
|
2931
|
+
spinner.text = "Detecting repository...";
|
|
2932
|
+
try {
|
|
2933
|
+
repoRoot = detectRepoRoot();
|
|
2934
|
+
repoName = detectRepoName(repoRoot);
|
|
2935
|
+
spinner.succeed(`Repository: ${chalk6.bold(repoName)}`);
|
|
2936
|
+
} catch {
|
|
2937
|
+
spinner.fail(
|
|
2938
|
+
chalk6.red(
|
|
2939
|
+
"Not in a git repository. Run this command from your repo directory, or select a repo on the dashboard first."
|
|
2940
|
+
)
|
|
2941
|
+
);
|
|
2942
|
+
process.exitCode = 1;
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const repos = await apiRequest("/v1/repos");
|
|
2947
|
+
const match = repos.find((r) => r.name === repoName || r.fullName.endsWith(`/${repoName}`));
|
|
2948
|
+
if (match) {
|
|
2949
|
+
repoId = match.repoId;
|
|
2950
|
+
repoPlatform = match.platform;
|
|
2951
|
+
repoExternalId = match.externalId;
|
|
2952
|
+
}
|
|
2953
|
+
} catch {
|
|
2954
|
+
}
|
|
2955
|
+
} else {
|
|
2956
|
+
try {
|
|
2957
|
+
repoRoot = detectRepoRoot();
|
|
2958
|
+
} catch {
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
if (!repoId) {
|
|
2962
|
+
spinner.fail(
|
|
2963
|
+
chalk6.red(
|
|
2964
|
+
"Could not find this repository in your RepoWise account. Connect it on the dashboard first."
|
|
2965
|
+
)
|
|
2966
|
+
);
|
|
2967
|
+
process.exitCode = 1;
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
let useFreeRescan = false;
|
|
2971
|
+
try {
|
|
2972
|
+
const pricing = await apiRequest(`/v1/repos/${repoId}/rescan-pricing`);
|
|
2973
|
+
if (pricing.lastFullScanAt) {
|
|
2974
|
+
if (pricing.allowed && pricing.isFree) {
|
|
2975
|
+
spinner.succeed(chalk6.cyan("Free rescan available. Will trigger a full rescan."));
|
|
2976
|
+
useFreeRescan = true;
|
|
2977
|
+
} else {
|
|
2978
|
+
spinner.fail(chalk6.red("This repository already has context generated."));
|
|
2979
|
+
console.log(
|
|
2980
|
+
chalk6.cyan(
|
|
2981
|
+
`
|
|
2982
|
+
If you're a team member, run: ${chalk6.bold("repowise member")}
|
|
2983
|
+
This will download context and configure your AI tools.
|
|
2984
|
+
`
|
|
2985
|
+
)
|
|
2986
|
+
);
|
|
2987
|
+
console.log(
|
|
2988
|
+
chalk6.dim(
|
|
2989
|
+
` To trigger a new full scan, visit: https://app.repowise.ai/repos/${repoId}
|
|
2990
|
+
To sync recent changes, use: repowise sync
|
|
2991
|
+
`
|
|
2992
|
+
)
|
|
2993
|
+
);
|
|
2994
|
+
process.exitCode = 1;
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
} catch {
|
|
2999
|
+
}
|
|
3000
|
+
const { tools, hasOther } = await selectAiTools();
|
|
3001
|
+
if (hasOther) {
|
|
3002
|
+
console.log(
|
|
3003
|
+
chalk6.cyan(
|
|
3004
|
+
"\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"
|
|
3005
|
+
)
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
if (tools.length === 0 && !hasOther) {
|
|
3009
|
+
console.log(
|
|
3010
|
+
chalk6.yellow(
|
|
3011
|
+
"\nNo AI tools selected. You can configure them later with `repowise config`."
|
|
3012
|
+
)
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
const contextStorage = "server";
|
|
3016
|
+
spinner.start("Starting context generation pipeline...");
|
|
3017
|
+
let syncId;
|
|
3018
|
+
try {
|
|
3019
|
+
const endpoint = useFreeRescan ? `/v1/repos/${repoId}/full-rescan` : `/v1/repos/${repoId}/sync`;
|
|
3020
|
+
const triggerResult = await apiRequest(endpoint, {
|
|
3021
|
+
method: "POST",
|
|
3022
|
+
body: useFreeRescan ? void 0 : JSON.stringify({ scanType: "full", contextStorage })
|
|
3023
|
+
});
|
|
3024
|
+
syncId = triggerResult.syncId;
|
|
3025
|
+
} catch (triggerErr) {
|
|
3026
|
+
const msg = triggerErr instanceof Error ? triggerErr.message : "";
|
|
3027
|
+
if (!msg.toLowerCase().includes("already running") && !msg.toLowerCase().includes("already in progress")) {
|
|
3028
|
+
throw triggerErr;
|
|
3029
|
+
}
|
|
3030
|
+
spinner.text = "Resuming existing pipeline...";
|
|
3031
|
+
const syncs = await apiRequest(
|
|
3032
|
+
`/v1/repos/${repoId}/syncs?limit=10`
|
|
3033
|
+
);
|
|
3034
|
+
const active = syncs.items.find(
|
|
3035
|
+
(s) => s.status === "in_progress" || s.status === "awaiting_input"
|
|
3036
|
+
);
|
|
3037
|
+
if (!active) {
|
|
3038
|
+
spinner.text = "Retrying...";
|
|
3039
|
+
const endpoint = useFreeRescan ? `/v1/repos/${repoId}/full-rescan` : `/v1/repos/${repoId}/sync`;
|
|
3040
|
+
const retryResult = await apiRequest(endpoint, {
|
|
3041
|
+
method: "POST",
|
|
3042
|
+
body: useFreeRescan ? void 0 : JSON.stringify({ scanType: "full", contextStorage })
|
|
3043
|
+
});
|
|
3044
|
+
syncId = retryResult.syncId;
|
|
3045
|
+
} else {
|
|
3046
|
+
syncId = active.syncId;
|
|
3047
|
+
spinner.info(chalk6.cyan("Resuming existing pipeline..."));
|
|
3048
|
+
spinner.start();
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
let pollAttempts = 0;
|
|
3052
|
+
let pollErrors = 0;
|
|
3053
|
+
const MAX_POLL_ERRORS = 5;
|
|
3054
|
+
const progressRenderer = new ProgressRenderer();
|
|
3055
|
+
while (true) {
|
|
3056
|
+
if (++pollAttempts > MAX_POLL_ATTEMPTS) {
|
|
3057
|
+
spinner.fail(chalk6.red("Pipeline timed out. Check dashboard for status."));
|
|
3058
|
+
process.exitCode = 1;
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
3062
|
+
let syncResult;
|
|
3063
|
+
try {
|
|
3064
|
+
syncResult = await apiRequest(`/v1/sync/${syncId}/status`);
|
|
3065
|
+
pollErrors = 0;
|
|
3066
|
+
} catch (pollErr) {
|
|
3067
|
+
pollErrors++;
|
|
3068
|
+
if (pollErrors >= MAX_POLL_ERRORS) {
|
|
3069
|
+
throw pollErr;
|
|
3070
|
+
}
|
|
3071
|
+
continue;
|
|
3072
|
+
}
|
|
3073
|
+
progressRenderer.update(syncResult, spinner);
|
|
3074
|
+
if (syncResult.status === "awaiting_input" && syncResult.questionId && syncResult.questionText) {
|
|
3075
|
+
spinner.stop();
|
|
3076
|
+
await handleInterview(
|
|
3077
|
+
syncId,
|
|
3078
|
+
syncResult.questionId,
|
|
3079
|
+
syncResult.questionText,
|
|
3080
|
+
syncResult.questionContext ?? void 0,
|
|
3081
|
+
syncResult.discoveryResult?.estimatedInterviewQuestions
|
|
3082
|
+
);
|
|
3083
|
+
spinner.start("Resuming pipeline...");
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
if (syncResult.status === "completed") {
|
|
3087
|
+
progressRenderer.finalize();
|
|
3088
|
+
const generatedFiles = syncResult.filesGenerated ?? [];
|
|
3089
|
+
const fileCount = generatedFiles.length;
|
|
3090
|
+
if (fileCount > 0) {
|
|
3091
|
+
const coreCount = generatedFiles.filter(
|
|
3092
|
+
(f) => CORE_FILES.has(f.split("/").pop() ?? f)
|
|
3093
|
+
).length;
|
|
3094
|
+
const tailoredCount = fileCount - coreCount;
|
|
3095
|
+
spinner.succeed(
|
|
3096
|
+
`Context generation complete \u2014 ${coreCount} core + ${tailoredCount} tailored files`
|
|
3097
|
+
);
|
|
3098
|
+
} else {
|
|
3099
|
+
spinner.warn(chalk6.yellow("Pipeline completed but no context files were generated."));
|
|
3100
|
+
console.log(
|
|
3101
|
+
chalk6.yellow(
|
|
3102
|
+
" This may be due to AI throttling or a parsing issue. Try running `repowise create` again."
|
|
3103
|
+
)
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
break;
|
|
3107
|
+
}
|
|
3108
|
+
if (syncResult.status === "failed") {
|
|
3109
|
+
progressRenderer.finalize();
|
|
3110
|
+
spinner.fail(chalk6.red(`Pipeline failed: ${syncResult.error ?? "Unknown error"}`));
|
|
3111
|
+
process.exitCode = 1;
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
if (repoRoot) {
|
|
3116
|
+
spinner.start("Downloading context files from server...");
|
|
3117
|
+
try {
|
|
3118
|
+
const listResult = await apiRequest(`/v1/repos/${repoId}/context`);
|
|
3119
|
+
const files = listResult.data?.files ?? listResult.files ?? [];
|
|
3120
|
+
if (files.length > 0) {
|
|
3121
|
+
const contextDir = join17(repoRoot, DEFAULT_CONTEXT_FOLDER);
|
|
3122
|
+
mkdirSync(contextDir, { recursive: true });
|
|
3123
|
+
let downloadedCount = 0;
|
|
3124
|
+
let failedCount = 0;
|
|
3125
|
+
for (const file of files) {
|
|
3126
|
+
if (file.fileName.includes("..")) {
|
|
3127
|
+
failedCount++;
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
const urlResult = await apiRequest(`/v1/repos/${repoId}/context/files/${file.fileName}`);
|
|
3131
|
+
const presignedUrl = urlResult.data?.url ?? urlResult.url;
|
|
3132
|
+
const response = await fetch(presignedUrl);
|
|
3133
|
+
if (response.ok) {
|
|
3134
|
+
const content = await response.text();
|
|
3135
|
+
const filePath = join17(contextDir, file.fileName);
|
|
3136
|
+
mkdirSync(dirname4(filePath), { recursive: true });
|
|
3137
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
3138
|
+
downloadedCount++;
|
|
3139
|
+
} else {
|
|
3140
|
+
failedCount++;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
if (failedCount > 0) {
|
|
3144
|
+
spinner.warn(
|
|
3145
|
+
`Downloaded ${downloadedCount}/${files.length} files to ./${DEFAULT_CONTEXT_FOLDER}/ (${failedCount} failed)`
|
|
3146
|
+
);
|
|
3147
|
+
} else {
|
|
3148
|
+
spinner.succeed(`Context files downloaded to ./${DEFAULT_CONTEXT_FOLDER}/`);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
ensureGitignore(repoRoot, DEFAULT_CONTEXT_FOLDER);
|
|
3152
|
+
} catch {
|
|
3153
|
+
}
|
|
3154
|
+
} else {
|
|
3155
|
+
spinner.warn("No context files found on server");
|
|
3156
|
+
}
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
3159
|
+
spinner.warn(
|
|
3160
|
+
chalk6.yellow(
|
|
3161
|
+
`Cannot reach RepoWise servers to download context: ${msg}
|
|
3162
|
+
Files are stored on our servers (not in git). Retry when online.`
|
|
3163
|
+
)
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const contextFolder = DEFAULT_CONTEXT_FOLDER;
|
|
3168
|
+
let contextFiles = [];
|
|
3169
|
+
if (repoRoot) {
|
|
3170
|
+
contextFiles = await scanLocalContextFiles(repoRoot, contextFolder);
|
|
3171
|
+
}
|
|
3172
|
+
if (contextFiles.length === 0) {
|
|
3173
|
+
console.log(
|
|
3174
|
+
chalk6.yellow(
|
|
3175
|
+
` No context files found in ${contextFolder}/. Try re-running \`repowise create\`.`
|
|
3176
|
+
)
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
if (tools.length > 0 && repoRoot) {
|
|
3180
|
+
spinner.start("Configuring AI tools...");
|
|
3181
|
+
const results = [];
|
|
3182
|
+
for (const tool of tools) {
|
|
3183
|
+
const { created: wasCreated } = await updateToolConfig(
|
|
3184
|
+
repoRoot,
|
|
3185
|
+
tool,
|
|
3186
|
+
repoName,
|
|
3187
|
+
contextFolder,
|
|
3188
|
+
contextFiles
|
|
3189
|
+
);
|
|
3190
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
3191
|
+
const action = wasCreated ? "Created" : "Updated";
|
|
3192
|
+
results.push(` ${action} ${config2.filePath}`);
|
|
3193
|
+
}
|
|
3194
|
+
spinner.succeed("AI tools configured");
|
|
3195
|
+
console.log(chalk6.dim(results.join("\n")));
|
|
3196
|
+
}
|
|
3197
|
+
const updatedRepos = [];
|
|
3198
|
+
if (repoRoot) {
|
|
3199
|
+
const repoEntry = {
|
|
3200
|
+
repoId,
|
|
3201
|
+
localPath: repoRoot,
|
|
3202
|
+
apiUrl: getEnvConfig().apiUrl
|
|
3203
|
+
};
|
|
3204
|
+
if (repoPlatform) repoEntry.platform = repoPlatform;
|
|
3205
|
+
if (repoExternalId) repoEntry.externalId = repoExternalId;
|
|
3206
|
+
updatedRepos.push(repoEntry);
|
|
3207
|
+
}
|
|
3208
|
+
const configUpdate = { aiTools: tools, contextFolder };
|
|
3209
|
+
if (updatedRepos.length > 0) configUpdate.repos = updatedRepos;
|
|
3210
|
+
await mergeAndSaveConfig(configUpdate);
|
|
3211
|
+
let listenerRunning = false;
|
|
3212
|
+
try {
|
|
3213
|
+
await install();
|
|
3214
|
+
listenerRunning = true;
|
|
3215
|
+
} catch {
|
|
3216
|
+
try {
|
|
3217
|
+
await startBackground();
|
|
3218
|
+
listenerRunning = true;
|
|
3219
|
+
} catch {
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
if (!listenerRunning) {
|
|
3223
|
+
console.log(
|
|
3224
|
+
chalk6.yellow(
|
|
3225
|
+
"Warning: Could not start listener automatically. Run the following to enable it:"
|
|
3226
|
+
)
|
|
3227
|
+
);
|
|
3228
|
+
console.log(chalk6.yellow(` $ repowise listen --install`));
|
|
3229
|
+
}
|
|
3230
|
+
const elapsed = formatElapsed(Date.now() - startTime);
|
|
3231
|
+
console.log("");
|
|
3232
|
+
console.log(chalk6.green.bold(" All done! Setup complete!"));
|
|
3233
|
+
console.log(
|
|
3234
|
+
chalk6.green(
|
|
3235
|
+
` Your AI tools now have access to project context for ${chalk6.bold(repoName)}.`
|
|
3236
|
+
)
|
|
3237
|
+
);
|
|
3238
|
+
if (listenerRunning) {
|
|
3239
|
+
console.log("");
|
|
3240
|
+
console.log(chalk6.cyan(" The RepoWise listener is running in the background \u2014"));
|
|
3241
|
+
console.log(chalk6.cyan(" your context will stay in sync automatically."));
|
|
3242
|
+
console.log(chalk6.cyan(" Go back to coding, we've got it from here!"));
|
|
3243
|
+
}
|
|
3244
|
+
console.log("");
|
|
3245
|
+
console.log(
|
|
3246
|
+
chalk6.cyan(
|
|
3247
|
+
' Head back to the dashboard and click "Complete Onboarding" to explore your RepoWise dashboard!'
|
|
3248
|
+
)
|
|
3249
|
+
);
|
|
3250
|
+
console.log(chalk6.dim(`
|
|
3251
|
+
Total time: ${elapsed}`));
|
|
3252
|
+
} catch (err) {
|
|
3253
|
+
const message = err instanceof Error ? err.message : "Create failed";
|
|
3254
|
+
spinner.fail(chalk6.red(message));
|
|
3255
|
+
process.exitCode = 1;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
// src/commands/member.ts
|
|
3260
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3261
|
+
import { dirname as dirname5, join as join18 } from "path";
|
|
3262
|
+
import chalk7 from "chalk";
|
|
3263
|
+
import ora2 from "ora";
|
|
3264
|
+
var DEFAULT_CONTEXT_FOLDER2 = "repowise-context";
|
|
3265
|
+
function formatElapsed2(ms) {
|
|
3266
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
3267
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3268
|
+
const seconds = totalSeconds % 60;
|
|
3269
|
+
if (minutes === 0) return `${seconds}s`;
|
|
3270
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
3271
|
+
}
|
|
3272
|
+
async function member() {
|
|
3273
|
+
const startTime = Date.now();
|
|
3274
|
+
const spinner = ora2("Checking authentication...").start();
|
|
3275
|
+
try {
|
|
3276
|
+
let credentials = await getValidCredentials2();
|
|
3277
|
+
if (!credentials) {
|
|
3278
|
+
spinner.info(chalk7.yellow("Not logged in. Opening browser to authenticate..."));
|
|
3279
|
+
credentials = await performLogin();
|
|
3280
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
3281
|
+
spinner.succeed(chalk7.green(`Authenticated as ${chalk7.bold(email)}`));
|
|
3282
|
+
} else {
|
|
3283
|
+
spinner.succeed("Authenticated");
|
|
3284
|
+
}
|
|
3285
|
+
let repoId;
|
|
3286
|
+
let repoName;
|
|
3287
|
+
let repoRoot;
|
|
3288
|
+
let repoPlatform;
|
|
3289
|
+
let repoExternalId;
|
|
3290
|
+
spinner.start("Checking for pending repository...");
|
|
3291
|
+
try {
|
|
3292
|
+
const pending = await apiRequest("/v1/onboarding/pending");
|
|
3293
|
+
if (pending?.repoId) {
|
|
3294
|
+
repoId = pending.repoId;
|
|
3295
|
+
repoName = pending.repoName;
|
|
3296
|
+
spinner.succeed(`Found pending repository: ${chalk7.bold(repoName)}`);
|
|
3297
|
+
apiRequest("/v1/onboarding/pending", { method: "DELETE" }).catch(() => {
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
} catch {
|
|
3301
|
+
}
|
|
3302
|
+
if (!repoId) {
|
|
3303
|
+
spinner.text = "Detecting repository...";
|
|
3304
|
+
try {
|
|
3305
|
+
repoRoot = detectRepoRoot();
|
|
3306
|
+
repoName = detectRepoName(repoRoot);
|
|
3307
|
+
spinner.succeed(`Repository: ${chalk7.bold(repoName)}`);
|
|
3308
|
+
} catch {
|
|
3309
|
+
spinner.fail(
|
|
3310
|
+
chalk7.red("Not in a git repository. Run this command from your project directory.")
|
|
3311
|
+
);
|
|
3312
|
+
process.exitCode = 1;
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
} else {
|
|
3316
|
+
try {
|
|
3317
|
+
repoRoot = detectRepoRoot();
|
|
3318
|
+
const localRepoName = detectRepoName(repoRoot);
|
|
3319
|
+
if (repoName && localRepoName !== repoName && !repoName.endsWith(`/${localRepoName}`)) {
|
|
3320
|
+
spinner.warn(
|
|
3321
|
+
chalk7.yellow(
|
|
3322
|
+
`Pending repo is ${chalk7.bold(repoName)} but you're in ${chalk7.bold(localRepoName)}.
|
|
3323
|
+
Make sure you're in the right project directory, or the context files will be saved here.`
|
|
3324
|
+
)
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
} catch {
|
|
3328
|
+
spinner.fail(
|
|
3329
|
+
chalk7.red(
|
|
3330
|
+
"Please navigate to your project directory first, then run `repowise member` again."
|
|
3331
|
+
)
|
|
3332
|
+
);
|
|
3333
|
+
process.exitCode = 1;
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (!repoId || !repoPlatform) {
|
|
3338
|
+
spinner.start("Looking up repository...");
|
|
3339
|
+
try {
|
|
3340
|
+
const repos = await apiRequest("/v1/repos");
|
|
3341
|
+
const match = repos.find(
|
|
3342
|
+
(r) => r.repoId === repoId || r.name === repoName || r.fullName.endsWith(`/${repoName}`)
|
|
3343
|
+
);
|
|
3344
|
+
if (match) {
|
|
3345
|
+
repoId = match.repoId;
|
|
3346
|
+
repoPlatform = match.platform;
|
|
3347
|
+
repoExternalId = match.externalId;
|
|
3348
|
+
if (!repoName) repoName = match.fullName;
|
|
3349
|
+
spinner.succeed(`Repository: ${chalk7.bold(match.fullName)}`);
|
|
3350
|
+
}
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
if (!repoId) {
|
|
3355
|
+
spinner.fail(
|
|
3356
|
+
chalk7.red(
|
|
3357
|
+
"This repository is not connected to your team. Ask your team admin to add it on the dashboard."
|
|
3358
|
+
)
|
|
3359
|
+
);
|
|
3360
|
+
process.exitCode = 1;
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
spinner.start("Checking for context files...");
|
|
3364
|
+
let files = [];
|
|
3365
|
+
try {
|
|
3366
|
+
const listResult = await apiRequest(`/v1/repos/${repoId}/context`);
|
|
3367
|
+
files = listResult.data?.files ?? listResult.files ?? [];
|
|
3368
|
+
} catch {
|
|
3369
|
+
spinner.fail(chalk7.red("Failed to check context files on server."));
|
|
3370
|
+
process.exitCode = 1;
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (files.length === 0) {
|
|
3374
|
+
spinner.fail(
|
|
3375
|
+
chalk7.red(
|
|
3376
|
+
"No context files found for this repository. Your team admin needs to run the initial scan first."
|
|
3377
|
+
)
|
|
3378
|
+
);
|
|
3379
|
+
process.exitCode = 1;
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
spinner.succeed(`Found ${chalk7.bold(files.length)} context files on server`);
|
|
3383
|
+
const { tools } = await selectAiTools();
|
|
3384
|
+
spinner.start("Downloading context files...");
|
|
3385
|
+
const contextDir = join18(repoRoot, DEFAULT_CONTEXT_FOLDER2);
|
|
3386
|
+
mkdirSync2(contextDir, { recursive: true });
|
|
3387
|
+
let downloadedCount = 0;
|
|
3388
|
+
let failedCount = 0;
|
|
3389
|
+
for (const file of files) {
|
|
3390
|
+
if (file.fileName.includes("..")) {
|
|
3391
|
+
failedCount++;
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
try {
|
|
3395
|
+
const urlResult = await apiRequest(`/v1/repos/${repoId}/context/files/${file.fileName}`);
|
|
3396
|
+
const presignedUrl = urlResult.data?.url ?? urlResult.url;
|
|
3397
|
+
const response = await fetch(presignedUrl);
|
|
3398
|
+
if (response.ok) {
|
|
3399
|
+
const content = await response.text();
|
|
3400
|
+
const filePath = join18(contextDir, file.fileName);
|
|
3401
|
+
mkdirSync2(dirname5(filePath), { recursive: true });
|
|
3402
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
3403
|
+
downloadedCount++;
|
|
3404
|
+
} else {
|
|
3405
|
+
failedCount++;
|
|
3406
|
+
}
|
|
3407
|
+
} catch {
|
|
3408
|
+
failedCount++;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
if (failedCount > 0) {
|
|
3412
|
+
spinner.warn(
|
|
3413
|
+
`Downloaded ${downloadedCount}/${files.length} files to ./${DEFAULT_CONTEXT_FOLDER2}/ (${failedCount} failed)`
|
|
3414
|
+
);
|
|
3415
|
+
} else {
|
|
3416
|
+
spinner.succeed(`Downloaded ${downloadedCount} files to ./${DEFAULT_CONTEXT_FOLDER2}/`);
|
|
3417
|
+
}
|
|
3418
|
+
try {
|
|
3419
|
+
ensureGitignore(repoRoot, DEFAULT_CONTEXT_FOLDER2);
|
|
3420
|
+
} catch {
|
|
3421
|
+
}
|
|
3422
|
+
if (tools.length > 0) {
|
|
3423
|
+
spinner.start("Configuring AI tools...");
|
|
3424
|
+
const contextFiles = await scanLocalContextFiles(repoRoot, DEFAULT_CONTEXT_FOLDER2);
|
|
3425
|
+
const configured = [];
|
|
3426
|
+
for (const tool of tools) {
|
|
3427
|
+
const { created } = await updateToolConfig(
|
|
3428
|
+
repoRoot,
|
|
3429
|
+
tool,
|
|
3430
|
+
repoName,
|
|
3431
|
+
DEFAULT_CONTEXT_FOLDER2,
|
|
3432
|
+
contextFiles
|
|
3433
|
+
);
|
|
3434
|
+
const config2 = AI_TOOL_CONFIG[tool];
|
|
3435
|
+
configured.push(`${created ? "Created" : "Updated"} ${config2.filePath}`);
|
|
3436
|
+
}
|
|
3437
|
+
spinner.succeed("AI tools configured");
|
|
3438
|
+
for (const msg of configured) {
|
|
3439
|
+
console.log(chalk7.dim(` ${msg}`));
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
spinner.start("Saving configuration...");
|
|
3443
|
+
const repoEntry = {
|
|
3444
|
+
repoId,
|
|
3445
|
+
localPath: repoRoot,
|
|
3446
|
+
apiUrl: getEnvConfig().apiUrl
|
|
3447
|
+
};
|
|
3448
|
+
if (repoPlatform) repoEntry.platform = repoPlatform;
|
|
3449
|
+
if (repoExternalId) repoEntry.externalId = repoExternalId;
|
|
3450
|
+
await mergeAndSaveConfig({
|
|
3451
|
+
aiTools: tools,
|
|
3452
|
+
contextFolder: DEFAULT_CONTEXT_FOLDER2,
|
|
3453
|
+
repos: [repoEntry]
|
|
3454
|
+
});
|
|
3455
|
+
spinner.succeed("Configuration saved");
|
|
3456
|
+
let listenerRunning = false;
|
|
3457
|
+
try {
|
|
3458
|
+
await install();
|
|
3459
|
+
listenerRunning = true;
|
|
3460
|
+
} catch {
|
|
3461
|
+
try {
|
|
3462
|
+
await startBackground();
|
|
3463
|
+
listenerRunning = true;
|
|
3464
|
+
} catch {
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
try {
|
|
3468
|
+
await apiRequest("/v1/onboarding/complete", {
|
|
3469
|
+
method: "POST",
|
|
3470
|
+
body: JSON.stringify({ repoId })
|
|
3471
|
+
});
|
|
3472
|
+
} catch {
|
|
3473
|
+
}
|
|
3474
|
+
const elapsed = formatElapsed2(Date.now() - startTime);
|
|
3475
|
+
console.log("");
|
|
3476
|
+
console.log(chalk7.green.bold("All done! Context downloaded and configured."));
|
|
3477
|
+
console.log("");
|
|
3478
|
+
console.log(`Your AI tools now have access to project context for ${chalk7.bold(repoName)}.`);
|
|
3479
|
+
console.log(
|
|
3480
|
+
`Downloaded ${chalk7.bold(downloadedCount)} context files to ./${DEFAULT_CONTEXT_FOLDER2}/`
|
|
3481
|
+
);
|
|
3482
|
+
console.log("");
|
|
3483
|
+
if (listenerRunning) {
|
|
3484
|
+
console.log(
|
|
3485
|
+
chalk7.dim(
|
|
3486
|
+
"The RepoWise listener is running in the background \u2014\nyour context will stay in sync automatically."
|
|
3487
|
+
)
|
|
3488
|
+
);
|
|
3489
|
+
} else {
|
|
3490
|
+
console.log(
|
|
3491
|
+
chalk7.yellow(
|
|
3492
|
+
"Could not start listener automatically. Run `repowise listen --install` to enable auto-sync."
|
|
3493
|
+
)
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
console.log("");
|
|
3497
|
+
console.log(
|
|
3498
|
+
chalk7.dim(`Head back to the dashboard and click "Complete Onboarding" to finish setup.`)
|
|
3499
|
+
);
|
|
3500
|
+
console.log(chalk7.dim(`Total time: ${elapsed}`));
|
|
3501
|
+
} catch (err) {
|
|
3502
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3503
|
+
spinner.fail(chalk7.red(`Setup failed: ${message}`));
|
|
3504
|
+
process.exitCode = 1;
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// src/commands/login.ts
|
|
3509
|
+
import chalk8 from "chalk";
|
|
3510
|
+
import ora3 from "ora";
|
|
3511
|
+
async function login(options = {}) {
|
|
3512
|
+
const spinner = ora3("Preparing login...").start();
|
|
3513
|
+
try {
|
|
3514
|
+
const codeVerifier = generateCodeVerifier();
|
|
3515
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
3516
|
+
const state = generateState();
|
|
3517
|
+
const authorizeUrl = getAuthorizeUrl(codeChallenge, state);
|
|
3518
|
+
const callbackPromise = startCallbackServer();
|
|
3519
|
+
if (options.browser === false) {
|
|
3520
|
+
spinner.stop();
|
|
3521
|
+
console.log(`
|
|
3522
|
+
Open this URL in your browser to authenticate:
|
|
3523
|
+
`);
|
|
3524
|
+
console.log(chalk8.cyan(authorizeUrl));
|
|
3525
|
+
console.log(`
|
|
3526
|
+
Waiting for authentication...`);
|
|
3527
|
+
} else {
|
|
3528
|
+
spinner.text = "Opening browser for authentication...";
|
|
3529
|
+
try {
|
|
3530
|
+
const open = (await import("open")).default;
|
|
3531
|
+
await open(authorizeUrl);
|
|
3532
|
+
spinner.text = "Waiting for authentication in browser...";
|
|
3533
|
+
} catch {
|
|
3534
|
+
spinner.stop();
|
|
3535
|
+
console.log(`
|
|
3536
|
+
Could not open browser automatically. Open this URL:
|
|
3537
|
+
`);
|
|
3538
|
+
console.log(chalk8.cyan(authorizeUrl));
|
|
3539
|
+
console.log(`
|
|
3540
|
+
Waiting for authentication...`);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
3544
|
+
if (returnedState !== state) {
|
|
3545
|
+
throw new Error("State mismatch \u2014 possible CSRF attack. Please try again.");
|
|
3546
|
+
}
|
|
3547
|
+
spinner.start("Exchanging authorization code...");
|
|
3548
|
+
const credentials = await exchangeCodeForTokens(code, codeVerifier);
|
|
3549
|
+
credentials.cognito = getCognitoConfigForStorage();
|
|
3550
|
+
await storeCredentials2(credentials);
|
|
3551
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
3552
|
+
spinner.succeed(chalk8.green(`Logged in as ${chalk8.bold(email)}`));
|
|
3553
|
+
} catch (err) {
|
|
3554
|
+
const message = err instanceof Error ? err.message : "Login failed";
|
|
3555
|
+
spinner.fail(chalk8.red(message));
|
|
3556
|
+
process.exitCode = 1;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// src/commands/logout.ts
|
|
3561
|
+
import chalk9 from "chalk";
|
|
3562
|
+
async function logout() {
|
|
3563
|
+
const creds = await getStoredCredentials2();
|
|
3564
|
+
if (!creds) {
|
|
3565
|
+
console.log(chalk9.yellow("Not logged in."));
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
await clearCredentials();
|
|
3569
|
+
console.log(chalk9.green("Logged out successfully."));
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// src/commands/status.ts
|
|
3573
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3574
|
+
import { basename as basename2, join as join19 } from "path";
|
|
3575
|
+
async function status() {
|
|
3576
|
+
const configDir = getConfigDir2();
|
|
3577
|
+
const STATE_PATH = join19(configDir, "listener-state.json");
|
|
3578
|
+
const CONFIG_PATH = join19(configDir, "config.json");
|
|
3579
|
+
let state = null;
|
|
3580
|
+
try {
|
|
3581
|
+
const data = await readFile8(STATE_PATH, "utf-8");
|
|
3582
|
+
state = JSON.parse(data);
|
|
3583
|
+
} catch {
|
|
3584
|
+
}
|
|
3585
|
+
let processRunning = false;
|
|
3586
|
+
let pid = null;
|
|
3587
|
+
try {
|
|
3588
|
+
const processStatus = await getStatus();
|
|
3589
|
+
processRunning = processStatus.running;
|
|
3590
|
+
pid = processStatus.pid;
|
|
3591
|
+
} catch {
|
|
3592
|
+
}
|
|
3593
|
+
let serviceInstalled = false;
|
|
3594
|
+
try {
|
|
3595
|
+
serviceInstalled = await isInstalled();
|
|
3596
|
+
} catch {
|
|
3597
|
+
}
|
|
3598
|
+
console.log("RepoWise Status");
|
|
3599
|
+
console.log("===============");
|
|
3600
|
+
if (processRunning) {
|
|
3601
|
+
console.log(`Listener: running (PID: ${pid})`);
|
|
3602
|
+
} else {
|
|
3603
|
+
console.log("Listener: stopped");
|
|
3604
|
+
}
|
|
3605
|
+
if (serviceInstalled) {
|
|
3606
|
+
console.log("Auto-start: enabled");
|
|
3607
|
+
} else {
|
|
3608
|
+
console.log("Auto-start: disabled");
|
|
3609
|
+
}
|
|
3610
|
+
console.log("");
|
|
3611
|
+
if (!state || Object.keys(state.repos).length === 0) {
|
|
3612
|
+
console.log("No sync history. Run `repowise listen` to start syncing.");
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
const repoNames = /* @__PURE__ */ new Map();
|
|
3616
|
+
try {
|
|
3617
|
+
const configData = await readFile8(CONFIG_PATH, "utf-8");
|
|
3618
|
+
const config2 = JSON.parse(configData);
|
|
3619
|
+
for (const repo of config2.repos ?? []) {
|
|
3620
|
+
repoNames.set(repo.repoId, basename2(repo.localPath));
|
|
3621
|
+
}
|
|
3622
|
+
} catch {
|
|
3623
|
+
}
|
|
3624
|
+
console.log("Watched Repos:");
|
|
3625
|
+
for (const [repoId, repoState] of Object.entries(state.repos)) {
|
|
3626
|
+
const name = repoNames.get(repoId);
|
|
3627
|
+
const label = name ? `${name} (${repoId.slice(0, 8)})` : repoId;
|
|
3628
|
+
const syncTime = repoState.lastSyncTimestamp ? new Date(repoState.lastSyncTimestamp).toLocaleString() : "never";
|
|
3629
|
+
const commit = repoState.lastSyncCommitSha ? repoState.lastSyncCommitSha.slice(0, 7) : "none";
|
|
3630
|
+
console.log(` ${label}: last sync ${syncTime} (commit: ${commit})`);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// src/commands/sync.ts
|
|
3635
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
3636
|
+
import { dirname as dirname6, join as join20 } from "path";
|
|
3637
|
+
import chalk10 from "chalk";
|
|
3638
|
+
import ora4 from "ora";
|
|
3639
|
+
var POLL_INTERVAL_MS2 = 3e3;
|
|
3640
|
+
var MAX_POLL_ATTEMPTS2 = 7200;
|
|
3641
|
+
var DEFAULT_CONTEXT_FOLDER3 = "repowise-context";
|
|
3642
|
+
function formatElapsed3(ms) {
|
|
3643
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
3644
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3645
|
+
const seconds = totalSeconds % 60;
|
|
3646
|
+
if (minutes === 0) return `${seconds}s`;
|
|
3647
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
3648
|
+
}
|
|
3649
|
+
async function sync() {
|
|
3650
|
+
const startTime = Date.now();
|
|
3651
|
+
const spinner = ora4("Checking authentication...").start();
|
|
3652
|
+
try {
|
|
3653
|
+
let credentials = await getValidCredentials2();
|
|
3654
|
+
if (!credentials) {
|
|
3655
|
+
spinner.info(chalk10.yellow("Not logged in. Opening browser to authenticate..."));
|
|
3656
|
+
credentials = await performLogin();
|
|
3657
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
3658
|
+
spinner.succeed(chalk10.green(`Authenticated as ${chalk10.bold(email)}`));
|
|
3659
|
+
} else {
|
|
3660
|
+
spinner.succeed("Authenticated");
|
|
3661
|
+
}
|
|
3662
|
+
let repoRoot;
|
|
3663
|
+
let repoName;
|
|
3664
|
+
spinner.start("Detecting repository...");
|
|
3665
|
+
try {
|
|
3666
|
+
repoRoot = detectRepoRoot();
|
|
3667
|
+
repoName = detectRepoName(repoRoot);
|
|
3668
|
+
spinner.succeed(`Repository: ${chalk10.bold(repoName)}`);
|
|
3669
|
+
} catch {
|
|
3670
|
+
spinner.fail(
|
|
3671
|
+
chalk10.red("Not in a git repository. Run this command from your repo directory.")
|
|
3672
|
+
);
|
|
3673
|
+
process.exitCode = 1;
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
let repoId;
|
|
3677
|
+
let repoPlatform;
|
|
3678
|
+
let repoExternalId;
|
|
3679
|
+
spinner.start("Resolving repository...");
|
|
3680
|
+
try {
|
|
3681
|
+
const repos = await apiRequest("/v1/repos");
|
|
3682
|
+
const match = repos.find((r) => r.name === repoName || r.fullName.endsWith(`/${repoName}`));
|
|
3683
|
+
if (match) {
|
|
3684
|
+
repoId = match.repoId;
|
|
3685
|
+
repoPlatform = match.platform;
|
|
3686
|
+
repoExternalId = match.externalId;
|
|
3687
|
+
}
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
if (!repoId) {
|
|
3691
|
+
spinner.fail(
|
|
3692
|
+
chalk10.red(
|
|
3693
|
+
"Could not find this repository in your RepoWise account. Run `repowise create` first."
|
|
3694
|
+
)
|
|
3695
|
+
);
|
|
3696
|
+
process.exitCode = 1;
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
spinner.succeed("Repository resolved");
|
|
3700
|
+
spinner.start("Triggering incremental sync...");
|
|
3701
|
+
let syncId;
|
|
3702
|
+
try {
|
|
3703
|
+
const triggerResult = await apiRequest(`/v1/repos/${repoId}/sync`, {
|
|
3704
|
+
method: "POST",
|
|
3705
|
+
body: JSON.stringify({ scanType: "incremental" })
|
|
3706
|
+
});
|
|
3707
|
+
syncId = triggerResult.syncId;
|
|
3708
|
+
} catch (triggerErr) {
|
|
3709
|
+
const msg = triggerErr instanceof Error ? triggerErr.message : "";
|
|
3710
|
+
if (!msg.toLowerCase().includes("already running")) {
|
|
3711
|
+
throw triggerErr;
|
|
3712
|
+
}
|
|
3713
|
+
spinner.text = "Resuming existing sync...";
|
|
3714
|
+
const syncs = await apiRequest(
|
|
3715
|
+
`/v1/repos/${repoId}/syncs?limit=10`
|
|
3716
|
+
);
|
|
3717
|
+
const active = syncs.items.find(
|
|
3718
|
+
(s) => s.status === "in_progress" || s.status === "awaiting_input"
|
|
3719
|
+
);
|
|
3720
|
+
if (!active) {
|
|
3721
|
+
spinner.text = "Retrying...";
|
|
3722
|
+
const retryResult = await apiRequest(`/v1/repos/${repoId}/sync`, {
|
|
3723
|
+
method: "POST",
|
|
3724
|
+
body: JSON.stringify({ scanType: "incremental" })
|
|
3725
|
+
});
|
|
3726
|
+
syncId = retryResult.syncId;
|
|
3727
|
+
} else {
|
|
3728
|
+
syncId = active.syncId;
|
|
3729
|
+
spinner.info(chalk10.cyan("Resuming existing sync..."));
|
|
3730
|
+
spinner.start();
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
let pollAttempts = 0;
|
|
3734
|
+
const progressRenderer = new ProgressRenderer();
|
|
3735
|
+
while (true) {
|
|
3736
|
+
if (++pollAttempts > MAX_POLL_ATTEMPTS2) {
|
|
3737
|
+
spinner.fail(chalk10.red("Sync timed out. Check dashboard for status."));
|
|
3738
|
+
process.exitCode = 1;
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
|
|
3742
|
+
const syncResult = await apiRequest(`/v1/sync/${syncId}/status`);
|
|
3743
|
+
progressRenderer.update(syncResult, spinner);
|
|
3744
|
+
if (syncResult.status === "awaiting_input" && syncResult.questionId && syncResult.questionText) {
|
|
3745
|
+
spinner.stop();
|
|
3746
|
+
await handleInterview(
|
|
3747
|
+
syncId,
|
|
3748
|
+
syncResult.questionId,
|
|
3749
|
+
syncResult.questionText,
|
|
3750
|
+
syncResult.questionContext ?? void 0,
|
|
3751
|
+
syncResult.discoveryResult?.estimatedInterviewQuestions
|
|
3752
|
+
);
|
|
3753
|
+
spinner.start("Resuming sync...");
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
if (syncResult.status === "completed") {
|
|
3757
|
+
progressRenderer.finalize();
|
|
3758
|
+
const generatedFiles = syncResult.filesGenerated ?? [];
|
|
3759
|
+
const fileCount = generatedFiles.length;
|
|
3760
|
+
if (fileCount > 0) {
|
|
3761
|
+
spinner.succeed(chalk10.green(`Incremental update complete \u2014 ${fileCount} files updated`));
|
|
3762
|
+
} else {
|
|
3763
|
+
spinner.succeed(chalk10.green("Incremental sync complete \u2014 no files needed updating"));
|
|
3764
|
+
}
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
if (syncResult.status === "failed") {
|
|
3768
|
+
progressRenderer.finalize();
|
|
3769
|
+
spinner.fail(chalk10.red(`Sync failed: ${syncResult.error ?? "Unknown error"}`));
|
|
3770
|
+
process.exitCode = 1;
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
spinner.start("Downloading context files from server...");
|
|
3775
|
+
try {
|
|
3776
|
+
const listResult = await apiRequest(`/v1/repos/${repoId}/context`);
|
|
3777
|
+
const files = listResult.data?.files ?? listResult.files ?? [];
|
|
3778
|
+
if (files.length > 0) {
|
|
3779
|
+
const contextDir = join20(repoRoot, DEFAULT_CONTEXT_FOLDER3);
|
|
3780
|
+
mkdirSync3(contextDir, { recursive: true });
|
|
3781
|
+
let downloadedCount = 0;
|
|
3782
|
+
let failedCount = 0;
|
|
3783
|
+
for (const file of files) {
|
|
3784
|
+
if (file.fileName.includes("..")) {
|
|
3785
|
+
failedCount++;
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
const urlResult = await apiRequest(`/v1/repos/${repoId}/context/files/${file.fileName}`);
|
|
3789
|
+
const presignedUrl = urlResult.data?.url ?? urlResult.url;
|
|
3790
|
+
const response = await fetch(presignedUrl);
|
|
3791
|
+
if (response.ok) {
|
|
3792
|
+
const content = await response.text();
|
|
3793
|
+
const filePath = join20(contextDir, file.fileName);
|
|
3794
|
+
mkdirSync3(dirname6(filePath), { recursive: true });
|
|
3795
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
3796
|
+
downloadedCount++;
|
|
3797
|
+
} else {
|
|
3798
|
+
failedCount++;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
if (failedCount > 0) {
|
|
3802
|
+
spinner.warn(
|
|
3803
|
+
`Downloaded ${downloadedCount}/${files.length} files to ./${DEFAULT_CONTEXT_FOLDER3}/ (${failedCount} failed)`
|
|
3804
|
+
);
|
|
3805
|
+
} else {
|
|
3806
|
+
spinner.succeed(`Context files downloaded to ./${DEFAULT_CONTEXT_FOLDER3}/`);
|
|
3807
|
+
}
|
|
3808
|
+
try {
|
|
3809
|
+
ensureGitignore(repoRoot, DEFAULT_CONTEXT_FOLDER3);
|
|
3810
|
+
} catch {
|
|
3811
|
+
}
|
|
3812
|
+
} else {
|
|
3813
|
+
spinner.info("No context files found on server");
|
|
3814
|
+
}
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
3817
|
+
spinner.warn(chalk10.yellow(`Could not download context files: ${msg}
|
|
3818
|
+
Retry when online.`));
|
|
3819
|
+
}
|
|
3820
|
+
if (repoRoot && repoId) {
|
|
3821
|
+
try {
|
|
3822
|
+
const existingConfig = await getConfig();
|
|
3823
|
+
const existingRepos = existingConfig.repos ?? [];
|
|
3824
|
+
if (!existingRepos.some((r) => r.repoId === repoId)) {
|
|
3825
|
+
const repoEntry = { repoId, localPath: repoRoot, apiUrl: getEnvConfig().apiUrl };
|
|
3826
|
+
if (repoPlatform) repoEntry.platform = repoPlatform;
|
|
3827
|
+
if (repoExternalId) repoEntry.externalId = repoExternalId;
|
|
3828
|
+
await mergeAndSaveConfig({ repos: [repoEntry] });
|
|
3829
|
+
}
|
|
3830
|
+
} catch {
|
|
3831
|
+
}
|
|
3832
|
+
try {
|
|
3833
|
+
await install();
|
|
3834
|
+
} catch {
|
|
3835
|
+
try {
|
|
3836
|
+
await startBackground();
|
|
3837
|
+
} catch {
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
const elapsed = formatElapsed3(Date.now() - startTime);
|
|
3842
|
+
console.log(chalk10.dim(`
|
|
3843
|
+
Total time: ${elapsed}`));
|
|
3844
|
+
} catch (err) {
|
|
3845
|
+
const message = err instanceof Error ? err.message : "Sync failed";
|
|
3846
|
+
spinner.fail(chalk10.red(message));
|
|
3847
|
+
process.exitCode = 1;
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
// src/commands/listen.ts
|
|
3852
|
+
async function listen(options) {
|
|
3853
|
+
if (options.install) {
|
|
3854
|
+
try {
|
|
3855
|
+
await install();
|
|
3856
|
+
console.log("Auto-start service installed. The listener will start on boot.");
|
|
3857
|
+
} catch (err) {
|
|
3858
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3859
|
+
console.error(`Failed to install auto-start service: ${message}`);
|
|
3860
|
+
console.error("You can still run the listener manually with `repowise listen`.");
|
|
3861
|
+
process.exitCode = 1;
|
|
3862
|
+
}
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
if (options.uninstall) {
|
|
3866
|
+
try {
|
|
3867
|
+
await uninstall();
|
|
3868
|
+
console.log("Auto-start service removed.");
|
|
3869
|
+
} catch (err) {
|
|
3870
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3871
|
+
console.error(`Failed to uninstall auto-start service: ${message}`);
|
|
3872
|
+
process.exitCode = 1;
|
|
3873
|
+
}
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
const credentials = await getValidCredentials2();
|
|
3877
|
+
if (!credentials) {
|
|
3878
|
+
console.error("Not logged in. Run `repowise login` first.");
|
|
3879
|
+
process.exitCode = 1;
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
console.log("Starting RepoWise listener...");
|
|
3883
|
+
try {
|
|
3884
|
+
await startListener();
|
|
3885
|
+
} catch {
|
|
3886
|
+
console.error("Failed to start listener.");
|
|
3887
|
+
process.exitCode = 1;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// src/commands/start.ts
|
|
3892
|
+
async function start() {
|
|
3893
|
+
try {
|
|
3894
|
+
if (await isRunning()) {
|
|
3895
|
+
console.log("Listener is already running.");
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
try {
|
|
3899
|
+
await install();
|
|
3900
|
+
console.log("Listener service installed and started.");
|
|
3901
|
+
} catch {
|
|
3902
|
+
const pid = await startBackground();
|
|
3903
|
+
console.log(`Listener started (PID: ${pid}).`);
|
|
3904
|
+
}
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3907
|
+
console.error(`Failed to start listener: ${message}`);
|
|
3908
|
+
process.exitCode = 1;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// src/commands/stop.ts
|
|
3913
|
+
async function stop2() {
|
|
3914
|
+
try {
|
|
3915
|
+
if (!await isRunning()) {
|
|
3916
|
+
console.log("Listener is not running.");
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
await stopProcess();
|
|
3920
|
+
try {
|
|
3921
|
+
await stopService();
|
|
3922
|
+
} catch {
|
|
3923
|
+
}
|
|
3924
|
+
console.log("Listener stopped.");
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3927
|
+
console.error(`Failed to stop listener: ${message}`);
|
|
3928
|
+
process.exitCode = 1;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/commands/config.ts
|
|
3933
|
+
import chalk11 from "chalk";
|
|
3934
|
+
import ora5 from "ora";
|
|
3935
|
+
import { select } from "@inquirer/prompts";
|
|
3936
|
+
async function config() {
|
|
3937
|
+
const spinner = ora5("Checking authentication...").start();
|
|
3938
|
+
try {
|
|
3939
|
+
let credentials = await getValidCredentials2();
|
|
3940
|
+
if (!credentials) {
|
|
3941
|
+
spinner.info(chalk11.yellow("Not logged in. Opening browser to authenticate..."));
|
|
3942
|
+
credentials = await performLogin();
|
|
3943
|
+
const { email } = decodeIdToken(credentials.idToken);
|
|
3944
|
+
spinner.succeed(chalk11.green(`Authenticated as ${chalk11.bold(email)}`));
|
|
3945
|
+
} else {
|
|
3946
|
+
spinner.succeed("Authenticated");
|
|
3947
|
+
}
|
|
3948
|
+
spinner.start("Loading repositories...");
|
|
3949
|
+
const repos = await apiRequest("/v1/repos");
|
|
3950
|
+
spinner.stop();
|
|
3951
|
+
if (repos.length === 0) {
|
|
3952
|
+
console.log(chalk11.yellow("No repositories connected. Run `repowise create` first."));
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
const repoId = await select({
|
|
3956
|
+
message: "Select a repository to configure",
|
|
3957
|
+
choices: repos.map((r) => ({
|
|
3958
|
+
name: r.fullName,
|
|
3959
|
+
value: r.repoId
|
|
3960
|
+
}))
|
|
3961
|
+
});
|
|
3962
|
+
const repo = repos.find((r) => r.repoId === repoId);
|
|
3963
|
+
const currentDelivery = repo.deliveryMode ?? "direct";
|
|
3964
|
+
const currentBranch = repo.monitoredBranch ?? repo.defaultBranch;
|
|
3965
|
+
console.log("");
|
|
3966
|
+
console.log(chalk11.bold(`Settings for ${repo.fullName}`));
|
|
3967
|
+
console.log(chalk11.dim(` Context storage: RepoWise servers`));
|
|
3968
|
+
console.log(chalk11.dim(` Delivery mode: ${currentDelivery}`));
|
|
3969
|
+
console.log(chalk11.dim(` Monitored branch: ${currentBranch}`));
|
|
3970
|
+
console.log("");
|
|
3971
|
+
const setting = await select({
|
|
3972
|
+
message: "What would you like to change?",
|
|
3973
|
+
choices: [
|
|
3974
|
+
{ name: `Delivery mode (current: ${currentDelivery})`, value: "deliveryMode" },
|
|
3975
|
+
{ name: `Monitored branch (current: ${currentBranch})`, value: "monitoredBranch" }
|
|
3976
|
+
]
|
|
3977
|
+
});
|
|
3978
|
+
const patch = {};
|
|
3979
|
+
if (setting === "deliveryMode") {
|
|
3980
|
+
const newMode = await select({
|
|
3981
|
+
message: "Delivery mode",
|
|
3982
|
+
choices: [
|
|
3983
|
+
{ name: "Direct push to branch", value: "direct" },
|
|
3984
|
+
{ name: "Create pull request", value: "pr" }
|
|
3985
|
+
],
|
|
3986
|
+
default: currentDelivery
|
|
3987
|
+
});
|
|
3988
|
+
if (newMode === currentDelivery) {
|
|
3989
|
+
console.log(chalk11.dim("No change."));
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
patch.deliveryMode = newMode;
|
|
3993
|
+
} else if (setting === "monitoredBranch") {
|
|
3994
|
+
const { input: input2 } = await import("@inquirer/prompts");
|
|
3995
|
+
const newBranch = await input2({
|
|
3996
|
+
message: "Monitored branch",
|
|
3997
|
+
default: currentBranch
|
|
3998
|
+
});
|
|
3999
|
+
if (newBranch === currentBranch) {
|
|
4000
|
+
console.log(chalk11.dim("No change."));
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
patch.monitoredBranch = newBranch;
|
|
4004
|
+
}
|
|
4005
|
+
spinner.start("Saving...");
|
|
4006
|
+
await apiRequest(`/v1/repos/${repoId}`, {
|
|
4007
|
+
method: "PATCH",
|
|
4008
|
+
body: JSON.stringify(patch)
|
|
4009
|
+
});
|
|
4010
|
+
spinner.succeed(chalk11.green("Setting updated"));
|
|
4011
|
+
} catch (err) {
|
|
4012
|
+
spinner.stop();
|
|
4013
|
+
const message = err instanceof Error ? err.message : "Config failed";
|
|
4014
|
+
console.error(chalk11.red(message));
|
|
4015
|
+
process.exitCode = 1;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
// bin/repowise.ts
|
|
4020
|
+
var __filename = fileURLToPath3(import.meta.url);
|
|
4021
|
+
var __dirname = dirname7(__filename);
|
|
4022
|
+
var pkg = JSON.parse(readFileSync2(join21(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
4023
|
+
var program = new Command();
|
|
4024
|
+
program.name(getPackageName2()).description("AI-optimized codebase context generator").version(pkg.version).hook("preAction", async () => {
|
|
4025
|
+
await showWelcome(pkg.version);
|
|
4026
|
+
});
|
|
4027
|
+
program.command("create").description("Create context for a repository").action(async () => {
|
|
4028
|
+
await create();
|
|
4029
|
+
});
|
|
4030
|
+
program.command("member").description("Download context and configure AI tools (for team members)").action(async () => {
|
|
4031
|
+
await member();
|
|
4032
|
+
});
|
|
4033
|
+
program.command("login").description("Authenticate with RepoWise").option("--no-browser", "Print login URL instead of opening browser").action(async (options) => {
|
|
4034
|
+
await login(options);
|
|
4035
|
+
});
|
|
4036
|
+
program.command("logout").description("Sign out of RepoWise").action(async () => {
|
|
4037
|
+
await logout();
|
|
4038
|
+
});
|
|
4039
|
+
program.command("status").description("Show current status").action(async () => {
|
|
4040
|
+
await status();
|
|
4041
|
+
});
|
|
4042
|
+
program.command("sync").description("Trigger a manual sync").action(async () => {
|
|
4043
|
+
await sync();
|
|
4044
|
+
});
|
|
4045
|
+
program.command("listen").description("Start the context listener").option("--install", "Install auto-start service").option("--uninstall", "Remove auto-start service").action(async (options) => {
|
|
4046
|
+
await listen(options);
|
|
4047
|
+
});
|
|
4048
|
+
program.command("start").description("Start the listener as a background process").action(async () => {
|
|
4049
|
+
await start();
|
|
4050
|
+
});
|
|
4051
|
+
program.command("stop").description("Stop the running listener process").action(async () => {
|
|
4052
|
+
await stop2();
|
|
4053
|
+
});
|
|
4054
|
+
program.command("config").description("Manage configuration").action(async () => {
|
|
4055
|
+
await config();
|
|
4056
|
+
});
|
|
4057
|
+
if (process.argv[2] === "__listener") {
|
|
4058
|
+
process.on("uncaughtException", (err) => {
|
|
4059
|
+
console.error("Listener uncaught exception:", err);
|
|
4060
|
+
});
|
|
4061
|
+
process.on("unhandledRejection", (reason) => {
|
|
4062
|
+
console.error("Listener unhandled rejection:", reason);
|
|
4063
|
+
});
|
|
4064
|
+
startListener().catch((err) => {
|
|
4065
|
+
console.error("Listener fatal error:", err);
|
|
4066
|
+
process.exitCode = 1;
|
|
4067
|
+
});
|
|
4068
|
+
} else {
|
|
4069
|
+
program.parse();
|
|
4070
|
+
}
|