@qodalis/cli 2.4.1 → 2.5.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/public-api.js CHANGED
@@ -519,7 +519,7 @@ var CliKeyValueStore_TOKEN = cliCore.createCliToken("cli-key-value-store");
519
519
  var SyntaxHighlighterRegistry_TOKEN = cliCore.createCliToken("syntax-highlighter-registry");
520
520
 
521
521
  // src/lib/version.ts
522
- var LIBRARY_VERSION = "2.4.1";
522
+ var LIBRARY_VERSION = "2.5.0";
523
523
  var API_VERSION = 2;
524
524
  var ANONYMOUS = "\0anonymous";
525
525
  function aliasOwner(context2) {
@@ -5887,6 +5887,72 @@ var CliServerConnection = class _CliServerConnection {
5887
5887
  return httpUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
5888
5888
  }
5889
5889
  };
5890
+ function plainText(value, keepLineBreaks = false) {
5891
+ const text = String(value ?? "");
5892
+ return keepLineBreaks ? text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "") : text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
5893
+ }
5894
+ function optionalText(value) {
5895
+ return value === void 0 || value === null ? void 0 : plainText(value);
5896
+ }
5897
+ function serverCommandAliases(descriptor) {
5898
+ return (descriptor.aliases ?? []).map((a) => plainText(a).trim()).filter((a) => a && !/\s/.test(a));
5899
+ }
5900
+ var DEFAULT_SERVER_COMMAND_ICON = "\u{1F5A5}";
5901
+ function serverCommandMetadata(descriptor, fallbackModule, server) {
5902
+ return {
5903
+ module: plainText(descriptor.module) || fallbackModule,
5904
+ icon: plainText(descriptor.icon) || DEFAULT_SERVER_COMMAND_ICON,
5905
+ requireServer: true,
5906
+ ...server ? { server } : {}
5907
+ };
5908
+ }
5909
+ function serverCommandParameters(descriptor) {
5910
+ return descriptor.parameters?.map((p) => {
5911
+ const allowed = p.allowedValues?.length ? p.allowedValues.map((v) => plainText(v)) : void 0;
5912
+ const examples = p.examples?.map((v) => plainText(v)).filter(Boolean);
5913
+ return {
5914
+ name: p.name,
5915
+ aliases: p.aliases?.map((a) => plainText(a).replace(/^-+/, "")).filter(Boolean),
5916
+ description: optionalText(p.description),
5917
+ required: p.required,
5918
+ type: p.type,
5919
+ defaultValue: p.defaultValue,
5920
+ ...examples?.length ? { examples } : {},
5921
+ ...allowed ? {
5922
+ allowedValues: allowed,
5923
+ validator: (value) => {
5924
+ const values = Array.isArray(value) ? value : [value];
5925
+ const valid = values.every(
5926
+ (v) => allowed.some((a) => a.toLowerCase() === String(v).toLowerCase())
5927
+ );
5928
+ return valid ? { valid: true } : { valid: false, message: `--${p.name} must be one of: ${allowed.join(", ")}` };
5929
+ }
5930
+ } : {}
5931
+ };
5932
+ });
5933
+ }
5934
+ function serverCommandWriteDescription(descriptor) {
5935
+ const paragraphs = plainText(descriptor.documentation, true).split(/\r?\n\s*\r?\n/).map((p) => p.trim()).filter(Boolean);
5936
+ const examples = (descriptor.examples ?? []).filter((e) => e?.command).map((e) => ({ command: plainText(e.command), description: optionalText(e.description) }));
5937
+ if (paragraphs.length === 0 && examples.length === 0) {
5938
+ return void 0;
5939
+ }
5940
+ return (context2) => {
5941
+ paragraphs.forEach((paragraph, i) => {
5942
+ if (i > 0) context2.writer.writeln();
5943
+ cliCore.writeWrapped(context2, paragraph);
5944
+ });
5945
+ if (examples.length > 0) {
5946
+ if (paragraphs.length > 0) context2.writer.writeln();
5947
+ cliCore.writeExamples(
5948
+ context2,
5949
+ examples.map((e) => [e.command, e.description ?? ""])
5950
+ );
5951
+ }
5952
+ };
5953
+ }
5954
+
5955
+ // src/lib/server/cli-server-proxy-processor.ts
5890
5956
  function renderSingleOutput(output2, context2, rawCommand) {
5891
5957
  switch (output2.type) {
5892
5958
  case "text": {
@@ -6018,21 +6084,15 @@ var CliServerProxyProcessor = class _CliServerProxyProcessor {
6018
6084
  this.serverName = serverName;
6019
6085
  this.commandPath = [...parentPath, descriptor.command];
6020
6086
  this.command = isNested ? descriptor.command : `${serverName}:${descriptor.command}`;
6021
- this.description = descriptor.description;
6087
+ this.description = descriptor.description === void 0 ? void 0 : plainText(descriptor.description);
6022
6088
  this.version = descriptor.version;
6023
- this.metadata = {
6024
- module: `server:${serverName}`,
6025
- icon: "\u{1F5A5}",
6026
- requireServer: true
6027
- };
6028
- this.parameters = descriptor.parameters?.map((p) => ({
6029
- name: p.name,
6030
- aliases: p.aliases,
6031
- description: p.description,
6032
- required: p.required,
6033
- type: p.type,
6034
- defaultValue: p.defaultValue
6035
- }));
6089
+ const aliases = serverCommandAliases(descriptor);
6090
+ if (aliases.length) {
6091
+ this.aliases = isNested ? [...aliases] : aliases.map((a) => `${serverName}:${a}`);
6092
+ }
6093
+ this.metadata = serverCommandMetadata(descriptor, `server:${serverName}`, serverName);
6094
+ this.parameters = serverCommandParameters(descriptor);
6095
+ this.writeDescription = serverCommandWriteDescription(descriptor);
6036
6096
  this.processors = descriptor.processors?.map(
6037
6097
  (sub) => new _CliServerProxyProcessor(connection, sub, serverName, true, this.commandPath)
6038
6098
  );
@@ -6058,20 +6118,14 @@ var CliServerMultiProxyProcessor = class _CliServerMultiProxyProcessor {
6058
6118
  const first = entries[0].descriptor;
6059
6119
  this.commandPath = [...parentPath, first.command];
6060
6120
  this.command = first.command;
6061
- this.description = first.description;
6062
- this.metadata = {
6063
- module: "server:multi",
6064
- icon: "\u{1F5A5}",
6065
- requireServer: true
6066
- };
6067
- this.parameters = first.parameters?.map((p) => ({
6068
- name: p.name,
6069
- aliases: p.aliases,
6070
- description: p.description,
6071
- required: p.required,
6072
- type: p.type,
6073
- defaultValue: p.defaultValue
6074
- }));
6121
+ this.description = first.description === void 0 ? void 0 : plainText(first.description);
6122
+ const aliases = serverCommandAliases(first);
6123
+ if (parentPath.length > 0 && aliases.length) {
6124
+ this.aliases = aliases;
6125
+ }
6126
+ this.metadata = serverCommandMetadata(first, "server:multi");
6127
+ this.parameters = serverCommandParameters(first);
6128
+ this.writeDescription = serverCommandWriteDescription(first);
6075
6129
  this.processors = first.processors?.map((sub) => {
6076
6130
  const subEntries = entries.map((e) => {
6077
6131
  const subDesc = e.descriptor.processors?.find(
@@ -6180,9 +6234,15 @@ var CliServerManager = class {
6180
6234
  /** Bare-name aliases this manager registered (only these are ever removed) */
6181
6235
  this._bareAliases = /* @__PURE__ */ new Set();
6182
6236
  this._hidden = [];
6237
+ /** Servers the user linked (`server add`), as opposed to the host's */
6238
+ this._linked = /* @__PURE__ */ new Set();
6183
6239
  /** `server:command` pairs already warned about, so each warns once */
6184
6240
  this._warnedHidden = /* @__PURE__ */ new Set();
6185
6241
  }
6242
+ /** Whether the user linked this server (`server add`): only those can be removed or edited. */
6243
+ isLinked(name) {
6244
+ return this._linked.has(name);
6245
+ }
6186
6246
  get defaultServer() {
6187
6247
  return this._defaultServer;
6188
6248
  }
@@ -6203,53 +6263,97 @@ var CliServerManager = class {
6203
6263
  this.unregisterBareAliases();
6204
6264
  this.registerBareAliases();
6205
6265
  }
6206
- async connectAll(servers, logger, backgroundServices, cliLogger) {
6266
+ /** Where connections report and run (set once at boot, before any server connects). */
6267
+ configure(logger, backgroundServices, cliLogger) {
6207
6268
  this._logger = logger;
6208
6269
  this._cliLogger = cliLogger;
6209
6270
  this._backgroundServices = backgroundServices;
6271
+ }
6272
+ async connectAll(servers, logger, backgroundServices, cliLogger) {
6273
+ this.configure(logger, backgroundServices, cliLogger);
6210
6274
  for (const config of servers) {
6211
6275
  if (config.enabled === false) continue;
6212
- const connection = new CliServerConnection(
6213
- config,
6214
- backgroundServices,
6215
- cliLogger,
6216
- this.authService
6276
+ await this.connectServer(config);
6277
+ }
6278
+ this.registerBareAliases();
6279
+ }
6280
+ /**
6281
+ * Connects one server and registers its commands (without the bare
6282
+ * aliases: callers refresh them once every server is connected).
6283
+ */
6284
+ async connectServer(config) {
6285
+ const logger = this._logger;
6286
+ const connection = new CliServerConnection(
6287
+ config,
6288
+ this._backgroundServices,
6289
+ this._cliLogger,
6290
+ this.authService
6291
+ );
6292
+ this.connections.set(config.name, connection);
6293
+ connection.onDisconnect = () => {
6294
+ this.handleDisconnect(config.name);
6295
+ };
6296
+ connection.onReconnected = () => {
6297
+ logger?.info(
6298
+ `Server '${config.name}' reconnected (${connection.commands.length} commands, API v${connection.apiVersion}).`
6217
6299
  );
6218
- this.connections.set(config.name, connection);
6219
- connection.onDisconnect = () => {
6220
- this.handleDisconnect(config.name);
6221
- };
6222
- connection.onReconnected = () => {
6223
- logger?.info(
6224
- `Server '${config.name}' reconnected (${connection.commands.length} commands, API v${connection.apiVersion}).`
6225
- );
6226
- this.emitConnected(connection, true);
6227
- this.registerProxyProcessors(connection, config.name);
6228
- this.unregisterBareAliases();
6229
- this.registerBareAliases();
6230
- };
6231
- await connection.connect();
6232
- if (connection.connected) {
6233
- logger?.info(
6234
- `Connected to server '${config.name}' (${connection.commands.length} commands)`
6235
- );
6236
- this.registerProxyProcessors(connection, config.name);
6237
- this.emitConnected(connection, false);
6238
- } else if (connection.needsSignIn) {
6239
- logger?.info(
6240
- `Server '${config.name}' needs a sign-in; its commands load once you are signed in.`
6241
- );
6242
- } else {
6243
- logger?.warn(
6244
- `Could not connect to server '${config.name}' at ${config.url}. Commands from this server will not be available.`
6245
- );
6246
- this.events?.emit("server:unavailable", {
6247
- name: config.name,
6248
- url: config.url
6249
- });
6250
- }
6300
+ this.emitConnected(connection, true);
6301
+ this.registerProxyProcessors(connection, config.name);
6302
+ this.unregisterBareAliases();
6303
+ this.registerBareAliases();
6304
+ };
6305
+ await connection.connect();
6306
+ if (connection.connected) {
6307
+ logger?.info(
6308
+ `Connected to server '${config.name}' (${connection.commands.length} commands)`
6309
+ );
6310
+ this.registerProxyProcessors(connection, config.name);
6311
+ this.emitConnected(connection, false);
6312
+ } else if (connection.needsSignIn) {
6313
+ logger?.info(
6314
+ `Server '${config.name}' needs a sign-in; its commands load once you are signed in.`
6315
+ );
6316
+ } else {
6317
+ logger?.warn(
6318
+ `Could not connect to server '${config.name}' at ${config.url}. Commands from this server will not be available.`
6319
+ );
6320
+ this.events?.emit("server:unavailable", {
6321
+ name: config.name,
6322
+ url: config.url
6323
+ });
6324
+ }
6325
+ return connection;
6326
+ }
6327
+ /**
6328
+ * Connects a server added while the terminal runs (`server add`): its
6329
+ * commands are available at once. Throws when the name is taken.
6330
+ */
6331
+ async addServer(config, options = {}) {
6332
+ if (this.connections.has(config.name)) {
6333
+ throw new Error(`Server '${config.name}' already exists`);
6251
6334
  }
6335
+ if (options.linked) this._linked.add(config.name);
6336
+ const connection = await this.connectServer(config);
6337
+ this.unregisterBareAliases();
6252
6338
  this.registerBareAliases();
6339
+ return connection;
6340
+ }
6341
+ /**
6342
+ * Disconnects a server and forgets it (`server remove`): its commands go
6343
+ * away. False when there is no such server.
6344
+ */
6345
+ removeServer(name) {
6346
+ const connection = this.connections.get(name);
6347
+ if (!connection) return false;
6348
+ connection.onDisconnect = void 0;
6349
+ connection.onReconnected = void 0;
6350
+ connection.disconnect();
6351
+ this.connections.delete(name);
6352
+ this._linked.delete(name);
6353
+ if (this._defaultServer === name) this._defaultServer = null;
6354
+ this.unregisterServerProcessors(name);
6355
+ this.events?.emit("server:disconnected", { name });
6356
+ return true;
6253
6357
  }
6254
6358
  async reconnect(name) {
6255
6359
  const connection = this.connections.get(name);
@@ -6379,7 +6483,7 @@ var CliServerManager = class {
6379
6483
  serverName
6380
6484
  );
6381
6485
  alias.command = command2;
6382
- alias.aliases = [namespacedCommand];
6486
+ alias.aliases = [namespacedCommand, ...this.freeAliases(descriptor)];
6383
6487
  this.registry.registerProcessor(alias);
6384
6488
  this._bareAliases.add(alias);
6385
6489
  } else {
@@ -6391,11 +6495,20 @@ var CliServerManager = class {
6391
6495
  return { serverName, connection, descriptor };
6392
6496
  });
6393
6497
  const multi = new CliServerMultiProxyProcessor(entries, this);
6498
+ const aliases = this.freeAliases(entries[0].descriptor);
6499
+ if (aliases.length) multi.aliases = aliases;
6394
6500
  this.registry.registerProcessor(multi);
6395
6501
  this._bareAliases.add(multi);
6396
6502
  }
6397
6503
  }
6398
6504
  }
6505
+ /**
6506
+ * The server's aliases of a command that no registered command answers
6507
+ * to yet: a local command (or an earlier server alias) keeps its name.
6508
+ */
6509
+ freeAliases(descriptor) {
6510
+ return serverCommandAliases(descriptor).filter((a) => !this.registry.findProcessor(a, []));
6511
+ }
6399
6512
  recordHidden(server, command2, localCommand) {
6400
6513
  this._hidden.push({ server, command: command2, localCommand });
6401
6514
  const key = `${server}:${command2}`;
@@ -6655,9 +6768,13 @@ var CliHelpCommandProcessor = class {
6655
6768
  }
6656
6769
  }
6657
6770
  if (processor.metadata?.requireServer) {
6771
+ const server = processor.metadata.server;
6658
6772
  writer.writeln();
6659
6773
  writer.writeln(
6660
- ` ${writer.wrapInColor(t.t("cli.help.requires_server", "Requires a connected server"), cliCore.CliForegroundColor.Red)}`
6774
+ ` ${writer.wrapInColor(
6775
+ server ? t.t("cli.help.runs_on_server", "Runs on server {server}", { server }) : t.t("cli.help.requires_server", "Requires a connected server"),
6776
+ cliCore.CliForegroundColor.Red
6777
+ )}`
6661
6778
  );
6662
6779
  }
6663
6780
  writer.writeln();
@@ -6683,14 +6800,24 @@ var CliHelpCommandProcessor = class {
6683
6800
  const typeStr = `${DIM}<${param.type}>${RESET}`;
6684
6801
  const required = param.required ? writer.wrapInColor(` ${translator.t("cli.help.required", "(required)")}`, cliCore.CliForegroundColor.Red) : "";
6685
6802
  const defaultVal = param.defaultValue !== void 0 && !param.required ? `${DIM} [${param.defaultValue}]${RESET}` : "";
6803
+ const allowed = param.allowedValues?.length ? `${DIM}${translator.t("cli.help.one_of", "one of:")}${RESET} ${param.allowedValues.map((v) => writer.wrapInColor(v, cliCore.CliForegroundColor.Magenta)).join(", ")}` : "";
6804
+ const examples = param.examples?.length ? `${DIM}${translator.t("cli.help.for_example", "e.g.")}${RESET} ${param.examples.map((v) => writer.wrapInColor(v, cliCore.CliForegroundColor.Magenta)).join(", ")}` : "";
6686
6805
  if (narrow) {
6687
6806
  writer.writeln(` ${nameStr} ${typeStr}${required}${defaultVal}`);
6688
6807
  cliCore.writeWrapped(context2, param.description, 6);
6808
+ if (allowed) writer.writeln(` ${allowed}`);
6809
+ if (examples) writer.writeln(` ${examples}`);
6689
6810
  return;
6690
6811
  }
6691
6812
  writer.writeln(
6692
6813
  ` ${nameStr} ${typeStr} ${param.description}${required}${defaultVal}`
6693
6814
  );
6815
+ if (allowed) {
6816
+ writer.writeln(` ${allowed}`);
6817
+ }
6818
+ if (examples) {
6819
+ writer.writeln(` ${examples}`);
6820
+ }
6694
6821
  }
6695
6822
  };
6696
6823
  function isNarrow(context2) {
@@ -15157,6 +15284,129 @@ var CliClipboard = class {
15157
15284
  }
15158
15285
  }
15159
15286
  };
15287
+
15288
+ // src/lib/server/linked/cli-linked-servers.ts
15289
+ var SERVERS_KEY = "qcli-linked-servers";
15290
+ var tokenKey = (name) => `server-token:${name.toLowerCase()}`;
15291
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
15292
+ function normalizeServerUrl(input) {
15293
+ const value = input.trim();
15294
+ if (!value) return null;
15295
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
15296
+ try {
15297
+ const url = new URL(withScheme);
15298
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
15299
+ if (!url.hostname || url.username || url.password) return null;
15300
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
15301
+ } catch {
15302
+ return null;
15303
+ }
15304
+ }
15305
+ function isPrivateHost(hostname) {
15306
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
15307
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
15308
+ if (host.includes(":")) {
15309
+ return host === "::1" || /^f[cd][0-9a-f]{0,2}:/.test(host) || /^fe[89ab][0-9a-f]?:/.test(host);
15310
+ }
15311
+ const ip = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
15312
+ if (!ip) return false;
15313
+ const [a, b] = [Number(ip[1]), Number(ip[2])];
15314
+ return a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
15315
+ }
15316
+ function checkServerUrl(input, pageProtocol) {
15317
+ const url = normalizeServerUrl(input);
15318
+ if (!url) return "Not a valid http(s) URL";
15319
+ const { protocol, hostname } = new URL(url);
15320
+ if (protocol === "http:" && !isPrivateHost(hostname)) {
15321
+ return "Only https servers can be linked (http is accepted for localhost and private addresses)";
15322
+ }
15323
+ return {
15324
+ url,
15325
+ insecure: protocol === "http:",
15326
+ mixedContent: protocol === "http:" && pageProtocol === "https:"
15327
+ };
15328
+ }
15329
+ function userServersAllow(options, url) {
15330
+ const policy = options?.userServers;
15331
+ if (policy === false) return false;
15332
+ if (policy === void 0 || policy === true) return true;
15333
+ const origin = new URL(url).origin.toLowerCase();
15334
+ return (policy.allowedOrigins ?? []).some((pattern) => {
15335
+ const escaped = pattern.toLowerCase().replace(/\/+$/, "").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
15336
+ return new RegExp(`^${escaped}$`).test(origin);
15337
+ });
15338
+ }
15339
+ function userServersEnabled(options) {
15340
+ return options?.userServers !== false;
15341
+ }
15342
+ function suggestServerName(url, taken) {
15343
+ const { hostname, port } = new URL(url);
15344
+ const host = hostname.replace(/^\[|\]$/g, "");
15345
+ const isIp = /^[\d.]+$/.test(host) || host.includes(":");
15346
+ const base = (isIp ? host.replace(/[.:]/g, "-") : host.split(".")[0]) + (port && (isIp || host === "localhost") ? `-${port}` : "");
15347
+ let name = base.replace(/[^A-Za-z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 28) || "server";
15348
+ if (!SERVER_NAME_PATTERN.test(name)) name = "server";
15349
+ let candidate = name;
15350
+ for (let i = 2; taken(candidate); i++) candidate = `${name}-${i}`;
15351
+ return candidate;
15352
+ }
15353
+ function maskToken(token) {
15354
+ if (!token) return "-";
15355
+ return `\u2022\u2022\u2022\u2022${token.slice(-4)}`;
15356
+ }
15357
+ function redactSecrets(command2) {
15358
+ return command2.replace(
15359
+ // A following flag (`--token --default`) is not a value
15360
+ /(--(?:token|password)(?:=|\s+))("[^"]*"|'[^']*'|(?!--)\S+)/gi,
15361
+ (_match, flag) => `${flag}***`
15362
+ );
15363
+ }
15364
+ var CliLinkedServers = class {
15365
+ constructor(kv, secrets) {
15366
+ this.kv = kv;
15367
+ this.secrets = secrets;
15368
+ }
15369
+ async list() {
15370
+ const saved = await this.kv.get(SERVERS_KEY);
15371
+ return Array.isArray(saved) ? saved.filter((s) => s && typeof s.name === "string" && typeof s.url === "string") : [];
15372
+ }
15373
+ async get(name) {
15374
+ return (await this.list()).find((s) => s.name.toLowerCase() === name.toLowerCase());
15375
+ }
15376
+ /** Saves a server (replacing one with the same name) and its token (none: deleted). */
15377
+ async save(server, token) {
15378
+ if (token) await this.secrets.set(tokenKey(server.name), token);
15379
+ else await this.secrets.delete(tokenKey(server.name));
15380
+ const others = (await this.list()).filter((s) => s.name.toLowerCase() !== server.name.toLowerCase());
15381
+ await this.kv.set(SERVERS_KEY, [...others, server]);
15382
+ }
15383
+ async setToken(name, token) {
15384
+ await this.secrets.set(tokenKey(name), token);
15385
+ }
15386
+ async token(name) {
15387
+ return this.secrets.get(tokenKey(name));
15388
+ }
15389
+ async remove(name) {
15390
+ const all = await this.list();
15391
+ const rest = all.filter((s) => s.name.toLowerCase() !== name.toLowerCase());
15392
+ await this.secrets.delete(tokenKey(name));
15393
+ if (rest.length === all.length) return false;
15394
+ await this.kv.set(SERVERS_KEY, rest);
15395
+ return true;
15396
+ }
15397
+ /** Removes every linked server and every secret. */
15398
+ async clear() {
15399
+ await this.kv.remove(SERVERS_KEY);
15400
+ await this.secrets.clear();
15401
+ }
15402
+ };
15403
+ function linkedServerConfig(server, token) {
15404
+ return {
15405
+ name: server.name,
15406
+ url: server.url,
15407
+ ...token ? { headers: () => ({ Authorization: `Bearer ${token}` }) } : {}
15408
+ };
15409
+ }
15160
15410
  var InputModeBase = class {
15161
15411
  constructor(host, resolve) {
15162
15412
  this.host = host;
@@ -17908,7 +18158,8 @@ var CliExecutionContext = class {
17908
18158
  const startedAt = Date.now();
17909
18159
  await this.executor.executeCommand(command2, this);
17910
18160
  this.events?.emit("command:executed", {
17911
- command: command2,
18161
+ // As in the history: `--token`/`--password` values never leave
18162
+ command: redactSecrets(command2),
17912
18163
  exitCode: this.executor.exitStatus ?? 0,
17913
18164
  durationMs: Date.now() - startedAt
17914
18165
  });
@@ -18000,6 +18251,19 @@ var CliExecutionContext = class {
18000
18251
  isProgressRunning() {
18001
18252
  return this.progressBar.isRunning || this.spinner.isRunning || this.textAnimator.isRunning;
18002
18253
  }
18254
+ /**
18255
+ * What pressing Ctrl+C does: the current input mode handles the key, so a
18256
+ * running command is aborted, a prompt cancelled or a context left exactly
18257
+ * as with the keyboard.
18258
+ */
18259
+ interrupt() {
18260
+ const event = new KeyboardEvent("keydown", {
18261
+ key: "c",
18262
+ code: "KeyC",
18263
+ ctrlKey: true
18264
+ });
18265
+ this.currentMode?.handleKeyEvent(event);
18266
+ }
18003
18267
  abort() {
18004
18268
  if (this.progressBar.isRunning) {
18005
18269
  this.progressBar.complete();
@@ -19375,7 +19639,7 @@ var CliCommandHistory = class {
19375
19639
  return this.loading;
19376
19640
  }
19377
19641
  async addCommand(command2) {
19378
- const normalizedCommand = command2.trim();
19642
+ const normalizedCommand = redactSecrets(command2.trim());
19379
19643
  if (!normalizedCommand) return;
19380
19644
  await this.loading;
19381
19645
  const existingIndex = this.commandHistory.indexOf(normalizedCommand);
@@ -20964,6 +21228,129 @@ var CliStateStoreManager = class {
20964
21228
  };
20965
21229
  }
20966
21230
  };
21231
+
21232
+ // src/lib/server/linked/cli-web-crypto-secret-store.ts
21233
+ var KEY_KEY = "qcli-secret-key";
21234
+ var NAMES_KEY = "qcli-secret-names";
21235
+ var SECRET_PREFIX = "qcli-secret:";
21236
+ var CliWebCryptoSecretStore = class {
21237
+ constructor(kv, crypto2 = globalThis.crypto ?? null) {
21238
+ this.kv = kv;
21239
+ this.crypto = crypto2;
21240
+ this.keyPromise = null;
21241
+ }
21242
+ isAvailable() {
21243
+ return !!this.crypto?.subtle && typeof this.crypto.getRandomValues === "function";
21244
+ }
21245
+ async get(key) {
21246
+ if (!this.isAvailable()) return void 0;
21247
+ const record = await this.kv.get(SECRET_PREFIX + key);
21248
+ if (!record?.iv || !record?.data) return void 0;
21249
+ const cryptoKey = await this.kv.get(KEY_KEY);
21250
+ if (!cryptoKey) return void 0;
21251
+ try {
21252
+ const plain = await this.crypto.subtle.decrypt(
21253
+ { name: "AES-GCM", iv: record.iv },
21254
+ cryptoKey,
21255
+ record.data
21256
+ );
21257
+ return new TextDecoder().decode(plain);
21258
+ } catch {
21259
+ return void 0;
21260
+ }
21261
+ }
21262
+ async set(key, value) {
21263
+ if (!this.isAvailable()) {
21264
+ throw new Error("Secrets cannot be saved: this page has no Web Crypto (it must be served over https)");
21265
+ }
21266
+ const cryptoKey = await this.key();
21267
+ const iv = this.crypto.getRandomValues(new Uint8Array(12));
21268
+ const data = await this.crypto.subtle.encrypt(
21269
+ { name: "AES-GCM", iv },
21270
+ cryptoKey,
21271
+ new TextEncoder().encode(value)
21272
+ );
21273
+ await this.kv.set(SECRET_PREFIX + key, { iv, data: new Uint8Array(data) });
21274
+ const names = await this.kv.get(NAMES_KEY) ?? [];
21275
+ if (!names.includes(key)) await this.kv.set(NAMES_KEY, [...names, key]);
21276
+ }
21277
+ async delete(key) {
21278
+ await this.kv.remove(SECRET_PREFIX + key);
21279
+ const names = await this.kv.get(NAMES_KEY) ?? [];
21280
+ if (names.includes(key)) await this.kv.set(NAMES_KEY, names.filter((n) => n !== key));
21281
+ }
21282
+ async clear() {
21283
+ for (const name of await this.kv.get(NAMES_KEY) ?? []) {
21284
+ await this.kv.remove(SECRET_PREFIX + name);
21285
+ }
21286
+ await this.kv.remove(NAMES_KEY);
21287
+ await this.kv.remove(KEY_KEY);
21288
+ this.keyPromise = null;
21289
+ }
21290
+ /** The saved key, or a new one (created once, even for parallel calls). */
21291
+ key() {
21292
+ this.keyPromise ??= (async () => {
21293
+ const saved = await this.kv.get(KEY_KEY);
21294
+ if (saved) return saved;
21295
+ const created = await this.crypto.subtle.generateKey(
21296
+ { name: "AES-GCM", length: 256 },
21297
+ false,
21298
+ ["encrypt", "decrypt"]
21299
+ );
21300
+ await this.kv.set(KEY_KEY, created);
21301
+ return created;
21302
+ })();
21303
+ const pending = this.keyPromise;
21304
+ pending.catch(() => {
21305
+ if (this.keyPromise === pending) this.keyPromise = null;
21306
+ });
21307
+ return pending;
21308
+ }
21309
+ };
21310
+
21311
+ // src/lib/server/linked/cli-linked-servers-runtime.ts
21312
+ var webCryptoStores = /* @__PURE__ */ new WeakMap();
21313
+ function secretStoreOf(context2) {
21314
+ const provided = context2.services.get(cliCore.ICliSecretStore_TOKEN);
21315
+ if (provided) return provided;
21316
+ const kv = context2.services.getRequired(cliCore.ICliKeyValueStore_TOKEN);
21317
+ let store = webCryptoStores.get(kv);
21318
+ if (!store) {
21319
+ store = new CliWebCryptoSecretStore(kv);
21320
+ webCryptoStores.set(kv, store);
21321
+ }
21322
+ return store;
21323
+ }
21324
+ function linkedServersOf(context2) {
21325
+ const kv = context2.services.getRequired(cliCore.ICliKeyValueStore_TOKEN);
21326
+ return new CliLinkedServers(kv, secretStoreOf(context2));
21327
+ }
21328
+ function rejectedTokenMessage(name) {
21329
+ return `Server '${name}' rejected its token. Link it again with server add ${name}, or set a new token with server edit ${name} --token.`;
21330
+ }
21331
+ async function restoreLinkedServers(context2, manager, warn) {
21332
+ if (!userServersEnabled(context2.options)) return;
21333
+ const linked = linkedServersOf(context2);
21334
+ let servers;
21335
+ try {
21336
+ servers = await linked.list();
21337
+ } catch {
21338
+ return;
21339
+ }
21340
+ for (const server of servers) {
21341
+ if (manager.connections.has(server.name)) {
21342
+ warn(`Linked server '${server.name}' is not connected: the app has a server with the same name.`);
21343
+ continue;
21344
+ }
21345
+ if (!userServersAllow(context2.options, server.url)) {
21346
+ warn(`Linked server '${server.name}' (${server.url}) is not allowed by this app.`);
21347
+ continue;
21348
+ }
21349
+ const token = await linked.token(server.name).catch(() => void 0);
21350
+ const connection = await manager.addServer(linkedServerConfig(server, token), { linked: true });
21351
+ if (token && connection.needsSignIn) warn(rejectedTokenMessage(server.name));
21352
+ }
21353
+ }
20967
21354
  var CliServerAuthService = class {
20968
21355
  constructor(services) {
20969
21356
  this.services = services;
@@ -21035,14 +21422,17 @@ function writeUnknownServer(context2, manager, name) {
21035
21422
  function writeConfigureHint(context2) {
21036
21423
  const { writer } = context2;
21037
21424
  writer.writeln(
21038
- ` Servers are configured by the app: ${writer.wrapInColor("servers: [{ name: 'node', url: 'http://localhost:8047' }]", cliCore.CliForegroundColor.Yellow)}`
21425
+ ` Link one: ${writer.wrapInColor("server add", cliCore.CliForegroundColor.Cyan)}, or configure it in the app: ${writer.wrapInColor("servers: [{ name: 'node', url: 'http://localhost:8047' }]", cliCore.CliForegroundColor.Yellow)}`
21039
21426
  );
21040
21427
  writer.writeln(
21041
21428
  ` See ${writer.wrapInColor("https://cli.qodalis.com/docs/server-integration", cliCore.CliForegroundColor.Blue)}`
21042
21429
  );
21043
21430
  }
21044
21431
  function statusLabel(context2, connection) {
21045
- return connection.connected ? context2.writer.wrapInColor("\u25CF connected", cliCore.CliForegroundColor.Green) : context2.writer.wrapInColor("\u25CB unreachable", cliCore.CliForegroundColor.Red);
21432
+ if (connection.connected) {
21433
+ return context2.writer.wrapInColor("\u25CF connected", cliCore.CliForegroundColor.Green);
21434
+ }
21435
+ return connection.needsSignIn ? context2.writer.wrapInColor("\u25D0 sign-in needed", cliCore.CliForegroundColor.Yellow) : context2.writer.wrapInColor("\u25CB unreachable", cliCore.CliForegroundColor.Red);
21046
21436
  }
21047
21437
  async function pingServer(connection) {
21048
21438
  const started = performance.now();
@@ -21084,6 +21474,546 @@ function editDistance(a, b) {
21084
21474
  return row[b.length];
21085
21475
  }
21086
21476
 
21477
+ // src/lib/server/linked/cli-server-link-client.ts
21478
+ var V1 = "/api/v1/qcli";
21479
+ function plainText2(value) {
21480
+ return String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
21481
+ }
21482
+ async function readJson(response) {
21483
+ try {
21484
+ return await response.json();
21485
+ } catch {
21486
+ return null;
21487
+ }
21488
+ }
21489
+ async function probeServer(fetchFn, url) {
21490
+ let versions;
21491
+ try {
21492
+ versions = await fetchFn(`${url}/api/qcli/versions`, { headers: { Accept: "application/json" } });
21493
+ } catch (e) {
21494
+ throw new Error(`Cannot reach ${url}: ${e.message || "network error"}`, { cause: e });
21495
+ }
21496
+ const body = versions.ok ? await readJson(versions) : null;
21497
+ if (!body || !Array.isArray(body.supportedVersions)) {
21498
+ throw new Error(`${url} is not a Qodalis CLI server (no version discovery at /api/qcli/versions)`);
21499
+ }
21500
+ let capabilities = null;
21501
+ try {
21502
+ const res = await fetchFn(`${url}${V1}/capabilities`, { headers: { Accept: "application/json" } });
21503
+ if (res.ok) capabilities = await readJson(res);
21504
+ } catch {
21505
+ }
21506
+ let needsSignIn = false;
21507
+ try {
21508
+ const res = await fetchFn(`${url}${V1}/commands`, { headers: { Accept: "application/json" } });
21509
+ needsSignIn = res.status === 401 || res.status === 403;
21510
+ } catch {
21511
+ }
21512
+ return { version: capabilities?.version, capabilities, needsSignIn };
21513
+ }
21514
+ async function requestLink(fetchFn, url, client) {
21515
+ const res = await fetchFn(`${url}${V1}/link`, {
21516
+ method: "POST",
21517
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
21518
+ body: JSON.stringify({ client })
21519
+ });
21520
+ if (res.status === 404 || res.status === 405) return null;
21521
+ const body = await readJson(res);
21522
+ if (!res.ok || !body?.linkId) {
21523
+ throw new Error(body?.error ? plainText2(body.error) : `The server refused the link request (${res.status})`);
21524
+ }
21525
+ let verificationUrl;
21526
+ try {
21527
+ verificationUrl = new URL(String(body.verificationUrl), `${url}/`);
21528
+ } catch {
21529
+ throw new Error("The server sent an invalid approval page address");
21530
+ }
21531
+ if (verificationUrl.protocol !== "https:" && verificationUrl.protocol !== "http:") {
21532
+ throw new Error("The server sent an invalid approval page address");
21533
+ }
21534
+ return {
21535
+ linkId: String(body.linkId),
21536
+ userCode: plainText2(body.userCode),
21537
+ verificationUrl: verificationUrl.toString(),
21538
+ interval: Math.max(1, Number(body.interval) || 2),
21539
+ expiresIn: Number(body.expiresIn) || 600
21540
+ };
21541
+ }
21542
+ async function pollLink(fetchFn, url, linkId) {
21543
+ const res = await fetchFn(`${url}${V1}/link/${encodeURIComponent(linkId)}`, { headers: { Accept: "application/json" } });
21544
+ if (res.status === 404) return { status: "expired" };
21545
+ const body = await readJson(res);
21546
+ if (!res.ok || !body) throw new Error(`Link status unavailable (${res.status})`);
21547
+ if (body.status === "approved" && body.token) return { status: "approved", token: body.token, userName: body.userName };
21548
+ if (body.status === "denied") return { status: "denied" };
21549
+ if (body.status === "expired") return { status: "expired" };
21550
+ return { status: "pending" };
21551
+ }
21552
+ async function createTokenWithPassword(fetchFn, url, username, password, tokenName) {
21553
+ const login = await fetchFn(`${url}${V1}/sessions`, {
21554
+ method: "POST",
21555
+ headers: { "Content-Type": "application/json" },
21556
+ body: JSON.stringify({ username, password })
21557
+ });
21558
+ const session = await readJson(login);
21559
+ if (!login.ok || !session?.token) {
21560
+ throw new Error(login.status === 404 ? "This server has no user sign-in" : plainText2(session?.error ?? "Sign-in failed"));
21561
+ }
21562
+ const auth = { Authorization: `Bearer ${session.token}` };
21563
+ try {
21564
+ for (let suffix = 1; ; suffix++) {
21565
+ const name = suffix === 1 ? tokenName : `${tokenName.slice(0, 60)} ${suffix}`;
21566
+ const created = await fetchFn(`${url}${V1}/tokens`, {
21567
+ method: "POST",
21568
+ headers: { "Content-Type": "application/json", ...auth },
21569
+ body: JSON.stringify({ name })
21570
+ });
21571
+ const body = await readJson(created);
21572
+ if (created.status === 409 && suffix < 20) continue;
21573
+ if (!created.ok || !body?.token) throw new Error(body?.error ? plainText2(body.error) : `Could not create a token (${created.status})`);
21574
+ return body.token;
21575
+ }
21576
+ } finally {
21577
+ await fetchFn(`${url}${V1}/sessions/current`, { method: "DELETE", headers: auth }).catch(() => void 0);
21578
+ }
21579
+ }
21580
+ function deviceName(appName = "Qodalis CLI") {
21581
+ const nav = globalThis.navigator;
21582
+ const ua = nav?.userAgent ?? "";
21583
+ const platform = nav?.userAgentData?.platform || (/Android/i.test(ua) ? "Android" : /iPhone|iPad/i.test(ua) ? "iOS" : /Mac/i.test(ua) ? "macOS" : /Windows/i.test(ua) ? "Windows" : /Linux/i.test(ua) ? "Linux" : "");
21584
+ const name = platform ? `${appName} on ${platform}` : appName;
21585
+ return name.replace(/[^A-Za-z0-9._ -]/g, "-").slice(0, 64);
21586
+ }
21587
+
21588
+ // src/lib/server/linked/cli-server-link-processors.ts
21589
+ var fetchOf = (context2) => (url, init) => context2.http.fetch(url, init);
21590
+ function sleep(ms, signal) {
21591
+ return new Promise((resolve) => {
21592
+ const timer = setTimeout(resolve, ms);
21593
+ signal?.addEventListener("abort", () => {
21594
+ clearTimeout(timer);
21595
+ resolve();
21596
+ }, { once: true });
21597
+ });
21598
+ }
21599
+ function linkingDisabled(context2) {
21600
+ if (userServersEnabled(context2.options)) return false;
21601
+ context2.writer.writeError(
21602
+ context2.translator.t("cli.server.link.disabled", "This app does not allow linking servers from the terminal.")
21603
+ );
21604
+ return true;
21605
+ }
21606
+ function parseAddArgs(value) {
21607
+ const words = cliCore.splitShellWords(value ?? "");
21608
+ if (words.length >= 2) return { name: words[0], url: words[1] };
21609
+ if (words.length === 1) {
21610
+ return /[.:/]/.test(words[0]) ? { url: words[0] } : { name: words[0] };
21611
+ }
21612
+ return {};
21613
+ }
21614
+ async function replaceConnection(manager, config) {
21615
+ const current = [...manager.connections.keys()].find((n) => n.toLowerCase() === config.name.toLowerCase());
21616
+ const wasDefault = !!current && manager.defaultServer === current;
21617
+ if (current) manager.removeServer(current);
21618
+ const connection = await manager.addServer(config, { linked: true });
21619
+ if (wasDefault) manager.setDefaultServer(config.name);
21620
+ return connection;
21621
+ }
21622
+ var ServerAddProcessor = class {
21623
+ constructor() {
21624
+ this.command = "add";
21625
+ this.aliases = ["link"];
21626
+ this.description = "Link a server to this terminal (kept across reloads)";
21627
+ this.acceptsRawInput = true;
21628
+ this.valueRequired = false;
21629
+ this.parameters = [
21630
+ { name: "token", description: "API token (qcli_pat_\u2026) to link with (asked for, hidden, when given without a value)", required: false, type: "string" },
21631
+ { name: "default", description: "Make it the default server", required: false, type: "boolean" },
21632
+ { name: "no-browser", description: "Link with a token or a password, without the browser", required: false, type: "boolean" }
21633
+ ];
21634
+ }
21635
+ async processCommand(command2, context2) {
21636
+ const t = context2.translator;
21637
+ const { writer, reader } = context2;
21638
+ const manager = serverManagerOf(context2);
21639
+ if (!manager || linkingDisabled(context2)) return;
21640
+ const linked = linkedServersOf(context2);
21641
+ const args = parseAddArgs(command2.value);
21642
+ const rawUrl = args.url ?? await reader.readLine(t.t("cli.server.link.url_prompt", "Server URL: "), { placeholder: "https://my-server.example.com" });
21643
+ if (!rawUrl) return;
21644
+ const pageProtocol = globalThis.location?.protocol;
21645
+ const checked = checkServerUrl(rawUrl, pageProtocol);
21646
+ if (typeof checked === "string") {
21647
+ writer.writeError(checked);
21648
+ return;
21649
+ }
21650
+ const url = checked.url;
21651
+ if (!userServersAllow(context2.options, url)) {
21652
+ writer.writeError(t.t("cli.server.link.origin_not_allowed", "This app does not allow linking {url}.", { url }));
21653
+ return;
21654
+ }
21655
+ if (checked.mixedContent) {
21656
+ writer.writeWarning(t.t("cli.server.link.mixed_content", "This page is served over https: the browser may block requests to an http server."));
21657
+ }
21658
+ let probe;
21659
+ context2.spinner?.show(t.t("cli.server.link.checking", "Checking {url}\u2026", { url }));
21660
+ try {
21661
+ probe = await probeServer(fetchOf(context2), url);
21662
+ } catch (e) {
21663
+ writer.writeError(e.message);
21664
+ return;
21665
+ } finally {
21666
+ context2.spinner?.hide();
21667
+ }
21668
+ writeProbe(context2, url, probe);
21669
+ if (checked.insecure) {
21670
+ writer.writeWarning(t.t("cli.server.link.http_warning", "{url} is not encrypted (http): its token travels in clear text on the network.", { url }));
21671
+ const go = await reader.readConfirm(t.t("cli.server.link.http_confirm", "Link it anyway?"), false);
21672
+ if (!go) return;
21673
+ }
21674
+ let name = args.name;
21675
+ if (name) {
21676
+ if (!await this.nameAvailable(context2, manager, linked, name)) return;
21677
+ }
21678
+ const tokenFlag = command2.args?.["token"];
21679
+ const tokenArg = typeof tokenFlag === "string" && tokenFlag.trim() ? tokenFlag.trim() : void 0;
21680
+ const method = tokenFlag !== void 0 && tokenFlag !== false ? "token" : await this.chooseMethod(context2, probe, !!command2.args?.["no-browser"]);
21681
+ if (!method) return;
21682
+ if (method !== "none" && !await secretStoreOf(context2).isAvailable()) {
21683
+ writer.writeError(t.t("cli.server.link.no_secret_store", "Tokens cannot be kept safely on this page (no Web Crypto: serve it over https). The server was not linked."));
21684
+ return;
21685
+ }
21686
+ let token;
21687
+ let userName;
21688
+ if (method === "browser") {
21689
+ const result = await this.linkInBrowser(context2, url);
21690
+ if (!result) return;
21691
+ token = result.token;
21692
+ userName = result.userName;
21693
+ } else if (method === "token") {
21694
+ token = tokenArg ?? (await reader.readPassword(t.t("cli.server.link.token_prompt", "API token: ")))?.trim();
21695
+ if (!token) return;
21696
+ } else if (method === "password") {
21697
+ const username = await reader.readLine(t.t("cli.server.link.username_prompt", "Username: "));
21698
+ if (!username) return;
21699
+ const password = await reader.readPassword(t.t("cli.server.link.password_prompt", "Password: "));
21700
+ if (!password) return;
21701
+ try {
21702
+ token = await createTokenWithPassword(fetchOf(context2), url, username.trim(), password, deviceName());
21703
+ userName = username.trim();
21704
+ } catch (e) {
21705
+ writer.writeError(e.message);
21706
+ return;
21707
+ }
21708
+ }
21709
+ if (!name) {
21710
+ const suggested = suggestServerName(url, (n) => manager.connections.has(n));
21711
+ const answer = await reader.readLine(t.t("cli.server.link.name_prompt", "Name for this server: "), {
21712
+ default: suggested,
21713
+ placeholder: suggested,
21714
+ validate: (value) => !value || SERVER_NAME_PATTERN.test(value.trim()) ? null : t.t("cli.server.link.invalid_name", "Letters, digits, - and _ (at most 32), starting with a letter or digit")
21715
+ });
21716
+ if (answer === null) return;
21717
+ name = answer.trim() || suggested;
21718
+ if (!await this.nameAvailable(context2, manager, linked, name)) return;
21719
+ }
21720
+ const server = { name, url, addedAt: Date.now() };
21721
+ const previous = (await linked.get(name))?.name;
21722
+ if (previous && previous !== name) await linked.remove(previous);
21723
+ await linked.save(server, token);
21724
+ const connection = await replaceConnection(manager, linkedServerConfig(server, token));
21725
+ if (connection.connected) {
21726
+ writer.writeSuccess(
21727
+ t.t("cli.server.link.linked", "Linked {name}: {count} commands{user}.", {
21728
+ name,
21729
+ count: connection.commands.length,
21730
+ user: userName ? t.t("cli.server.link.as_user", " as {user}", { user: userName }) : ""
21731
+ })
21732
+ );
21733
+ } else if (token && connection.needsSignIn) {
21734
+ writer.writeWarning(rejectedTokenMessage(name));
21735
+ } else if (connection.needsSignIn) {
21736
+ writer.writeWarning(t.t("cli.server.link.needs_sign_in", "Linked {name}, but its commands need a sign-in: link it with a token or through the browser.", { name }));
21737
+ } else {
21738
+ writer.writeWarning(t.t("cli.server.link.unreachable", "Linked {name}, but it is unreachable now: its commands load when it is back.", { name }));
21739
+ }
21740
+ if (command2.args?.["default"]) {
21741
+ manager.setDefaultServer(name);
21742
+ context2.state.updateState({ defaultServer: name });
21743
+ writer.writeln(t.t("cli.server.link.now_default", " {name} is the default server.", { name }));
21744
+ }
21745
+ }
21746
+ async nameAvailable(context2, manager, linked, name) {
21747
+ const t = context2.translator;
21748
+ if (!SERVER_NAME_PATTERN.test(name)) {
21749
+ context2.writer.writeError(t.t("cli.server.link.invalid_name", "Letters, digits, - and _ (at most 32), starting with a letter or digit"));
21750
+ return false;
21751
+ }
21752
+ const existing = [...manager.connections.keys()].find((n) => n.toLowerCase() === name.toLowerCase());
21753
+ if (existing && !manager.isLinked(existing)) {
21754
+ context2.writer.writeError(t.t("cli.server.link.host_server", "'{name}' is configured by the app and cannot be replaced.", { name: existing }));
21755
+ return false;
21756
+ }
21757
+ if (existing || await linked.get(name)) {
21758
+ const replace = await context2.reader.readConfirm(
21759
+ t.t("cli.server.link.replace_confirm", "A linked server '{name}' exists. Replace it?", { name }),
21760
+ false
21761
+ );
21762
+ return !!replace;
21763
+ }
21764
+ return true;
21765
+ }
21766
+ async chooseMethod(context2, probe, noBrowser) {
21767
+ const t = context2.translator;
21768
+ const options = [
21769
+ ...noBrowser ? [] : [{ label: t.t("cli.server.link.method_browser", "Open the server in the browser and approve (recommended)"), value: "browser" }],
21770
+ { label: t.t("cli.server.link.method_token", "Paste an API token"), value: "token" },
21771
+ { label: t.t("cli.server.link.method_password", "Sign in with user and password"), value: "password" },
21772
+ ...probe.needsSignIn ? [] : [{ label: t.t("cli.server.link.method_none", "No sign-in (open server)"), value: "none" }]
21773
+ ];
21774
+ const choice = await context2.reader.readSelect(t.t("cli.server.link.method_prompt", "How do you want to link it?"), options);
21775
+ return choice ?? null;
21776
+ }
21777
+ /** The device flow: a link request, the page to approve it, then polling until a decision. */
21778
+ async linkInBrowser(context2, url) {
21779
+ const t = context2.translator;
21780
+ const { writer } = context2;
21781
+ const fetchFn = fetchOf(context2);
21782
+ let request;
21783
+ try {
21784
+ request = await requestLink(fetchFn, url, deviceName());
21785
+ } catch (e) {
21786
+ writer.writeError(e.message);
21787
+ return null;
21788
+ }
21789
+ if (!request) {
21790
+ writer.writeError(t.t("cli.server.link.no_browser_link", "This server cannot link through the browser (it needs the users plugin, 1.0.0-beta.25 or later). Use server add --no-browser."));
21791
+ return null;
21792
+ }
21793
+ writer.writeln();
21794
+ writer.writeln(t.t("cli.server.link.open_page", " Open this page, sign in and approve:"));
21795
+ writer.write(" ");
21796
+ writer.writeLink(request.verificationUrl, request.verificationUrl);
21797
+ writer.writeln();
21798
+ writer.writeln(
21799
+ t.t("cli.server.link.compare_code", " Check that it shows the code {code}", {
21800
+ code: writer.wrapInColor(request.userCode, cliCore.CliForegroundColor.Yellow)
21801
+ })
21802
+ );
21803
+ writer.writeln();
21804
+ try {
21805
+ globalThis.open?.(request.verificationUrl, "_blank");
21806
+ } catch {
21807
+ }
21808
+ const deadline = Date.now() + request.expiresIn * 1e3;
21809
+ const cancelled = () => {
21810
+ context2.spinner?.hide();
21811
+ const minutes = Math.max(1, Math.ceil((deadline - Date.now()) / 6e4));
21812
+ writer.writeInfo(
21813
+ t.t("cli.server.link.cancelled", "Cancelled. The request expires on the server in {minutes} min.", { minutes })
21814
+ );
21815
+ return null;
21816
+ };
21817
+ context2.spinner?.show(t.t("cli.server.link.waiting", "Waiting for the approval\u2026 (Ctrl+C to cancel)"));
21818
+ try {
21819
+ while (Date.now() < deadline) {
21820
+ if (context2.signal?.aborted) return cancelled();
21821
+ await sleep(request.interval * 1e3, context2.signal);
21822
+ if (context2.signal?.aborted) return cancelled();
21823
+ let poll;
21824
+ try {
21825
+ poll = await pollLink(fetchFn, url, request.linkId);
21826
+ } catch {
21827
+ continue;
21828
+ }
21829
+ if (poll.status === "approved") return { token: poll.token, userName: poll.userName };
21830
+ if (poll.status === "denied") {
21831
+ context2.spinner?.hide();
21832
+ writer.writeError(t.t("cli.server.link.denied", "The link request was denied."));
21833
+ return null;
21834
+ }
21835
+ if (poll.status === "expired") break;
21836
+ }
21837
+ } finally {
21838
+ context2.spinner?.hide();
21839
+ }
21840
+ writer.writeError(t.t("cli.server.link.expired", "The link request expired. Run server add again."));
21841
+ return null;
21842
+ }
21843
+ writeDescription(context2) {
21844
+ const t = context2.translator;
21845
+ cliCore.writeWrapped(
21846
+ context2,
21847
+ t.t("cli.server.add.help.description", "Links a Node, .NET or Python server to this terminal: its commands are available at once and after every reload. Asks for what is missing: the URL, how to sign in (approve in the browser, paste an API token, or a user and password) and a name. Tokens are kept encrypted and never shown again; only https servers can be linked (http for localhost and private addresses).")
21848
+ );
21849
+ context2.writer.writeln();
21850
+ cliCore.writeUsage(context2, [
21851
+ ["server add", t.t("cli.server.add.help.interactive", "Asks for everything")],
21852
+ ["server add <url>", t.t("cli.server.add.help.url", "Link this server")],
21853
+ ["server add <name> <url> --token [token]", t.t("cli.server.add.help.token", "Link with an API token (scripts)")]
21854
+ ]);
21855
+ context2.writer.writeln();
21856
+ cliCore.writeExamples(context2, [
21857
+ ["server add https://cli.example.com", t.t("cli.server.add.help.example_browser", "Approve in the browser")],
21858
+ ["server add home http://192.168.1.20:8047 --no-browser", t.t("cli.server.add.help.example_lan", "A server on the local network")]
21859
+ ]);
21860
+ }
21861
+ };
21862
+ function writeProbe(context2, url, probe) {
21863
+ const { writer } = context2;
21864
+ const caps = probe.capabilities;
21865
+ const details = [caps?.os, caps?.shell ? "shell" : null, caps?.streaming ? "streaming" : null, probe.needsSignIn ? "sign-in required" : null].filter(Boolean).join(" \xB7 ");
21866
+ writer.writeSuccess(
21867
+ `${context2.translator.t("cli.server.link.found", "Qodalis CLI server")} ${probe.version ?? ""} ${writer.wrapInColor(url, cliCore.CliForegroundColor.Cyan)}${details ? ` (${details})` : ""}`.replace(/\s+/g, " ")
21868
+ );
21869
+ }
21870
+ var ServerRemoveProcessor = class {
21871
+ constructor() {
21872
+ this.command = "remove";
21873
+ this.aliases = ["rm", "unlink"];
21874
+ this.description = "Unlink a server linked with server add (its token is deleted)";
21875
+ this.acceptsRawInput = true;
21876
+ this.valueRequired = true;
21877
+ }
21878
+ async processCommand(command2, context2) {
21879
+ const t = context2.translator;
21880
+ const manager = serverManagerOf(context2);
21881
+ if (!manager) return;
21882
+ const name = cliCore.splitShellWords(command2.value ?? "")[0] ?? "";
21883
+ const linked = linkedServersOf(context2);
21884
+ const connected = [...manager.connections.keys()].find((n) => n.toLowerCase() === name.toLowerCase());
21885
+ const saved = await linked.get(name);
21886
+ if (connected && !manager.isLinked(connected) && !saved) {
21887
+ context2.writer.writeError(t.t("cli.server.link.cannot_remove_host", "'{name}' is configured by the app and cannot be removed.", { name: connected }));
21888
+ return;
21889
+ }
21890
+ if (!saved && !connected) {
21891
+ context2.writer.writeError(t.t("cli.server.link.unknown_linked", "No linked server '{name}'. See server list.", { name }));
21892
+ return;
21893
+ }
21894
+ await linked.remove(saved?.name ?? name);
21895
+ const linkedConnection = connected && manager.isLinked(connected) ? connected : void 0;
21896
+ if (linkedConnection) manager.removeServer(linkedConnection);
21897
+ const removedName = saved?.name ?? linkedConnection ?? name;
21898
+ if (linkedConnection && context2.state.getState().defaultServer === linkedConnection) {
21899
+ context2.state.updateState({ defaultServer: null });
21900
+ }
21901
+ context2.writer.writeSuccess(t.t("cli.server.link.removed", "Unlinked {name}; its token is deleted from this device.", { name: removedName }));
21902
+ context2.writer.writeln(t.t("cli.server.link.revoke_hint", " The token stays valid on the server until it expires: revoke it there with token revoke."));
21903
+ }
21904
+ };
21905
+ var ServerEditProcessor = class {
21906
+ constructor() {
21907
+ this.command = "edit";
21908
+ this.description = "Change the URL or the token of a linked server";
21909
+ this.acceptsRawInput = true;
21910
+ this.valueRequired = true;
21911
+ this.parameters = [
21912
+ { name: "url", description: "New URL", required: false, type: "string" },
21913
+ { name: "token", description: "New API token (asked for, hidden, when given without a value)", required: false, type: "string" }
21914
+ ];
21915
+ }
21916
+ async processCommand(command2, context2) {
21917
+ const t = context2.translator;
21918
+ const { writer } = context2;
21919
+ const manager = serverManagerOf(context2);
21920
+ if (!manager || linkingDisabled(context2)) return;
21921
+ const name = cliCore.splitShellWords(command2.value ?? "")[0] ?? "";
21922
+ const connected = [...manager.connections.keys()].find((n) => n.toLowerCase() === name.toLowerCase());
21923
+ if (connected && !manager.isLinked(connected)) {
21924
+ writer.writeError(t.t("cli.server.link.cannot_edit_host", "'{name}' is configured by the app and cannot be edited.", { name: connected }));
21925
+ return;
21926
+ }
21927
+ const linked = linkedServersOf(context2);
21928
+ const saved = await linked.get(name);
21929
+ if (!saved) {
21930
+ writer.writeError(t.t("cli.server.link.unknown_linked", "No linked server '{name}'. See server list.", { name }));
21931
+ return;
21932
+ }
21933
+ const urlArg = command2.args?.["url"];
21934
+ const tokenArg = command2.args?.["token"];
21935
+ if (urlArg === void 0 && tokenArg === void 0) {
21936
+ writer.writeError(t.t("cli.server.link.edit_nothing", "Give --url <url> and/or --token."));
21937
+ return;
21938
+ }
21939
+ let url = saved.url;
21940
+ if (typeof urlArg === "string") {
21941
+ const checked = checkServerUrl(urlArg, globalThis.location?.protocol);
21942
+ if (typeof checked === "string") {
21943
+ writer.writeError(checked);
21944
+ return;
21945
+ }
21946
+ if (!userServersAllow(context2.options, checked.url)) {
21947
+ writer.writeError(t.t("cli.server.link.origin_not_allowed", "This app does not allow linking {url}.", { url: checked.url }));
21948
+ return;
21949
+ }
21950
+ if (checked.insecure) {
21951
+ writer.writeWarning(t.t("cli.server.link.http_warning", "{url} is not encrypted (http): its token travels in clear text on the network.", { url: checked.url }));
21952
+ if (!await context2.reader.readConfirm(t.t("cli.server.link.http_confirm", "Link it anyway?"), false)) return;
21953
+ }
21954
+ if (checked.mixedContent) {
21955
+ writer.writeWarning(t.t("cli.server.link.mixed_content", "This page is served over https: the browser may block requests to an http server."));
21956
+ }
21957
+ url = checked.url;
21958
+ }
21959
+ let token = await linked.token(saved.name);
21960
+ if (tokenArg !== void 0) {
21961
+ token = typeof tokenArg === "string" && tokenArg.trim() ? tokenArg.trim() : (await context2.reader.readPassword(t.t("cli.server.link.token_prompt", "API token: ")))?.trim();
21962
+ if (!token) return;
21963
+ }
21964
+ const updated = { ...saved, url };
21965
+ await linked.save(updated, token);
21966
+ const connection = await replaceConnection(manager, linkedServerConfig(updated, token));
21967
+ if (connection.connected) {
21968
+ writer.writeSuccess(t.t("cli.server.link.updated", "Updated {name}: {count} commands.", { name: saved.name, count: connection.commands.length }));
21969
+ } else if (token && connection.needsSignIn) {
21970
+ writer.writeWarning(rejectedTokenMessage(saved.name));
21971
+ } else {
21972
+ writer.writeWarning(t.t("cli.server.link.updated_unreachable", "Updated {name}, but it is unreachable now.", { name: saved.name }));
21973
+ }
21974
+ }
21975
+ };
21976
+ var ServerForgetAllProcessor = class {
21977
+ constructor() {
21978
+ this.command = "forget-all";
21979
+ this.description = "Unlink every linked server and delete every saved token";
21980
+ }
21981
+ async processCommand(_command, context2) {
21982
+ const t = context2.translator;
21983
+ const manager = serverManagerOf(context2);
21984
+ if (!manager) return;
21985
+ const linked = linkedServersOf(context2);
21986
+ const servers = await linked.list();
21987
+ if (servers.length === 0) {
21988
+ context2.writer.writeInfo(t.t("cli.server.link.none_linked", "No linked servers. Link one with server add."));
21989
+ return;
21990
+ }
21991
+ const ok = await context2.reader.readConfirm(
21992
+ t.t("cli.server.link.forget_all_confirm", "Unlink every linked server ({count}) and delete their saved tokens?", { count: servers.length }),
21993
+ false
21994
+ );
21995
+ if (!ok) return;
21996
+ await linked.clear();
21997
+ for (const name of [...manager.connections.keys()]) {
21998
+ if (manager.isLinked(name)) manager.removeServer(name);
21999
+ }
22000
+ const saved = context2.state.getState().defaultServer;
22001
+ if (saved && !manager.connections.has(saved)) context2.state.updateState({ defaultServer: null });
22002
+ context2.writer.writeSuccess(t.t("cli.server.link.forgot_all", "Every linked server and saved token is gone."));
22003
+ }
22004
+ };
22005
+ async function linkedTokenLabels(context2, manager) {
22006
+ const labels = /* @__PURE__ */ new Map();
22007
+ const names = [...manager.connections.keys()].filter((n) => manager.isLinked(n));
22008
+ if (names.length === 0) return labels;
22009
+ const linked = linkedServersOf(context2);
22010
+ for (const name of names) {
22011
+ const token = await linked.token(name).catch(() => void 0);
22012
+ if (token) labels.set(name, maskToken(token));
22013
+ }
22014
+ return labels;
22015
+ }
22016
+
21087
22017
  // src/lib/server/cli-server-command-processor.ts
21088
22018
  var CliServerCommandProcessor = class {
21089
22019
  constructor() {
@@ -21105,7 +22035,11 @@ var CliServerCommandProcessor = class {
21105
22035
  new ServerStatusProcessor(),
21106
22036
  new ServerCommandsProcessor(),
21107
22037
  new ServerReconnectProcessor(),
21108
- new ServerDefaultProcessor()
22038
+ new ServerDefaultProcessor(),
22039
+ new ServerAddProcessor(),
22040
+ new ServerRemoveProcessor(),
22041
+ new ServerEditProcessor(),
22042
+ new ServerForgetAllProcessor()
21109
22043
  ];
21110
22044
  }
21111
22045
  /** Restore the default server saved by `server default <name>`. */
@@ -21121,7 +22055,7 @@ var CliServerCommandProcessor = class {
21121
22055
  const t = context2.translator;
21122
22056
  const manager = serverManagerOf(context2);
21123
22057
  if (!manager) return;
21124
- writeServerTable(context2, manager);
22058
+ writeServerTable(context2, manager, await linkedTokenLabels(context2, manager));
21125
22059
  context2.writer.writeln();
21126
22060
  cliCore.writeHint(context2, "help server", t.t("cli.server.help.see_everything_you_can", "to see everything you can do"));
21127
22061
  }
@@ -21152,6 +22086,10 @@ var CliServerCommandProcessor = class {
21152
22086
  ["server commands|cmds [name]", t.t("cli.server.help.commands_each_server_provides", "Commands each server provides")],
21153
22087
  ["server reconnect|rc [name]", t.t("cli.server.help.connect_again_unreachable_servers", "Connect again (unreachable servers, or one)")],
21154
22088
  ["server default|use [name]", t.t("cli.server.help.show_set_server_shared", "Show or set the server for shared commands")],
22089
+ ["server add [name] [url]", t.t("cli.server.help.add", "Link a server to this terminal (kept across reloads)")],
22090
+ ["server edit <name> --url|--token", t.t("cli.server.help.edit", "Change a linked server")],
22091
+ ["server remove <name>", t.t("cli.server.help.remove", "Unlink a server and delete its token")],
22092
+ ["server forget-all", t.t("cli.server.help.forget_all", "Unlink every linked server")],
21155
22093
  ["ssh [name] [command]", t.t("cli.server.help.open_shell_server_run", "Open a shell on a server, or run one command")]
21156
22094
  ]);
21157
22095
  writer.writeln();
@@ -21174,7 +22112,7 @@ var ServerListProcessor = class {
21174
22112
  }
21175
22113
  async processCommand(_command, context2) {
21176
22114
  const manager = serverManagerOf(context2);
21177
- if (manager) writeServerTable(context2, manager);
22115
+ if (manager) writeServerTable(context2, manager, await linkedTokenLabels(context2, manager));
21178
22116
  }
21179
22117
  writeDescription(context2) {
21180
22118
  const t = context2.translator;
@@ -21186,7 +22124,7 @@ var ServerListProcessor = class {
21186
22124
  cliCore.writeUsage(context2, [["server list", ""], ["server ls", ""]]);
21187
22125
  }
21188
22126
  };
21189
- function writeServerTable(context2, manager) {
22127
+ function writeServerTable(context2, manager, tokenLabels = /* @__PURE__ */ new Map()) {
21190
22128
  const t = context2.translator;
21191
22129
  const { writer } = context2;
21192
22130
  if (manager.connections.size === 0) {
@@ -21206,12 +22144,13 @@ function writeServerTable(context2, manager) {
21206
22144
  connected ? connection.capabilities?.version ?? "-" : "-",
21207
22145
  connected ? `v${connection.apiVersion}` : "-",
21208
22146
  connected ? String(connection.commands.length) : "-",
21209
- connected ? connection.capabilities?.shell ? "yes" : "no" : "-"
22147
+ connected ? connection.capabilities?.shell ? "yes" : "no" : "-",
22148
+ manager.isLinked(name) ? `linked ${tokenLabels.get(name) ?? ""}`.trim() : "app"
21210
22149
  ]);
21211
22150
  }
21212
22151
  writer.writeln(`\u{1F5A5} Servers (${manager.connections.size}):`);
21213
22152
  writer.writeTable(
21214
- ["Name", "Status", "URL", "Version", "API", "Commands", "Shell"],
22153
+ ["Name", "Status", "URL", "Version", "API", "Commands", "Shell", "Source"],
21215
22154
  rows
21216
22155
  );
21217
22156
  if (manager.defaultServer) {
@@ -22474,6 +23413,7 @@ function resolveCompletionPath(tokens, tokenIndex, processors, currentToken = ""
22474
23413
  let descriptors = [];
22475
23414
  let candidates = processors;
22476
23415
  let isFlagValue = false;
23416
+ let flag;
22477
23417
  for (let i = 0; i < parsed.length; i++) {
22478
23418
  const token = parsed[i];
22479
23419
  if (token.kind === "flag") {
@@ -22482,6 +23422,7 @@ function resolveCompletionPath(tokens, tokenIndex, processors, currentToken = ""
22482
23422
  if (parsed[i + 1].kind === "word") i++;
22483
23423
  } else if (!currentToken.startsWith("-")) {
22484
23424
  isFlagValue = true;
23425
+ flag = findParameter(token.name, descriptors);
22485
23426
  }
22486
23427
  continue;
22487
23428
  }
@@ -22495,7 +23436,11 @@ function resolveCompletionPath(tokens, tokenIndex, processors, currentToken = ""
22495
23436
  }
22496
23437
  candidates = match?.processors?.length ? match.processors : void 0;
22497
23438
  }
22498
- return { processor, descriptors, subCommands: candidates, isFlagValue };
23439
+ return { processor, descriptors, subCommands: candidates, isFlagValue, flag };
23440
+ }
23441
+ function findParameter(name, descriptors) {
23442
+ const bare = (a) => a.replace(/^-+/, "");
23443
+ return descriptors.find((d) => d.name === name) ?? descriptors.find((d) => d.aliases?.some((a) => bare(a) === name));
22499
23444
  }
22500
23445
  function toParsedToken(text) {
22501
23446
  const flag = /^(--?)([a-zA-Z0-9_][^\s=]*)(=.*)?$/.exec(text);
@@ -22594,6 +23539,36 @@ var CliParameterCompletionProvider = class {
22594
23539
  }
22595
23540
  };
22596
23541
 
23542
+ // src/lib/completion/cli-parameter-value-completion-provider.ts
23543
+ var CliParameterValueCompletionProvider = class {
23544
+ constructor(registry) {
23545
+ this.registry = registry;
23546
+ this.priority = 190;
23547
+ }
23548
+ getCompletions(context2) {
23549
+ const { tokens, tokenIndex, token } = context2;
23550
+ if (tokens.length === 0) return [];
23551
+ const inline = /^(--?)([^\s=]+)=(.*)$/.exec(token);
23552
+ if (inline) {
23553
+ const path2 = resolveCompletionPath(tokens, tokenIndex, this.registry.processors, token);
23554
+ const values2 = findParameter(inline[2], path2.descriptors)?.allowedValues;
23555
+ if (!values2?.length) return [];
23556
+ const prefix = `${inline[1]}${inline[2]}=`;
23557
+ return matchIgnoringCase(values2, inline[3]).map((v) => `${prefix}${v}`);
23558
+ }
23559
+ if (token.startsWith("-")) return [];
23560
+ const path = resolveCompletionPath(tokens, tokenIndex, this.registry.processors, token);
23561
+ const values = path.isFlagValue ? path.flag?.allowedValues : void 0;
23562
+ return values?.length ? matchIgnoringCase(values, token) : [];
23563
+ }
23564
+ };
23565
+ function matchIgnoringCase(values, prefix) {
23566
+ const exact = getAccelerator().prefixMatch(values, prefix);
23567
+ if (exact.length) return exact;
23568
+ const lower = prefix.toLowerCase();
23569
+ return values.filter((v) => v.toLowerCase().startsWith(lower));
23570
+ }
23571
+
22597
23572
  // src/lib/completion/cli-service-name-completion-provider.ts
22598
23573
  var SERVICE_NAME_SUBCOMMANDS = /* @__PURE__ */ new Set([
22599
23574
  "start",
@@ -22881,6 +23856,7 @@ function createCompletionProviders(deps) {
22881
23856
  new CliServerNameCompletionProvider(serverManager),
22882
23857
  new CliConfigKeyCompletionProvider(registry),
22883
23858
  new CliCommandCompletionProvider(registry),
23859
+ new CliParameterValueCompletionProvider(registry),
22884
23860
  new CliParameterCompletionProvider(registry, executor)
22885
23861
  ];
22886
23862
  const fromPlugins = services.getAll(
@@ -23101,29 +24077,31 @@ var CliEngine = class {
23101
24077
  ]);
23102
24078
  return services.getRequired(CliServerManager_TOKEN);
23103
24079
  }
23104
- /** 4. Connect to the configured servers, one boot step each. */
24080
+ /**
24081
+ * 4. Connect to the configured servers, one boot step each, then to the
24082
+ * servers the user linked (`server add`).
24083
+ */
23105
24084
  async connectServers(serverManager, bootLog) {
23106
- const servers = this.options?.servers;
23107
- if (!servers || servers.length === 0) return;
23108
24085
  const ctx = this.executionContext;
23109
- bootLog.setCurrent("Connecting to servers");
23110
- await serverManager.connectAll(
23111
- servers,
23112
- {
23113
- warn: (msg) => {
23114
- ctx.logger.warn(msg);
23115
- bootLog.warn(msg);
23116
- bootLog.completeStep();
23117
- },
23118
- info: (msg) => {
23119
- ctx.logger.info(msg);
23120
- bootLog.ok(msg);
23121
- bootLog.completeStep();
23122
- }
24086
+ const logger = {
24087
+ warn: (msg) => {
24088
+ ctx.logger.warn(msg);
24089
+ bootLog.warn(msg);
24090
+ bootLog.completeStep();
23123
24091
  },
23124
- ctx.backgroundServices,
23125
- ctx.logger
23126
- );
24092
+ info: (msg) => {
24093
+ ctx.logger.info(msg);
24094
+ bootLog.ok(msg);
24095
+ bootLog.completeStep();
24096
+ }
24097
+ };
24098
+ serverManager.configure(logger, ctx.backgroundServices, ctx.logger);
24099
+ const servers = this.options?.servers;
24100
+ if (servers && servers.length > 0) {
24101
+ bootLog.setCurrent("Connecting to servers");
24102
+ await serverManager.connectAll(servers, logger, ctx.backgroundServices, ctx.logger);
24103
+ }
24104
+ await restoreLinkedServers(ctx, serverManager, logger.warn);
23127
24105
  }
23128
24106
  /**
23129
24107
  * 5. Built-in welcome and server modules, the app's modules, and the
@@ -23999,6 +24977,18 @@ function killProcess(engine, pid) {
23999
24977
  }
24000
24978
  return registry?.kill(pid) ?? false;
24001
24979
  }
24980
+ function interruptCommand(engines) {
24981
+ let interrupted = false;
24982
+ for (const engine of engines) {
24983
+ const context2 = engine.getContext();
24984
+ if (!context2?.interrupt) continue;
24985
+ if (context2.isExecuting || context2.contextProcessor) {
24986
+ context2.interrupt();
24987
+ interrupted = true;
24988
+ }
24989
+ }
24990
+ return interrupted;
24991
+ }
24002
24992
  var CliPanelStatusTracker = class {
24003
24993
  constructor(listener) {
24004
24994
  this.listener = listener;
@@ -24068,6 +25058,11 @@ var CliPanelStatusTracker = class {
24068
25058
  const entry = this.activeTabId != null ? this.tabs.get(this.activeTabId) : void 0;
24069
25059
  return entry?.engines.values().next().value;
24070
25060
  }
25061
+ /** The engines of the active tab, one per pane. */
25062
+ get activeTabEngines() {
25063
+ const entry = this.activeTabId != null ? this.tabs.get(this.activeTabId) : void 0;
25064
+ return entry ? [...entry.engines.values()] : [];
25065
+ }
24071
25066
  /** Recompute everything now. */
24072
25067
  refresh() {
24073
25068
  for (const tabId of this.tabs.keys()) this.refreshTab(tabId);
@@ -24586,6 +25581,15 @@ var CliPanelController = class {
24586
25581
  serversDropdownOpen: false
24587
25582
  });
24588
25583
  }
25584
+ /**
25585
+ * Stop the command running in the active tab, as Ctrl+C does. False when
25586
+ * no command runs there.
25587
+ */
25588
+ stopCommand() {
25589
+ const stopped = interruptCommand(this.tracker.activeTabEngines);
25590
+ if (stopped) this.tracker.refresh();
25591
+ return stopped;
25592
+ }
24589
25593
  /**
24590
25594
  * Stop a running background service listed in the services dropdown, as
24591
25595
  * `kill <pid>` does; the list refreshes. False when nothing was running
@@ -25025,6 +26029,8 @@ var PANEL_LABELS = {
25025
26029
  servicesHeader: "Background Services",
25026
26030
  servicesEmpty: "No services registered",
25027
26031
  stopService: "Stop",
26032
+ stopCommand: "Stop",
26033
+ stopCommandTitle: "Stop the running command (Ctrl+C)",
25028
26034
  serversHeader: "Server Connections",
25029
26035
  serversEmpty: "No servers configured"
25030
26036
  };
@@ -25089,10 +26095,11 @@ function statusIndicators(s) {
25089
26095
  items.push({
25090
26096
  key: "execution",
25091
26097
  itemClass: running ? "status-running" : "",
25092
- compactClass: "",
26098
+ compactClass: running ? "status-clickable" : "",
25093
26099
  dotClass: running ? "dot-running" : "dot-idle",
25094
26100
  label: s.status.executionState,
25095
- title: s.status.executionState
26101
+ title: running ? PANEL_LABELS.stopCommandTitle : s.status.executionState,
26102
+ stop: running ? { label: PANEL_LABELS.stopCommand, title: PANEL_LABELS.stopCommandTitle } : void 0
25096
26103
  });
25097
26104
  if (s.global.serviceCount.total > 0) {
25098
26105
  const label = `${s.global.serviceCount.running}/${s.global.serviceCount.total} services`;
@@ -25289,6 +26296,7 @@ exports.CliJsonCommandProcessor = CliJsonCommandProcessor;
25289
26296
  exports.CliKeyValueStore = CliKeyValueStore;
25290
26297
  exports.CliKeyValueStore_TOKEN = CliKeyValueStore_TOKEN;
25291
26298
  exports.CliKillCommandProcessor = CliKillCommandProcessor;
26299
+ exports.CliLinkedServers = CliLinkedServers;
25292
26300
  exports.CliLogger = CliLogger;
25293
26301
  exports.CliLoremCommandProcessor = CliLoremCommandProcessor;
25294
26302
  exports.CliModuleRegistry_TOKEN = CliModuleRegistry_TOKEN;
@@ -25340,6 +26348,7 @@ exports.CliUnameCommandProcessor = CliUnameCommandProcessor;
25340
26348
  exports.CliUnsetCommandProcessor = CliUnsetCommandProcessor;
25341
26349
  exports.CliUptimeCommandProcessor = CliUptimeCommandProcessor;
25342
26350
  exports.CliVersionCommandProcessor = CliVersionCommandProcessor;
26351
+ exports.CliWebCryptoSecretStore = CliWebCryptoSecretStore;
25343
26352
  exports.CliYesCommandProcessor = CliYesCommandProcessor;
25344
26353
  exports.CommandParser = CommandParser;
25345
26354
  exports.DEFAULT_CLI_NAME = DEFAULT_CLI_NAME;
@@ -25365,6 +26374,7 @@ exports.PANEL_LABELS = PANEL_LABELS;
25365
26374
  exports.PANEL_POSITIONS = PANEL_POSITIONS;
25366
26375
  exports.ProcessExitedError = ProcessExitedError;
25367
26376
  exports.RESTORE_ICON = RESTORE_ICON;
26377
+ exports.SERVER_NAME_PATTERN = SERVER_NAME_PATTERN;
25368
26378
  exports.SERVICES_ICON = SERVICES_ICON;
25369
26379
  exports.SUCCESS_ICON = SUCCESS_ICON;
25370
26380
  exports.SVG_COMMON_ATTRS = SVG_COMMON_ATTRS;
@@ -25378,6 +26388,7 @@ exports.appendStderrToFile = appendStderrToFile;
25378
26388
  exports.applyEnvironmentChange = applyEnvironmentChange;
25379
26389
  exports.builtinProcessors = builtinProcessors;
25380
26390
  exports.capitalize = capitalize;
26391
+ exports.checkServerUrl = checkServerUrl;
25381
26392
  exports.clampPanelSize = clampPanelSize;
25382
26393
  exports.cliScriptRunner = cliScriptRunner;
25383
26394
  exports.collapseChevronIcon = collapseChevronIcon;
@@ -25389,8 +26400,10 @@ exports.contextMenuItems = contextMenuItems;
25389
26400
  exports.contextMenuStyle = contextMenuStyle;
25390
26401
  exports.createCliEngine = createCliEngine;
25391
26402
  exports.createServerModule = createServerModule;
26403
+ exports.createTokenWithPassword = createTokenWithPassword;
25392
26404
  exports.defaultFontSize = defaultFontSize;
25393
26405
  exports.describeShellClose = describeShellClose;
26406
+ exports.deviceName = deviceName;
25394
26407
  exports.distributePaneWidths = distributePaneWidths;
25395
26408
  exports.dropdownAnchorStyle = dropdownAnchorStyle;
25396
26409
  exports.executeOnServer = executeOnServer;
@@ -25413,15 +26426,21 @@ exports.hotkeysInfo = hotkeysInfo;
25413
26426
  exports.initWasmAccelerator = initWasmAccelerator;
25414
26427
  exports.initializeServices = initializeServices;
25415
26428
  exports.initializeTerminal = initializeTerminal;
26429
+ exports.interruptCommand = interruptCommand;
25416
26430
  exports.isHorizontalPosition = isHorizontalPosition;
26431
+ exports.isPrivateHost = isPrivateHost;
25417
26432
  exports.isWasmAccelerated = isWasmAccelerated;
25418
26433
  exports.joinClasses = joinClasses;
25419
26434
  exports.killProcess = killProcess;
26435
+ exports.linkedServerConfig = linkedServerConfig;
26436
+ exports.linkedServersOf = linkedServersOf;
26437
+ exports.maskToken = maskToken;
25420
26438
  exports.matchGlob = matchGlob;
25421
26439
  exports.mergeCliOptions = mergeCliOptions;
25422
26440
  exports.miscProcessors = miscProcessors;
25423
26441
  exports.nextRowsCommand = nextRowsCommand;
25424
26442
  exports.normalizePaneWidths = normalizePaneWidths;
26443
+ exports.normalizeServerUrl = normalizeServerUrl;
25425
26444
  exports.openLink = openLink;
25426
26445
  exports.openStatusDropdown = openStatusDropdown;
25427
26446
  exports.paneClass = paneClass;
@@ -25435,28 +26454,38 @@ exports.panesContainerClass = panesContainerClass;
25435
26454
  exports.panesContainerWidth = panesContainerWidth;
25436
26455
  exports.parseAssignments = parseAssignments;
25437
26456
  exports.parseScript = parseScript;
26457
+ exports.plainText = plainText2;
26458
+ exports.pollLink = pollLink;
25438
26459
  exports.positionDropdownItemClass = positionDropdownItemClass;
25439
26460
  exports.positionIcon = positionIcon;
25440
26461
  exports.positionOptions = positionOptions;
26462
+ exports.probeServer = probeServer;
25441
26463
  exports.reconcileArgs = reconcileArgs;
26464
+ exports.redactSecrets = redactSecrets;
25442
26465
  exports.renderServerResponse = renderServerResponse;
25443
26466
  exports.renderSingleOutput = renderSingleOutput;
26467
+ exports.requestLink = requestLink;
25444
26468
  exports.resizePaneWidths = resizePaneWidths;
25445
26469
  exports.resizedPanelSize = resizedPanelSize;
25446
26470
  exports.resolveCliBranding = resolveCliBranding;
25447
26471
  exports.resolveCliHostConfig = resolveCliHostConfig;
26472
+ exports.restoreLinkedServers = restoreLinkedServers;
25448
26473
  exports.runContextMenuAction = runContextMenuAction;
25449
26474
  exports.runScript = runScript;
26475
+ exports.secretStoreOf = secretStoreOf;
25450
26476
  exports.splitWords = splitWords;
25451
26477
  exports.statusIndicators = statusIndicators;
25452
26478
  exports.substituteCommands = substituteCommands;
25453
26479
  exports.substituteVars = substituteVars;
26480
+ exports.suggestServerName = suggestServerName;
25454
26481
  exports.svgAttrs = svgAttrs;
25455
26482
  exports.systemProcessors = systemProcessors;
25456
26483
  exports.tabDotClass = tabDotClass;
25457
26484
  exports.themeInfos = themeInfos;
25458
26485
  exports.themes = themes;
25459
26486
  exports.tryExecuteScript = tryExecuteScript;
26487
+ exports.userServersAllow = userServersAllow;
26488
+ exports.userServersEnabled = userServersEnabled;
25460
26489
  exports.versionGlobalParameter = versionGlobalParameter;
25461
26490
  exports.waitForLayout = waitForLayout;
25462
26491
  exports.welcomeModule = welcomeModule;