repowise 0.1.42 → 0.1.44

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