@telepath-computer/television 1.4.6 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -34472,12 +34472,14 @@ var import_node_url = require("node:url");
34472
34472
 
34473
34473
  // ../server/src/config.ts
34474
34474
  var import_node_fs2 = require("node:fs");
34475
+ var import_node_net = require("node:net");
34475
34476
  var import_node_os2 = __toESM(require("node:os"), 1);
34476
34477
  var import_node_path2 = __toESM(require("node:path"), 1);
34477
34478
  var TRUE_ENV_VALUE = "true";
34478
34479
  var APP_HIDDEN_DIRECTORY = ".television";
34479
- var TELEVISION_STORAGE_PATH_ENV = "TELEVISION_STORAGE_PATH";
34480
- var TELEVISION_PORT_ENV = "TELEVISION_PORT";
34480
+ var HOME_POINTER_FILE = ".tv-home";
34481
+ var CONFIG_FILE = "config.json";
34482
+ var CONFIG_KEYS = ["port", "listen", "auth", "installedByAgent"];
34481
34483
  var TELEVISION_ACP_AGENT_ENV = "TELEVISION_ACP_AGENT";
34482
34484
  var TELEVISION_DEVELOPER_HOME_ENV = "TELEVISION_DEVELOPER_HOME";
34483
34485
  var MAX_PORT = 65535;
@@ -34513,25 +34515,153 @@ var DEFAULT_SERVER_URL = buildServerURL(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT
34513
34515
  function buildServerURL(host, port) {
34514
34516
  return `http://${host}:${port}`;
34515
34517
  }
34516
- function getDefaultTelevisionStoragePath() {
34517
- return import_node_path2.default.join(import_node_os2.default.homedir(), APP_HIDDEN_DIRECTORY);
34518
+ var TelevisionConfigError = class extends Error {
34519
+ configPath;
34520
+ constructor(configPath, message) {
34521
+ super(message);
34522
+ this.name = "TelevisionConfigError";
34523
+ this.configPath = configPath;
34524
+ }
34525
+ };
34526
+ function resolveTelevisionHome(input) {
34527
+ const { homeOption, operatingSystemHomeDir, cwd } = input;
34528
+ if (homeOption !== void 0) {
34529
+ if (homeOption.trim() === "") {
34530
+ throw new Error("--home requires a path; an empty value does not select the default home.");
34531
+ }
34532
+ return import_node_path2.default.resolve(cwd, homeOption);
34533
+ }
34534
+ const pointerPath = import_node_path2.default.join(operatingSystemHomeDir, HOME_POINTER_FILE);
34535
+ const builtInHome = import_node_path2.default.join(operatingSystemHomeDir, APP_HIDDEN_DIRECTORY);
34536
+ let contents;
34537
+ try {
34538
+ contents = (0, import_node_fs2.readFileSync)(pointerPath, "utf8");
34539
+ } catch (error48) {
34540
+ if (isMissingFileError(error48)) return builtInHome;
34541
+ throw new Error(`${pointerPath} cannot be read: ${errorMessage(error48)}`);
34542
+ }
34543
+ const pointer = contents.trim();
34544
+ if (pointer === "") {
34545
+ throw new Error(`${pointerPath} holds no path. Write the Television home's path in it, or delete it to use ${builtInHome}.`);
34546
+ }
34547
+ if (/[\r\n]/.test(pointer)) {
34548
+ throw new Error(`${pointerPath} holds more than one line. It must hold only the Television home's path.`);
34549
+ }
34550
+ if (pointer === "~" || pointer.startsWith("~/")) {
34551
+ return import_node_path2.default.resolve(import_node_path2.default.join(operatingSystemHomeDir, pointer.slice(1)));
34552
+ }
34553
+ if (pointer.startsWith("~")) {
34554
+ throw new Error(`${pointerPath} holds ${pointer}, but ~ followed by a user name is not supported. Use an absolute path instead.`);
34555
+ }
34556
+ return import_node_path2.default.resolve(operatingSystemHomeDir, pointer);
34518
34557
  }
34519
- function getTelevisionStoragePath() {
34520
- return process.env[TELEVISION_STORAGE_PATH_ENV] ?? getDefaultTelevisionStoragePath();
34558
+ function readTelevisionConfig(home) {
34559
+ const configPath = getConfigPath(home);
34560
+ const stored = readStoredConfig(configPath);
34561
+ if (stored === void 0) return buildConfig(configPath, false, {});
34562
+ return buildConfig(configPath, true, validateConfigFile(configPath, stored));
34521
34563
  }
34522
- function getTelevisionPortEnv() {
34523
- const rawPort = process.env[TELEVISION_PORT_ENV];
34524
- if (rawPort === void 0 || rawPort.trim() === "") {
34525
- return void 0;
34564
+ function updateTelevisionConfig(home, changes) {
34565
+ const configPath = getConfigPath(home);
34566
+ const merged = { ...readStoredConfig(configPath) };
34567
+ for (const [key, value] of Object.entries(changes)) {
34568
+ if (value !== void 0) merged[key] = value;
34526
34569
  }
34527
- if (!/^\d+$/.test(rawPort)) {
34528
- throw new Error(`${TELEVISION_PORT_ENV} must be a decimal integer between 0 and ${MAX_PORT}.`);
34570
+ const file2 = validateConfigFile(configPath, merged);
34571
+ const resolvedHome = import_node_path2.default.dirname(configPath);
34572
+ (0, import_node_fs2.mkdirSync)(resolvedHome, { recursive: true });
34573
+ const temporaryPath = import_node_path2.default.join(resolvedHome, `.${CONFIG_FILE}.${process.pid}.${Date.now()}.tmp`);
34574
+ try {
34575
+ (0, import_node_fs2.writeFileSync)(temporaryPath, `${JSON.stringify(merged, null, 2)}
34576
+ `, { flag: "wx" });
34577
+ (0, import_node_fs2.renameSync)(temporaryPath, configPath);
34578
+ } catch (error48) {
34579
+ (0, import_node_fs2.rmSync)(temporaryPath, { force: true });
34580
+ throw error48;
34529
34581
  }
34530
- const port = Number(rawPort);
34531
- if (!Number.isSafeInteger(port) || port < 0 || port > MAX_PORT) {
34532
- throw new Error(`${TELEVISION_PORT_ENV} must be a decimal integer between 0 and ${MAX_PORT}.`);
34582
+ return buildConfig(configPath, true, file2);
34583
+ }
34584
+ function getConfigPath(home) {
34585
+ return import_node_path2.default.join(import_node_path2.default.resolve(home), CONFIG_FILE);
34586
+ }
34587
+ function readStoredConfig(configPath) {
34588
+ let contents;
34589
+ try {
34590
+ contents = (0, import_node_fs2.readFileSync)(configPath, "utf8");
34591
+ } catch (error48) {
34592
+ if (isMissingFileError(error48)) return void 0;
34593
+ throw new TelevisionConfigError(configPath, `Config file ${configPath} cannot be read: ${errorMessage(error48)}`);
34533
34594
  }
34534
- return port;
34595
+ let parsed;
34596
+ try {
34597
+ parsed = JSON.parse(contents);
34598
+ } catch (error48) {
34599
+ throw new TelevisionConfigError(configPath, `Config file ${configPath} is not valid JSON: ${errorMessage(error48)}`);
34600
+ }
34601
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
34602
+ const found = Array.isArray(parsed) ? "an array" : parsed === null ? "null" : `a ${typeof parsed}`;
34603
+ throw new TelevisionConfigError(configPath, `Config file ${configPath} must hold a JSON object, not ${found}.`);
34604
+ }
34605
+ return parsed;
34606
+ }
34607
+ function validateConfigFile(configPath, stored) {
34608
+ const invalid = (problem) => new TelevisionConfigError(configPath, `Config file ${configPath}: ${problem}`);
34609
+ const file2 = {};
34610
+ for (const [key, value] of Object.entries(stored)) {
34611
+ const found = `got ${JSON.stringify(value)}`;
34612
+ switch (key) {
34613
+ case "port":
34614
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_PORT) {
34615
+ throw invalid(`"port" must be a whole number from 0 through ${MAX_PORT}; ${found}.`);
34616
+ }
34617
+ file2.port = value;
34618
+ break;
34619
+ case "listen":
34620
+ if (!Array.isArray(value)) {
34621
+ throw invalid(`"listen" must be an array of IPv4 address strings; ${found}.`);
34622
+ }
34623
+ for (const item of value) {
34624
+ if (typeof item !== "string" || !(0, import_node_net.isIPv4)(item)) {
34625
+ throw invalid(`each "listen" item must be an IPv4 address string; got ${JSON.stringify(item)}.`);
34626
+ }
34627
+ }
34628
+ file2.listen = [...value];
34629
+ break;
34630
+ case "auth":
34631
+ if (typeof value !== "boolean") {
34632
+ throw invalid(`"auth" must be true or false; ${found}.`);
34633
+ }
34634
+ file2.auth = value;
34635
+ break;
34636
+ case "installedByAgent":
34637
+ if (typeof value !== "string") {
34638
+ throw invalid(`"installedByAgent" must be a string; ${found}.`);
34639
+ }
34640
+ file2.installedByAgent = value;
34641
+ break;
34642
+ default:
34643
+ throw invalid(`unknown key ${JSON.stringify(key)}. Valid keys are ${CONFIG_KEYS.join(", ")}.`);
34644
+ }
34645
+ }
34646
+ return file2;
34647
+ }
34648
+ function buildConfig(configPath, configFileExists, file2) {
34649
+ const settings = {
34650
+ port: file2.port ?? DEFAULT_SERVER_PORT,
34651
+ listen: file2.listen ?? [],
34652
+ auth: file2.auth ?? true
34653
+ };
34654
+ if (file2.installedByAgent !== void 0) settings.installedByAgent = file2.installedByAgent;
34655
+ return { home: import_node_path2.default.dirname(configPath), configPath, configFileExists, settings };
34656
+ }
34657
+ function isMissingFileError(error48) {
34658
+ return error48?.code === "ENOENT";
34659
+ }
34660
+ function errorMessage(error48) {
34661
+ return error48 instanceof Error ? error48.message : String(error48);
34662
+ }
34663
+ function getDefaultTelevisionStoragePath() {
34664
+ return import_node_path2.default.join(import_node_os2.default.homedir(), APP_HIDDEN_DIRECTORY);
34535
34665
  }
34536
34666
  function resolveACPAgentProfile(env = process.env) {
34537
34667
  const rawAgent = env[TELEVISION_ACP_AGENT_ENV];
@@ -48942,7 +49072,7 @@ function isNewerVersion(candidate, baseline) {
48942
49072
  }
48943
49073
 
48944
49074
  // ../server/src/telemetry/derivation.ts
48945
- var import_node_net = require("node:net");
49075
+ var import_node_net2 = require("node:net");
48946
49076
 
48947
49077
  // ../../node_modules/ulid/dist/node/index.js
48948
49078
  var import_node_crypto = __toESM(require("node:crypto"), 1);
@@ -49943,7 +50073,7 @@ function isLoopbackAddress(address) {
49943
50073
  return normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || normalized.startsWith("127.");
49944
50074
  }
49945
50075
  function isTailnetIPv4(hostname3) {
49946
- if ((0, import_node_net.isIP)(hostname3) !== IPV4_VERSION) return false;
50076
+ if ((0, import_node_net2.isIP)(hostname3) !== IPV4_VERSION) return false;
49947
50077
  const [a, b2] = hostname3.split(".").map((part) => Number.parseInt(part, 10));
49948
50078
  return a === TAILNET_FIRST_OCTET && b2 !== void 0 && b2 >= TAILNET_MIN_SECOND_OCTET && b2 <= TAILNET_MAX_SECOND_OCTET;
49949
50079
  }
@@ -52744,6 +52874,11 @@ function trackChild(child) {
52744
52874
  child.once("error", untrack);
52745
52875
  }
52746
52876
  var ACP_STUB_CWD = import_node_path8.default.join((0, import_node_os4.homedir)(), ".television", "acp");
52877
+ function buildACPAgentTVArgs(input) {
52878
+ const args = ["--home", import_node_path8.default.resolve(input.home)];
52879
+ if (input.configuredPort === 0) args.push("--port", String(input.acquiredPort));
52880
+ return args;
52881
+ }
52747
52882
  var ACPBridge = class extends withDisposable(class {
52748
52883
  }) {
52749
52884
  child = null;
@@ -52753,12 +52888,14 @@ var ACPBridge = class extends withDisposable(class {
52753
52888
  stderrBuffer = "";
52754
52889
  send;
52755
52890
  profile;
52891
+ tvArgs;
52756
52892
  launchProcess;
52757
52893
  sessionCwd;
52758
52894
  constructor(options) {
52759
52895
  super();
52760
52896
  this.send = options.send;
52761
52897
  this.profile = options.profile;
52898
+ this.tvArgs = options.tvArgs;
52762
52899
  this.launchProcess = options.launchProcess ?? launchACPProcess;
52763
52900
  this.sessionCwd = import_node_path8.default.resolve(process.cwd());
52764
52901
  }
@@ -52887,7 +53024,8 @@ var ACPBridge = class extends withDisposable(class {
52887
53024
  status: "ready",
52888
53025
  agent: this.profile.agent,
52889
53026
  sessionIdStrategy: this.profile.sessionIdStrategy,
52890
- sessionCwd: this.sessionCwd
53027
+ sessionCwd: this.sessionCwd,
53028
+ tvArgs: [...this.tvArgs]
52891
53029
  });
52892
53030
  return;
52893
53031
  }
@@ -52912,6 +53050,7 @@ var ACPServer = class extends withDisposable(class {
52912
53050
  authToken;
52913
53051
  authRequired;
52914
53052
  profile;
53053
+ getAgentTVArgs;
52915
53054
  constructor(options) {
52916
53055
  super();
52917
53056
  if (!options.profile) {
@@ -52920,6 +53059,7 @@ var ACPServer = class extends withDisposable(class {
52920
53059
  this.authToken = options.authToken;
52921
53060
  this.authRequired = options.authRequired;
52922
53061
  this.profile = options.profile;
53062
+ this.getAgentTVArgs = options.getAgentTVArgs;
52923
53063
  this.wsServer = new import_websocket_server.default({ noServer: true });
52924
53064
  this.wsServer.on("connection", (socket, request) => {
52925
53065
  if (this.authRequired && !isAuthorizedQueryToken(request.url, this.authToken)) {
@@ -52928,6 +53068,7 @@ var ACPServer = class extends withDisposable(class {
52928
53068
  }
52929
53069
  const bridge = new ACPBridge({
52930
53070
  profile: this.profile,
53071
+ tvArgs: this.getAgentTVArgs(),
52931
53072
  send: (message) => {
52932
53073
  if (socket.readyState === import_websocket.default.OPEN) {
52933
53074
  socket.send(JSON.stringify(message));
@@ -52977,14 +53118,14 @@ var ACPServer = class extends withDisposable(class {
52977
53118
  };
52978
53119
 
52979
53120
  // ../server/src/bind-addresses.ts
52980
- var import_node_net2 = require("node:net");
53121
+ var import_node_net3 = require("node:net");
52981
53122
  var LOCALHOST_IPV4 = "127.0.0.1";
52982
53123
  var ANY_IPV4 = "0.0.0.0";
52983
53124
  function resolveBindAddresses(listenValues = []) {
52984
53125
  const addresses = [LOCALHOST_IPV4];
52985
53126
  for (const address of splitListenValues(listenValues)) {
52986
- if (!(0, import_node_net2.isIPv4)(address)) {
52987
- throw new Error(`Invalid --listen address \`${address}\`. Use an IPv4 address.`);
53127
+ if (!(0, import_node_net3.isIPv4)(address)) {
53128
+ throw new Error(`Invalid listen address \`${address}\`. Use an IPv4 address.`);
52988
53129
  }
52989
53130
  addresses.push(address);
52990
53131
  }
@@ -53216,7 +53357,7 @@ var StartupBindError = class extends Error {
53216
53357
  this.name = "StartupBindError";
53217
53358
  }
53218
53359
  };
53219
- function buildStartupBindFailureRecord(attempts) {
53360
+ function buildStartupBindFailureRecord(attempts, launchMode) {
53220
53361
  return {
53221
53362
  reason: "required-listener-bind-failed",
53222
53363
  exitStatus: STARTUP_BIND_EXIT_STATUS,
@@ -53231,7 +53372,7 @@ function buildStartupBindFailureRecord(attempts) {
53231
53372
  ...code === void 0 ? {} : { code },
53232
53373
  ...typeof serializedError.syscall === "string" ? { syscall: serializedError.syscall } : {},
53233
53374
  cause: bindFailureCause(attempt, code),
53234
- hint: bindFailureHint(attempt, code)
53375
+ hint: bindFailureHint(attempt, code, launchMode)
53235
53376
  };
53236
53377
  })
53237
53378
  };
@@ -53248,9 +53389,9 @@ function bindFailureCause(attempt, code) {
53248
53389
  }
53249
53390
  return `Cannot bind ${attempt.address}:${attempt.port}: ${attempt.error.message}`;
53250
53391
  }
53251
- function bindFailureHint(attempt, code) {
53252
- if (code === "EADDRNOTAVAIL" && isCgnatAddress(attempt.address)) {
53253
- return `Address ${attempt.address} is not assigned to any interface. If this is a Tailscale IP, tailscaled may not be up yet (transient at boot), or the tailnet IP may have changed (permanent until \`tv serve --persist\` is re-run to refresh the configured address). The service manager will retry.`;
53392
+ function bindFailureHint(attempt, code, launchMode) {
53393
+ if (code === "EADDRNOTAVAIL" && launchMode === "daemon" && isCgnatAddress(attempt.address)) {
53394
+ return `Address ${attempt.address} is not assigned to any interface. If this is a Tailscale IP, tailscaled may not be up yet (transient at boot), or the tailnet IP may have changed (permanent until the config file's \`listen\` setting is corrected). The service manager will retry.`;
53254
53395
  }
53255
53396
  if (code === "EADDRNOTAVAIL") {
53256
53397
  return `Confirm that ${attempt.address} is assigned to a local interface, then retry.`;
@@ -53984,7 +54125,7 @@ function disabledTelemetryRuntime() {
53984
54125
 
53985
54126
  // ../server/src/updates/version.ts
53986
54127
  function readVersionStamp() {
53987
- if ("1.4.6".length > 0) return "1.4.6";
54128
+ if ("1.4.8".length > 0) return "1.4.8";
53988
54129
  return void 0;
53989
54130
  }
53990
54131
  function resolveUpdateReleaseVersion(env = process.env, stamp = readVersionStamp()) {
@@ -54150,7 +54291,6 @@ function parseIntervalOverride(value) {
54150
54291
  // ../server/src/server.ts
54151
54292
  var import_meta = {};
54152
54293
  var HTTP_UNAUTHORIZED_STATUS = 401;
54153
- var LOOPBACK_IPV4 = "127.0.0.1";
54154
54294
  var CANONICAL_VERSION_DIRECTORY_PATTERN = /^v\d+(?:\.\d+)?$/;
54155
54295
  function resolveCanonicalVersionDirectories(canonicalDir) {
54156
54296
  const canonicalRoot = import_node_path13.default.resolve(canonicalDir);
@@ -54185,6 +54325,7 @@ var Server = class {
54185
54325
  authMode;
54186
54326
  bindAddresses;
54187
54327
  telemetryOptions;
54328
+ launchMode;
54188
54329
  telemetryRuntime = null;
54189
54330
  updateChannel = null;
54190
54331
  listeningPort;
@@ -54197,6 +54338,7 @@ var Server = class {
54197
54338
  this.authMode = options.auth === true ? "auth" : options.auth === false ? "no-auth" : "none";
54198
54339
  this.bindAddresses = resolveBindAddresses(options.host ? [options.host] : options.listen);
54199
54340
  this.telemetryOptions = options.telemetry ?? {};
54341
+ this.launchMode = this.telemetryOptions.launchMode ?? "cli";
54200
54342
  this.listeningPort = this.port;
54201
54343
  this.baseURLs = [];
54202
54344
  this.app = (0, import_express2.default)();
@@ -54233,7 +54375,12 @@ var Server = class {
54233
54375
  this.acp = options.acpProfile ? new ACPServer({
54234
54376
  authToken: this.store.authToken,
54235
54377
  authRequired: this.authRequired,
54236
- profile: options.acpProfile
54378
+ profile: options.acpProfile,
54379
+ getAgentTVArgs: () => buildACPAgentTVArgs({
54380
+ home: this.store.storagePath,
54381
+ configuredPort: this.port,
54382
+ acquiredPort: this.getListeningPort()
54383
+ })
54237
54384
  }) : null;
54238
54385
  registerRoutes(this.app, this.store, {
54239
54386
  requireAuth: this.requireAuthorization,
@@ -54333,7 +54480,11 @@ var Server = class {
54333
54480
  }
54334
54481
  }
54335
54482
  if (attempts.some((attempt) => attempt.outcome === "failed")) {
54336
- log(this.store.storagePath, "server startup failed", buildStartupBindFailureRecord(attempts));
54483
+ log(
54484
+ this.store.storagePath,
54485
+ "server startup failed",
54486
+ buildStartupBindFailureRecord(attempts, this.launchMode)
54487
+ );
54337
54488
  await Promise.allSettled(boundServers.map((server) => this.closeServer(server)));
54338
54489
  throw new StartupBindError(attempts);
54339
54490
  }
@@ -54348,16 +54499,6 @@ var Server = class {
54348
54499
  log(this.store.storagePath, "server started", { snapshot: this.configurationSnapshot() });
54349
54500
  if (!this.authRequired) {
54350
54501
  log(this.store.storagePath, "server running without bearer-token enforcement", { authMode: this.authMode });
54351
- const nonLoopbackBoundAddresses = this.getNonLoopbackBoundAddresses();
54352
- process.stderr.write(
54353
- "WARNING: running without an auth token. Tokenless mode is insecure for typical setups \u2014 be sure you mean to run without authentication. Start with --auth to require the bearer token.\n"
54354
- );
54355
- if (this.authMode === "no-auth" && nonLoopbackBoundAddresses.length > 0) {
54356
- process.stderr.write(
54357
- `Non-loopback listeners without auth: ${nonLoopbackBoundAddresses.join(", ")}
54358
- `
54359
- );
54360
- }
54361
54502
  }
54362
54503
  return this.httpServer;
54363
54504
  }
@@ -54437,9 +54578,6 @@ var Server = class {
54437
54578
  server.listen(port, address);
54438
54579
  });
54439
54580
  }
54440
- getNonLoopbackBoundAddresses() {
54441
- return this.baseURLs.map((url2) => new URL(url2).hostname).filter((address) => address !== LOOPBACK_IPV4);
54442
- }
54443
54581
  /**
54444
54582
  * Update-channel poller lifecycle (specs/arch/updates/update-channel.md):
54445
54583
  * owned by the Server — started here, stopped in dispose() with its timer
@@ -54454,7 +54592,6 @@ var Server = class {
54454
54592
  }
54455
54593
  async startTelemetry() {
54456
54594
  const version2 = this.telemetryOptions.version ?? readServerPackageVersion();
54457
- const launchMode = this.telemetryOptions.launchMode ?? "cli";
54458
54595
  const runtime = await createTelemetryRuntime({
54459
54596
  storagePath: this.store.storagePath,
54460
54597
  version: version2,
@@ -54481,7 +54618,7 @@ var Server = class {
54481
54618
  port: this.getListeningPort(),
54482
54619
  bindAddresses: this.bindAddresses,
54483
54620
  authMode: this.authMode,
54484
- launchMode,
54621
+ launchMode: this.launchMode,
54485
54622
  installedByAgent: this.telemetryOptions.installedByAgent ?? null
54486
54623
  }),
54487
54624
  { telemetry_opted_out: runtime.boot.state.optedOut }
@@ -54530,8 +54667,8 @@ var Server = class {
54530
54667
  };
54531
54668
  };
54532
54669
  function readServerPackageVersion() {
54533
- if ("1.4.6".length > 0) {
54534
- return telemetryVersion("1.4.6");
54670
+ if ("1.4.8".length > 0) {
54671
+ return telemetryVersion("1.4.8");
54535
54672
  }
54536
54673
  const packageJsonPath = import_node_path13.default.resolve(import_node_path13.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
54537
54674
  if (!(0, import_node_fs11.existsSync)(packageJsonPath)) return telemetryVersion("0.0.0");
@@ -56674,12 +56811,35 @@ var import_meta2 = {};
56674
56811
  var DAEMON_NAME = "com.television.server";
56675
56812
  var TELEMETRY_LAUNCH_MODE_ENV = "TELEVISION_LAUNCH_MODE";
56676
56813
  var SKILL_INSTALL_TELEMETRY_TIMEOUT_MS2 = 1e3;
56814
+ var MIN_CLIENT_PORT = 1;
56677
56815
  var MAX_PORT2 = 65535;
56678
56816
  var MAX_PROCESS_EXIT_STATUS = 255;
56817
+ var LOOPBACK_IPV4 = "127.0.0.1";
56818
+ var CONFIG_KEYS2 = ["port", "listen", "auth", "installedByAgent"];
56679
56819
  var HTTP_UNAUTHORIZED_STATUS2 = 401;
56680
56820
  var HELP_POINTER = "Television ships bundled skills. The main skill is `television` \u2014 keep its guidance available for channels, lifecycle, the `tv` CLI, artifact workflow, and theming. Re-read it only if it is not already in context or you know the installed skill changed. Additional `tv-*` skills cover specialized artifact types. Install all bundled skills with `tv skills install <path>` (e.g. ~/.openclaw/skills) or `tv skills install -i`.";
56681
- var PORT_ZERO_WARNING = "WARNING: --port 0 can make Television hard for later agent CLI commands to rediscover. Use a stable port and export TELEVISION_PORT in the user's environment when agents need to find the server later.";
56821
+ var PORT_ZERO_WARNING = "WARNING: config port 0 lets the operating system choose this server's port. Commands that contact this server must pass --port <port>, using the port from the startup URL.";
56822
+ var TOKENLESS_WARNING = "WARNING: running without an auth token. Tokenless mode is insecure for typical setups \u2014 be sure you mean to run without authentication. Run `tv config set auth true` and restart to require the bearer token.";
56682
56823
  var SAFE_ARG_PATTERN = /^[A-Za-z0-9_./:@%+=,-]+$/;
56824
+ var RETIRED_SERVE_OPTIONS = ["--port", "--listen", "--auth", "--no-auth", "--installed-by-agent"];
56825
+ var RETIRED_VALUE_OPTIONS = ["--port", "--listen", "--installed-by-agent", "--storage-path"];
56826
+ var ADMIN_GUIDE_URL = "https://television.run/install.md";
56827
+ var RETIRED_OPTIONS_GUIDANCE = [
56828
+ "Server settings and the storage directory are no longer command options:",
56829
+ "",
56830
+ " --port <number> -> tv config set port <number>",
56831
+ " --listen <ipv4>[,<ipv4>...] -> tv config set listen <ipv4>[,<ipv4>...]",
56832
+ " --auth -> tv config set auth true (the default)",
56833
+ " --no-auth -> tv config set auth false",
56834
+ " --installed-by-agent <name> -> tv config set installedByAgent <name>",
56835
+ " --storage-path <path> -> tv --home <path> <command>, or write <path> into ~/.tv-home",
56836
+ "",
56837
+ "tv config set writes the config file in the Television home. For a home other than the default, put --home <path> before the command name, as in tv --home <path> config set port <number>.",
56838
+ "After changing settings, rerun tv serve --persist to update an installed service.",
56839
+ `The administrator guide covers the full upgrade and is written for your agent to carry out: ${ADMIN_GUIDE_URL}`
56840
+ ].join("\n");
56841
+ var RETIRED_SERVICE_RECORD = "service runs from a retired service definition";
56842
+ var LEGACY_VARIABLES_WARNING = "WARNING: TELEVISION_PORT or TELEVISION_STORAGE_PATH is set in this environment, and Television no longer uses either. Set the server's port with tv config set port <number>. Television uses the home given with tv --home <path> <command>, otherwise the path written in ~/.tv-home, otherwise ~/.television.";
56683
56843
  var CLIDirectiveError = class extends Error {
56684
56844
  name = "CLIDirectiveError";
56685
56845
  };
@@ -56737,8 +56897,8 @@ function resolveVercelSkillsInstallerBin() {
56737
56897
  return localRequire.resolve("skills/bin/cli.mjs");
56738
56898
  }
56739
56899
  function readCLIVersion() {
56740
- if ("1.4.6".length > 0) {
56741
- return "1.4.6";
56900
+ if ("1.4.8".length > 0) {
56901
+ return "1.4.8";
56742
56902
  }
56743
56903
  const devPackageJsonPath = import_node_path20.default.join(getDevPackageDir(), "package.json");
56744
56904
  if (!(0, import_node_fs18.existsSync)(devPackageJsonPath)) {
@@ -56799,28 +56959,72 @@ function formatEnteredInvocation(argv) {
56799
56959
  function formatEnteredCommand(argv) {
56800
56960
  return argv.length > 0 ? argv.map(formatArgvToken).join(" ") : "tv";
56801
56961
  }
56802
- function storagePathFromArgv(argv) {
56962
+ function exitStatusFromError(error48) {
56963
+ if (typeof error48 !== "object" || error48 === null || !("exitStatus" in error48)) return void 0;
56964
+ const exitStatus = error48.exitStatus;
56965
+ return typeof exitStatus === "number" && Number.isInteger(exitStatus) && exitStatus > 0 && exitStatus <= MAX_PROCESS_EXIT_STATUS ? exitStatus : void 0;
56966
+ }
56967
+ function splitOptionToken(token) {
56968
+ const equals = token.indexOf("=");
56969
+ return token.startsWith("--") && equals !== -1 ? [token.slice(0, equals), token.slice(equals + 1)] : [token, void 0];
56970
+ }
56971
+ function findCommandName(argv) {
56803
56972
  for (let index = 0; index < argv.length; index += 1) {
56804
- const arg = argv[index];
56805
- if (arg === "--storage-path") {
56806
- return argv[index + 1];
56973
+ const token = argv[index];
56974
+ if (token === "--") return void 0;
56975
+ if (!token.startsWith("-")) return token;
56976
+ if (token === "--home" || RETIRED_VALUE_OPTIONS.includes(token)) index += 1;
56977
+ }
56978
+ return void 0;
56979
+ }
56980
+ function extractRetiredOptions(argv) {
56981
+ const command = findCommandName(argv);
56982
+ const retired = [];
56983
+ const remaining = [];
56984
+ for (let index = 0; index < argv.length; index += 1) {
56985
+ const token = argv[index];
56986
+ if (token === "--") {
56987
+ remaining.push(...argv.slice(index));
56988
+ break;
56807
56989
  }
56808
- if (arg.startsWith("--storage-path=")) {
56809
- return arg.slice("--storage-path=".length);
56990
+ const [name, inlineValue] = splitOptionToken(token);
56991
+ const isRetired = name === "--storage-path" || command === "serve" && RETIRED_SERVE_OPTIONS.includes(name);
56992
+ if (!isRetired) {
56993
+ remaining.push(token);
56994
+ } else if (inlineValue !== void 0 || !RETIRED_VALUE_OPTIONS.includes(name)) {
56995
+ retired.push({ name, value: inlineValue });
56996
+ } else {
56997
+ retired.push({ name, value: argv[index + 1] });
56998
+ index += 1;
56810
56999
  }
56811
57000
  }
56812
- return void 0;
57001
+ return { command, retired, remaining };
56813
57002
  }
56814
- function exitStatusFromError(error48) {
56815
- if (typeof error48 !== "object" || error48 === null || !("exitStatus" in error48)) return void 0;
56816
- const exitStatus = error48.exitStatus;
56817
- return typeof exitStatus === "number" && Number.isInteger(exitStatus) && exitStatus > 0 && exitStatus <= MAX_PROCESS_EXIT_STATUS ? exitStatus : void 0;
57003
+ function readRetiredServiceDefinition(command, retired, remaining, processEnv) {
57004
+ const options = remaining.slice(0, remaining.includes("--") ? remaining.indexOf("--") : void 0);
57005
+ const otherForm = options.some((token) => ["--persist", "--persist-uninstall", "--home"].includes(splitOptionToken(token)[0]));
57006
+ if (resolveTelemetryLaunchMode(processEnv) !== "daemon" || command !== "serve" || otherForm) return void 0;
57007
+ const values = (name) => retired.filter((option) => option.name === name).map((option) => option.value ?? "");
57008
+ const settings = { auth: values("--auth").length > 0 };
57009
+ const port = values("--port").at(-1);
57010
+ if (port !== void 0) {
57011
+ settings.port = parseWholePort(port, 0) ?? port;
57012
+ } else {
57013
+ const environmentPort = parseWholePort(processEnv.TELEVISION_PORT ?? "", 0);
57014
+ if (environmentPort !== void 0) settings.port = environmentPort;
57015
+ }
57016
+ const listen = values("--listen");
57017
+ if (listen.length > 0) settings.listen = listen.flatMap((value) => value.split(",").map((address) => address.trim()).filter(Boolean));
57018
+ const installedByAgent = values("--installed-by-agent").at(-1);
57019
+ if (installedByAgent !== void 0) settings.installedByAgent = installedByAgent;
57020
+ const storagePath = values("--storage-path").at(-1);
57021
+ return { ...storagePath === void 0 ? {} : { storagePath }, settings };
56818
57022
  }
56819
- function formatCLIError(error48, argv = []) {
57023
+ function formatCLIError(error48, invocation) {
56820
57024
  if (error48 instanceof CLIDirectiveError) {
56821
57025
  return error48.message;
56822
57026
  }
56823
- if (error48 instanceof ValidationError) {
57027
+ if (error48 instanceof ValidationError || error48 instanceof TelevisionConfigError) {
56824
57028
  return ensureHelpPointer(error48.message);
56825
57029
  }
56826
57030
  if (typeof error48 === "object" && error48 !== null && "serverURL" in error48 && "message" in error48) {
@@ -56829,7 +57033,7 @@ function formatCLIError(error48, argv = []) {
56829
57033
  const status = typeof error48.status === "number" ? Number(error48.status) : void 0;
56830
57034
  if (status === HTTP_UNAUTHORIZED_STATUS2) {
56831
57035
  return ensureHelpPointer(
56832
- `Television server at ${serverURL} rejected the request as unauthorized. Check the token in ${import_node_path20.default.join(storagePathFromArgv(argv) ?? getTelevisionStoragePath(), "state", "token")}.`
57036
+ `Television server at ${serverURL} rejected the request as unauthorized. Check the token in ${import_node_path20.default.join(invocation.home ?? "<home>", "state", "token")}.`
56833
57037
  );
56834
57038
  }
56835
57039
  if (status !== void 0) {
@@ -56839,44 +57043,56 @@ function formatCLIError(error48, argv = []) {
56839
57043
  }
56840
57044
  return error48 instanceof Error ? error48.message : String(error48);
56841
57045
  }
56842
- function parsePortOption(value) {
56843
- if (!/^\d+$/.test(value)) {
56844
- throw new InvalidArgumentError(`port must be a decimal integer between 0 and ${MAX_PORT2}`);
56845
- }
57046
+ function parseWholePort(value, minimum) {
57047
+ if (!/^\d+$/.test(value)) return void 0;
56846
57048
  const port = Number(value);
56847
- if (!Number.isSafeInteger(port) || port < 0 || port > MAX_PORT2) {
56848
- throw new InvalidArgumentError(`port must be a decimal integer between 0 and ${MAX_PORT2}`);
56849
- }
56850
- return port;
56851
- }
56852
- function collectOption(value, previous = []) {
56853
- return [...previous, value];
57049
+ return Number.isSafeInteger(port) && port >= minimum && port <= MAX_PORT2 ? port : void 0;
56854
57050
  }
56855
- function resolveClientPort(port) {
56856
- if (port !== void 0) {
56857
- return port;
57051
+ function parseClientPortOption(value) {
57052
+ const port = parseWholePort(value, MIN_CLIENT_PORT);
57053
+ if (port === void 0) {
57054
+ throw new InvalidArgumentError(`port must be a whole decimal integer from ${MIN_CLIENT_PORT} through ${MAX_PORT2}`);
56858
57055
  }
56859
- try {
56860
- return getTelevisionPortEnv() ?? DEFAULT_SERVER_PORT;
56861
- } catch (error48) {
56862
- throw createDirectiveError(error48 instanceof Error ? error48.message : String(error48));
56863
- }
56864
- }
56865
- function buildLocalServerURL(port) {
56866
- return buildServerURL(DEFAULT_SERVER_HOST, resolveClientPort(port));
57056
+ return port;
56867
57057
  }
56868
- function resolveServeAuth(argv, hasListen) {
56869
- const wantsAuth = argv.includes("--auth");
56870
- const wantsNoAuth = argv.includes("--no-auth");
56871
- if (wantsAuth && wantsNoAuth) {
56872
- throw createDirectiveError("Use either `--auth` or `--no-auth`, not both.");
56873
- }
56874
- if (hasListen && !wantsAuth && !wantsNoAuth) {
56875
- throw createDirectiveError("tv serve with --listen requires either `--auth` or `--no-auth`.");
57058
+ function parseConfigSetPairs(pairs, invocation) {
57059
+ const keyList = CONFIG_KEYS2.join(", ");
57060
+ if (pairs.length === 0) {
57061
+ throw createDirectiveError(`${invocation} requires at least one <key> <value> pair. Keys: ${keyList}.`);
57062
+ }
57063
+ if (pairs.length % 2 !== 0) {
57064
+ throw createDirectiveError(`${invocation} requires a value for ${formatArgvToken(pairs[pairs.length - 1])}.`);
57065
+ }
57066
+ const keys = [];
57067
+ const changes = {};
57068
+ for (let index = 0; index < pairs.length; index += 2) {
57069
+ const key = pairs[index];
57070
+ const value = pairs[index + 1];
57071
+ if (!CONFIG_KEYS2.includes(key)) {
57072
+ throw createDirectiveError(`${invocation} does not support config key ${formatArgvToken(key)}. Keys: ${keyList}.`);
57073
+ }
57074
+ if (keys.includes(key)) {
57075
+ throw createDirectiveError(`${invocation} sets ${key} more than once.`);
57076
+ }
57077
+ keys.push(key);
57078
+ if (key === "port") {
57079
+ const port = parseWholePort(value, 0);
57080
+ if (port === void 0) {
57081
+ throw createDirectiveError(`${invocation} received an invalid port value ${formatArgvToken(value)}: use a whole decimal integer from 0 through ${MAX_PORT2}.`);
57082
+ }
57083
+ changes.port = port;
57084
+ } else if (key === "listen") {
57085
+ changes.listen = value.split(",").map((address) => address.trim()).filter((address) => address.length > 0);
57086
+ } else if (key === "auth") {
57087
+ if (value !== "true" && value !== "false") {
57088
+ throw createDirectiveError(`${invocation} received an invalid auth value ${formatArgvToken(value)}: use true or false.`);
57089
+ }
57090
+ changes.auth = value === "true";
57091
+ } else {
57092
+ changes.installedByAgent = value;
57093
+ }
56876
57094
  }
56877
- if (wantsAuth) return true;
56878
- if (wantsNoAuth) return false;
56879
- return void 0;
57095
+ return { keys, changes };
56880
57096
  }
56881
57097
  function resolveStaticDir() {
56882
57098
  if (process.argv[1]) {
@@ -57031,29 +57247,22 @@ function formatCommanderError(argv, error48) {
57031
57247
  }
57032
57248
  return null;
57033
57249
  }
57034
- function readAuthToken(storagePath) {
57035
- const tokenPath = import_node_path20.default.join(storagePath, "state", "token");
57250
+ function readAuthToken(home) {
57251
+ const tokenPath = import_node_path20.default.join(home, "state", "token");
57036
57252
  if (!(0, import_node_fs18.existsSync)(tokenPath)) {
57037
57253
  return void 0;
57038
57254
  }
57039
57255
  const token = (0, import_node_fs18.readFileSync)(tokenPath, "utf8").trim();
57040
57256
  return token.length > 0 ? token : void 0;
57041
57257
  }
57042
- function readOrCreateAuthToken(storagePath) {
57258
+ function readOrCreateAuthToken(home) {
57043
57259
  return new ServerStore({
57044
- storagePath,
57260
+ storagePath: home,
57045
57261
  installOnboardingChannels: false
57046
57262
  }).authToken;
57047
57263
  }
57048
- function buildDaemonServeArgs(options = {}) {
57049
- const args = [process.argv[1] ?? "tv", "serve"];
57050
- for (const address of options.listen ?? []) args.push("--listen", address);
57051
- if (options.auth === true) args.push("--auth");
57052
- if (options.auth === false) args.push("--no-auth");
57053
- if (options.port !== void 0) args.push("--port", String(options.port));
57054
- if (options.storagePath) args.push("--storage-path", options.storagePath);
57055
- if (options.installedByAgent) args.push("--installed-by-agent", options.installedByAgent);
57056
- return args;
57264
+ function buildDaemonServeArgs(home) {
57265
+ return [process.argv[1] ?? "tv", "--home", home, "serve"];
57057
57266
  }
57058
57267
  function redactSensitiveEnv(env) {
57059
57268
  const redacted = {};
@@ -57079,7 +57288,7 @@ function createEnvironment(environment) {
57079
57288
  // resolved content root when one resolved
57080
57289
  // (specs/arch/cli/index.md, build and packaged asset layout).
57081
57290
  store: new ServerStore({
57082
- storagePath: options.storagePath ?? getTelevisionStoragePath(),
57291
+ storagePath: options.home,
57083
57292
  ...options.bundledViewsPath ? { bundledViewsPath: options.bundledViewsPath } : {},
57084
57293
  ...options.onboardingContentPath ? { onboardingContentPath: options.onboardingContentPath } : {},
57085
57294
  ...options.bundledThemesPath ? { bundledThemesPath: options.bundledThemesPath } : {},
@@ -57087,17 +57296,18 @@ function createEnvironment(environment) {
57087
57296
  }),
57088
57297
  listen: options.listen,
57089
57298
  port: options.port,
57299
+ // Always explicit: the Server constructor's own default is tokenless.
57090
57300
  auth: options.auth,
57091
57301
  staticDir: options.staticDir,
57092
57302
  canonicalDir: options.canonicalDir,
57093
57303
  acpProfile: options.acpProfile,
57094
57304
  telemetry: {
57095
- launchMode: options.launchMode ?? "cli",
57305
+ launchMode: options.launchMode,
57096
57306
  installedByAgent: options.installedByAgent ?? null
57097
57307
  }
57098
57308
  })),
57099
- createDaemon: environment.createDaemon ?? ((options = {}) => {
57100
- const args = buildDaemonServeArgs(options);
57309
+ createDaemon: environment.createDaemon ?? ((options) => {
57310
+ const args = options ? buildDaemonServeArgs(options.home) : [process.argv[1] ?? "tv", "serve"];
57101
57311
  return new Daemon({
57102
57312
  name: DAEMON_NAME,
57103
57313
  description: "Television server \u2014 virtual display for agents",
@@ -57106,7 +57316,7 @@ function createEnvironment(environment) {
57106
57316
  // the service is how a later Node selection takes effect.
57107
57317
  command: process.execPath,
57108
57318
  args,
57109
- env: options.env
57319
+ env: options?.env
57110
57320
  });
57111
57321
  }),
57112
57322
  resolveStaticDir: environment.resolveStaticDir ?? (() => resolveStaticDir()),
@@ -57127,160 +57337,163 @@ function createEnvironment(environment) {
57127
57337
  onSignal: environment.onSignal ?? ((signal, handler) => process.on(signal, handler))
57128
57338
  };
57129
57339
  }
57130
- function resolveClientStoragePath(storagePath) {
57131
- return storagePath ?? getTelevisionStoragePath();
57340
+ function startRetiredService(home, definition) {
57341
+ const wroteConfig = !(0, import_node_fs18.existsSync)(import_node_path20.default.join(home, "config.json"));
57342
+ if (wroteConfig) updateTelevisionConfig(home, definition.settings);
57343
+ log(home, RETIRED_SERVICE_RECORD, {
57344
+ wroteConfig,
57345
+ guidance: `Reinstall the service with \`tv serve --persist\`, adding \`--home ${home}\` before \`serve\` unless this is the default home, so it runs from the home and its config file. The administrator guide describes the upgrade and is written for an agent to carry out: ${ADMIN_GUIDE_URL}`
57346
+ });
57132
57347
  }
57133
- function createProgram(env, argv = []) {
57348
+ function createProgram(env, invocation = { argv: [] }) {
57349
+ const { argv } = invocation;
57134
57350
  const program2 = new Command();
57135
- const warnForExplicitPortZero = (port) => {
57136
- if (port === 0) {
57137
- writeLine(env.stderr, PORT_ZERO_WARNING);
57351
+ const resolveHome = () => {
57352
+ if (invocation.home !== void 0) return invocation.home;
57353
+ try {
57354
+ invocation.home = resolveTelevisionHome({
57355
+ homeOption: invocation.retiredService?.storagePath ?? program2.opts().home,
57356
+ operatingSystemHomeDir: env.resolveHomeDir(),
57357
+ cwd: process.cwd()
57358
+ });
57359
+ } catch (error48) {
57360
+ throw createDirectiveError(error48 instanceof Error ? error48.message : String(error48));
57138
57361
  }
57362
+ return invocation.home;
57139
57363
  };
57140
- const createAuthenticatedClient = (options = {}) => {
57141
- warnForExplicitPortZero(options.port);
57142
- return env.createClient(
57143
- buildLocalServerURL(options.port),
57144
- readAuthToken(resolveClientStoragePath(options.storagePath))
57145
- );
57364
+ const resolveClientPort = (config2, portOption) => {
57365
+ const enteredInvocation = formatEnteredInvocation(argv);
57366
+ if (config2.settings.port === 0) {
57367
+ if (portOption === void 0) {
57368
+ throw createDirectiveError(
57369
+ `${enteredInvocation} requires --port because ${config2.configPath} sets port 0. Pass the port from the Television server's startup URL.`
57370
+ );
57371
+ }
57372
+ return portOption;
57373
+ }
57374
+ if (portOption !== void 0) {
57375
+ throw createDirectiveError(
57376
+ `${enteredInvocation} does not accept --port because ${config2.configPath} sets port ${config2.settings.port}. Omit --port to use the configured port.`
57377
+ );
57378
+ }
57379
+ return config2.settings.port;
57380
+ };
57381
+ const resolveServerURL = (portOption) => {
57382
+ const port = resolveClientPort(readTelevisionConfig(resolveHome()), portOption);
57383
+ return buildServerURL(DEFAULT_SERVER_HOST, port);
57384
+ };
57385
+ const createAuthenticatedClient = (options) => {
57386
+ const serverURL = resolveServerURL(options.port);
57387
+ return env.createClient(serverURL, readAuthToken(resolveHome()));
57146
57388
  };
57147
57389
  const emitSkillInstallTelemetry = async (input) => {
57148
57390
  await bestEffortWithinTimeout2(env.emitSkillInstalledTelemetry({
57149
- storagePath: getTelevisionStoragePath(),
57391
+ storagePath: input.home,
57150
57392
  version: telemetryVersion(readCLIVersion()),
57151
57393
  agentType: input.agentType,
57152
57394
  ...input.installedByAgent === void 0 ? {} : { installedByAgent: input.installedByAgent },
57153
57395
  env: process.env
57154
57396
  }), SKILL_INSTALL_TELEMETRY_TIMEOUT_MS2);
57155
57397
  };
57156
- const uninstallPersistedService = async (storagePath) => {
57398
+ const uninstallPersistedService = async (home) => {
57157
57399
  const daemon = env.createDaemon();
57158
57400
  let outcome = "uninstalled";
57159
57401
  try {
57160
57402
  await daemon.uninstall();
57161
57403
  } catch (error48) {
57162
57404
  outcome = "error";
57163
- log(storagePath, "persisted service uninstall failed", { daemonName: DAEMON_NAME, outcome, error: error48 });
57405
+ log(home, "persisted service uninstall failed", { daemonName: DAEMON_NAME, outcome, error: error48 });
57164
57406
  throw error48;
57165
57407
  }
57166
- log(storagePath, "persisted service uninstalled", { daemonName: DAEMON_NAME, outcome });
57408
+ log(home, "persisted service uninstalled", { daemonName: DAEMON_NAME, outcome });
57167
57409
  writeJSON(env.stdout, { status: "stopped" });
57168
57410
  };
57169
- program2.name("tv").description("Television \u2014 virtual display for agents").version(readCLIVersionOutput(env.resolveHomeDir)).option("-v", "Output the version number").showSuggestionAfterError(false).addHelpText("after", buildAgentHelpNote()).configureOutput({
57170
- writeOut: (str) => env.stdout.write(str),
57171
- writeErr: (str) => env.stderr.write(str),
57172
- outputError: () => {
57411
+ const installPersistedService = async (home, config2) => {
57412
+ const { settings, configPath } = config2;
57413
+ if (settings.port === 0) {
57414
+ throw createDirectiveError(
57415
+ `${formatEnteredInvocation(argv)} requires a stable port, but ${configPath} sets port 0. Choose one with \`tv config set port <number>\`.`
57416
+ );
57173
57417
  }
57174
- }).exitOverride();
57175
- program2.command("serve").description("Start the Television server").option("--listen <address>", "Additional IPv4 address to bind to (repeatable or comma-separated)", collectOption, []).option("--auth", "Require bearer-token auth on every listener").option("--no-auth", "Disable bearer-token auth on every listener").option("--port <number>", "Port to listen on", parsePortOption).option("--storage-path <path>", "Directory for persisted channel data").option("--installed-by-agent <agent-runtime-harness-name>", "Agent runtime harness name operating this server").option("--persist", "Install as a persistent system service").option("--persist-uninstall", "Uninstall the persistent system service").action(async (opts) => {
57176
- if (opts.persist && opts.persistUninstall) {
57177
- throw createDirectiveError("Use either `--persist` or `--persist-uninstall`, not both.");
57418
+ const profile = resolveACPAgentProfile(process.env);
57419
+ const daemonEnv = buildPersistedACPEnvironment(process.env, { developerHome: env.resolveHomeDir() });
57420
+ daemonEnv[TELEMETRY_LAUNCH_MODE_ENV] = "daemon";
57421
+ if (profile && !isACPCommandResolvable(profile.command, daemonEnv)) {
57422
+ const error48 = createDirectiveError(
57423
+ `Could not find ACP agent command \`${profile.command}\` for \`TELEVISION_ACP_AGENT=${profile.agent}\` on the persisted PATH. Set TELEVISION_ACP_AGENT to \`openclaw\` or \`hermes\`, and ensure the selected agent binary is available on PATH before running \`tv serve --persist\`.`
57424
+ );
57425
+ log(home, "server startup refused before binding", { argv, error: error48 });
57426
+ throw error48;
57178
57427
  }
57179
- const storagePathForLogs = opts.storagePath ?? getTelevisionStoragePath();
57180
- if (opts.persistUninstall) {
57181
- await uninstallPersistedService(storagePathForLogs);
57182
- return;
57428
+ const token = settings.auth ? readOrCreateAuthToken(home) : null;
57429
+ const daemonOptions = { home, env: daemonEnv };
57430
+ const execStart = [process.execPath, ...buildDaemonServeArgs(home)];
57431
+ const daemon = env.createDaemon(daemonOptions);
57432
+ const daemonStatus = await daemon.status();
57433
+ if (daemonStatus.installed || daemonStatus.running) {
57434
+ await daemon.uninstall();
57183
57435
  }
57184
- let auth;
57185
57436
  try {
57186
- auth = resolveServeAuth(argv, opts.listen.length > 0);
57187
- resolveBindAddresses(opts.listen);
57437
+ await daemon.install();
57438
+ log(home, "persisted service installed", {
57439
+ daemonName: DAEMON_NAME,
57440
+ execStart,
57441
+ env: redactSensitiveEnv(daemonEnv),
57442
+ replacedExisting: daemonStatus.installed || daemonStatus.running,
57443
+ outcome: "installed"
57444
+ });
57188
57445
  } catch (error48) {
57189
- log(storagePathForLogs, "server startup refused before binding", { argv, error: error48 });
57190
- throw error48;
57191
- }
57192
- if (opts.persist) {
57193
- if (opts.port === 0) {
57194
- throw createDirectiveError(
57195
- "tv serve --persist --port 0 requires a stable port; port 0 lets the operating system choose a runtime port and cannot produce a stable persisted-service startup URL. Set TELEVISION_PORT to a stable port or pass a non-zero --port."
57196
- );
57197
- }
57198
- const clientPort = resolveClientPort(opts.port);
57199
- const storagePath = import_node_path20.default.resolve(opts.storagePath ?? getTelevisionStoragePath());
57200
- const profile2 = resolveACPAgentProfile(process.env);
57201
- const daemonEnv = buildPersistedACPEnvironment(process.env, { developerHome: env.resolveHomeDir() });
57202
- if (process.env.TELEVISION_PORT !== void 0 && process.env.TELEVISION_PORT !== "") {
57203
- daemonEnv.TELEVISION_PORT = process.env.TELEVISION_PORT;
57204
- }
57205
- if (process.env.TELEVISION_STORAGE_PATH !== void 0 && process.env.TELEVISION_STORAGE_PATH !== "") {
57206
- daemonEnv.TELEVISION_STORAGE_PATH = process.env.TELEVISION_STORAGE_PATH;
57207
- }
57208
- daemonEnv[TELEMETRY_LAUNCH_MODE_ENV] = "daemon";
57209
- if (profile2 && !isACPCommandResolvable(profile2.command, daemonEnv)) {
57210
- const error48 = createDirectiveError(
57211
- `Could not find ACP agent command \`${profile2.command}\` for \`TELEVISION_ACP_AGENT=${profile2.agent}\` on the persisted PATH. Set TELEVISION_ACP_AGENT to \`openclaw\` or \`hermes\`, and ensure the selected agent binary is available on PATH before running \`tv serve --persist\`.`
57212
- );
57213
- log(storagePath, "server startup refused before binding", { argv, error: error48 });
57214
- throw error48;
57215
- }
57216
- const daemonOptions = {
57217
- listen: opts.listen,
57218
- auth,
57219
- port: opts.port,
57220
- storagePath,
57221
- ...opts.installedByAgent === void 0 ? {} : { installedByAgent: opts.installedByAgent },
57222
- env: daemonEnv
57223
- };
57224
- const daemon = env.createDaemon(daemonOptions);
57225
- const daemonStatus = await daemon.status();
57226
- if (daemonStatus.installed || daemonStatus.running) {
57227
- await daemon.uninstall();
57228
- }
57229
- try {
57230
- await daemon.install();
57231
- log(storagePath, "persisted service installed", {
57232
- daemonName: DAEMON_NAME,
57233
- execStart: [process.execPath, ...buildDaemonServeArgs(daemonOptions)],
57234
- env: redactSensitiveEnv(daemonEnv),
57235
- replacedExisting: daemonStatus.installed || daemonStatus.running,
57236
- outcome: "installed"
57237
- });
57238
- } catch (error48) {
57239
- log(storagePath, "persisted service install failed", {
57240
- daemonName: DAEMON_NAME,
57241
- execStart: [process.execPath, ...buildDaemonServeArgs(daemonOptions)],
57242
- env: redactSensitiveEnv(daemonEnv),
57243
- outcome: "error",
57244
- error: error48
57245
- });
57246
- throw error48;
57247
- }
57248
- const serverURLs2 = resolveBindAddresses(opts.listen).map((address) => buildServerURL(address, clientPort));
57249
- writeConnectURLs(env.stdout, serverURLs2, {
57250
- installed: true,
57251
- token: auth === true ? readOrCreateAuthToken(storagePath) : null
57446
+ log(home, "persisted service install failed", {
57447
+ daemonName: DAEMON_NAME,
57448
+ execStart,
57449
+ env: redactSensitiveEnv(daemonEnv),
57450
+ outcome: "error",
57451
+ error: error48
57252
57452
  });
57253
- return;
57453
+ throw error48;
57254
57454
  }
57255
- warnForExplicitPortZero(opts.port);
57455
+ const serverURLs = resolveBindAddresses(settings.listen).map((address) => buildServerURL(address, settings.port));
57456
+ writeConnectURLs(env.stdout, serverURLs, { installed: true, token });
57457
+ };
57458
+ const serveInForeground = async (home, settings) => {
57256
57459
  const profile = resolveACPAgentProfile(process.env);
57257
57460
  if (profile && !isACPCommandResolvable(profile.command, process.env)) {
57258
57461
  const error48 = createDirectiveError(
57259
57462
  `Could not find ACP agent command \`${profile.command}\` for \`TELEVISION_ACP_AGENT=${profile.agent}\` on PATH. Set TELEVISION_ACP_AGENT to \`openclaw\` or \`hermes\`, and ensure the selected agent binary is available on PATH before running \`tv serve\`.`
57260
57463
  );
57261
- log(storagePathForLogs, "server startup refused before binding", { argv, error: error48 });
57464
+ log(home, "server startup refused before binding", { argv, error: error48 });
57262
57465
  throw error48;
57263
57466
  }
57467
+ if (settings.port === 0) {
57468
+ writeLine(env.stderr, PORT_ZERO_WARNING);
57469
+ }
57264
57470
  const onboardingContentPath = env.resolveOnboardingContentPath();
57265
57471
  const bundledThemesPath = env.resolveBundledThemesPath();
57266
57472
  const server = env.createServer({
57267
- listen: opts.listen,
57268
- auth,
57269
- port: resolveClientPort(opts.port),
57270
- storagePath: opts.storagePath,
57473
+ home,
57474
+ listen: settings.listen,
57475
+ port: settings.port,
57476
+ auth: settings.auth,
57477
+ ...settings.installedByAgent === void 0 ? {} : { installedByAgent: settings.installedByAgent },
57271
57478
  staticDir: env.resolveStaticDir(),
57272
57479
  canonicalDir: env.resolveCanonicalDir(),
57273
57480
  bundledViewsPath: env.resolveBundledViewsPath(),
57274
57481
  ...onboardingContentPath !== void 0 ? { onboardingContentPath } : {},
57275
57482
  ...bundledThemesPath !== void 0 ? { bundledThemesPath } : {},
57276
57483
  launchMode: resolveTelemetryLaunchMode(process.env),
57277
- ...opts.installedByAgent === void 0 ? {} : { installedByAgent: opts.installedByAgent },
57278
57484
  ...profile ? { acpProfile: profile } : {}
57279
57485
  });
57280
57486
  await server.start();
57487
+ if (!settings.auth) {
57488
+ writeLine(env.stderr, TOKENLESS_WARNING);
57489
+ const nonLoopbackAddresses = resolveBindAddresses(settings.listen).filter((address) => address !== LOOPBACK_IPV4);
57490
+ if (nonLoopbackAddresses.length > 0) {
57491
+ writeLine(env.stderr, `Non-loopback listeners without auth: ${nonLoopbackAddresses.join(", ")}`);
57492
+ }
57493
+ }
57281
57494
  const serverURLs = server.getBaseURLs?.() ?? [server.getBaseURL()];
57282
57495
  writeConnectURLs(env.stdout, serverURLs, {
57283
- token: auth === true ? server.getAuthToken() : null
57496
+ token: settings.auth ? server.getAuthToken() : null
57284
57497
  });
57285
57498
  await new Promise((resolve, reject) => {
57286
57499
  let shuttingDown = false;
@@ -57301,10 +57514,61 @@ function createProgram(env, argv = []) {
57301
57514
  void shutdown("SIGTERM");
57302
57515
  });
57303
57516
  });
57517
+ };
57518
+ const versionOutput = readCLIVersionOutput(env.resolveHomeDir);
57519
+ program2.name("tv").description("Television \u2014 virtual display for agents").version(versionOutput).option("-v", "Output the version number").option("--home <path>", "Television home directory (default: the path in ~/.tv-home, otherwise ~/.television)").showSuggestionAfterError(false).addHelpText("after", buildAgentHelpNote()).configureOutput({
57520
+ writeOut: (str) => env.stdout.write(str),
57521
+ writeErr: (str) => env.stderr.write(str),
57522
+ outputError: () => {
57523
+ }
57524
+ }).exitOverride();
57525
+ program2.on("option:v", () => {
57526
+ env.stdout.write(`${versionOutput}
57527
+ `);
57528
+ throw new CommanderError(0, "commander.version", versionOutput);
57529
+ });
57530
+ program2.command("serve").description("Start the Television server with the selected home's settings").option("--persist", "Install as a persistent system service").option("--persist-uninstall", "Uninstall the persistent system service").action(async (opts) => {
57531
+ if (opts.persist && opts.persistUninstall) {
57532
+ throw createDirectiveError("Use either `--persist` or `--persist-uninstall`, not both.");
57533
+ }
57534
+ const home = resolveHome();
57535
+ if (opts.persistUninstall) {
57536
+ await uninstallPersistedService(home);
57537
+ return;
57538
+ }
57539
+ let config2;
57540
+ try {
57541
+ if (invocation.retiredService) startRetiredService(home, invocation.retiredService);
57542
+ config2 = readTelevisionConfig(home);
57543
+ } catch (error48) {
57544
+ log(home, "server startup refused before binding", { argv, error: error48 });
57545
+ throw error48;
57546
+ }
57547
+ if (opts.persist) {
57548
+ await installPersistedService(home, config2);
57549
+ return;
57550
+ }
57551
+ await serveInForeground(home, config2.settings);
57552
+ });
57553
+ const configCommand = program2.command("config").description("Show the effective settings of the selected home, or change settings in its config file");
57554
+ configCommand.command("set").description("Change settings in the selected home's config file").argument("[pairs...]", `<key> <value> pairs; keys: ${CONFIG_KEYS2.join(", ")}`).action((pairs) => {
57555
+ const { keys, changes } = parseConfigSetPairs(pairs, formatEnteredInvocation(argv));
57556
+ const config2 = updateTelevisionConfig(resolveHome(), changes);
57557
+ writeLine(env.stdout, `Updated ${config2.configPath}: ${keys.join(", ")}.`);
57558
+ writeLine(env.stdout, "Restart foreground `tv serve`, or rerun `tv serve --persist`, for a running server to use the new settings.");
57559
+ });
57560
+ configCommand.command("show").description("Print the selected home, its config file, and the effective settings as JSON").action(() => {
57561
+ const config2 = readTelevisionConfig(resolveHome());
57562
+ writeJSON(env.stdout, {
57563
+ home: config2.home,
57564
+ configPath: config2.configPath,
57565
+ configFileExists: config2.configFileExists,
57566
+ settings: { ...config2.settings, installedByAgent: config2.settings.installedByAgent ?? null }
57567
+ });
57304
57568
  });
57305
57569
  program2.command("create-path-artifact").description(
57306
57570
  "Create a path artifact that points at an existing absolute markdown or HTML file, or at a directory with a root index.html/index.htm (trailing separator optional; the server detects file vs directory on disk). Television records the pointer only; HTML and directory targets are read-only, and markdown targets are written through only by in-UI editor saves via PUT /markdown/<id>."
57307
- ).requiredOption("--channel <id>", "Target channel ID").requiredOption("--title <title>", "Artifact title").requiredOption("--path <path>", "Absolute path to an existing markdown/HTML file or indexed directory (trailing separator optional)").option("--focus-artifact", "Focus the new artifact after creation").option("--no-focus", "Create the artifact in the background without changing focus").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").addHelpText("after", buildArtifactWorkflowHelpNote(true)).action(async (opts) => {
57571
+ ).requiredOption("--channel <id>", "Target channel ID").requiredOption("--title <title>", "Artifact title").requiredOption("--path <path>", "Absolute path to an existing markdown/HTML file or indexed directory (trailing separator optional)").option("--focus-artifact", "Focus the new artifact after creation").option("--no-focus", "Create the artifact in the background without changing focus").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).addHelpText("after", buildArtifactWorkflowHelpNote(true)).action(async (opts) => {
57308
57572
  const shouldFocus = resolveFocusDirective(argv, "create-path-artifact", "--focus-artifact");
57309
57573
  const artifactPath = opts.path.trim();
57310
57574
  const client = createAuthenticatedClient(opts);
@@ -57322,7 +57586,7 @@ function createProgram(env, argv = []) {
57322
57586
  });
57323
57587
  program2.command("create-url-artifact").description(
57324
57588
  "Create a URL artifact that points at an absolute http(s) URL. Television records the pointer only and does not own or modify the remote page."
57325
- ).requiredOption("--channel <id>", "Target channel ID").requiredOption("--title <title>", "Artifact title").requiredOption("--url <url>", "http(s) URL").option("--focus-artifact", "Focus the new artifact after creation").option("--no-focus", "Create the artifact in the background without changing focus").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").addHelpText("after", buildArtifactWorkflowHelpNote(true)).action(async (opts) => {
57589
+ ).requiredOption("--channel <id>", "Target channel ID").requiredOption("--title <title>", "Artifact title").requiredOption("--url <url>", "http(s) URL").option("--focus-artifact", "Focus the new artifact after creation").option("--no-focus", "Create the artifact in the background without changing focus").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).addHelpText("after", buildArtifactWorkflowHelpNote(true)).action(async (opts) => {
57326
57590
  const shouldFocus = resolveFocusDirective(argv, "create-url-artifact", "--focus-artifact");
57327
57591
  const artifactURL = opts.url.trim();
57328
57592
  const client = createAuthenticatedClient(opts);
@@ -57338,12 +57602,9 @@ function createProgram(env, argv = []) {
57338
57602
  writeLine(env.stdout, `URL artifact ${artifact.id} created.`);
57339
57603
  writeLine(env.stdout, `Television registered ${artifactURL}.`);
57340
57604
  });
57341
- program2.command("storage-path").description("Print the Television storage path").action(() => {
57342
- writeJSON(env.stdout, { storagePath: getTelevisionStoragePath() });
57343
- });
57344
57605
  program2.command("update-artifact").description(
57345
57606
  "Update artifact metadata in place: --title on any artifact, --path to repoint a path artifact, --url to repoint a URL artifact. ID and kind are immutable. The server validates the new pointer; for path artifacts the target must exist (trailing separator optional) and the content watcher follows it."
57346
- ).requiredOption("--id <id>", "Artifact ID").option("--title <title>", "New title").option("--path <path>", "New absolute path for a path artifact: markdown/HTML file or indexed directory (trailing separator optional)").option("--url <url>", "New http(s) URL for a URL artifact").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57607
+ ).requiredOption("--id <id>", "Artifact ID").option("--title <title>", "New title").option("--path <path>", "New absolute path for a path artifact: markdown/HTML file or indexed directory (trailing separator optional)").option("--url <url>", "New http(s) URL for a URL artifact").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57347
57608
  if (opts.title === void 0 && opts.path === void 0 && opts.url === void 0) {
57348
57609
  throw createDirectiveError("tv update-artifact requires at least one of --title, --path, or --url.");
57349
57610
  }
@@ -57361,20 +57622,20 @@ function createProgram(env, argv = []) {
57361
57622
  });
57362
57623
  program2.command("delete-artifact").description(
57363
57624
  "Delete an artifact registry record, remove its card from its owning channel, and delete its metadata. Television never removes or mutates the pointed-to path or URL."
57364
- ).requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57625
+ ).requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57365
57626
  const client = createAuthenticatedClient(opts);
57366
57627
  const result = await client.artifacts.delete({ artifactID: opts.id });
57367
57628
  for (const line of formatDeleteArtifactResult(result)) {
57368
57629
  writeLine(env.stdout, line);
57369
57630
  }
57370
57631
  });
57371
- program2.command("get-artifact").description("Fetch an artifact's metadata as JSON").requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57632
+ program2.command("get-artifact").description("Fetch an artifact's metadata as JSON").requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57372
57633
  const client = createAuthenticatedClient(opts);
57373
57634
  writeJSON(env.stdout, await client.artifacts.get({ artifactID: opts.id }));
57374
57635
  });
57375
57636
  program2.command("list-artifacts").description(
57376
57637
  "List artifacts as JSON. With no filter, every artifact is returned. --channel <id> filters to artifacts on that channel."
57377
- ).option("--channel <id>", "Filter to artifacts on this channel").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57638
+ ).option("--channel <id>", "Filter to artifacts on this channel").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57378
57639
  const client = createAuthenticatedClient(opts);
57379
57640
  const filter = {};
57380
57641
  if (opts.channel !== void 0) filter.channelID = opts.channel;
@@ -57382,7 +57643,7 @@ function createProgram(env, argv = []) {
57382
57643
  });
57383
57644
  program2.command("create-channel").description(
57384
57645
  "Create a new channel. Pick --focus-channel when the user should be taken to the new channel immediately (creating a workspace they're about to use), or --no-focus to create it in the background without disturbing their current view. A new channel starts empty; create new path or URL artifacts on it with the create-*-artifact commands using --channel."
57385
- ).requiredOption("--name <name>", "Channel name").option("--focus-channel", "Focus the new channel after creating it").option("--no-focus", "Create the channel in the background without changing focus").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57646
+ ).requiredOption("--name <name>", "Channel name").option("--focus-channel", "Focus the new channel after creating it").option("--no-focus", "Create the channel in the background without changing focus").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57386
57647
  const shouldFocus = resolveFocusDirective(argv, "create-channel", "--focus-channel");
57387
57648
  const client = createAuthenticatedClient(opts);
57388
57649
  const { channel } = await client.channels.create({ name: opts.name });
@@ -57391,7 +57652,7 @@ function createProgram(env, argv = []) {
57391
57652
  }
57392
57653
  writeLine(env.stdout, `Channel created: ${channel.id} (${channel.name})`);
57393
57654
  });
57394
- program2.command("update-channel").description("Rename a channel in place").requiredOption("--channel <id>", "Channel ID").requiredOption("--name <name>", "New channel name").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57655
+ program2.command("update-channel").description("Rename a channel in place").requiredOption("--channel <id>", "Channel ID").requiredOption("--name <name>", "New channel name").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57395
57656
  const client = createAuthenticatedClient(opts);
57396
57657
  const { channel } = await client.channels.update({
57397
57658
  channelID: opts.channel,
@@ -57401,26 +57662,26 @@ function createProgram(env, argv = []) {
57401
57662
  });
57402
57663
  program2.command("remove-channel").description(
57403
57664
  "Delete a channel. Artifacts on the channel are permanently deleted from the Television registry; their pointed-to path or URL targets are not touched."
57404
- ).requiredOption("--channel <id>", "Channel ID").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57665
+ ).requiredOption("--channel <id>", "Channel ID").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57405
57666
  const client = createAuthenticatedClient(opts);
57406
57667
  const result = await client.channels.remove({ channelID: opts.channel });
57407
57668
  for (const line of formatChannelRemovalResult(result)) {
57408
57669
  writeLine(env.stdout, line);
57409
57670
  }
57410
57671
  });
57411
- program2.command("list-channels").description("List all channels").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57672
+ program2.command("list-channels").description("List all channels").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57412
57673
  const client = createAuthenticatedClient(opts);
57413
57674
  writeJSON(env.stdout, await client.channels.list());
57414
57675
  });
57415
57676
  program2.command("get-channel").description(
57416
57677
  "Get a channel and its path/url artifact records as JSON. With no --channel, auto-selects when exactly one channel exists; otherwise --channel is required."
57417
- ).option("--channel <id>", "Channel ID (auto-selects if only one exists)").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57678
+ ).option("--channel <id>", "Channel ID (auto-selects if only one exists)").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57418
57679
  const client = createAuthenticatedClient(opts);
57419
57680
  writeJSON(env.stdout, await client.channels.get({ channelID: opts.channel }));
57420
57681
  });
57421
57682
  program2.command("focus-status").description(
57422
57683
  "Print display state as JSON: the active (persistently focused) channel ID and the active theme ID."
57423
- ).option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57684
+ ).option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57424
57685
  const client = createAuthenticatedClient(opts);
57425
57686
  const display = await client.display.get();
57426
57687
  writeJSON(env.stdout, {
@@ -57431,14 +57692,14 @@ function createProgram(env, argv = []) {
57431
57692
  });
57432
57693
  program2.command("focus-channel").description(
57433
57694
  "Set persistent channel focus. The change is broadcast to every connected GUI client and survives reconnects. This is the right command when the user wants to switch to a different channel and stay there. For just nudging attention to a specific artifact (which may live on the current channel), prefer `tv focus-artifact`."
57434
- ).requiredOption("--channel <id>", "Channel ID").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57695
+ ).requiredOption("--channel <id>", "Channel ID").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57435
57696
  const client = createAuthenticatedClient(opts);
57436
57697
  await client.display.patch({ focusedChannelId: opts.channel });
57437
57698
  writeLine(env.stdout, `Focused channel ${opts.channel}.`);
57438
57699
  });
57439
57700
  program2.command("set-theme").description(
57440
57701
  "Activate an installed display theme by its exact theme ID, or use `none` to run without a theme."
57441
- ).argument("<theme>", "Exact theme ID to activate, or `none` to use no theme").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").addHelpText("after", [
57702
+ ).argument("<theme>", "Exact theme ID to activate, or `none` to use no theme").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).addHelpText("after", [
57442
57703
  "",
57443
57704
  "Examples:",
57444
57705
  " tv set-theme paperlike Activate the `paperlike` theme.",
@@ -57476,9 +57737,12 @@ function createProgram(env, argv = []) {
57476
57737
  }
57477
57738
  writeLine(env.stdout, `Active theme changed from '${previousLabel}' to '${nextLabel}'.`);
57478
57739
  });
57740
+ program2.command("themes-path").description("Print the themes directory of the selected home as JSON").action(() => {
57741
+ writeJSON(env.stdout, { themesPath: import_node_path20.default.join(resolveHome(), "themes") });
57742
+ });
57479
57743
  program2.command("focus-artifact").description(
57480
57744
  "Send a transient focus nudge for a specific artifact. Connected clients select the artifact's tab page and switch to its channel when needed. This is NOT persisted as state \u2014 there is no concept of a 'focused artifact' that survives reconnects (the focused channel is persistent, but artifact focus is a one-shot event)."
57481
- ).requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57745
+ ).requiredOption("--id <id>", "Artifact ID").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57482
57746
  const client = createAuthenticatedClient(opts);
57483
57747
  const result = await client.display.focus({ artifactID: opts.id });
57484
57748
  writeLine(env.stdout, `Focused artifact ${result.artifactID} on channel ${result.channelID}.`);
@@ -57491,6 +57755,7 @@ function createProgram(env, argv = []) {
57491
57755
  "`tv skills install -i` runs the external skills installer against Television's bundled skills root."
57492
57756
  ].join("\n"));
57493
57757
  skillsCommand.command("install").description("Copy bundled Television skills into an agent harness skills folder, or run the external installer against the bundled skills root").argument("[path]", "Destination agent skills folder (for example: ~/.openclaw/skills, ~/.hermes/skills, ~/.agents/skills)").option("-i, --interactive", "Run the external skills installer against the bundled Television skills root").option("--installed-by-agent <agent-runtime-harness-name>", "Agent runtime harness name performing this install").action(async function(inputPath, opts) {
57758
+ const home = resolveHome();
57494
57759
  const bundledTelevisionSkillsCollectionRoot = env.resolveBundledSkillsRoot();
57495
57760
  if (opts.interactive) {
57496
57761
  if (inputPath) {
@@ -57502,6 +57767,7 @@ function createProgram(env, argv = []) {
57502
57767
  const args = ["add", bundledTelevisionSkillsCollectionRoot];
57503
57768
  await env.runSkillsInstaller(args);
57504
57769
  await emitSkillInstallTelemetry({
57770
+ home,
57505
57771
  agentType: "interactive-install",
57506
57772
  ...opts.installedByAgent === void 0 ? {} : { installedByAgent: opts.installedByAgent }
57507
57773
  });
@@ -57524,30 +57790,33 @@ function createProgram(env, argv = []) {
57524
57790
  writeLine(env.stdout, ` to: ${skill.destinationPath}`);
57525
57791
  }
57526
57792
  await emitSkillInstallTelemetry({
57793
+ home,
57527
57794
  agentType: deriveAgentTypeFromPath(destinationRoot).agent_type,
57528
57795
  ...opts.installedByAgent === void 0 ? {} : { installedByAgent: opts.installedByAgent }
57529
57796
  });
57530
57797
  });
57531
57798
  const telemetryCommand = program2.command("telemetry").description("Control telemetry for the running Television server");
57532
- telemetryCommand.command("disable").description("Disable telemetry for the running Television server").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57799
+ telemetryCommand.command("disable").description("Disable telemetry for the running Television server").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57533
57800
  const client = createAuthenticatedClient(opts);
57534
57801
  writeJSON(env.stdout, await client.telemetry.disable());
57535
57802
  });
57536
- telemetryCommand.command("enable").description("Enable telemetry for the running Television server").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57803
+ telemetryCommand.command("enable").description("Enable telemetry for the running Television server").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57537
57804
  const client = createAuthenticatedClient(opts);
57538
57805
  writeJSON(env.stdout, await client.telemetry.enable());
57539
57806
  });
57540
57807
  program2.command("stop").description("Stop the Television system service").action(async () => {
57541
- await uninstallPersistedService(getTelevisionStoragePath());
57808
+ await uninstallPersistedService(resolveHome());
57542
57809
  });
57543
- program2.command("status").description("Check Television server and service status").option("--port <number>", "Server port", parsePortOption).option("--storage-path <path>", "Directory containing the Television token").action(async (opts) => {
57544
- const serverURL = buildLocalServerURL(opts.port);
57810
+ program2.command("status").description("Print the selected home, server health, version, telemetry state, and service status as JSON").option("--port <number>", "Server port; required when the config file sets port 0", parseClientPortOption).action(async (opts) => {
57811
+ const home = resolveHome();
57812
+ const serverURL = resolveServerURL(opts.port);
57545
57813
  const result = {
57814
+ home,
57546
57815
  serverURL,
57547
57816
  healthy: false
57548
57817
  };
57549
57818
  try {
57550
- const client = createAuthenticatedClient(opts);
57819
+ const client = env.createClient(serverURL, readAuthToken(home));
57551
57820
  const health = await client.health();
57552
57821
  result.healthy = true;
57553
57822
  if (health.version !== void 0) result.version = health.version;
@@ -57572,17 +57841,25 @@ function listVisibleCLICommandNames() {
57572
57841
  }
57573
57842
  async function runCLI(argv, environment = {}) {
57574
57843
  const env = createEnvironment(environment);
57844
+ const { command, retired, remaining } = extractRetiredOptions(argv);
57845
+ const retiredService = retired.length > 0 ? readRetiredServiceDefinition(command, retired, remaining, process.env) : void 0;
57846
+ if (retired.length > 0 && !retiredService) {
57847
+ const named = [...new Set(retired.map((option) => option.name))].join(", ");
57848
+ env.stderr.write(`${ensureHelpPointer(`This version of Television does not accept ${named}. ${RETIRED_OPTIONS_GUIDANCE}`)}
57849
+ `);
57850
+ return 1;
57851
+ }
57852
+ if (process.env[TELEMETRY_LAUNCH_MODE_ENV] !== "daemon" && (process.env.TELEVISION_PORT || process.env.TELEVISION_STORAGE_PATH)) {
57853
+ writeLine(env.stderr, LEGACY_VARIABLES_WARNING);
57854
+ }
57855
+ const invocation = { argv, ...retiredService ? { retiredService } : {} };
57575
57856
  try {
57576
- if (argv.length === 1 && argv[0] === "-v") {
57577
- writeLine(env.stdout, readCLIVersionOutput(env.resolveHomeDir));
57578
- return 0;
57579
- }
57580
- const program2 = createProgram(env, argv);
57857
+ const program2 = createProgram(env, invocation);
57581
57858
  if (argv.length === 0) {
57582
57859
  program2.outputHelp();
57583
57860
  return 0;
57584
57861
  }
57585
- await program2.parseAsync(argv, { from: "user" });
57862
+ await program2.parseAsync(remaining, { from: "user" });
57586
57863
  return 0;
57587
57864
  } catch (error48) {
57588
57865
  if (error48 instanceof Error && "exitCode" in error48) {
@@ -57596,7 +57873,7 @@ async function runCLI(argv, environment = {}) {
57596
57873
  }
57597
57874
  return 1;
57598
57875
  }
57599
- env.stderr.write(`${formatCLIError(error48, argv)}
57876
+ env.stderr.write(`${formatCLIError(error48, invocation)}
57600
57877
  `);
57601
57878
  return exitStatusFromError(error48) ?? 1;
57602
57879
  }