revyse 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headless.js CHANGED
@@ -3116,9 +3116,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3116
3116
  * @param {string} [path]
3117
3117
  * @return {(string|null|Command)}
3118
3118
  */
3119
- executableDir(path29) {
3120
- if (path29 === void 0) return this._executableDir;
3121
- this._executableDir = path29;
3119
+ executableDir(path30) {
3120
+ if (path30 === void 0) return this._executableDir;
3121
+ this._executableDir = path30;
3122
3122
  return this;
3123
3123
  }
3124
3124
  /**
@@ -3374,8 +3374,8 @@ function useColor() {
3374
3374
  var program = new Command();
3375
3375
 
3376
3376
  // src/commands/change.ts
3377
- import { readFileSync as readFileSync12 } from "fs";
3378
- import path28 from "path";
3377
+ import { readFileSync as readFileSync13 } from "fs";
3378
+ import path29 from "path";
3379
3379
 
3380
3380
  // ../agent-core/src/path-policy.ts
3381
3381
  import { realpathSync } from "fs";
@@ -4292,10 +4292,10 @@ function mergeDefs(...defs) {
4292
4292
  function cloneDef(schema) {
4293
4293
  return mergeDefs(schema._zod.def);
4294
4294
  }
4295
- function getElementAtPath(obj, path29) {
4296
- if (!path29)
4295
+ function getElementAtPath(obj, path30) {
4296
+ if (!path30)
4297
4297
  return obj;
4298
- return path29.reduce((acc, key) => acc?.[key], obj);
4298
+ return path30.reduce((acc, key) => acc?.[key], obj);
4299
4299
  }
4300
4300
  function promiseAllObject(promisesObj) {
4301
4301
  const keys = Object.keys(promisesObj);
@@ -4635,11 +4635,11 @@ function explicitlyAborted(x, startIndex = 0) {
4635
4635
  }
4636
4636
  return false;
4637
4637
  }
4638
- function prefixIssues(path29, issues) {
4638
+ function prefixIssues(path30, issues) {
4639
4639
  return issues.map((iss) => {
4640
4640
  var _a3;
4641
4641
  (_a3 = iss).path ?? (_a3.path = []);
4642
- iss.path.unshift(path29);
4642
+ iss.path.unshift(path30);
4643
4643
  return iss;
4644
4644
  });
4645
4645
  }
@@ -5101,16 +5101,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
5101
5101
  }
5102
5102
  function formatError(error62, mapper = (issue2) => issue2.message) {
5103
5103
  const fieldErrors = { _errors: [] };
5104
- const processError = (error63, path29 = []) => {
5104
+ const processError = (error63, path30 = []) => {
5105
5105
  for (const issue2 of error63.issues) {
5106
5106
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5107
- issue2.errors.map((issues) => processError({ issues }, [...path29, ...issue2.path]));
5107
+ issue2.errors.map((issues) => processError({ issues }, [...path30, ...issue2.path]));
5108
5108
  } else if (issue2.code === "invalid_key") {
5109
- processError({ issues: issue2.issues }, [...path29, ...issue2.path]);
5109
+ processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5110
5110
  } else if (issue2.code === "invalid_element") {
5111
- processError({ issues: issue2.issues }, [...path29, ...issue2.path]);
5111
+ processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5112
5112
  } else {
5113
- const fullpath = [...path29, ...issue2.path];
5113
+ const fullpath = [...path30, ...issue2.path];
5114
5114
  if (fullpath.length === 0) {
5115
5115
  fieldErrors._errors.push(mapper(issue2));
5116
5116
  } else {
@@ -5149,17 +5149,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
5149
5149
  }
5150
5150
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
5151
5151
  const result = { errors: [] };
5152
- const processError = (error63, path29 = []) => {
5152
+ const processError = (error63, path30 = []) => {
5153
5153
  var _a3;
5154
5154
  for (const issue2 of error63.issues) {
5155
5155
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5156
- issue2.errors.map((issues) => processError({ issues }, [...path29, ...issue2.path]));
5156
+ issue2.errors.map((issues) => processError({ issues }, [...path30, ...issue2.path]));
5157
5157
  } else if (issue2.code === "invalid_key") {
5158
- processError({ issues: issue2.issues }, [...path29, ...issue2.path]);
5158
+ processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5159
5159
  } else if (issue2.code === "invalid_element") {
5160
- processError({ issues: issue2.issues }, [...path29, ...issue2.path]);
5160
+ processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5161
5161
  } else {
5162
- const fullpath = [...path29, ...issue2.path];
5162
+ const fullpath = [...path30, ...issue2.path];
5163
5163
  if (fullpath.length === 0) {
5164
5164
  result.errors.push(mapper(issue2));
5165
5165
  continue;
@@ -5198,8 +5198,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
5198
5198
  }
5199
5199
  function toDotPath(_path) {
5200
5200
  const segs = [];
5201
- const path29 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5202
- for (const seg of path29) {
5201
+ const path30 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5202
+ for (const seg of path30) {
5203
5203
  if (typeof seg === "number")
5204
5204
  segs.push(`[${seg}]`);
5205
5205
  else if (typeof seg === "symbol")
@@ -6301,9 +6301,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
6301
6301
  function stripTabAndNewline(value) {
6302
6302
  return value.replace(asciiTabOrNewline, "");
6303
6303
  }
6304
- function urlHostnameOk(url2, hostname3) {
6305
- hostname3.lastIndex = 0;
6306
- return hostname3.test(url2.hostname);
6304
+ function urlHostnameOk(url2, hostname4) {
6305
+ hostname4.lastIndex = 0;
6306
+ return hostname4.test(url2.hostname);
6307
6307
  }
6308
6308
  function urlProtocolOk(url2, protocol) {
6309
6309
  protocol.lastIndex = 0;
@@ -22337,13 +22337,13 @@ function resolveRef(ref, ctx) {
22337
22337
  if (!ref.startsWith("#")) {
22338
22338
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
22339
22339
  }
22340
- const path29 = ref.slice(1).split("/").filter(Boolean);
22341
- if (path29.length === 0) {
22340
+ const path30 = ref.slice(1).split("/").filter(Boolean);
22341
+ if (path30.length === 0) {
22342
22342
  return ctx.rootSchema;
22343
22343
  }
22344
22344
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
22345
- if (path29[0] === defsKey) {
22346
- const key = path29[1] === void 0 ? void 0 : decodeJSONPointerSegment(path29[1]);
22345
+ if (path30[0] === defsKey) {
22346
+ const key = path30[1] === void 0 ? void 0 : decodeJSONPointerSegment(path30[1]);
22347
22347
  if (!key || !ctx.defs[key]) {
22348
22348
  throw new Error(`Reference not found: ${ref}`);
22349
22349
  }
@@ -24641,16 +24641,16 @@ var Diff = class {
24641
24641
  }
24642
24642
  }
24643
24643
  }
24644
- addToPath(path29, added, removed, oldPosInc, options) {
24645
- const last = path29.lastComponent;
24644
+ addToPath(path30, added, removed, oldPosInc, options) {
24645
+ const last = path30.lastComponent;
24646
24646
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
24647
24647
  return {
24648
- oldPos: path29.oldPos + oldPosInc,
24648
+ oldPos: path30.oldPos + oldPosInc,
24649
24649
  lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
24650
24650
  };
24651
24651
  } else {
24652
24652
  return {
24653
- oldPos: path29.oldPos + oldPosInc,
24653
+ oldPos: path30.oldPos + oldPosInc,
24654
24654
  lastComponent: { count: 1, added, removed, previousComponent: last }
24655
24655
  };
24656
24656
  }
@@ -30455,17 +30455,17 @@ var writeToFiles = (chunks, stdioItems, outputFiles) => {
30455
30455
  const writtenPaths = /* @__PURE__ */ new Set();
30456
30456
  for (const stdioItem of fileItems) {
30457
30457
  const { type, append } = stdioItem;
30458
- const path29 = type === "fileNumber" ? stdioItem.path : getFileTarget(stdioItem);
30459
- if (writtenPaths.has(path29)) {
30458
+ const path30 = type === "fileNumber" ? stdioItem.path : getFileTarget(stdioItem);
30459
+ if (writtenPaths.has(path30)) {
30460
30460
  continue;
30461
30461
  }
30462
- writtenPaths.add(path29);
30463
- if (append || outputFiles.has(path29)) {
30464
- appendFileSync(path29, contents);
30462
+ writtenPaths.add(path30);
30463
+ if (append || outputFiles.has(path30)) {
30464
+ appendFileSync(path30, contents);
30465
30465
  } else {
30466
- writeFileSync2(path29, contents);
30466
+ writeFileSync2(path30, contents);
30467
30467
  }
30468
- outputFiles.add(path29);
30468
+ outputFiles.add(path30);
30469
30469
  }
30470
30470
  };
30471
30471
 
@@ -33611,7 +33611,7 @@ function createAgentApi(opts) {
33611
33611
  const f = opts.fetch ?? fetch;
33612
33612
  const base = opts.apiUrl.replace(/\/+$/, "");
33613
33613
  const auth = { authorization: `Bearer ${opts.token}` };
33614
- const call = (path29, body, signal) => f(`${base}${path29}`, {
33614
+ const call = (path30, body, signal) => f(`${base}${path30}`, {
33615
33615
  method: "POST",
33616
33616
  headers: { "content-type": "application/json", ...auth },
33617
33617
  body: JSON.stringify(body),
@@ -33627,18 +33627,18 @@ function createAgentApi(opts) {
33627
33627
  }
33628
33628
  return {
33629
33629
  baseUrl: base,
33630
- async get(path29) {
33631
- const res = await f(`${base}${path29}`, { headers: auth });
33630
+ async get(path30) {
33631
+ const res = await f(`${base}${path30}`, { headers: auth });
33632
33632
  if (!res.ok) throw await failure2(res);
33633
33633
  return await res.json();
33634
33634
  },
33635
- async post(path29, body) {
33636
- const res = await call(path29, body);
33635
+ async post(path30, body) {
33636
+ const res = await call(path30, body);
33637
33637
  if (!res.ok) throw await failure2(res);
33638
33638
  return await res.json();
33639
33639
  },
33640
- async *stream(path29, body, signal) {
33641
- const res = await call(path29, body, signal);
33640
+ async *stream(path30, body, signal) {
33641
+ const res = await call(path30, body, signal);
33642
33642
  if (!res.ok) throw await failure2(res);
33643
33643
  if (!res.body) return;
33644
33644
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
@@ -33823,27 +33823,27 @@ var StagedFiles = class {
33823
33823
  /** Paths written by the package manager (I7), not by the model: the write rules don't apply. */
33824
33824
  fromPackageManager = /* @__PURE__ */ new Set();
33825
33825
  /** Current content as the agent sees it: staged if written, else the project's. */
33826
- async read(path29) {
33827
- const s = this.staged.get(path29);
33826
+ async read(path30) {
33827
+ const s = this.staged.get(path30);
33828
33828
  if (s) return s.content;
33829
- const content = await this.host.read(path29);
33830
- if (!this.seen.has(path29)) this.seen.set(path29, content === null ? null : hashContent(content));
33829
+ const content = await this.host.read(path30);
33830
+ if (!this.seen.has(path30)) this.seen.set(path30, content === null ? null : hashContent(content));
33831
33831
  return content;
33832
33832
  }
33833
33833
  /** Like `read`, without recording a snapshot: for searching, which only looks. */
33834
- async peek(path29) {
33835
- const s = this.staged.get(path29);
33836
- return s ? s.content : this.host.read(path29);
33834
+ async peek(path30) {
33835
+ const s = this.staged.get(path30);
33836
+ return s ? s.content : this.host.read(path30);
33837
33837
  }
33838
33838
  /** Writes `content` (or deletes, with null) in the staging layer. */
33839
- async write(path29, content) {
33840
- const existing = this.staged.get(path29);
33839
+ async write(path30, content) {
33840
+ const existing = this.staged.get(path30);
33841
33841
  if (existing) {
33842
33842
  existing.content = content;
33843
33843
  return;
33844
33844
  }
33845
- const before = await this.read(path29);
33846
- this.staged.set(path29, {
33845
+ const before = await this.read(path30);
33846
+ this.staged.set(path30, {
33847
33847
  content,
33848
33848
  existed: before !== null,
33849
33849
  baseHash: before === null ? null : hashContent(before)
@@ -33854,9 +33854,9 @@ var StagedFiles = class {
33854
33854
  * a supported SDK). The patch engine applies it with the rest, so it lands, is checked and is
33855
33855
  * reverted together; the write rules that keep the model out of these files don't apply.
33856
33856
  */
33857
- async writeFromPackageManager(path29, content) {
33858
- await this.write(path29, content);
33859
- this.fromPackageManager.add(path29);
33857
+ async writeFromPackageManager(path30, content) {
33858
+ await this.write(path30, content);
33859
+ this.fromPackageManager.add(path30);
33860
33860
  }
33861
33861
  /** The paths the package manager wrote. */
33862
33862
  packageManagerPaths() {
@@ -33874,18 +33874,18 @@ var StagedFiles = class {
33874
33874
  /** What the apply phase changed, net: a file created then deleted is no change at all. */
33875
33875
  changes() {
33876
33876
  const out = [];
33877
- for (const [path29, s] of [...this.staged].sort(([a2], [b]) => a2.localeCompare(b))) {
33877
+ for (const [path30, s] of [...this.staged].sort(([a2], [b]) => a2.localeCompare(b))) {
33878
33878
  if (!s.existed && s.content === null) continue;
33879
- if (!s.existed) out.push({ path: path29, kind: "create", content: s.content });
33880
- else if (s.content === null) out.push({ path: path29, kind: "delete", baseHash: s.baseHash });
33879
+ if (!s.existed) out.push({ path: path30, kind: "create", content: s.content });
33880
+ else if (s.content === null) out.push({ path: path30, kind: "delete", baseHash: s.baseHash });
33881
33881
  else if (hashContent(s.content) !== s.baseHash)
33882
- out.push({ path: path29, kind: "modify", content: s.content, baseHash: s.baseHash });
33882
+ out.push({ path: path30, kind: "modify", content: s.content, baseHash: s.baseHash });
33883
33883
  }
33884
33884
  return out;
33885
33885
  }
33886
33886
  /** Every file the agent read or wrote, with its hash when first seen (null: didn't exist). */
33887
33887
  snapshots() {
33888
- return [...this.seen].map(([path29, hash2]) => ({ path: path29, hash: hash2 })).sort((a2, b) => a2.path.localeCompare(b.path));
33888
+ return [...this.seen].map(([path30, hash2]) => ({ path: path30, hash: hash2 })).sort((a2, b) => a2.path.localeCompare(b.path));
33889
33889
  }
33890
33890
  };
33891
33891
 
@@ -33922,7 +33922,7 @@ var ok = (callId, output2) => ({
33922
33922
  output: output2,
33923
33923
  isError: false
33924
33924
  });
33925
- var secretFile = (path29) => `Can't change ${path29}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
33925
+ var secretFile = (path30) => `Can't change ${path30}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
33926
33926
  var fail = (callId, output2) => ({
33927
33927
  callId,
33928
33928
  output: output2,
@@ -33965,12 +33965,12 @@ function createToolRuntime(opts) {
33965
33965
  const found = [];
33966
33966
  let withheld = 0;
33967
33967
  let stopped = false;
33968
- for (const path29 of await visible(input2.path, input2.glob)) {
33968
+ for (const path30 of await visible(input2.path, input2.glob)) {
33969
33969
  if (found.length >= max || Date.now() - started > budget) {
33970
33970
  stopped = true;
33971
33971
  break;
33972
33972
  }
33973
- const content = await staging.peek(path29);
33973
+ const content = await staging.peek(path30);
33974
33974
  if (content === null) continue;
33975
33975
  if (containsSecret(content)) {
33976
33976
  withheld++;
@@ -33982,7 +33982,7 @@ function createToolRuntime(opts) {
33982
33982
  if (line.length > MAX_SEARCH_LINE || !matches(line)) continue;
33983
33983
  const text = line.trim();
33984
33984
  found.push(
33985
- `${path29}:${i2 + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
33985
+ `${path30}:${i2 + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
33986
33986
  );
33987
33987
  }
33988
33988
  }
@@ -34183,7 +34183,7 @@ function createChangeRunner(opts) {
34183
34183
  log(`change ${changeId ?? "(new)"} failed: ${err.message}`);
34184
34184
  fail2(session, changeId, {
34185
34185
  code: "internal",
34186
- message: "Something went wrong in the Revyse agent. Check the `revyse dev` output.",
34186
+ message: "Something went wrong in the Revyse agent. Check your dev server's output.",
34187
34187
  retryable: true
34188
34188
  });
34189
34189
  }
@@ -34201,7 +34201,7 @@ function createChangeRunner(opts) {
34201
34201
  }
34202
34202
  function sessionOf(changeId) {
34203
34203
  const s = sessions.get(changeId);
34204
- if (!s) throw new Error("This change isn't open in this `revyse dev` session");
34204
+ if (!s) throw new Error("This change isn't open in this session of your dev server");
34205
34205
  return s;
34206
34206
  }
34207
34207
  function readFiles(paths) {
@@ -34302,7 +34302,7 @@ function createChangeRunner(opts) {
34302
34302
  `/api/changes/${session.id}/plan`,
34303
34303
  {
34304
34304
  element: session.element,
34305
- files: gathered.files.map(({ path: path29, content, hash: hash2 }) => ({ path: path29, content, hash: hash2 })),
34305
+ files: gathered.files.map(({ path: path30, content, hash: hash2 }) => ({ path: path30, content, hash: hash2 })),
34306
34306
  modelTier: session.tier,
34307
34307
  model: session.model,
34308
34308
  ...extra
@@ -34396,11 +34396,11 @@ function createChangeRunner(opts) {
34396
34396
  if (!patch) return;
34397
34397
  const [browser, baseline] = await Promise.all([browserPrep, baselinePrep]);
34398
34398
  session.stage = "applying";
34399
- const snapshots = files.map(({ path: path29, hash: hash2 }) => ({ path: path29, hash: hash2 }));
34399
+ const snapshots = files.map(({ path: path30, hash: hash2 }) => ({ path: path30, hash: hash2 }));
34400
34400
  const applied = applyPatch({
34401
34401
  projectRoot: opts.projectRoot,
34402
34402
  changeId: session.id,
34403
- patch: { files: patch.files.map(({ path: path29, edits }) => ({ path: path29, edits })) },
34403
+ patch: { files: patch.files.map(({ path: path30, edits }) => ({ path: path30, edits })) },
34404
34404
  snapshots
34405
34405
  });
34406
34406
  if (applied.status === "conflict") {
@@ -34978,13 +34978,113 @@ ${r.output.slice(-3e3)}`,
34978
34978
  };
34979
34979
  }
34980
34980
 
34981
+ // src/api-client.ts
34982
+ var DEFAULT_API_URL = "https://revyse.dev";
34983
+ function resolveApiUrl(flag, env = process.env) {
34984
+ return (flag || env.REVYSE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
34985
+ }
34986
+ var ApiError = class extends Error {
34987
+ constructor(status, code2, message) {
34988
+ super(message);
34989
+ this.status = status;
34990
+ this.code = code2;
34991
+ this.name = "ApiError";
34992
+ }
34993
+ status;
34994
+ code;
34995
+ };
34996
+ function createApiClient({ apiUrl, token, fetch: f = fetch }) {
34997
+ async function request(method, path30, body) {
34998
+ const res = await f(`${apiUrl}${path30}`, {
34999
+ method,
35000
+ headers: {
35001
+ "content-type": "application/json",
35002
+ ...token ? { authorization: `Bearer ${token}` } : {}
35003
+ },
35004
+ body: body === void 0 ? void 0 : JSON.stringify(body)
35005
+ });
35006
+ const data = await res.json().catch(() => ({}));
35007
+ if (!res.ok) {
35008
+ const code2 = data.error?.code ?? "error";
35009
+ throw new ApiError(res.status, code2, data.error?.message ?? `Request failed (${res.status})`);
35010
+ }
35011
+ return data;
35012
+ }
35013
+ return {
35014
+ get: (path30) => request("GET", path30),
35015
+ post: (path30, body) => request("POST", path30, body ?? {})
35016
+ };
35017
+ }
35018
+
35019
+ // src/connect.ts
35020
+ import { spawn as spawn2 } from "child_process";
35021
+ import { hostname as hostname3 } from "os";
35022
+
35023
+ // src/credentials.ts
35024
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
35025
+ import { homedir } from "os";
35026
+ import path25 from "path";
35027
+ function credentialsPath(env = process.env) {
35028
+ const base = env.XDG_CONFIG_HOME || path25.join(homedir(), ".config");
35029
+ return path25.join(base, "revyse", "credentials.json");
35030
+ }
35031
+ function readAll(file2) {
35032
+ if (!existsSync4(file2)) return {};
35033
+ return JSON.parse(readFileSync9(file2, "utf8"));
35034
+ }
35035
+ function writeAll(file2, data) {
35036
+ mkdirSync3(path25.dirname(file2), { recursive: true, mode: 448 });
35037
+ writeFileSync5(file2, `${JSON.stringify(data, null, 2)}
35038
+ `, { mode: 384 });
35039
+ chmodSync(file2, 384);
35040
+ }
35041
+ function loadCredential(projectRoot, file2 = credentialsPath()) {
35042
+ return readAll(file2)[path25.resolve(projectRoot)] ?? null;
35043
+ }
35044
+ function saveCredential(projectRoot, cred, file2 = credentialsPath()) {
35045
+ writeAll(file2, { ...readAll(file2), [path25.resolve(projectRoot)]: cred });
35046
+ }
35047
+
35048
+ // src/device-login.ts
35049
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
35050
+ async function deviceLogin({
35051
+ client,
35052
+ clientName,
35053
+ onCode,
35054
+ sleep = defaultSleep
35055
+ }) {
35056
+ const start = await client.post("/api/cli/device", { clientName });
35057
+ onCode(start);
35058
+ let interval = start.interval;
35059
+ const deadline = Date.now() + start.expiresIn * 1e3;
35060
+ while (Date.now() < deadline) {
35061
+ await sleep(interval * 1e3);
35062
+ try {
35063
+ return await client.post("/api/cli/token", { deviceCode: start.deviceCode });
35064
+ } catch (err) {
35065
+ if (!(err instanceof ApiError)) throw err;
35066
+ if (err.code === "authorization_pending") continue;
35067
+ if (err.code === "slow_down") {
35068
+ interval += 5;
35069
+ continue;
35070
+ }
35071
+ if (err.code === "access_denied") {
35072
+ throw new Error("Login was denied in the browser.", { cause: err });
35073
+ }
35074
+ if (err.code === "expired_token") break;
35075
+ throw err;
35076
+ }
35077
+ }
35078
+ throw new Error("Login code expired. Run `revyse login` again.");
35079
+ }
35080
+
34981
35081
  // src/init-flow.ts
34982
- import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
34983
- import path27 from "path";
35082
+ import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35083
+ import path28 from "path";
34984
35084
 
34985
35085
  // src/detect.ts
34986
- import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
34987
- import path25 from "path";
35086
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
35087
+ import path26 from "path";
34988
35088
  var LOCKFILES2 = [
34989
35089
  ["pnpm-lock.yaml", "pnpm"],
34990
35090
  ["yarn.lock", "yarn"],
@@ -34994,11 +35094,11 @@ var LOCKFILES2 = [
34994
35094
  ];
34995
35095
  var SOURCE_DIR_CANDIDATES = ["src", "app", "pages", "components", "lib"];
34996
35096
  function detectProject(root) {
34997
- const pkgPath = path25.join(root, "package.json");
34998
- if (!existsSync4(pkgPath)) {
35097
+ const pkgPath = path26.join(root, "package.json");
35098
+ if (!existsSync5(pkgPath)) {
34999
35099
  throw new Error(`No package.json found in ${root}. Run revyse init from your project root.`);
35000
35100
  }
35001
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
35101
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
35002
35102
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
35003
35103
  const warnings = [];
35004
35104
  const framework = detectFramework(deps);
@@ -35007,13 +35107,13 @@ function detectProject(root) {
35007
35107
  } else if (framework !== "nextjs") {
35008
35108
  warnings.push(`${framework} support is experimental; Next.js is the supported target.`);
35009
35109
  }
35010
- const managers = LOCKFILES2.filter(([file2]) => existsSync4(path25.join(root, file2)));
35110
+ const managers = LOCKFILES2.filter(([file2]) => existsSync5(path26.join(root, file2)));
35011
35111
  const packageManager = managers[0]?.[1] ?? "npm";
35012
35112
  if (managers.length === 0) warnings.push("No lockfile found; defaulting to npm.");
35013
35113
  if (new Set(managers.map(([, m]) => m)).size > 1) {
35014
35114
  warnings.push(`Multiple lockfiles found; using ${packageManager}.`);
35015
35115
  }
35016
- const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync4(path25.join(root, d)));
35116
+ const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync5(path26.join(root, d)));
35017
35117
  for (const d of aliasedDirs(root)) if (!sourceDirs.includes(d)) sourceDirs.push(d);
35018
35118
  if (sourceDirs.length === 0) {
35019
35119
  warnings.push("No source directories found; edit sourceDirs in revyse.config.json.");
@@ -35086,17 +35186,17 @@ function hasSource(dir, depth = 3) {
35086
35186
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
35087
35187
  if (entry.isFile() && SOURCE_FILE.test(entry.name)) return true;
35088
35188
  if (entry.isDirectory() && depth > 1 && !NOT_SOURCE.has(entry.name)) {
35089
- if (hasSource(path25.join(dir, entry.name), depth - 1)) return true;
35189
+ if (hasSource(path26.join(dir, entry.name), depth - 1)) return true;
35090
35190
  }
35091
35191
  }
35092
35192
  return false;
35093
35193
  }
35094
35194
  function aliasedDirs(root) {
35095
- const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path25.join(root, f)).find((f) => existsSync4(f));
35195
+ const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path26.join(root, f)).find((f) => existsSync5(f));
35096
35196
  if (!file2) return [];
35097
35197
  let compilerOptions;
35098
35198
  try {
35099
- compilerOptions = parseJsonc(readFileSync9(file2, "utf8")).compilerOptions;
35199
+ compilerOptions = parseJsonc(readFileSync10(file2, "utf8")).compilerOptions;
35100
35200
  } catch {
35101
35201
  return [];
35102
35202
  }
@@ -35104,15 +35204,15 @@ function aliasedDirs(root) {
35104
35204
  const baseUrl = compilerOptions?.baseUrl ?? ".";
35105
35205
  const dirs = /* @__PURE__ */ new Set();
35106
35206
  for (const target of Object.values(paths).flat()) {
35107
- const rel = path25.relative(root, path25.join(root, baseUrl, target)).split(path25.sep).join("/");
35207
+ const rel = path26.relative(root, path26.join(root, baseUrl, target)).split(path26.sep).join("/");
35108
35208
  const top = rel.split("/")[0];
35109
35209
  if (rel.startsWith("..") || NOT_SOURCE.has(top)) continue;
35110
35210
  if (top === "*" || top === "") {
35111
35211
  for (const entry of readdirSync4(root, { withFileTypes: true }))
35112
35212
  if (entry.isDirectory() && !NOT_SOURCE.has(entry.name) && !entry.name.startsWith(".")) {
35113
- if (hasSource(path25.join(root, entry.name))) dirs.add(entry.name);
35213
+ if (hasSource(path26.join(root, entry.name))) dirs.add(entry.name);
35114
35214
  }
35115
- } else if (existsSync4(path25.join(root, top)) && statSync6(path25.join(root, top)).isDirectory()) {
35215
+ } else if (existsSync5(path26.join(root, top)) && statSync6(path26.join(root, top)).isDirectory()) {
35116
35216
  dirs.add(top);
35117
35217
  }
35118
35218
  }
@@ -35154,8 +35254,8 @@ Full policy: ${DATA_HANDLING_URL}
35154
35254
  `;
35155
35255
 
35156
35256
  // src/next-config.ts
35157
- import { existsSync as existsSync5, readFileSync as readFileSync10 } from "fs";
35158
- import path26 from "path";
35257
+ import { existsSync as existsSync6, readFileSync as readFileSync11 } from "fs";
35258
+ import path27 from "path";
35159
35259
 
35160
35260
  // ../../node_modules/.pnpm/@babel+parser@8.0.7/node_modules/@babel/parser/lib/index.js
35161
35261
  var Position = class {
@@ -50913,7 +51013,7 @@ var NEXT_CONFIG_FILES = [
50913
51013
  ];
50914
51014
  function findNextConfig(root) {
50915
51015
  for (const name of NEXT_CONFIG_FILES) {
50916
- if (existsSync5(path26.join(root, name))) return name;
51016
+ if (existsSync6(path27.join(root, name))) return name;
50917
51017
  }
50918
51018
  return null;
50919
51019
  }
@@ -50968,20 +51068,72 @@ ${importLine}`);
50968
51068
  return { status: "unsupported", reason: "it has no `export default` or `module.exports`" };
50969
51069
  }
50970
51070
  function readNextConfig(root, file2) {
50971
- return readFileSync10(path26.join(root, file2), "utf8");
51071
+ return readFileSync11(path27.join(root, file2), "utf8");
50972
51072
  }
50973
51073
 
51074
+ // src/version.ts
51075
+ import { createRequire } from "module";
51076
+ var VERSION = createRequire(import.meta.url)("../package.json").version;
51077
+
50974
51078
  // src/init-flow.ts
50975
51079
  var CONFIG_FILE = "revyse.config.json";
50976
51080
  var GITIGNORE_ENTRY = ".revyse/";
51081
+ var PACKAGES = ["revyse", "@revyse/next"];
51082
+ var INSTALL = {
51083
+ npm: ["npm", "install", "--save-dev", "--no-audit", "--no-fund"],
51084
+ pnpm: ["pnpm", "add", "--save-dev"],
51085
+ yarn: ["yarn", "add", "--dev"],
51086
+ bun: ["bun", "add", "--dev"]
51087
+ };
51088
+ var installCommand = (pm, specs) => [
51089
+ ...INSTALL[pm],
51090
+ ...specs
51091
+ ];
51092
+ var runScript = (pm, script) => pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
50977
51093
  async function runInit(opts, io) {
50978
51094
  const { root } = opts;
50979
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
50980
51095
  const ask = (q) => opts.yes ? Promise.resolve(true) : io.confirm(q);
50981
- if (existsSync6(path27.join(root, CONFIG_FILE)) && !opts.force) {
50982
- io.warn(`${CONFIG_FILE} already exists. Use --force to re-run init.`);
50983
- return { status: "exists" };
51096
+ let configured;
51097
+ let status;
51098
+ if (existsSync7(path28.join(root, CONFIG_FILE)) && !opts.force) {
51099
+ const config3 = projectConfigSchema.parse(
51100
+ JSON.parse(readFileSync12(path28.join(root, CONFIG_FILE), "utf8"))
51101
+ );
51102
+ io.log(`${CONFIG_FILE} is already here; checking the rest of the setup.`);
51103
+ if (!config3.dataDisclosure || config3.dataDisclosure.version < DISCLOSURE_VERSION)
51104
+ io.warn(
51105
+ "How Revyse handles your code has changed: run `npx revyse init --force` to review it."
51106
+ );
51107
+ configured = { config: config3, nextConfigEdited: false };
51108
+ status = "exists";
51109
+ } else {
51110
+ configured = await configure(opts, io, ask);
51111
+ if (!configured) return { status: "declined" };
51112
+ status = "initialized";
51113
+ }
51114
+ const { config: config2 } = configured;
51115
+ const nextjs = config2.framework === "nextjs";
51116
+ let installed = !nextjs;
51117
+ if (nextjs && opts.setup) installed = await installPackages(root, config2, opts.setup, io, ask);
51118
+ let connection = null;
51119
+ if (opts.setup && opts.connect !== false)
51120
+ connection = await connectProject(root, config2, opts.setup, io, ask);
51121
+ if (nextjs) {
51122
+ const steps = [
51123
+ ...installed ? [] : [`install ${PACKAGES.join(" and ")} as dev dependencies`],
51124
+ ...connection ? [] : ["connect it to Revyse with `npx revyse login`"]
51125
+ ];
51126
+ io.log(
51127
+ steps.length === 0 ? `
51128
+ Done. Start your app as usual (\`${runScript(config2.packageManager, config2.scripts.dev ?? "dev")}\`) and click the Revyse button.` : `
51129
+ Next: ${steps.join(", then ")}. Then start your app as usual.`
51130
+ );
50984
51131
  }
51132
+ return { status, ...configured, connection };
51133
+ }
51134
+ async function configure(opts, io, ask) {
51135
+ const { root } = opts;
51136
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
50985
51137
  const { config: config2, warnings } = detectProject(root);
50986
51138
  io.log(`Detected ${config2.framework} (${config2.packageManager})`);
50987
51139
  io.log(`Source directories: ${config2.sourceDirs.join(", ")}`);
@@ -50989,7 +51141,7 @@ async function runInit(opts, io) {
50989
51141
  io.log(DISCLOSURE_TEXT);
50990
51142
  if (!await ask("Do you accept how Revyse handles your code?")) {
50991
51143
  io.log("Nothing was changed. Run `revyse init` again if you change your mind.");
50992
- return { status: "declined" };
51144
+ return null;
50993
51145
  }
50994
51146
  const accepted = {
50995
51147
  ...config2,
@@ -51022,46 +51174,146 @@ Revyse needs this change to ${file2} (it only has an effect in \`next dev\`):
51022
51174
  );
51023
51175
  if (await ask(`Apply this change to ${file2}?`))
51024
51176
  nextConfigEdit = { file: file2, code: result.code };
51025
- else
51026
- io.log(
51027
- `Skipped. Wrap the export of ${file2} with withRevyse() yourself before \`revyse dev\`.`
51028
- );
51177
+ else io.log(`Skipped. Wrap the export of ${file2} with withRevyse() yourself.`);
51029
51178
  }
51030
51179
  }
51031
51180
  }
51032
- writeFileSync5(path27.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
51181
+ writeFileSync6(path28.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
51033
51182
  `);
51034
- if (nextConfigEdit) writeFileSync5(path27.join(root, nextConfigEdit.file), nextConfigEdit.code);
51183
+ if (nextConfigEdit) writeFileSync6(path28.join(root, nextConfigEdit.file), nextConfigEdit.code);
51035
51184
  if (ensureGitignored(root))
51036
51185
  io.log(`Added ${GITIGNORE_ENTRY} to .gitignore (local change history).`);
51037
51186
  io.log(`
51038
51187
  Wrote ${CONFIG_FILE}.`);
51039
- if (config2.framework === "nextjs") {
51040
- io.log(
51041
- hasDependency(root, "@revyse/next") ? "Next: run `revyse dev`, then start your app's dev server as usual." : `Next: add the dev dependency \`@revyse/next\` with ${config2.packageManager}, then run \`revyse dev\`.`
51042
- );
51188
+ return { config: accepted, nextConfigEdited: Boolean(nextConfigEdit) };
51189
+ }
51190
+ async function installPackages(root, config2, setup2, io, ask) {
51191
+ const missing = PACKAGES.filter((name) => !devDependencies(root)[name]);
51192
+ if (missing.length === 0) return true;
51193
+ const specs = missing.map((name) => `${name}@${VERSION}`);
51194
+ const command = installCommand(config2.packageManager, specs).join(" ");
51195
+ if (!await ask(`Install ${missing.join(" and ")} as dev dependencies (${command})?`)) {
51196
+ io.log(`Skipped. Install them yourself: ${command}`);
51197
+ return false;
51198
+ }
51199
+ try {
51200
+ await setup2.install(root, config2.packageManager, specs);
51201
+ io.log(`Installed ${missing.join(" and ")}.`);
51202
+ return true;
51203
+ } catch (err) {
51204
+ io.warn(`Couldn't install them (${err.message}). Run: ${command}`);
51205
+ return false;
51043
51206
  }
51044
- return { status: "initialized", config: accepted, nextConfigEdited: Boolean(nextConfigEdit) };
51207
+ }
51208
+ async function connectProject(root, config2, setup2, io, ask) {
51209
+ const existing = await setup2.connection(root).catch(() => null);
51210
+ if (existing) {
51211
+ io.log(`Connected to Revyse: project "${existing.project}" (${existing.email}).`);
51212
+ return existing;
51213
+ }
51214
+ if (!await ask(
51215
+ "Connect this app to Revyse? Your browser opens to sign in and create the project."
51216
+ ))
51217
+ return null;
51218
+ try {
51219
+ const connection = await setup2.connect(root, {
51220
+ name: projectName(root),
51221
+ framework: config2.framework,
51222
+ packageManager: config2.packageManager
51223
+ });
51224
+ io.log(`Connected to Revyse: project "${connection.project}" (${connection.email}).`);
51225
+ return connection;
51226
+ } catch (err) {
51227
+ io.warn(`Couldn't connect to Revyse (${err.message}).`);
51228
+ return null;
51229
+ }
51230
+ }
51231
+ function projectName(root) {
51232
+ const name = readPackageJson(root)?.name?.replace(/^@[^/]+\//, "").trim();
51233
+ return (name || path28.basename(root)).slice(0, 100);
51045
51234
  }
51046
51235
  function ensureGitignored(root) {
51047
- const file2 = path27.join(root, ".gitignore");
51048
- const current = existsSync6(file2) ? readFileSync11(file2, "utf8") : "";
51236
+ const file2 = path28.join(root, ".gitignore");
51237
+ const current = existsSync7(file2) ? readFileSync12(file2, "utf8") : "";
51049
51238
  const lines = current.split(/\r?\n/).map((l) => l.trim());
51050
51239
  if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".revyse")) return false;
51051
51240
  const separator = current === "" ? "" : current.endsWith("\n") ? "\n" : "\n\n";
51052
- writeFileSync5(file2, `${current}${separator}# Revyse local state
51241
+ writeFileSync6(file2, `${current}${separator}# Revyse local state
51053
51242
  ${GITIGNORE_ENTRY}
51054
51243
  `);
51055
51244
  return true;
51056
51245
  }
51057
- function hasDependency(root, name) {
51246
+ function readPackageJson(root) {
51058
51247
  try {
51059
- const pkg = JSON.parse(readFileSync11(path27.join(root, "package.json"), "utf8"));
51060
- return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
51248
+ return JSON.parse(readFileSync12(path28.join(root, "package.json"), "utf8"));
51061
51249
  } catch {
51062
- return false;
51250
+ return null;
51063
51251
  }
51064
51252
  }
51253
+ var devDependencies = (root) => readPackageJson(root)?.devDependencies ?? {};
51254
+
51255
+ // src/connect.ts
51256
+ function openBrowser(url2) {
51257
+ if (process.env.BROWSER === "none") return;
51258
+ const [command, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2.replace(/&/g, "^&")]] : ["xdg-open", [url2]];
51259
+ try {
51260
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
51261
+ child.on("error", () => {
51262
+ });
51263
+ child.unref();
51264
+ } catch {
51265
+ }
51266
+ }
51267
+ function activationUrl(verificationUriComplete, suggestion) {
51268
+ if (!suggestion) return verificationUriComplete;
51269
+ const url2 = new URL(verificationUriComplete);
51270
+ url2.searchParams.set("name", suggestion.name);
51271
+ url2.searchParams.set("framework", suggestion.framework);
51272
+ url2.searchParams.set("pm", suggestion.packageManager);
51273
+ return url2.toString();
51274
+ }
51275
+ async function connectInBrowser(opts) {
51276
+ const { token, projectId } = await deviceLogin({
51277
+ client: createApiClient({ apiUrl: opts.apiUrl }),
51278
+ clientName: `revyse-cli on ${hostname3()}`,
51279
+ onCode: ({ userCode, verificationUriComplete }) => {
51280
+ const url2 = activationUrl(verificationUriComplete, opts.suggestion);
51281
+ opts.log(`
51282
+ Your browser is opening to connect this app to Revyse. If it doesn't, go to:
51283
+ `);
51284
+ opts.log(` ${url2}
51285
+ `);
51286
+ opts.log(`and check the code matches: ${userCode}
51287
+ `);
51288
+ opts.log("Waiting for you to approve it\u2026");
51289
+ (opts.open ?? openBrowser)(url2);
51290
+ }
51291
+ });
51292
+ saveCredential(opts.root, { apiUrl: opts.apiUrl, token, projectId });
51293
+ const me = await createApiClient({ apiUrl: opts.apiUrl, token }).get("/api/cli/whoami");
51294
+ return { email: me.user.email, project: me.project.name };
51295
+ }
51296
+ async function currentConnection(root) {
51297
+ const cred = loadCredential(root);
51298
+ if (!cred) return null;
51299
+ try {
51300
+ const me = await createApiClient(cred).get("/api/cli/whoami");
51301
+ return { email: me.user.email, project: me.project.name };
51302
+ } catch (err) {
51303
+ if (err instanceof ApiError && (err.status === 401 || err.status === 404)) return null;
51304
+ throw err;
51305
+ }
51306
+ }
51307
+ function initSetup(apiUrl, log) {
51308
+ return {
51309
+ install: async (root, pm, specs) => {
51310
+ const [command, ...args] = installCommand(pm, specs);
51311
+ await execa(command, args, { cwd: root, stdio: "inherit" });
51312
+ },
51313
+ connection: currentConnection,
51314
+ connect: (root, suggestion) => connectInBrowser({ root, apiUrl, suggestion, log })
51315
+ };
51316
+ }
51065
51317
 
51066
51318
  // src/prompt.ts
51067
51319
  import { createInterface } from "readline";
@@ -51092,18 +51344,29 @@ function createPrompter(input2 = process.stdin, output2 = process.stdout) {
51092
51344
  }
51093
51345
 
51094
51346
  // src/commands/init.ts
51095
- var initCommand = new Command("init").description("Detect your project, review the data disclosure, and set up Revyse").option("--force", "re-run init when revyse.config.json exists").option("-y, --yes", "accept the disclosure and proposed edits without prompting").action(async (options) => {
51096
- const prompter = createPrompter();
51097
- try {
51098
- const outcome = await runInit(
51099
- { root: process.cwd(), force: options.force, yes: options.yes },
51100
- { log: (m) => console.log(m), warn: (m) => console.warn(m), confirm: prompter.confirm }
51101
- );
51102
- if (outcome.status !== "initialized") process.exitCode = 1;
51103
- } finally {
51104
- prompter.close();
51347
+ var initCommand = new Command("init").description(
51348
+ "Set Revyse up in this app: the data disclosure, next.config, the packages, and the project on Revyse"
51349
+ ).option("--force", "re-run the disclosure and next.config steps when revyse.config.json exists").option("-y, --yes", "accept the disclosure and proposed edits without prompting").option("--no-connect", "set up without connecting to Revyse (CI, offline)").option("--api-url <url>", "Revyse API URL (default: $REVYSE_API_URL or https://revyse.dev)").action(
51350
+ async (options) => {
51351
+ const prompter = createPrompter();
51352
+ const log = (m) => console.log(m);
51353
+ try {
51354
+ const outcome = await runInit(
51355
+ {
51356
+ root: process.cwd(),
51357
+ force: options.force,
51358
+ yes: options.yes,
51359
+ connect: options.connect,
51360
+ setup: initSetup(resolveApiUrl(options.apiUrl), log)
51361
+ },
51362
+ { log, warn: (m) => console.warn(m), confirm: prompter.confirm }
51363
+ );
51364
+ if (outcome.status === "declined") process.exitCode = 1;
51365
+ } finally {
51366
+ prompter.close();
51367
+ }
51105
51368
  }
51106
- });
51369
+ );
51107
51370
 
51108
51371
  // src/commands/change.ts
51109
51372
  var changeCommand = new Command("change").description("Headless change operations");
@@ -51117,7 +51380,7 @@ var common2 = (cmd) => cmd.requiredOption("--api-url <url>", "Revyse API URL").r
51117
51380
  function setup(opts) {
51118
51381
  const root = process.cwd();
51119
51382
  const config2 = projectConfigSchema.parse(
51120
- JSON.parse(readFileSync12(path28.join(root, CONFIG_FILE), "utf8"))
51383
+ JSON.parse(readFileSync13(path29.join(root, CONFIG_FILE), "utf8"))
51121
51384
  );
51122
51385
  const token = opts.token ?? process.env.REVYSE_TOKEN;
51123
51386
  if (!token) throw new Error("A project token is required (--token or REVYSE_TOKEN)");