@meistrari/remy-cli 1.20.0 → 1.21.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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/remy.js +235 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,16 @@ remy
36
36
 
37
37
  On first use, Remy opens browser-based device authorization, validates your Coding Agent identity, and opens the session dashboard. Later, the same command takes you straight to that dashboard.
38
38
 
39
+ ## Update Remy
40
+
41
+ Run `remy update` to install a newer CLI release using the package manager that owns your installation (mise, Bun, pnpm, or npm). No sign-in is required. Use `remy --version` to print the installed version.
42
+
43
+ Stable versions check npm’s `latest` channel. Release candidates check both `rc` and `latest` and choose the newer version; Remy never downgrades. To switch from stable to RC, use the explicit `@rc` install command above.
44
+
45
+ Each interactive `remy` or `remy dashboard` startup checks for an update before sign-in or the splash. When one is available, type `y` or `yes`, then Enter, to upgrade. Press Enter or type `n` to continue with the current version. Lookup failures or a two-second timeout let startup continue. After a successful upgrade, Remy exits and asks you to run it again. Installation failures exit with an error so you can resolve the package-manager problem and retry.
46
+
47
+ Help, version, sign-in/out, direct session commands, and redirected/noninteractive invocations do not run startup checks or prompt. Development checkouts skip checks; local project and temporary `bunx`/`npx` installations must be updated through their original caller. Remy refuses to install if it cannot identify the owning manager.
48
+
39
49
  ## Your everyday workflow
40
50
 
41
51
  The dashboard is the starting point for all interactive work. It refreshes the visible session page every 10 seconds, so new sessions and status changes appear while you keep it open. If an update fails, Remy keeps showing the last successful list and retries on the next interval.
@@ -187,6 +197,8 @@ remy help | --help | -h
187
197
  remy login [--env production|staging] [--api-url <url> --auth-api-url <url> --requester-application-id <uuid> --target-application-id <uuid>]
188
198
  remy logout [--api-url <url>]
189
199
  remy whoami
200
+ remy update
201
+ remy --version | -v
190
202
  remy dashboard
191
203
  remy new [--repository <owner/name> ... --installation <id> --model <name> --reasoning-effort <low|medium|high|xhigh> --attach <path> ... --no-tui --json] <prompt>
192
204
  remy --session <session-id> [--no-tui] [--json] [prompt]
package/dist/remy.js CHANGED
@@ -17,10 +17,10 @@ var __export = (target, all) => {
17
17
 
18
18
  // src/commands.ts
19
19
  import { randomUUID as randomUUID5 } from "crypto";
20
- import { spawn } from "child_process";
20
+ import { spawn as spawn2 } from "child_process";
21
21
  import { mkdir as mkdir6, readFile as readFile5, readdir as readdir3, rename as rename4, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
22
22
  import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
23
- import { dirname as dirname6, join as join7 } from "path";
23
+ import { dirname as dirname6, join as join8 } from "path";
24
24
 
25
25
  // ../../node_modules/.bun/@meistrari+auth-cli@1.6.1+aff0271b2b55d60c/node_modules/@meistrari/auth-cli/dist/index.mjs
26
26
  import { setTimeout as setTimeout2 } from "timers/promises";
@@ -34234,6 +34234,141 @@ function formatSessionJson({
34234
34234
  })}
34235
34235
  `;
34236
34236
  }
34237
+ // src/version.ts
34238
+ var remyCliVersion = "1.21.0";
34239
+
34240
+ // src/self-update.ts
34241
+ import { spawn } from "child_process";
34242
+ import { realpath } from "fs/promises";
34243
+ import { join as join3, relative, isAbsolute } from "path";
34244
+ import { createInterface } from "readline/promises";
34245
+ var {semver } = globalThis.Bun;
34246
+ var packageName = "@meistrari/remy-cli";
34247
+ var versionSchema = exports_external2.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-rc\.(0|[1-9]\d*))?$/);
34248
+ var tagsSchema = exports_external2.object({ latest: versionSchema, rc: versionSchema.optional() });
34249
+ function createCliUpdates({
34250
+ currentVersion,
34251
+ entryPath,
34252
+ fetchClient,
34253
+ runProcess,
34254
+ confirm
34255
+ }) {
34256
+ return {
34257
+ currentVersion,
34258
+ confirm,
34259
+ async check(signal) {
34260
+ if (currentVersion === "0.0.0")
34261
+ return null;
34262
+ const response = await fetchClient(`https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`, {
34263
+ signal: AbortSignal.any([signal, AbortSignal.timeout(2000)]),
34264
+ headers: { accept: "application/json" }
34265
+ });
34266
+ if (!response.ok)
34267
+ throw new Error(`Cannot check Remy releases (HTTP ${response.status}).`);
34268
+ const tags = tagsSchema.parse(await response.json());
34269
+ const latest = currentVersion.includes("-rc.") && tags.rc && semver.order(tags.rc, tags.latest) > 0 ? tags.rc : tags.latest;
34270
+ return semver.order(latest, currentVersion) > 0 ? latest : null;
34271
+ },
34272
+ async install(version5, signal) {
34273
+ versionSchema.parse(version5);
34274
+ if (currentVersion === "0.0.0")
34275
+ throw new Error("This is a development checkout. Update the checkout instead of running remy update.");
34276
+ const runningEntry = await realpath(entryPath);
34277
+ const probeSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
34278
+ const probe = async (argv2) => {
34279
+ try {
34280
+ const result2 = await runProcess({ argv: argv2, signal: probeSignal, display: false });
34281
+ return result2.exitCode === 0 ? result2.stdout.trim() : "";
34282
+ } catch {
34283
+ signal.throwIfAborted();
34284
+ return "";
34285
+ }
34286
+ };
34287
+ const matchesEntry = async (candidate) => {
34288
+ try {
34289
+ return await realpath(candidate) === runningEntry;
34290
+ } catch {
34291
+ return false;
34292
+ }
34293
+ };
34294
+ let argv;
34295
+ let updatedEntry = runningEntry;
34296
+ const miseRoot = await probe(["mise", "where", `npm:${packageName}@${currentVersion}`]);
34297
+ const miseRelative = miseRoot ? relative(miseRoot, runningEntry) : "";
34298
+ const usesMise = miseRoot !== "" && miseRelative !== "" && !miseRelative.startsWith("..") && !isAbsolute(miseRelative);
34299
+ if (usesMise) {
34300
+ argv = ["mise", "upgrade", `npm:${packageName}@${version5}`, "--bump", "--no-prune"];
34301
+ } else {
34302
+ const bunBin = await probe(["bun", "pm", "bin", "-g"]);
34303
+ if (bunBin && await matchesEntry(join3(bunBin, "remy"))) {
34304
+ argv = ["bun", "add", "--global", `${packageName}@${version5}`];
34305
+ updatedEntry = join3(bunBin, "remy");
34306
+ }
34307
+ for (const manager of ["pnpm", "npm"]) {
34308
+ if (argv)
34309
+ break;
34310
+ const root = await probe([manager, "root", "--global"]);
34311
+ const candidate = join3(root, packageName, "dist", "remy.js");
34312
+ if (root && await matchesEntry(candidate)) {
34313
+ argv = [manager, "install", "--global", `${packageName}@${version5}`];
34314
+ updatedEntry = candidate;
34315
+ }
34316
+ }
34317
+ }
34318
+ signal.throwIfAborted();
34319
+ if (!argv)
34320
+ throw new Error("Cannot identify the manager for this Remy installation. Reinstall @meistrari/remy-cli with the manager you originally used (mise, bun, pnpm, or npm). Local and temporary bunx/npx installs must be updated by their caller.");
34321
+ const result = await runProcess({ argv, signal, display: true });
34322
+ if (result.exitCode !== 0)
34323
+ throw new Error(`Remy update failed: ${argv[0]} exited with code ${result.exitCode}. Resolve the package manager error and retry remy update.`);
34324
+ if (usesMise) {
34325
+ const located = await runProcess({ argv: ["mise", "where", `npm:${packageName}@${version5}`], signal, display: false });
34326
+ if (located.exitCode !== 0 || !located.stdout.trim())
34327
+ throw new Error("Update finished but the new mise installation could not be located. Check remy --version before retrying.");
34328
+ updatedEntry = join3(located.stdout.trim(), miseRelative);
34329
+ }
34330
+ const verified = await runProcess({ argv: [process.execPath, updatedEntry, "--version"], signal, display: false });
34331
+ if (verified.exitCode !== 0 || verified.stdout.trim() !== version5)
34332
+ throw new Error("The package manager finished, but the installed Remy version could not be verified. Check remy --version before retrying.");
34333
+ }
34334
+ };
34335
+ }
34336
+ var runUpdateProcess = async ({ argv, signal, display }) => {
34337
+ signal.throwIfAborted();
34338
+ return await new Promise((resolve, reject) => {
34339
+ const [executable, ...args] = argv;
34340
+ if (!executable) {
34341
+ reject(new Error("Missing update executable."));
34342
+ return;
34343
+ }
34344
+ const child = spawn(executable, args, { signal, stdio: ["ignore", "pipe", display ? "inherit" : "ignore"] });
34345
+ let stdout = "";
34346
+ child.stdout?.on("data", (chunk) => {
34347
+ if (display)
34348
+ process.stderr.write(chunk);
34349
+ else
34350
+ stdout += chunk.toString();
34351
+ });
34352
+ child.on("error", reject);
34353
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout }));
34354
+ });
34355
+ };
34356
+ async function confirmCliUpdate({ version: version5, signal, interrupt }) {
34357
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
34358
+ prompt.once("SIGINT", interrupt);
34359
+ const closed = new Promise((resolve) => prompt.once("close", () => resolve("")));
34360
+ try {
34361
+ const answer = await Promise.race([
34362
+ prompt.question(`Remy ${version5} is available. Upgrade now? [y/N] `, { signal }),
34363
+ closed
34364
+ ]);
34365
+ signal.throwIfAborted();
34366
+ return /^y(?:es)?$/i.test(answer.trim());
34367
+ } finally {
34368
+ prompt.close();
34369
+ process.stdin.pause();
34370
+ }
34371
+ }
34237
34372
 
34238
34373
  // src/session-agent-options.ts
34239
34374
  function newSessionModelLabel(model) {
@@ -34252,7 +34387,7 @@ var defaultNewSessionAgent = {
34252
34387
  // src/sessions/session-controller.ts
34253
34388
  import { randomUUID as randomUUID3 } from "crypto";
34254
34389
  import { mkdir as mkdir4, readFile as readFile4, rename as rename3, writeFile as writeFile2 } from "fs/promises";
34255
- import { dirname as dirname4, join as join3 } from "path";
34390
+ import { dirname as dirname4, join as join4 } from "path";
34256
34391
 
34257
34392
  // ../../packages/supervisor-protocol/src/agent-event-wrapper.ts
34258
34393
  var wrappedSessionStartedAgentEventSchema = sessionStartedAgentEventSchema.extend({
@@ -36103,7 +36238,7 @@ function resolveSessionCachePath({
36103
36238
  sessionId
36104
36239
  }) {
36105
36240
  const stateBase = resolveStateBase(environment);
36106
- return join3(stateBase, "remy", "sessions", `${sessionId}.json`);
36241
+ return join4(stateBase, "remy", "sessions", `${sessionId}.json`);
36107
36242
  }
36108
36243
  async function readSessionCache(path) {
36109
36244
  try {
@@ -36121,7 +36256,7 @@ async function writeSessionCache({
36121
36256
  }) {
36122
36257
  const directory = dirname4(path);
36123
36258
  await mkdir4(directory, { recursive: true, mode: 448 });
36124
- const temporaryPath = join3(directory, `.session.${process.pid}.${randomUUID3()}.tmp`);
36259
+ const temporaryPath = join4(directory, `.session.${process.pid}.${randomUUID3()}.tmp`);
36125
36260
  await writeFile2(temporaryPath, `${JSON.stringify(cache, null, 4)}
36126
36261
  `, { mode: 384 });
36127
36262
  await rename3(temporaryPath, path);
@@ -36140,7 +36275,7 @@ function resolveStateBase(environment) {
36140
36275
  }
36141
36276
  if (!environment.HOME)
36142
36277
  throw new Error("HOME is required when XDG_STATE_HOME is not set; refusing to write Remy session cache in the current directory.");
36143
- return join3(environment.HOME, ".local/state");
36278
+ return join4(environment.HOME, ".local/state");
36144
36279
  }
36145
36280
  function isMissingFileError2(error93) {
36146
36281
  return error93 instanceof Error && "code" in error93 && error93.code === "ENOENT";
@@ -36159,7 +36294,7 @@ async function sleepMs(ms, signal) {
36159
36294
 
36160
36295
  // src/sessions/downloads.ts
36161
36296
  import { link, mkdir as mkdir5, mkdtemp, open as open4, rm } from "fs/promises";
36162
- import { basename as basename2, extname, join as join4, resolve } from "path";
36297
+ import { basename as basename2, extname, join as join5, resolve } from "path";
36163
36298
  var downloadGrantSchema = exports_external2.object({
36164
36299
  url: exports_external2.url(),
36165
36300
  method: exports_external2.literal("GET"),
@@ -36182,16 +36317,16 @@ async function downloadSessionFile({ client, sessionId, file: file3, directory,
36182
36317
  throw new Error(`File download failed (HTTP ${bytes.status}). Try again to request a fresh download.`);
36183
36318
  const destination = resolve(directory);
36184
36319
  await mkdir5(destination, { recursive: true });
36185
- const staging = await mkdtemp(join4(destination, ".remy-download-"));
36320
+ const staging = await mkdtemp(join5(destination, ".remy-download-"));
36186
36321
  try {
36187
- const temporaryPath = join4(staging, "content");
36322
+ const temporaryPath = join5(staging, "content");
36188
36323
  await saveDownloadStream({ body: bytes.body, path: temporaryPath, signal });
36189
36324
  const filename = safeFilename(file3.filename);
36190
36325
  const extension = extname(filename);
36191
36326
  const stem = filename.slice(0, filename.length - extension.length);
36192
36327
  for (let suffix = 0;suffix < 1000; suffix += 1) {
36193
36328
  signal.throwIfAborted();
36194
- const path = join4(destination, suffix === 0 ? filename : `${stem} (${suffix})${extension}`);
36329
+ const path = join5(destination, suffix === 0 ? filename : `${stem} (${suffix})${extension}`);
36195
36330
  try {
36196
36331
  await link(temporaryPath, path);
36197
36332
  return path;
@@ -37597,7 +37732,7 @@ function renderPathCompletionMenu({ completions, selectedIndex, width, maxRows }
37597
37732
  // src/tui/path-completion.ts
37598
37733
  import { readdir } from "fs/promises";
37599
37734
  import { homedir } from "os";
37600
- import { basename as basename3, dirname as dirname5, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
37735
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve2, sep } from "path";
37601
37736
  var maximumCandidates = 50;
37602
37737
  async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
37603
37738
  const token = activePathToken(text);
@@ -37605,8 +37740,8 @@ async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
37605
37740
  return [];
37606
37741
  const pathText = token.startsWith("@") ? token.slice(1) : token;
37607
37742
  const pathUsesTilde = pathText === "~" || pathText.startsWith("~/");
37608
- const expandedPath = pathText === "~" ? homeDirectory : pathText.startsWith("~/") ? join5(homeDirectory, pathText.slice(2)) : pathText;
37609
- const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve2(cwd, expandedPath);
37743
+ const expandedPath = pathText === "~" ? homeDirectory : pathText.startsWith("~/") ? join6(homeDirectory, pathText.slice(2)) : pathText;
37744
+ const absolutePath = isAbsolute2(expandedPath) ? expandedPath : resolve2(cwd, expandedPath);
37610
37745
  const directory = pathText === "" || pathText === "~" || pathText.endsWith("/") ? absolutePath : dirname5(absolutePath);
37611
37746
  const prefix = pathText === "" || pathText === "~" || pathText.endsWith("/") ? "" : basename3(absolutePath);
37612
37747
  let entries;
@@ -37616,8 +37751,8 @@ async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
37616
37751
  return [];
37617
37752
  }
37618
37753
  return entries.filter((entry) => entry.name.toLowerCase().startsWith(prefix.toLowerCase())).sort((left, right) => left.name.localeCompare(right.name)).slice(0, maximumCandidates).map((entry) => {
37619
- const completedPath = join5(directory, entry.name);
37620
- const replacementPath = pathUsesTilde ? `~/${relative(homeDirectory, completedPath)}` : isAbsolute(pathText) ? completedPath : relative(cwd, completedPath) || entry.name;
37754
+ const completedPath = join6(directory, entry.name);
37755
+ const replacementPath = pathUsesTilde ? `~/${relative2(homeDirectory, completedPath)}` : isAbsolute2(pathText) ? completedPath : relative2(cwd, completedPath) || entry.name;
37621
37756
  const normalized = replacementPath.split(sep).join("/");
37622
37757
  const suffix = entry.isDirectory() ? "/" : "";
37623
37758
  const completed = `${normalized}${suffix}`;
@@ -40517,7 +40652,7 @@ import { Buffer as Buffer2 } from "buffer";
40517
40652
  import { randomUUID as randomUUID4 } from "crypto";
40518
40653
  import { mkdtemp as mkdtemp2, open as open5, readdir as readdir2, rm as rm2, stat, writeFile as writeFile3 } from "fs/promises";
40519
40654
  import { homedir as homedir2, tmpdir } from "os";
40520
- import { basename as basename4, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "path";
40655
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
40521
40656
  var directoryArchiveMediaType = "application/gzip";
40522
40657
  var directoryArchiveSuffix = ".tar.gz";
40523
40658
  var maxPortableFilenameBytes = 255;
@@ -40561,7 +40696,7 @@ async function writePastedImage({
40561
40696
  const extension = extensionForMediaType(mediaType);
40562
40697
  if (!extension)
40563
40698
  throw new Error(`Unsupported pasted image MIME type: ${mediaType}`);
40564
- const filePath = join6(temporaryDirectory, `remy-paste-${randomUUID4()}.${extension}`);
40699
+ const filePath = join7(temporaryDirectory, `remy-paste-${randomUUID4()}.${extension}`);
40565
40700
  await writeFile3(filePath, bytes, { mode: 384 });
40566
40701
  return filePath;
40567
40702
  }
@@ -40646,9 +40781,9 @@ async function createDirectoryArchive(directoryPath) {
40646
40781
  });
40647
40782
  if (Object.keys(files).length === 0)
40648
40783
  throw new Error(`Directory attachment contains no regular files: ${directoryPath}`);
40649
- const temporaryDirectory = await mkdtemp2(join6(tmpdir(), "remy-directory-attachment-"));
40784
+ const temporaryDirectory = await mkdtemp2(join7(tmpdir(), "remy-directory-attachment-"));
40650
40785
  const filename = directoryArchiveFilename(rootName);
40651
- const archivePath = join6(temporaryDirectory, filename);
40786
+ const archivePath = join7(temporaryDirectory, filename);
40652
40787
  try {
40653
40788
  await Bun.Archive.write(archivePath, files, { compress: "gzip" });
40654
40789
  if ((await stat(archivePath)).size > fileUploadMaxBytes)
@@ -40670,7 +40805,7 @@ async function collectDirectoryFiles({
40670
40805
  entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
40671
40806
  let byteSize = collectedByteSize;
40672
40807
  for (const entry of entries) {
40673
- const entryPath = join6(directoryPath, entry.name);
40808
+ const entryPath = join7(directoryPath, entry.name);
40674
40809
  const entryArchivePath = `${archivePath}/${entry.name}`;
40675
40810
  if (entry.isDirectory()) {
40676
40811
  byteSize = await collectDirectoryFiles({
@@ -40739,7 +40874,7 @@ function truncateUtf8(value, maxBytes) {
40739
40874
  function promptFilePaths({ text, cwd, selectedPaths }) {
40740
40875
  const paths = new Map;
40741
40876
  for (const selectedPath of selectedPaths)
40742
- paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve3(cwd, selectedPath), true);
40877
+ paths.set(isAbsolute3(selectedPath) ? selectedPath : resolve3(cwd, selectedPath), true);
40743
40878
  const tokens = text.match(/[([{]*@?(?:"[^"]*"|'[^']*'|`[^`]*`)|(?:\\.|[^\s\\])+/g) ?? [];
40744
40879
  for (const quotedToken of tokens) {
40745
40880
  const wrappedToken = quotedToken.replace(/^[([{]+/, "").replace(/[),.;:!?\]}]+$/, "");
@@ -40748,12 +40883,12 @@ function promptFilePaths({ text, cwd, selectedPaths }) {
40748
40883
  const pathText = token.startsWith("@") ? token.slice(1) : token;
40749
40884
  if (!pathText)
40750
40885
  continue;
40751
- const explicit = token.startsWith("@") || pathText.startsWith("./") || pathText.startsWith("../") || isAbsolute2(pathText) || pathText.startsWith("~/");
40886
+ const explicit = token.startsWith("@") || pathText.startsWith("./") || pathText.startsWith("../") || isAbsolute3(pathText) || pathText.startsWith("~/");
40752
40887
  const pathShaped = explicit || pathText.includes("/") || pathText.includes("\\") || pathText.includes(".");
40753
40888
  if (!pathShaped)
40754
40889
  continue;
40755
- const expandedPath = pathText.startsWith("~/") ? join6(homedir2(), pathText.slice(2)) : pathText;
40756
- const filePath = isAbsolute2(expandedPath) ? expandedPath : resolve3(cwd, expandedPath);
40890
+ const expandedPath = pathText.startsWith("~/") ? join7(homedir2(), pathText.slice(2)) : pathText;
40891
+ const filePath = isAbsolute3(expandedPath) ? expandedPath : resolve3(cwd, expandedPath);
40757
40892
  paths.set(filePath, paths.get(filePath) || explicit);
40758
40893
  }
40759
40894
  return [...paths].map(([filePath, explicit]) => ({ filePath, explicit }));
@@ -40775,6 +40910,7 @@ function isPng(bytes) {
40775
40910
 
40776
40911
  // src/tui/remy-splash.ts
40777
40912
  import { bold as bold6, BoxRenderable as BoxRenderable4, fg as fg11, NativeImage, StyledText as StyledText10, TextRenderable as TextRenderable5, bg as bg4 } from "@opentui/core";
40913
+
40778
40914
  // src/tui/remy-mark.ts
40779
40915
  var remyPixelFieldSource = new URL("../assets/remy-pixel-field.png", import.meta.url);
40780
40916
  function shouldShowRemySplash({ width, height }) {
@@ -40792,7 +40928,6 @@ var compactMarkRows = 9;
40792
40928
  var compactMinWidth = 48;
40793
40929
  var compactMinHeight = 20;
40794
40930
  var markBrightnessGain = 4.2;
40795
- var remyCliVersion = "1.20.0";
40796
40931
  async function showRemySplash({
40797
40932
  createRenderer = createRemyRenderer,
40798
40933
  durationMs = splashDurationMs,
@@ -41149,6 +41284,34 @@ async function dispatchCliCommandWithShutdown({
41149
41284
  dependencies.output.writeStdout(helpUsage(command.topic));
41150
41285
  return 0;
41151
41286
  }
41287
+ if (command.name === "version") {
41288
+ dependencies.output.writeStdout(`${dependencies.updates.currentVersion}
41289
+ `);
41290
+ return 0;
41291
+ }
41292
+ if (command.name === "update") {
41293
+ if (dependencies.updates.currentVersion === "0.0.0")
41294
+ throw new Error("This is a development checkout. Update the checkout instead of running remy update.");
41295
+ const version5 = await dependencies.updates.check(dependencies.abortSignal);
41296
+ if (version5)
41297
+ await installCliUpdate({ dependencies, version: version5 });
41298
+ else
41299
+ dependencies.output.writeStdout(`Remy ${dependencies.updates.currentVersion} is up to date.
41300
+ `);
41301
+ return 0;
41302
+ }
41303
+ if (command.name === "dashboard" && isInteractiveTerminal(dependencies)) {
41304
+ let version5 = null;
41305
+ try {
41306
+ version5 = await dependencies.updates.check(dependencies.abortSignal);
41307
+ } catch {
41308
+ throwIfAborted2(dependencies.abortSignal);
41309
+ }
41310
+ if (version5 && await dependencies.updates.confirm({ version: version5, signal: dependencies.abortSignal, interrupt })) {
41311
+ await installCliUpdate({ dependencies, version: version5 });
41312
+ return 0;
41313
+ }
41314
+ }
41152
41315
  if (command.name === "login")
41153
41316
  return await openExplicitLogin({ dependencies, flags: command.flags, interrupt });
41154
41317
  if (command.name === "logout")
@@ -41168,6 +41331,13 @@ async function dispatchCliCommandWithShutdown({
41168
41331
  return await attachSession({ dependencies, sessionId: command.sessionId, noTui: command.noTui, json: command.json, prompt: command.prompt });
41169
41332
  return await createNewSession({ dependencies, command });
41170
41333
  }
41334
+ async function installCliUpdate({ dependencies, version: version5 }) {
41335
+ dependencies.output.writeStderr(`Updating Remy ${dependencies.updates.currentVersion} to ${version5}...
41336
+ `);
41337
+ await dependencies.updates.install(version5, dependencies.abortSignal);
41338
+ dependencies.output.writeStdout(`Remy updated to ${version5}. Run remy again to use the new version.
41339
+ `);
41340
+ }
41171
41341
  function createCliShutdown({ abortSignal }) {
41172
41342
  const controller = new AbortController;
41173
41343
  const abortFromProcess = (signal) => {
@@ -41307,7 +41477,7 @@ ${approvalUrl}
41307
41477
  tokenPath: paths.tokenPath,
41308
41478
  candidatePath: candidateTokenPath,
41309
41479
  backupPath: previousTokenPath,
41310
- quarantinePath: join7(dirname6(paths.tokenPath), `.tokens.quarantine.${process.pid}.${randomUUID5()}.json`)
41480
+ quarantinePath: join8(dirname6(paths.tokenPath), `.tokens.quarantine.${process.pid}.${randomUUID5()}.json`)
41311
41481
  };
41312
41482
  await writeCredentialPromotionJournal({ stateDirectory: dirname6(paths.tokenPath), journal, ...dependencies.credentialStateFilesystem });
41313
41483
  journalPrepared = true;
@@ -42199,7 +42369,7 @@ async function runAttachedSession({
42199
42369
  client: operations.client,
42200
42370
  sessionId,
42201
42371
  file: file3,
42202
- directory: join7(dependencies.environment.HOME ?? homedir3(), "Downloads"),
42372
+ directory: join8(dependencies.environment.HOME ?? homedir3(), "Downloads"),
42203
42373
  signal,
42204
42374
  fetchClient: globalThis.fetch
42205
42375
  }),
@@ -42424,10 +42594,10 @@ async function collectAllUsers({
42424
42594
  }
42425
42595
  }
42426
42596
  function resolveSessionCachePathForCommand({ dependencies, sessionId }) {
42427
- const stateBase = dependencies.environment.XDG_STATE_HOME ?? (dependencies.environment.HOME ? join7(dependencies.environment.HOME, ".local/state") : undefined);
42597
+ const stateBase = dependencies.environment.XDG_STATE_HOME ?? (dependencies.environment.HOME ? join8(dependencies.environment.HOME, ".local/state") : undefined);
42428
42598
  if (!stateBase)
42429
42599
  throw new Error("HOME is required when XDG_STATE_HOME is not set; refusing to read Remy session cache.");
42430
- return join7(stateBase, "remy", "sessions", `${sessionId}.json`);
42600
+ return join8(stateBase, "remy", "sessions", `${sessionId}.json`);
42431
42601
  }
42432
42602
  function isInteractiveTerminal(dependencies) {
42433
42603
  return dependencies.isTerminal?.() ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
@@ -42445,6 +42615,18 @@ function parseCommand(argv) {
42445
42615
  throw new Error(`${commandName} does not accept arguments.`);
42446
42616
  return { name: "help", topic: "root" };
42447
42617
  }
42618
+ if (commandName === "--version" || commandName === "-v") {
42619
+ if (rest.length > 0)
42620
+ throw new Error(`${commandName} does not accept arguments.`);
42621
+ return { name: "version" };
42622
+ }
42623
+ if (commandName === "update") {
42624
+ if (isHelpRequest(rest))
42625
+ return { name: "help", topic: "update" };
42626
+ if (rest.length > 0)
42627
+ throw new Error("remy update does not accept arguments.");
42628
+ return { name: "update" };
42629
+ }
42448
42630
  if (commandName === "dashboard") {
42449
42631
  if (isHelpRequest(rest))
42450
42632
  return { name: "help", topic: "dashboard" };
@@ -42477,7 +42659,7 @@ function parseCommand(argv) {
42477
42659
  }
42478
42660
  if (commandName.startsWith("--"))
42479
42661
  throw new Error(`Unknown root flag ${commandName}. Use --session <session-id> to attach or remy new to create a session.`);
42480
- throw new Error(`Unknown command ${commandName}. Expected login, logout, whoami, or new.`);
42662
+ throw new Error(`Unknown command ${commandName}. Expected dashboard, login, logout, whoami, new, or update.`);
42481
42663
  }
42482
42664
  function isHelpRequest(argv) {
42483
42665
  return argv.length === 1 && isHelpFlag(argv[0]);
@@ -42486,6 +42668,17 @@ function isHelpFlag(value) {
42486
42668
  return value === "--help" || value === "-h";
42487
42669
  }
42488
42670
  function helpUsage(topic) {
42671
+ if (topic === "update") {
42672
+ return `Usage: remy update
42673
+
42674
+ Install a newer Remy CLI release using the original package manager (mise, bun, pnpm, or npm).
42675
+ Stable installations stay on latest; RC installations also check rc. No sign-in is required.
42676
+ After updating, run Remy again. Local and temporary installations must be updated by their caller.
42677
+
42678
+ Options:
42679
+ --help, -h Show this help
42680
+ `;
42681
+ }
42489
42682
  if (topic === "login") {
42490
42683
  return `Usage: remy login [options]
42491
42684
 
@@ -42577,6 +42770,8 @@ Commands:
42577
42770
  Sign in
42578
42771
  remy logout [--api-url <url>] Sign out
42579
42772
  remy whoami Show signed-in identity
42773
+ remy update Update the installed CLI
42774
+ remy --version Show the installed version
42580
42775
  remy help Show this help
42581
42776
 
42582
42777
  Options:
@@ -42701,7 +42896,7 @@ async function readOptionalCliConfiguration(path) {
42701
42896
  async function createCandidateTokenPath(activeTokenPath) {
42702
42897
  const directory = dirname6(activeTokenPath);
42703
42898
  await mkdir6(directory, { recursive: true, mode: 448 });
42704
- return join7(directory, `.tokens.${process.pid}.${randomUUID5()}.tmp`);
42899
+ return join8(directory, `.tokens.${process.pid}.${randomUUID5()}.tmp`);
42705
42900
  }
42706
42901
  async function removeCandidateToken({
42707
42902
  path,
@@ -42730,7 +42925,7 @@ async function removeCommittedTokenRecoveryArtifacts({
42730
42925
  for (const entry of entries) {
42731
42926
  if (!isCommittedTokenRecoveryArtifact(entry))
42732
42927
  continue;
42733
- await removeCandidateToken({ path: join7(directory, entry), removeTokenFile });
42928
+ await removeCandidateToken({ path: join8(directory, entry), removeTokenFile });
42734
42929
  }
42735
42930
  }
42736
42931
  function isCommittedTokenRecoveryArtifact(entry) {
@@ -42801,6 +42996,13 @@ function createDefaultCommandDependencies(argv, sourceEnvironment) {
42801
42996
  };
42802
42997
  return {
42803
42998
  argv,
42999
+ updates: createCliUpdates({
43000
+ currentVersion: remyCliVersion,
43001
+ entryPath: process.argv[1] ?? "",
43002
+ fetchClient: fetch,
43003
+ runProcess: runUpdateProcess,
43004
+ confirm: confirmCliUpdate
43005
+ }),
42804
43006
  environment: {
42805
43007
  HOME: environment.HOME,
42806
43008
  XDG_CONFIG_HOME: environment.XDG_CONFIG_HOME,
@@ -42833,7 +43035,7 @@ async function runBrowserLauncher(command) {
42833
43035
  if (!executable)
42834
43036
  throw new Error("Browser launcher command is empty.");
42835
43037
  return await new Promise((resolve4, reject) => {
42836
- const child = spawn(executable, arguments_, { stdio: "ignore" });
43038
+ const child = spawn2(executable, arguments_, { stdio: "ignore" });
42837
43039
  child.once("error", reject);
42838
43040
  child.once("exit", (code) => resolve4(code ?? 1));
42839
43041
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {