@scalequality/cli 0.4.13 → 0.4.14

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "48de33fc2a34d22eb0d512f22852f8a4ad9ce176ab610f84a4ca2317c795172c",
4
- "sourceCommit": "dc8c92467ad22a83c4351ab6ec259c7bbaf88b30"
3
+ "sha256": "d3ca465f1db41c45109f40d765ff928454585df4214acaab11ec84c8124ba5ec",
4
+ "sourceCommit": "ce4ddc0a595f557925ecce922fe14b938fcceba4"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -28,9 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  ));
29
29
 
30
30
  // src/main/workspace-connect.ts
31
- var import_fs10 = require("fs");
31
+ var import_fs11 = require("fs");
32
32
  var import_os4 = require("os");
33
- var import_path16 = require("path");
33
+ var import_path17 = require("path");
34
34
  var import_url = require("url");
35
35
 
36
36
  // src/application/services/workspaceSandbox/importers.ts
@@ -3405,7 +3405,7 @@ var MACHINE_USAGE = {
3405
3405
  "ScaleQuality and deletes its credential."
3406
3406
  ].join("\n"),
3407
3407
  service: [
3408
- "Usage: scalequality service <install|start|uninstall|status|logs> [--api URL] [--lines N]",
3408
+ "Usage: scalequality service <install|start|uninstall|status|logs|auto-update on|off> [--api URL] [--lines N]",
3409
3409
  "",
3410
3410
  "The background service that keeps this computer connected.",
3411
3411
  "",
@@ -3414,18 +3414,21 @@ var MACHINE_USAGE = {
3414
3414
  ' uninstall Stop and remove it. The computer stays paired; "scalequality',
3415
3415
  ' logout" disconnects it.',
3416
3416
  " status Whether it is installed and running, and where it logs.",
3417
- " logs The last lines of its log (--lines N, default 100)."
3417
+ " logs The last lines of its log (--lines N, default 100).",
3418
+ " auto-update on|off",
3419
+ " Whether the service installs new versions of this CLI by itself",
3420
+ " (on by default; only while no session runs)."
3418
3421
  ].join("\n")
3419
3422
  };
3420
3423
  var MACHINE_COMMANDS = ["login", "up", "add", "logout", "service"];
3421
- var SERVICE_ACTIONS = ["install", "start", "uninstall", "status", "logs"];
3424
+ var SERVICE_ACTIONS = ["install", "start", "uninstall", "status", "logs", "auto-update"];
3422
3425
  function isMachineCommand(c) {
3423
3426
  return !!c && MACHINE_COMMANDS.includes(c);
3424
3427
  }
3425
3428
  function parseMachineArgs(argv) {
3426
3429
  const [command, ...rest] = argv;
3427
3430
  if (!isMachineCommand(command)) return { ok: false, help: false, error: `Unknown command: ${command}` };
3428
- const out2 = { command, api: null, name: null, path: null, up: true, verbose: false, service: false, action: null, lines: 100 };
3431
+ const out2 = { command, api: null, name: null, path: null, up: true, verbose: false, service: false, action: null, lines: 100, toggle: null };
3429
3432
  for (let i = 0; i < rest.length; i++) {
3430
3433
  const a = rest[i];
3431
3434
  const value = () => {
@@ -3478,9 +3481,14 @@ function parseMachineArgs(argv) {
3478
3481
  out2.action = a;
3479
3482
  continue;
3480
3483
  }
3484
+ if (command === "service" && out2.action === "auto-update" && !out2.toggle && (a === "on" || a === "off")) {
3485
+ out2.toggle = a;
3486
+ continue;
3487
+ }
3481
3488
  return { ok: false, help: false, error: `Unexpected argument ${a}.` };
3482
3489
  }
3483
- if (command === "service" && !out2.action) return { ok: false, help: false, error: "Choose install, start, uninstall, status or logs." };
3490
+ if (command === "service" && !out2.action) return { ok: false, help: false, error: "Choose install, start, uninstall, status, logs or auto-update." };
3491
+ if (command === "service" && out2.action === "auto-update" && !out2.toggle) return { ok: false, help: false, error: "Say auto-update on or auto-update off." };
3484
3492
  return { ok: true, args: out2 };
3485
3493
  }
3486
3494
 
@@ -3684,9 +3692,10 @@ function installStableCopy(o) {
3684
3692
  }
3685
3693
  function removeStableCopies(home, keep = null) {
3686
3694
  const base = (0, import_path6.join)(sqHome(home), "cli");
3695
+ const kept = new Set(keep === null ? [] : typeof keep === "string" ? [keep] : keep);
3687
3696
  const removed = [];
3688
3697
  for (const name of (0, import_fs5.existsSync)(base) ? (0, import_fs5.readdirSync)(base) : []) {
3689
- if (name === keep || !/^\.?[0-9A-Za-z.+-]{1,60}$/.test(name)) continue;
3698
+ if (kept.has(name) || !/^\.?[0-9A-Za-z.+-]{1,60}$/.test(name)) continue;
3690
3699
  const full = (0, import_path6.join)(base, name);
3691
3700
  if (!(0, import_fs5.lstatSync)(full).isDirectory()) continue;
3692
3701
  (0, import_fs5.rmSync)(full, { recursive: true, force: true });
@@ -3891,7 +3900,7 @@ function releaseLock(home, api, pid = process.pid) {
3891
3900
  }
3892
3901
  function binFromDefinition(text3) {
3893
3902
  if (!text3) return null;
3894
- const m = /([^"<>\s]*[\\/]@scalequality[\\/]cli[\\/]bin[\\/]scalequality\.mjs)/.exec(text3);
3903
+ const m = /([^"<>\s]*[\\/]@scalequality[\\/]cli[\\/]bin[\\/]scalequality\.mjs)/.exec(text3) ?? /([^"<>\s]*[\\/]\.scalequality[\\/]cli[\\/]launcher\.mjs)/.exec(text3);
3895
3904
  return m ? m[1] : null;
3896
3905
  }
3897
3906
  async function serviceStatus(env, api) {
@@ -13571,7 +13580,226 @@ async function hasLocalTranscript(configDir, sessionId) {
13571
13580
  return dirs.some((d) => (0, import_fs9.existsSync)((0, import_path15.join)(projects, d, `${sessionId}.jsonl`)));
13572
13581
  }
13573
13582
 
13583
+ // src/application/services/workspaceSandbox/selfUpdate.ts
13584
+ var import_crypto8 = require("crypto");
13585
+ var import_fs10 = require("fs");
13586
+ var import_path16 = require("path");
13587
+ var REGISTRY_LATEST = "https://registry.npmjs.org/@scalequality%2fcli/latest";
13588
+ var UPDATE_FIRST_CHECK_MS = 2 * 6e4;
13589
+ var UPDATE_INTERVAL_MS = 6 * 36e5;
13590
+ var UPDATE_IDLE_POLL_MS = 6e4;
13591
+ var CONFIRM_AFTER_MS = 9e4;
13592
+ var RESTART_EXIT_CODE = 75;
13593
+ var MAX_START_ATTEMPTS = 3;
13594
+ var cliDir = (home) => (0, import_path16.join)(sqHome(home), "cli");
13595
+ var launcherPath = (home) => (0, import_path16.join)(cliDir(home), "launcher.mjs");
13596
+ var currentPath = (home) => (0, import_path16.join)(cliDir(home), "current.json");
13597
+ var autoUpdateOffPath = (home) => (0, import_path16.join)(cliDir(home), "auto-update-off");
13598
+ var VERSION = /^(\d{1,5})\.(\d{1,5})\.(\d{1,5})$/;
13599
+ function isNewer(candidate, current) {
13600
+ const a = VERSION.exec(candidate), b = VERSION.exec(current);
13601
+ if (!a || !b) return false;
13602
+ for (let i = 1; i <= 3; i++) {
13603
+ const d = Number(a[i]) - Number(b[i]);
13604
+ if (d !== 0) return d > 0;
13605
+ }
13606
+ return false;
13607
+ }
13608
+ function readJson2(file) {
13609
+ try {
13610
+ const v = JSON.parse((0, import_fs10.readFileSync)(file, "utf8"));
13611
+ return v && typeof v === "object" ? v : null;
13612
+ } catch {
13613
+ return null;
13614
+ }
13615
+ }
13616
+ function writeJsonAtomic(file, value) {
13617
+ (0, import_fs10.mkdirSync)((0, import_path16.dirname)(file), { recursive: true, mode: 448 });
13618
+ const tmp = `${file}.${process.pid}.tmp`;
13619
+ (0, import_fs10.writeFileSync)(tmp, `${JSON.stringify(value, null, 2)}
13620
+ `, { mode: 384 });
13621
+ (0, import_fs10.renameSync)(tmp, file);
13622
+ }
13623
+ var installed = (v) => {
13624
+ const r = v;
13625
+ return r && typeof r.version === "string" && typeof r.bin === "string" ? { version: r.version, bin: r.bin } : null;
13626
+ };
13627
+ function readCurrent(home) {
13628
+ const raw = readJson2(currentPath(home));
13629
+ const self = installed(raw);
13630
+ if (!raw || !self) return null;
13631
+ return {
13632
+ ...self,
13633
+ previous: installed(raw.previous),
13634
+ pending: raw.pending === true,
13635
+ attempts: typeof raw.attempts === "number" ? raw.attempts : 0,
13636
+ rejected: Array.isArray(raw.rejected) ? raw.rejected.filter((x) => typeof x === "string").slice(-20) : []
13637
+ };
13638
+ }
13639
+ function launcherSource(fallbackBin) {
13640
+ return `// ScaleQuality CLI launcher: the background service starts this file, and it runs the version named in
13641
+ // current.json beside it. A new version that fails to start ${MAX_START_ATTEMPTS} times goes back to the previous one.
13642
+ // Written by the ScaleQuality CLI; it is replaced on the next "scalequality login".
13643
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
13644
+ import { fileURLToPath, pathToFileURL } from 'node:url'
13645
+
13646
+ const file = fileURLToPath(new URL('./current.json', import.meta.url))
13647
+ const fallback = ${JSON.stringify(fallbackBin)}
13648
+ let current = null
13649
+ try { current = JSON.parse(readFileSync(file, 'utf8')) } catch { current = null }
13650
+ const save = (value) => {
13651
+ try { writeFileSync(file + '.launcher.tmp', JSON.stringify(value, null, 2) + '\\n', { mode: 0o600 }); renameSync(file + '.launcher.tmp', file) } catch {}
13652
+ }
13653
+ if (current && current.pending === true) {
13654
+ const attempts = (Number(current.attempts) || 0) + 1
13655
+ const previous = current.previous
13656
+ if (attempts > ${MAX_START_ATTEMPTS} && previous && typeof previous.bin === 'string' && existsSync(previous.bin)) {
13657
+ const rejected = [...(Array.isArray(current.rejected) ? current.rejected : []), current.version].slice(-20)
13658
+ current = { version: previous.version, bin: previous.bin, previous: null, pending: false, attempts: 0, rejected }
13659
+ } else current = { ...current, attempts }
13660
+ save(current)
13661
+ }
13662
+ const bin = current && typeof current.bin === 'string' && existsSync(current.bin) ? current.bin : fallback
13663
+ if (!existsSync(bin)) {
13664
+ process.stderr.write('The ScaleQuality CLI copy this service runs is missing. Run: npx @scalequality/cli@latest login\\n')
13665
+ process.exit(1)
13666
+ }
13667
+ process.argv[1] = bin
13668
+ process.env.SCALEQUALITY_LAUNCHER = '1'
13669
+ await import(pathToFileURL(bin).href)
13670
+ `;
13671
+ }
13672
+ function installLauncher(home, copy) {
13673
+ const current = readCurrent(home);
13674
+ writeJsonAtomic(currentPath(home), { version: copy.version, bin: copy.bin, previous: null, pending: false, attempts: 0, rejected: current?.rejected ?? [] });
13675
+ const file = launcherPath(home);
13676
+ const tmp = `${file}.${process.pid}.tmp`;
13677
+ (0, import_fs10.writeFileSync)(tmp, launcherSource(copy.bin), { mode: 420 });
13678
+ (0, import_fs10.renameSync)(tmp, file);
13679
+ return file;
13680
+ }
13681
+ function confirmCurrent(home, version) {
13682
+ const current = readCurrent(home);
13683
+ if (!current || current.version !== version || !current.pending) return false;
13684
+ writeJsonAtomic(currentPath(home), { ...current, pending: false, attempts: 0, previous: null });
13685
+ removeStableCopies(home, [version]);
13686
+ return true;
13687
+ }
13688
+ function autoUpdateEnabled(home, env) {
13689
+ return env.SCALEQUALITY_NO_AUTO_UPDATE !== "1" && !(0, import_fs10.existsSync)(autoUpdateOffPath(home));
13690
+ }
13691
+ function setAutoUpdate(home, on) {
13692
+ if (on) (0, import_fs10.rmSync)(autoUpdateOffPath(home), { force: true });
13693
+ else {
13694
+ (0, import_fs10.mkdirSync)(cliDir(home), { recursive: true, mode: 448 });
13695
+ (0, import_fs10.writeFileSync)(autoUpdateOffPath(home), `${(/* @__PURE__ */ new Date()).toISOString()}
13696
+ `, { mode: 384 });
13697
+ }
13698
+ }
13699
+ async function latestVersion(fetchImpl = fetch) {
13700
+ try {
13701
+ const res = await fetchImpl(REGISTRY_LATEST, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
13702
+ if (!res.ok) return null;
13703
+ const body = await res.json();
13704
+ return body?.name === "@scalequality/cli" && typeof body.version === "string" && VERSION.test(body.version) ? body.version : null;
13705
+ } catch {
13706
+ return null;
13707
+ }
13708
+ }
13709
+ function npmCli(nodePath, exists = import_fs10.existsSync, resolve8 = realOrNull, pathEnv = process.env.PATH ?? "") {
13710
+ const dir = (0, import_path16.dirname)(nodePath);
13711
+ const script = (p) => p && /npm-cli\.js$/.test(p) && exists(p) ? p : null;
13712
+ const linked = (p) => exists(p) ? script(resolve8(p)) : null;
13713
+ const layouts = [
13714
+ (0, import_path16.join)(dir, "node_modules", "npm", "bin", "npm-cli.js"),
13715
+ (0, import_path16.join)(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
13716
+ (0, import_path16.join)(dir, "..", "libexec", "lib", "node_modules", "npm", "bin", "npm-cli.js")
13717
+ ];
13718
+ return linked((0, import_path16.join)(dir, "npm")) ?? layouts.map(script).find(Boolean) ?? pathEnv.split(import_path16.delimiter).filter(Boolean).map((d) => linked((0, import_path16.join)(d, "npm"))).find(Boolean) ?? null;
13719
+ }
13720
+ function realOrNull(p) {
13721
+ try {
13722
+ return (0, import_fs10.realpathSync)(p);
13723
+ } catch {
13724
+ return null;
13725
+ }
13726
+ }
13727
+ var UpdateError = class extends Error {
13728
+ constructor(code, message) {
13729
+ super(message);
13730
+ this.code = code;
13731
+ }
13732
+ code;
13733
+ };
13734
+ async function installVersion(o) {
13735
+ const npm = o.npm === void 0 ? npmCli(o.nodePath) : o.npm;
13736
+ if (!npm) throw new UpdateError("NPM_NOT_FOUND", `npm was not found next to ${o.nodePath}.`);
13737
+ const staging = (0, import_path16.join)(cliDir(o.home), `.update-${o.version}`);
13738
+ (0, import_fs10.rmSync)(staging, { recursive: true, force: true });
13739
+ (0, import_fs10.mkdirSync)(staging, { recursive: true, mode: 448 });
13740
+ try {
13741
+ (0, import_fs10.writeFileSync)((0, import_path16.join)(staging, "package.json"), '{ "private": true }\n');
13742
+ const r = await o.run(o.nodePath, [
13743
+ npm,
13744
+ "install",
13745
+ "--prefix",
13746
+ staging,
13747
+ "--no-audit",
13748
+ "--no-fund",
13749
+ "--ignore-scripts",
13750
+ "--omit=dev",
13751
+ "--registry=https://registry.npmjs.org/",
13752
+ `@scalequality/cli@${o.version}`
13753
+ ]);
13754
+ if (r.code !== 0) throw new UpdateError("NPM_INSTALL_FAILED", `npm could not install @scalequality/cli@${o.version}: ${`${r.stderr || r.stdout}`.trim().split("\n").slice(-1)[0]?.slice(0, 200) ?? r.code}`);
13755
+ const root = (0, import_path16.join)(staging, "node_modules", "@scalequality", "cli");
13756
+ const pkg = readJson2((0, import_path16.join)(root, "package.json"));
13757
+ if (pkg?.name !== "@scalequality/cli" || pkg?.version !== o.version) throw new UpdateError("WRONG_PACKAGE", `npm installed ${String(pkg?.name)}@${String(pkg?.version)} instead of ${o.version}.`);
13758
+ const build = readJson2((0, import_path16.join)(root, "dist", "connect.build.json"));
13759
+ const bundle = (0, import_path16.join)(root, "dist", "connect.cjs");
13760
+ const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto8.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
13761
+ if (!sha || typeof build?.sha256 !== "string" || build.sha256 !== sha) throw new UpdateError("BUNDLE_MISMATCH", `The engine of ${o.version} does not match the sha256 it declares.`);
13762
+ let copy;
13763
+ try {
13764
+ copy = installStableCopy({ packageRoot: root, home: o.home });
13765
+ } catch (e) {
13766
+ throw new UpdateError("COPY_FAILED", e.message);
13767
+ }
13768
+ return { version: copy.version, bin: copy.bin };
13769
+ } finally {
13770
+ (0, import_fs10.rmSync)(staging, { recursive: true, force: true });
13771
+ }
13772
+ }
13773
+ function stageVersion(home, next, running) {
13774
+ const current = readCurrent(home);
13775
+ writeJsonAtomic(currentPath(home), { version: next.version, bin: next.bin, previous: running, pending: true, attempts: 0, rejected: current?.rejected ?? [] });
13776
+ }
13777
+ async function checkForUpdate(d) {
13778
+ if (!autoUpdateEnabled(d.home, d.env) || !d.launched()) return "NONE";
13779
+ const latest = await latestVersion(d.fetchImpl);
13780
+ if (!latest || !isNewer(latest, d.version)) return "NONE";
13781
+ if (readCurrent(d.home)?.rejected?.includes(latest)) return "NONE";
13782
+ const sleep2 = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
13783
+ while (d.busy()) await sleep2(UPDATE_IDLE_POLL_MS);
13784
+ const running = readCurrent(d.home);
13785
+ if (!running || running.version !== d.version) return "NONE";
13786
+ d.say(`A new version of the ScaleQuality CLI is available (${latest}); installing it beside ${d.version}.`);
13787
+ let next;
13788
+ try {
13789
+ next = await installVersion({ version: latest, home: d.home, nodePath: d.nodePath, run: d.run, npm: d.npm });
13790
+ } catch (e) {
13791
+ d.say(`The update to ${latest} was not installed (${e.message}); ${d.version} keeps running.`);
13792
+ return "SKIPPED";
13793
+ }
13794
+ while (d.busy()) await sleep2(UPDATE_IDLE_POLL_MS);
13795
+ stageVersion(d.home, next, { version: running.version, bin: running.bin });
13796
+ d.say(`Restarting on ${latest}. If it does not start, ${d.version} comes back on its own.`);
13797
+ d.restart();
13798
+ return "RESTARTING";
13799
+ }
13800
+
13574
13801
  // src/main/workspace-connect.ts
13802
+ var import_child_process4 = require("child_process");
13575
13803
  var importEsm = new Function("specifier", "return import(specifier)");
13576
13804
  async function loadSdk() {
13577
13805
  const resolved = require.resolve("@anthropic-ai/claude-agent-sdk");
@@ -13586,9 +13814,9 @@ var say = (line = "") => {
13586
13814
  var cliVersion = process.env.SCALEQUALITY_CLI_VERSION || "dev";
13587
13815
  var userAgent = (mode) => `scalequality-cli/${cliVersion} (${mode}; node ${process.versions.node}; ${process.platform})`;
13588
13816
  var HOME = (0, import_os4.homedir)();
13589
- var SQ_HOME = (0, import_path16.join)(HOME, ".scalequality");
13590
- var ENGINE_HOME = (0, import_path16.join)(SQ_HOME, "workspace");
13591
- var credentials = new CredentialStore((0, import_path16.join)(SQ_HOME, "credentials.json"));
13817
+ var SQ_HOME = (0, import_path17.join)(HOME, ".scalequality");
13818
+ var ENGINE_HOME = (0, import_path17.join)(SQ_HOME, "workspace");
13819
+ var credentials = new CredentialStore((0, import_path17.join)(SQ_HOME, "credentials.json"));
13592
13820
  function startFailure(e, api) {
13593
13821
  if (e instanceof NotLocalSessionError) return 'This code belongs to a cloud workspace session. In the AI Workspace, choose "Use a folder on your computer" to get a code for this command.';
13594
13822
  if (e instanceof SessionGoneError) {
@@ -13601,16 +13829,16 @@ function startFailure(e, api) {
13601
13829
  return "ScaleQuality could not start this session. Try again in a moment, or get a new code from the AI Workspace.";
13602
13830
  }
13603
13831
  function engineDirs() {
13604
- const configDir = (0, import_path16.join)(ENGINE_HOME, "claude-home");
13605
- const scratch = (0, import_path16.join)(ENGINE_HOME, "tmp");
13606
- (0, import_fs10.mkdirSync)(configDir, { recursive: true, mode: 448 });
13607
- (0, import_fs10.mkdirSync)(scratch, { recursive: true, mode: 448 });
13832
+ const configDir = (0, import_path17.join)(ENGINE_HOME, "claude-home");
13833
+ const scratch = (0, import_path17.join)(ENGINE_HOME, "tmp");
13834
+ (0, import_fs11.mkdirSync)(configDir, { recursive: true, mode: 448 });
13835
+ (0, import_fs11.mkdirSync)(scratch, { recursive: true, mode: 448 });
13608
13836
  return { configDir, scratch };
13609
13837
  }
13610
13838
  function createLocalEngine(o) {
13611
13839
  const { configDir, scratch } = engineDirs();
13612
- const sessions = (0, import_path16.join)(ENGINE_HOME, "sessions");
13613
- const stateDir = (0, import_path16.join)(sessions, o.sessionId);
13840
+ const sessions = (0, import_path17.join)(ENGINE_HOME, "sessions");
13841
+ const stateDir = (0, import_path17.join)(sessions, o.sessionId);
13614
13842
  void pruneSessionStates(sessions, stateDir).catch(() => void 0);
13615
13843
  const log = {
13616
13844
  info: (msg, ctx) => {
@@ -13797,7 +14025,14 @@ async function setUpService(api) {
13797
14025
  say(style.yellow(e.message));
13798
14026
  return false;
13799
14027
  }
13800
- const result = await installService(env, { bin, api });
14028
+ let launcher;
14029
+ try {
14030
+ launcher = installLauncher(HOME, { version, bin });
14031
+ } catch (e) {
14032
+ say(style.yellow(`The launcher of the background service could not be written: ${e.message}`));
14033
+ return false;
14034
+ }
14035
+ const result = await installService(env, { bin: launcher, api });
13801
14036
  if (!result.ok) {
13802
14037
  say(style.yellow(result.message));
13803
14038
  return false;
@@ -13903,7 +14138,7 @@ async function upMain(api, verbose, service) {
13903
14138
  let shuttingDown = false;
13904
14139
  let agent;
13905
14140
  const startSession = ({ sessionId, secret, root }) => {
13906
- const label = (0, import_path16.basename)(root);
14141
+ const label = (0, import_path17.basename)(root);
13907
14142
  const prefix = style.dim(`[${label}] `);
13908
14143
  const consoleLog = new ConsoleLog(style);
13909
14144
  let resolveDone = () => void 0;
@@ -13953,6 +14188,15 @@ async function upMain(api, verbose, service) {
13953
14188
  process.on("unhandledRejection", (e) => {
13954
14189
  if (verbose || service) say(style.dim(`[warn] unhandled rejection ${e?.message}`));
13955
14190
  });
14191
+ if (service && process.env.SCALEQUALITY_LAUNCHER === "1") startSelfUpdate(api, () => agent.sessions.size > 0, () => {
14192
+ agent.stop();
14193
+ releaseLock(HOME, api);
14194
+ if (process.platform === "win32") {
14195
+ (0, import_child_process4.spawn)(process.execPath, [launcherPath(HOME), "up", "--api", api, "--service"], { detached: true, stdio: "ignore", windowsHide: true, env: process.env }).unref();
14196
+ process.exit(0);
14197
+ }
14198
+ process.exit(RESTART_EXIT_CODE);
14199
+ });
13956
14200
  say(style.bold(`ScaleQuality: keeping "${credential.name}" connected to ${api}${service ? " (background service)" : ""}`));
13957
14201
  say(` Folders: ${credential.folders.length ? credential.folders.join(", ") : "none yet (scalequality add <folder>)"}`);
13958
14202
  say(style.dim(" Commands that sessions want to run are approved in the browser."));
@@ -13977,9 +14221,44 @@ async function upMain(api, verbose, service) {
13977
14221
  throw e;
13978
14222
  }
13979
14223
  }
14224
+ function startSelfUpdate(api, busy, restart) {
14225
+ const confirm = setTimeout(() => {
14226
+ try {
14227
+ if (confirmCurrent(HOME, cliVersion)) say(`Version ${cliVersion} started well; the previous copy was removed.`);
14228
+ } catch (e) {
14229
+ say(`[warn] version ${cliVersion} not confirmed: ${e.message}`);
14230
+ }
14231
+ }, CONFIRM_AFTER_MS);
14232
+ confirm.unref();
14233
+ let checking = false;
14234
+ const check = async () => {
14235
+ if (checking) return;
14236
+ checking = true;
14237
+ try {
14238
+ await checkForUpdate({
14239
+ home: HOME,
14240
+ env: process.env,
14241
+ version: cliVersion,
14242
+ nodePath: process.execPath,
14243
+ run: runCommand,
14244
+ busy,
14245
+ say,
14246
+ launched: () => process.env.SCALEQUALITY_LAUNCHER === "1",
14247
+ restart
14248
+ });
14249
+ } catch (e) {
14250
+ say(`[warn] update check failed: ${e.message}`);
14251
+ } finally {
14252
+ checking = false;
14253
+ }
14254
+ };
14255
+ setTimeout(() => void check(), UPDATE_FIRST_CHECK_MS).unref();
14256
+ setInterval(() => void check(), UPDATE_INTERVAL_MS).unref();
14257
+ say(style.dim(autoUpdateEnabled(HOME, process.env) ? ` Updates install by themselves while no session runs (scalequality service auto-update off${apiFlag(api)} to stop).` : " Automatic updates are off."));
14258
+ }
13980
14259
  async function addMain(api, path) {
13981
14260
  try {
13982
- const real = await addFolder(credentials, api, (0, import_path16.resolve)(path ?? process.cwd()), HOME);
14261
+ const real = await addFolder(credentials, api, (0, import_path17.resolve)(path ?? process.cwd()), HOME);
13983
14262
  say(style.green(`Added ${real}.`));
13984
14263
  const credential = credentials.get(api);
13985
14264
  const client = new MachineClient(api, { token: credential.machineToken, userAgent: userAgent("add") });
@@ -14022,8 +14301,13 @@ async function logoutMain(api) {
14022
14301
  credentials.remove(api);
14023
14302
  say(`Deleted the local credential for ${api}.`);
14024
14303
  }
14025
- async function serviceMain(action, api, lines2) {
14304
+ async function serviceMain(action, api, lines2, toggle = null) {
14026
14305
  const env = serviceEnv();
14306
+ if (action === "auto-update") {
14307
+ setAutoUpdate(HOME, toggle === "on");
14308
+ say(toggle === "on" ? "Automatic updates are on: the service installs new versions by itself while no session runs." : "Automatic updates are off. Update by hand with: npx @scalequality/cli@latest login");
14309
+ return;
14310
+ }
14027
14311
  if (action === "install" || action === "start") {
14028
14312
  if (!credentials.get(api)) {
14029
14313
  say(style.red(`This computer is not connected to ${api}. Run scalequality login${apiFlag(api)} first.`));
@@ -14048,6 +14332,8 @@ async function serviceMain(action, api, lines2) {
14048
14332
  `Running ${s.running ? `yes${s.pid ? ` (process ${s.pid})` : ""}` : "no"}`,
14049
14333
  ...s.definition ? [`Definition ${s.definition}`] : [],
14050
14334
  ...s.bin ? [`Runs ${s.bin}`] : [],
14335
+ ...readCurrent(HOME) ? [`Version ${readCurrent(HOME).version}${readCurrent(HOME).pending ? " (just updated, confirming)" : ""}`] : [],
14336
+ `Automatic updates ${autoUpdateEnabled(HOME, process.env) ? "on" : "off"}`,
14051
14337
  `Log ${s.logFile}`
14052
14338
  ].join("\n") + "\n");
14053
14339
  return;
@@ -14087,7 +14373,7 @@ async function main() {
14087
14373
  if (args.command === "login") await loginMain(api, args.name, args.up, args.verbose);
14088
14374
  else if (args.command === "up") await upMain(api, args.verbose, args.service || process.env.SCALEQUALITY_SERVICE === "1");
14089
14375
  else if (args.command === "add") await addMain(api, args.path);
14090
- else if (args.command === "service") await serviceMain(args.action, api, args.lines);
14376
+ else if (args.command === "service") await serviceMain(args.action, api, args.lines, args.toggle);
14091
14377
  else await logoutMain(api);
14092
14378
  }
14093
14379
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {