@tuned-tensor/cli 0.21.0 → 0.22.1

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/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  TRAINING_MODELS,
9
9
  assertFoundationSettingsMatch,
10
10
  assessHardware,
11
+ auditRun,
11
12
  buildDecisionPythonCommand,
12
13
  buildFoundationPythonCommand,
13
14
  buildSystemMessage,
@@ -18,10 +19,12 @@ import {
18
19
  configForSpec,
19
20
  createExecutionPlan,
20
21
  createLocalStore,
22
+ createRunConsoleReporter,
21
23
  decisionLabels,
22
24
  defaultLocalHome,
23
25
  defaultStoreRoot,
24
26
  deriveSampleSeed,
27
+ ensureDatasetCached,
25
28
  evalReportSchema,
26
29
  evaluateCapabilities,
27
30
  expandUserPath,
@@ -30,6 +33,7 @@ import {
30
33
  formatHostStatusLine,
31
34
  getAgentConfigDir,
32
35
  getConfigDir,
36
+ getTunedTensorHome,
33
37
  hardenExistingLocalStore,
34
38
  isFoundationPipeline,
35
39
  loadLocalRunInput,
@@ -55,13 +59,13 @@ import {
55
59
  warningsFromSnapshot,
56
60
  withDecisionPythonEnvironment,
57
61
  withFoundationPythonEnvironment
58
- } from "./chunk-PVLX7LJI.js";
62
+ } from "./chunk-RPWOCVIL.js";
59
63
 
60
64
  // src/cli.ts
61
- import { spawn as spawn4 } from "child_process";
65
+ import { spawn as spawn6 } from "child_process";
62
66
  import { constants as osConstants2 } from "os";
63
67
  import { Command } from "commander";
64
- import chalk13 from "chalk";
68
+ import chalk15 from "chalk";
65
69
 
66
70
  // src/local-agent-client.ts
67
71
  import { randomUUID as randomUUID5 } from "crypto";
@@ -2163,7 +2167,9 @@ function createTunedTensorTools(api, options = {}) {
2163
2167
  runtime: Type2.Optional(Type2.Union([Type2.Record(Type2.String(), Type2.Unknown()), Type2.Null()])),
2164
2168
  evaluation: Type2.Optional(Type2.Union([Type2.Record(Type2.String(), Type2.Unknown()), Type2.Null()])),
2165
2169
  pipeline: Type2.Optional(Type2.Union([Type2.Record(Type2.String(), Type2.Unknown()), Type2.Null()])),
2166
- dataset_prebuilt: Type2.Optional(Type2.Record(Type2.String(), Type2.Unknown()))
2170
+ dataset_prebuilt: Type2.Optional(Type2.Record(Type2.String(), Type2.Unknown(), {
2171
+ description: "Local chat JSONL files ({training, validation|test}), or a Hugging Face dataset: {huggingface: {repo, revision?: 40-char commit, columns?: {input, output}}, training: 'repo/path.jsonl', test|validation: ...}. The user downloads it with `tt datasets prefetch tunedtensor.json`."
2172
+ }))
2167
2173
  }, { additionalProperties: false })
2168
2174
  }, { additionalProperties: false }),
2169
2175
  async (p) => {
@@ -3752,6 +3758,8 @@ function gpuDetail(gpu) {
3752
3758
  } else if (gpu.memory_total_bytes !== void 0) {
3753
3759
  const used = gpu.memory_used_bytes;
3754
3760
  parts.push(used === void 0 ? `${formatGiB(gpu.memory_total_bytes)} GiB VRAM` : `VRAM ${formatGiB(used)}/${formatGiB(gpu.memory_total_bytes)} GiB`);
3761
+ } else if (gpu.unified_memory) {
3762
+ parts.push("unified with system RAM");
3755
3763
  }
3756
3764
  if (gpu.temperature_c !== void 0) parts.push(`${Math.round(gpu.temperature_c)}\xB0C`);
3757
3765
  return parts.join(" ");
@@ -3903,6 +3911,7 @@ function renderBanner(input) {
3903
3911
 
3904
3912
  // src/local-runtime/live-usage.ts
3905
3913
  import { execFile } from "child_process";
3914
+ import { readFileSync as readFileSync5 } from "fs";
3906
3915
  import { statfs } from "fs/promises";
3907
3916
  import { cpus, freemem, homedir, loadavg, totalmem } from "os";
3908
3917
  import { dirname as dirname3, join as join7, resolve as resolve2 } from "path";
@@ -3974,6 +3983,50 @@ function queryNvidia(env, timeoutMs) {
3974
3983
  function tidyCpuModel(model) {
3975
3984
  return model.replace(/\((?:R|TM|tm|r)\)/g, "").replace(/\s+(?:CPU|Processor)\b/gi, "").replace(/\s*@\s*[\d.]+\s*[GM]Hz/i, "").replace(/\s+\d+-Core$/i, "").replace(/\s+/g, " ").trim() || "CPU";
3976
3985
  }
3986
+ var ARM_CORES = {
3987
+ "0xd03": "Cortex-A53",
3988
+ "0xd05": "Cortex-A55",
3989
+ "0xd08": "Cortex-A72",
3990
+ "0xd0b": "Cortex-A76",
3991
+ "0xd0c": "Neoverse-N1",
3992
+ "0xd40": "Neoverse-V1",
3993
+ "0xd41": "Cortex-A78",
3994
+ "0xd44": "Cortex-X1",
3995
+ "0xd47": "Cortex-A710",
3996
+ "0xd48": "Cortex-X2",
3997
+ "0xd49": "Neoverse-N2",
3998
+ "0xd4d": "Cortex-A715",
3999
+ "0xd4e": "Cortex-X3",
4000
+ "0xd4f": "Neoverse-V2",
4001
+ "0xd80": "Cortex-A520",
4002
+ "0xd81": "Cortex-A720",
4003
+ "0xd82": "Cortex-X4",
4004
+ "0xd84": "Neoverse-V3",
4005
+ "0xd85": "Cortex-X925",
4006
+ "0xd87": "Cortex-A725",
4007
+ "0xd8e": "Neoverse-N3"
4008
+ };
4009
+ function armCpuModel(cpuinfo) {
4010
+ const names = [];
4011
+ let implementer;
4012
+ for (const line of cpuinfo.split(/\r?\n/)) {
4013
+ const [key, value] = line.split(":").map((item) => item.trim().toLowerCase());
4014
+ if (key === "cpu implementer") implementer = value;
4015
+ if (key !== "cpu part" || !value) continue;
4016
+ const name = implementer === "0x41" ? ARM_CORES[value] : void 0;
4017
+ if (name && !names.includes(name)) names.push(name);
4018
+ }
4019
+ return names.length > 0 ? names.join(" + ") : void 0;
4020
+ }
4021
+ function cpuModel(reported, platform, arch) {
4022
+ const model = tidyCpuModel(reported ?? "");
4023
+ if (platform !== "linux" || !/^(?:unknown|CPU)$/i.test(model)) return model;
4024
+ try {
4025
+ return armCpuModel(readFileSync5("/proc/cpuinfo", "utf8")) ?? (arch === "arm64" ? "Arm CPU" : model);
4026
+ } catch {
4027
+ return arch === "arm64" ? "Arm CPU" : model;
4028
+ }
4029
+ }
3977
4030
  async function modelCacheDisk(env) {
3978
4031
  const path = resolve2(env.HF_HOME?.trim() || join7(homedir(), ".cache", "huggingface"));
3979
4032
  for (let probe = path, depth = 0; depth < 8; depth += 1) {
@@ -3992,9 +4045,9 @@ async function modelCacheDisk(env) {
3992
4045
  }
3993
4046
  return void 0;
3994
4047
  }
3995
- function appleGpu(cpuModel, platform, arch) {
4048
+ function appleGpu(cpuModel2, platform, arch) {
3996
4049
  if (platform !== "darwin" || arch !== "arm64") return void 0;
3997
- const chip = /apple/i.test(cpuModel) ? cpuModel.trim() : "Apple Silicon";
4050
+ const chip = /apple/i.test(cpuModel2) ? cpuModel2.trim() : "Apple Silicon";
3998
4051
  return {
3999
4052
  index: 0,
4000
4053
  name: `${chip} GPU`,
@@ -4015,7 +4068,7 @@ async function sampleLiveUsage(options = {}) {
4015
4068
  ]);
4016
4069
  const after = cpuTimes();
4017
4070
  const list = cpus();
4018
- const model = tidyCpuModel(list[0]?.model ?? "");
4071
+ const model = cpuModel(list[0]?.model, platform, arch);
4019
4072
  const total = totalmem();
4020
4073
  const apple = nvidia.gpus.length === 0 ? appleGpu(model, platform, arch) : void 0;
4021
4074
  const [one, five, fifteen] = loadavg();
@@ -4061,17 +4114,21 @@ var COMMAND_CATALOG = [
4061
4114
  { path: "runs report", description: "Show a local run report.", group: "Inspect", modes: LOCAL },
4062
4115
  { path: "runs compare", description: "Compare two local run reports.", group: "Inspect", modes: LOCAL },
4063
4116
  { path: "runs events", description: "Show local run progress events.", group: "Inspect", modes: LOCAL },
4117
+ { path: "runs audit", description: "Verify a run's event log and report, with timeline and provenance.", group: "Inspect", modes: LOCAL },
4064
4118
  { path: "models list", description: "List local models.", group: "Inspect", modes: LOCAL },
4065
4119
  { path: "models get", description: "Show a local model.", group: "Inspect", modes: LOCAL },
4066
4120
  { path: "models serve", description: "Serve a model through an OpenAI-compatible API.", group: "Serving", modes: LOCAL },
4067
4121
  { path: "models verify", description: "Verify a local model artifact.", group: "Serving", modes: LOCAL },
4068
4122
  { path: "models prefetch", description: "Download the local base-model snapshot.", group: "Serving", modes: LOCAL },
4069
4123
  { path: "models verify-base", description: "Verify the local base-model snapshot.", group: "Serving", modes: LOCAL },
4124
+ { path: "datasets prefetch", description: "Download the spec's Hugging Face dataset files.", group: "Workflow", modes: LOCAL },
4125
+ { path: "datasets verify", description: "Verify the cached Hugging Face dataset files.", group: "Workflow", modes: LOCAL },
4070
4126
  { path: "models active", description: "Show the active local model.", group: "Serving", modes: LOCAL },
4071
4127
  { path: "models activate", description: "Activate a verified local model.", group: "Serving", modes: LOCAL },
4072
4128
  { path: "models rollback", description: "Roll back the active local model.", group: "Serving", modes: LOCAL },
4073
4129
  { path: "serve", description: "Serve a local adapter, active model, or base model.", group: "Serving", modes: LOCAL },
4074
4130
  { path: "info", description: "Show local runtime package information.", group: "Inspect", modes: LOCAL },
4131
+ { path: "upgrade", description: "Upgrade tt to the latest release.", group: "Inspect", modes: LOCAL },
4075
4132
  { path: "status", description: "Show local project context.", group: "Inspect", modes: LOCAL },
4076
4133
  { path: "agent models", description: "List provider models for the laptop-local TT agent.", group: "Inspect", modes: LOCAL },
4077
4134
  { path: "agent configure", description: "Select the laptop-local TT agent model.", group: "Inspect", modes: LOCAL },
@@ -6385,10 +6442,22 @@ function renderValidate(value) {
6385
6442
  ["Spec", text(value.behavior_spec_id)],
6386
6443
  ["Base model", text(value.base_model)],
6387
6444
  ["Dataset", text(value.dataset_format)],
6445
+ ["Hugging Face", huggingFaceDatasetLabel(value)],
6388
6446
  ["Artifacts", text(value.artifact_root)],
6389
6447
  ["Store", text(value.store_root)],
6390
6448
  ["Dry run", text(value.dry_run)]
6391
6449
  ]);
6450
+ if (value.dataset_status === "not_downloaded") {
6451
+ printWarning(`Dataset files are not downloaded yet; dataset contents were not checked. Run \`${text(value.next_step) ?? "tt datasets prefetch"}\`.`);
6452
+ }
6453
+ }
6454
+ function huggingFaceDatasetLabel(value) {
6455
+ const source = record3(value.dataset_source) ?? record3(value.dataset_huggingface);
6456
+ const repo = text(source?.repo);
6457
+ if (!repo) return void 0;
6458
+ const revision = text(source?.revision) ?? "main";
6459
+ const status2 = value.dataset_status === "not_downloaded" ? "not downloaded" : "cached";
6460
+ return `${repo}@${revision} (${status2}${source?.pinned === false || !text(source?.revision) ? ", unpinned" : ""})`;
6392
6461
  }
6393
6462
  function renderRun(value) {
6394
6463
  const runStatus = text(value.status) ?? "unknown";
@@ -6443,6 +6512,68 @@ function renderRunEvents(value) {
6443
6512
  ])
6444
6513
  );
6445
6514
  }
6515
+ function formatSeconds(value) {
6516
+ if (typeof value !== "number" || !Number.isFinite(value)) return "running";
6517
+ const seconds = Math.round(value);
6518
+ const hours = Math.floor(seconds / 3600);
6519
+ const minutes = Math.floor(seconds % 3600 / 60);
6520
+ const rest = seconds % 60;
6521
+ if (hours) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
6522
+ if (minutes) return `${minutes}m ${String(rest).padStart(2, "0")}s`;
6523
+ return `${rest}s`;
6524
+ }
6525
+ function shortHash(value) {
6526
+ const hash = text(value);
6527
+ return hash ? hash.slice(0, 12) : void 0;
6528
+ }
6529
+ function renderRunAudit(value) {
6530
+ const verdict = text(value.verdict);
6531
+ const chain = record3(value.chain);
6532
+ const reportInfo = record3(value.report);
6533
+ const findings = Array.isArray(value.findings) ? value.findings.map(String) : [];
6534
+ if (verdict === "verified") printSuccess("Run evidence verified: event log intact and report unchanged since completion");
6535
+ else if (verdict === "tampered") printError("Run evidence does not verify");
6536
+ else printWarning("Run evidence is incomplete");
6537
+ for (const finding of findings) console.log(` - ${finding}`);
6538
+ printDetail([
6539
+ ["Run", text(value.run_id)],
6540
+ ["Status", status(value.status)],
6541
+ ["Event chain", `${text(chain?.status) ?? "\u2014"} (${text(chain?.chained_events) ?? 0}/${text(chain?.events) ?? 0} chained, head ${shortHash(chain?.head_hash) ?? "\u2014"})`],
6542
+ ["Report sha256", shortHash(reportInfo?.recorded_sha256) ?? "not recorded"]
6543
+ ]);
6544
+ const timeline = rows(value.timeline);
6545
+ if (timeline.length > 0) {
6546
+ console.log("");
6547
+ printTable(
6548
+ ["Started", "Stage", "Duration", "Status"],
6549
+ timeline.map((entry) => [
6550
+ formatDate(text(entry.started_at)),
6551
+ text(entry.stage) ?? "\u2014",
6552
+ formatSeconds(entry.duration_seconds),
6553
+ status(entry.status)
6554
+ ])
6555
+ );
6556
+ }
6557
+ const provenance = record3(value.provenance);
6558
+ if (provenance) {
6559
+ const software = record3(provenance.software);
6560
+ const execution = record3(provenance.execution);
6561
+ const spec = record3(provenance.spec);
6562
+ const baseModel = record3(provenance.base_model);
6563
+ const data3 = record3(provenance.data);
6564
+ const files = record3(data3?.file_sha256) ?? {};
6565
+ console.log("");
6566
+ printDetail([
6567
+ ["TT", `${text(software?.tt_version) ?? "\u2014"} (node ${text(software?.node_version) ?? "\u2014"}, ${text(software?.platform) ?? "\u2014"}/${text(software?.arch) ?? "\u2014"})`],
6568
+ ["Execution", `${text(execution?.target) ?? "\u2014"}${text(execution?.instance_id) ? ` ${text(execution?.instance_id)}` : ""} via ${text(execution?.training_backend) ?? "\u2014"}`],
6569
+ ["Spec", `${text(spec?.file_path) ?? text(spec?.behavior_spec_id) ?? "\u2014"} (sha256 ${shortHash(spec?.file_sha256) ?? shortHash(spec?.snapshot_sha256) ?? "\u2014"})`],
6570
+ ["Base model", `${text(baseModel?.id) ?? "\u2014"}@${shortHash(baseModel?.revision) ?? "unpinned"}`],
6571
+ ...Object.entries(files).map(([split, hash]) => [`Data ${split}`, `sha256 ${shortHash(hash)}`]),
6572
+ ["Training data", `sha256 ${shortHash(data3?.compiled_training_sha256) ?? "\u2014"} (${text(data3?.training_examples) ?? "\u2014"} examples)`],
6573
+ ["Evaluation", `${text(data3?.eval_examples_used) ?? "\u2014"}/${text(data3?.eval_examples_total) ?? "\u2014"} ${text(data3?.eval_split) ?? ""} (seed ${text(data3?.eval_sample_seed) ?? "\u2014"})`]
6574
+ ]);
6575
+ }
6576
+ }
6446
6577
  function renderRunRecord(value) {
6447
6578
  printDetail([
6448
6579
  ["Run", text(value.id) ?? text(value.run_id)],
@@ -6548,6 +6679,22 @@ function renderModelPrefetch(value) {
6548
6679
  ["Cache", text(value.model_cache) ?? text(value.hf_home)]
6549
6680
  ]);
6550
6681
  }
6682
+ function renderDatasetPrefetch(value) {
6683
+ printSuccess(
6684
+ value.local_files_only === true ? "Verified the cached Hugging Face dataset" : "Dataset is ready locally"
6685
+ );
6686
+ const files = record3(value.files) ?? {};
6687
+ printDetail([
6688
+ ["Dataset", text(value.repo)],
6689
+ ["Revision", `${text(value.revision) ?? "\u2014"}${value.pinned === false ? " (unpinned: set dataset_prebuilt.huggingface.revision to pin it)" : ""}`],
6690
+ ...Object.entries(files).map(([split, file]) => {
6691
+ const entry = record3(file);
6692
+ return [split[0].toUpperCase() + split.slice(1), `${text(entry?.path) ?? "\u2014"} (${displayBytes(entry?.size_bytes) ?? "\u2014"}, sha256 ${String(text(entry?.sha256) ?? "").slice(0, 12)})`];
6693
+ }),
6694
+ ["Snapshot", text(value.snapshot_path)],
6695
+ ["Cache", text(value.hub_cache)]
6696
+ ]);
6697
+ }
6551
6698
  function renderActiveModel(value) {
6552
6699
  const active = text(value.active) ?? "base";
6553
6700
  const pointer = record3(value.pointer);
@@ -6649,6 +6796,10 @@ function renderKnownJson(args2, value) {
6649
6796
  renderRunEvents(value);
6650
6797
  return true;
6651
6798
  }
6799
+ if (subcommand === "audit" && isRecord4(value)) {
6800
+ renderRunAudit(value);
6801
+ return true;
6802
+ }
6652
6803
  if (subcommand === "get" && isRecord4(value)) {
6653
6804
  renderRunRecord(value);
6654
6805
  return true;
@@ -6685,6 +6836,10 @@ function renderKnownJson(args2, value) {
6685
6836
  return true;
6686
6837
  }
6687
6838
  }
6839
+ if (command === "datasets" && isRecord4(value)) {
6840
+ renderDatasetPrefetch(value);
6841
+ return true;
6842
+ }
6688
6843
  if (command === "serve" && isRecord4(value)) {
6689
6844
  renderServePlan(value);
6690
6845
  return true;
@@ -6862,6 +7017,14 @@ function selectedSpecArgument(args2, cwd) {
6862
7017
  if (command === "models" && ["prefetch", "verify-base"].includes(args2[1] ?? "")) {
6863
7018
  return positionalSpecArgument(args2, 2, join9(cwd, DEFAULT_SPEC_NAME));
6864
7019
  }
7020
+ if (command === "datasets") {
7021
+ if (args2[1] === void 0 || args2[1].startsWith("-") || args2[1].endsWith(".json")) {
7022
+ args2.splice(1, 0, "prefetch");
7023
+ }
7024
+ if (["prefetch", "verify"].includes(args2[1] ?? "")) {
7025
+ return positionalSpecArgument(args2, 2, join9(cwd, DEFAULT_SPEC_NAME));
7026
+ }
7027
+ }
6865
7028
  const serves = command === "serve" || command === "models" && args2[1] === "serve";
6866
7029
  if (!serves) return null;
6867
7030
  const explicit = optionValueArgument(args2, "--spec");
@@ -7002,6 +7165,8 @@ function normalizeProjectedPaths(value, replacements) {
7002
7165
  async function spawnLocalCli(args2) {
7003
7166
  const output = args2.stdout ?? process.stdout;
7004
7167
  const errors = args2.stderr ?? process.stderr;
7168
+ const errorTerminal = errors;
7169
+ const childEnv = errorTerminal.isTTY && !args2.jsonMode && args2.env.TT_PROGRESS_TTY === void 0 ? { ...args2.env, TT_PROGRESS_TTY: "1", TT_PROGRESS_COLUMNS: String(errorTerminal.columns ?? 100) } : args2.env;
7005
7170
  const pipeInput = Boolean(args2.stdin && args2.stdin !== process.stdin);
7006
7171
  const pipeStreamingOutput = args2.streamingStdout && Boolean(args2.stdout && args2.stdout !== process.stdout);
7007
7172
  const child = spawn(
@@ -7009,7 +7174,7 @@ async function spawnLocalCli(args2) {
7009
7174
  [args2.entrypoint, ...args2.commandArgs],
7010
7175
  {
7011
7176
  cwd: args2.cwd,
7012
- env: args2.env,
7177
+ env: childEnv,
7013
7178
  stdio: [
7014
7179
  pipeInput ? "pipe" : "inherit",
7015
7180
  args2.streamingStdout && !pipeStreamingOutput ? "inherit" : "pipe",
@@ -7173,7 +7338,8 @@ var LOCAL_COMMANDS = [
7173
7338
  "validate",
7174
7339
  "serve",
7175
7340
  "runs",
7176
- "models"
7341
+ "models",
7342
+ "datasets"
7177
7343
  ];
7178
7344
  var LOCAL_COMMAND_DESCRIPTIONS = {
7179
7345
  info: "Show the installed local runtime version and runner status",
@@ -7183,7 +7349,8 @@ var LOCAL_COMMAND_DESCRIPTIONS = {
7183
7349
  validate: "Validate a local behavior spec without executing it",
7184
7350
  serve: "Serve a verified adapter, the active model, or the protected base",
7185
7351
  runs: "Inspect locally stored runs",
7186
- models: "Inspect, verify, prefetch, or serve local models"
7352
+ models: "Inspect, verify, prefetch, or serve local models",
7353
+ datasets: "Download or verify the Hugging Face dataset a spec trains on"
7187
7354
  };
7188
7355
  function applyExitCode(result2) {
7189
7356
  if (result2?.exitCode !== null && result2?.exitCode !== void 0) {
@@ -7298,7 +7465,7 @@ function registerAgentCommands(parent, options) {
7298
7465
  }
7299
7466
 
7300
7467
  // src/commands/pipeline.ts
7301
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
7468
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
7302
7469
  import { dirname as dirname9, join as join13, resolve as resolve8 } from "path";
7303
7470
 
7304
7471
  // src/local-runtime/project-migration.ts
@@ -8186,7 +8353,7 @@ function loadPipelineFile(path) {
8186
8353
  const resolved = resolve8(path);
8187
8354
  if (!existsSync4(resolved)) throw new Error(`Pipeline file not found: ${path}. Run \`tt pipeline init\` to create one.`);
8188
8355
  try {
8189
- return JSON.parse(readFileSync5(resolved, "utf8"));
8356
+ return JSON.parse(readFileSync6(resolved, "utf8"));
8190
8357
  } catch (error) {
8191
8358
  throw new Error(`Pipeline file must be JSON: ${error.message}`);
8192
8359
  }
@@ -8202,7 +8369,7 @@ function resolvePipelineDocument(options, specRequired) {
8202
8369
  if (requested !== void 0) return { document: parsePipeline(requested) };
8203
8370
  throw new Error(`Behavior spec not found: ${options.spec}. Create tunedtensor.json or pass an explicit --file for a recipe-only preview.`);
8204
8371
  }
8205
- const source = readFileSync5(specPath, "utf8");
8372
+ const source = readFileSync6(specPath, "utf8");
8206
8373
  const input = parseLocalRunInput(JSON.parse(source), specPath);
8207
8374
  const document = pipelineForRunInput(input, requested);
8208
8375
  const spec = {
@@ -8260,7 +8427,7 @@ function registerPipelineCommands(parent) {
8260
8427
  pipeline2.command("init").description("Write a canonical v1 pipeline recipe").option("-f, --file <path>", "Legacy standalone recipe output (default: embed in tunedtensor.json)").option("--engine <engine>", "adapter (default) or foundation").option("--spec <path>", "Foundation spec whose hyperparameters stamp the DAG").action(async (options) => {
8261
8428
  if (!options.file) {
8262
8429
  const path2 = resolve8(options.spec ?? DEFAULT_SPEC_FILE);
8263
- const raw = JSON.parse(readFileSync5(path2, "utf8"));
8430
+ const raw = JSON.parse(readFileSync6(path2, "utf8"));
8264
8431
  const input = parseLocalRunInput(raw, path2);
8265
8432
  if (input.kind === "request") throw new Error("Pipeline init requires a behavior spec.");
8266
8433
  if (options.engine && options.engine !== (input.spec.engine ?? "adapter")) throw new Error("--engine must match the spec.");
@@ -8324,7 +8491,7 @@ function registerPipelineCommands(parent) {
8324
8491
  const plan = createExecutionPlan(document, { only: parseList(options.only), skip: parseList(options.skip) });
8325
8492
  outputPlan({ ...plan, ...spec ? { spec } : {} }, await hostWarningsForPipeline(document, input));
8326
8493
  });
8327
- pipeline2.command("run").description("Run an ordered local pipeline, or safely preview any pipeline").option("--dry-run", "Resolve and display only; never execute, transfer, or reserve credits").option("-f, --file <path>", "Explicit pipeline recipe (default: derive from behavior spec)").option("--spec <path>", "Local behavior spec", DEFAULT_SPEC_FILE).option("--config <path>", "Local runtime config").option("--output <path>", "Foundation or decision run directory (must not already exist)").option("--resume <path>", "Resume a foundation run directory").option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").action(async (options, command) => {
8494
+ pipeline2.command("run").description("Run an ordered local pipeline, or safely preview any pipeline").option("--dry-run", "Resolve and display only; never execute, transfer, or reserve credits").option("-f, --file <path>", "Explicit pipeline recipe (default: derive from behavior spec)").option("--spec <path>", "Local behavior spec", DEFAULT_SPEC_FILE).option("--config <path>", "Local runtime config").option("--output <path>", "Foundation or decision run directory (must not already exist)").option("--resume <path>", "Resume a foundation run directory").option("--only <ids>", "Comma-separated step IDs to include").option("--skip <ids>", "Comma-separated step IDs to omit").option("--verbose", "Also stream training and evaluation process logs").option("--quiet", "Do not print live stage progress").action(async (options, command) => {
8328
8495
  if (options.output && options.resume) {
8329
8496
  throw new Error("--output and --resume are mutually exclusive.");
8330
8497
  }
@@ -8387,7 +8554,19 @@ function registerPipelineCommands(parent) {
8387
8554
  ...plan.name ? { name: plan.name } : {},
8388
8555
  steps: plan.steps.map(({ transfers: _transfers, ...step }) => step)
8389
8556
  };
8390
- const result2 = await runLocalPipeline({ request: input.request, config, pipeline: localPipeline, ...input.kind === "spec" ? { projectSpec: input.spec } : {} });
8557
+ const reporter = options.quiet ? void 0 : createRunConsoleReporter({ verbose: options.verbose });
8558
+ const { request: request2 } = await ensureDatasetCached({ request: input.request, config, reporter });
8559
+ const result2 = await runLocalPipeline({
8560
+ request: request2,
8561
+ config,
8562
+ pipeline: localPipeline,
8563
+ ...input.kind === "spec" ? { projectSpec: input.spec } : {},
8564
+ ...spec ? { specFile: { path: spec.path, sha256: spec.sha256 } } : {},
8565
+ ...reporter ? { reporter } : {}
8566
+ });
8567
+ if (!isJsonMode()) {
8568
+ console.error(`Run ${result2.request.run_id}: \`tt runs audit ${result2.request.run_id}\` verifies its record and shows timing and provenance.`);
8569
+ }
8391
8570
  if (isJsonMode()) return printJson({ ...result2, spec });
8392
8571
  printSuccess(`Pipeline completed with status ${result2.status}.`);
8393
8572
  });
@@ -8660,7 +8839,7 @@ function registerTopupCommands(parent) {
8660
8839
 
8661
8840
  // src/commands/publish.ts
8662
8841
  import { createInterface as createInterface4 } from "readline/promises";
8663
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
8842
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
8664
8843
  import { homedir as homedir3 } from "os";
8665
8844
  import { isAbsolute as isAbsolute5, join as join14, resolve as resolve9 } from "path";
8666
8845
  import { stdin as defaultStdin, stdout as defaultStdout } from "process";
@@ -8677,13 +8856,13 @@ function resolvePublishStoreRoot(options) {
8677
8856
  const localConfigPath = join14(cwd, "local-runner.json");
8678
8857
  const projectPath = join14(cwd, "tunedtensor.json");
8679
8858
  if (existsSync5(projectPath)) {
8680
- const spec = localBehaviorSpecFileSchema.parse(JSON.parse(readFileSync6(projectPath, "utf8")));
8859
+ const spec = localBehaviorSpecFileSchema.parse(JSON.parse(readFileSync7(projectPath, "utf8")));
8681
8860
  if (existsSync5(localConfigPath) && (spec.runtime || spec.evaluation)) throw new Error("Conflicting configuration sources; migrate local-runner.json first.");
8682
8861
  if (!existsSync5(localConfigPath)) return expandPath2(spec.runtime?.storeRoot ?? (env.TT_LOCAL_HOME?.trim() || DEFAULT_PROJECT_STORE_ROOT), cwd, homeDirectory);
8683
8862
  }
8684
8863
  if (existsSync5(localConfigPath)) {
8685
8864
  try {
8686
- const raw = JSON.parse(readFileSync6(localConfigPath, "utf-8"));
8865
+ const raw = JSON.parse(readFileSync7(localConfigPath, "utf-8"));
8687
8866
  if (typeof raw.storeRoot === "string" && raw.storeRoot.trim()) {
8688
8867
  return expandPath2(raw.storeRoot, cwd, homeDirectory);
8689
8868
  }
@@ -8747,7 +8926,7 @@ function assertPublishPayloadSize(payload) {
8747
8926
  }
8748
8927
  }
8749
8928
  function registerPublishCommand(parent) {
8750
- parent.command("publish").description("Publish local run evidence to the Tuned Tensor dashboard").argument("[runId]", "Local run id or prefix (defaults to latest completed/failed run)").option("-k, --api-key <key>", "API key (tt_...)").option("-u, --base-url <url>", "API base URL").option("-y, --yes", "Skip confirmation prompt").option("--dry-run", "Show what would be published without uploading").action(async (runId, cmdOpts) => {
8929
+ parent.command("publish").description("Publish local run evidence to the Tuned Tensor dashboard").argument("[runId]", "Local run id or prefix (defaults to latest completed/failed run)").option("-k, --api-key <key>", "API key (tt_...)").option("-u, --base-url <url>", "API base URL").option("-y, --yes", "Skip confirmation prompt").option("--dry-run", "Show what would be published without uploading").option("--allow-unverified", "Publish evidence that cannot be fully verified (older TT versions, unreadable events)").action(async (runId, cmdOpts) => {
8751
8930
  const inherited = parent.optsWithGlobals();
8752
8931
  const opts = {
8753
8932
  apiKey: cmdOpts.apiKey ?? inherited.apiKey,
@@ -8766,12 +8945,27 @@ function registerPublishCommand(parent) {
8766
8945
  const report = runReportSchema.parse(rawReport);
8767
8946
  const specRecord = await store.getSpec(run.behavior_spec_id);
8768
8947
  const exampleCount = report.candidate.results.length || report.baseline.results.length;
8948
+ const audit = await auditRun(store, run.id);
8949
+ if (audit.verdict === "tampered") {
8950
+ throw new Error(
8951
+ `Run ${run.id} evidence does not verify: ${audit.findings.join(" ")} Inspect it with \`tt runs audit ${run.id}\`; tampered runs are not published.`
8952
+ );
8953
+ }
8769
8954
  const payload = {
8770
8955
  spec: {
8771
8956
  id: specRecord.id,
8772
8957
  ...specRecord.spec
8773
8958
  },
8774
- report
8959
+ report: {
8960
+ ...report,
8961
+ audit: {
8962
+ verdict: audit.verdict,
8963
+ findings: audit.findings,
8964
+ event_chain: { status: audit.chain.status, events: audit.chain.events, head_hash: audit.chain.head_hash },
8965
+ report_sha256: audit.report.recorded_sha256,
8966
+ timeline: audit.timeline
8967
+ }
8968
+ }
8775
8969
  };
8776
8970
  assertPublishPayloadSize(payload);
8777
8971
  if (isJsonMode() && cmdOpts.dryRun) {
@@ -8799,6 +8993,14 @@ function registerPublishCommand(parent) {
8799
8993
  printWarning(
8800
8994
  "Prompts and model outputs from the run report will be uploaded to your Tuned Tensor account."
8801
8995
  );
8996
+ if (audit.verdict !== "verified") {
8997
+ printWarning(`Run evidence is unverified: ${audit.findings.join(" ")}`);
8998
+ }
8999
+ }
9000
+ if (audit.verdict !== "verified" && !cmdOpts.allowUnverified && !cmdOpts.dryRun) {
9001
+ throw new Error(
9002
+ `Run ${run.id} evidence cannot be fully verified: ${audit.findings.join(" ")} Review it with \`tt runs audit\`, then pass --allow-unverified to publish it anyway; the dashboard will show it as unverified.`
9003
+ );
8802
9004
  }
8803
9005
  if (cmdOpts.dryRun) {
8804
9006
  if (isJsonMode()) {
@@ -8843,7 +9045,7 @@ function registerPublishCommand(parent) {
8843
9045
  }
8844
9046
 
8845
9047
  // src/commands/specs.ts
8846
- import { readFileSync as readFileSync7 } from "fs";
9048
+ import { readFileSync as readFileSync8 } from "fs";
8847
9049
 
8848
9050
  // src/resolve.ts
8849
9051
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -8947,7 +9149,7 @@ var SpecBodyError = class extends Error {
8947
9149
  function loadSpecBody(filePath, mode) {
8948
9150
  let parsed;
8949
9151
  try {
8950
- parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
9152
+ parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
8951
9153
  } catch (e) {
8952
9154
  throw new SpecBodyError(
8953
9155
  `Could not parse ${filePath}: ${e.message}`
@@ -9076,7 +9278,7 @@ function registerSpecsCommands(parent) {
9076
9278
  }
9077
9279
 
9078
9280
  // src/commands/datasets.ts
9079
- import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
9281
+ import { existsSync as existsSync6, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
9080
9282
  var DOCUMENT_OCR_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
9081
9283
  function formatBytes(bytes) {
9082
9284
  if (bytes < 1024) return `${bytes} B`;
@@ -9206,7 +9408,7 @@ function parseDatasetFormat(value) {
9206
9408
  throw new Error("--format must be one of: jsonl, document_ocr_jsonl");
9207
9409
  }
9208
9410
  function validateDatasetFile(file, requestedFormat) {
9209
- const lines = readFileSync8(file, "utf8").split(/\r?\n/);
9411
+ const lines = readFileSync9(file, "utf8").split(/\r?\n/);
9210
9412
  const errors = [];
9211
9413
  let rowCount = 0;
9212
9414
  let detectedFormat = null;
@@ -9338,7 +9540,7 @@ function registerDatasetsCommands(parent) {
9338
9540
  }
9339
9541
  const format = validateDatasetFile(file, parseDatasetFormat(cmdOpts.format));
9340
9542
  const name = cmdOpts.name || file.split("/").pop().replace(/\.jsonl?$/, "");
9341
- const fileBytes = readFileSync8(file);
9543
+ const fileBytes = readFileSync9(file);
9342
9544
  const uploadUrl = await post(
9343
9545
  "/datasets/upload-url",
9344
9546
  {
@@ -9383,7 +9585,7 @@ function registerDatasetsCommands(parent) {
9383
9585
  }
9384
9586
 
9385
9587
  // src/commands/label.ts
9386
- import { existsSync as existsSync7, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
9588
+ import { existsSync as existsSync7, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
9387
9589
  function formatCents2(cents) {
9388
9590
  return `$${(cents / 100).toFixed(2)}`;
9389
9591
  }
@@ -9427,7 +9629,7 @@ function validateUnlabeledFile(file, inputColumn) {
9427
9629
  "CSV uploads need --input-column <name> to say which column holds the input text."
9428
9630
  );
9429
9631
  }
9430
- const firstLine = readFileSync9(file, "utf8").split(/\r?\n/, 1)[0] ?? "";
9632
+ const firstLine = readFileSync10(file, "utf8").split(/\r?\n/, 1)[0] ?? "";
9431
9633
  const columns = parseCsvHeader(firstLine);
9432
9634
  if (!columns.includes(inputColumn)) {
9433
9635
  throw new Error(
@@ -9439,7 +9641,7 @@ function validateUnlabeledFile(file, inputColumn) {
9439
9641
  if (!lower.endsWith(".jsonl") && !lower.endsWith(".json")) {
9440
9642
  throw new Error("Labeling source file must be .jsonl or .csv");
9441
9643
  }
9442
- const lines = readFileSync9(file, "utf8").split(/\r?\n/);
9644
+ const lines = readFileSync10(file, "utf8").split(/\r?\n/);
9443
9645
  const errors = [];
9444
9646
  let rowCount = 0;
9445
9647
  for (const [index, rawLine] of lines.entries()) {
@@ -9562,7 +9764,7 @@ function registerLabelCommands(parent) {
9562
9764
  const { format } = validateUnlabeledFile(file, cmdOpts.inputColumn);
9563
9765
  const specId = await resolveSpecId(cmdOpts.spec, opts);
9564
9766
  const name = cmdOpts.name || file.split("/").pop().replace(/\.(jsonl|json|csv)$/, "");
9565
- const fileBytes = readFileSync9(file);
9767
+ const fileBytes = readFileSync10(file);
9566
9768
  const contentType = format === "csv" ? "text/csv" : "application/jsonl";
9567
9769
  const { data: uploadUrl } = await post(
9568
9770
  "/labeling-jobs/upload-url",
@@ -9735,7 +9937,7 @@ function registerLabelCommands(parent) {
9735
9937
  printError("Provide --output <text> or --file <path>.");
9736
9938
  process.exit(1);
9737
9939
  }
9738
- const output = cmdOpts.output ?? readFileSync9(cmdOpts.file, "utf8");
9940
+ const output = cmdOpts.output ?? readFileSync10(cmdOpts.file, "utf8");
9739
9941
  const jobId = await resolveLabelingJobId(id, opts);
9740
9942
  const indexes = parseIndexList(cmdOpts.row);
9741
9943
  const rowIds = await resolveRowIds(jobId, indexes, opts);
@@ -10225,7 +10427,7 @@ import {
10225
10427
  statSync as statSync4,
10226
10428
  createWriteStream,
10227
10429
  unlinkSync,
10228
- readFileSync as readFileSync11,
10430
+ readFileSync as readFileSync12,
10229
10431
  writeFileSync as writeFileSync5,
10230
10432
  readdirSync,
10231
10433
  rmSync
@@ -10236,7 +10438,7 @@ import { Readable, Transform } from "stream";
10236
10438
  import { pipeline } from "stream/promises";
10237
10439
 
10238
10440
  // src/commands/init.ts
10239
- import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
10441
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
10240
10442
  import { resolve as resolve10 } from "path";
10241
10443
  var DEFAULT_SPEC_FILE2 = "tunedtensor.json";
10242
10444
  function loadSpec(filePath) {
@@ -10248,7 +10450,7 @@ Run \`tt init\` to create one.`
10248
10450
  );
10249
10451
  process.exit(1);
10250
10452
  }
10251
- const raw = JSON.parse(readFileSync10(resolved, "utf-8"));
10453
+ const raw = JSON.parse(readFileSync11(resolved, "utf-8"));
10252
10454
  return raw;
10253
10455
  }
10254
10456
 
@@ -11462,7 +11664,7 @@ function findSpecPath(explicitPath, modelPath) {
11462
11664
  return null;
11463
11665
  }
11464
11666
  function loadSystemPromptFromSpec(specPath) {
11465
- const spec = JSON.parse(readFileSync11(specPath, "utf8"));
11667
+ const spec = JSON.parse(readFileSync12(specPath, "utf8"));
11466
11668
  return buildSystemPrompt(spec);
11467
11669
  }
11468
11670
  function loadJsonSchemaForServe(schemaPath) {
@@ -11471,7 +11673,7 @@ function loadJsonSchemaForServe(schemaPath) {
11471
11673
  throw new Error(`JSON schema file not found: ${schemaPath}`);
11472
11674
  }
11473
11675
  try {
11474
- const schema = JSON.parse(readFileSync11(resolved, "utf8"));
11676
+ const schema = JSON.parse(readFileSync12(resolved, "utf8"));
11475
11677
  return JSON.stringify(schema);
11476
11678
  } catch (err) {
11477
11679
  throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
@@ -12099,10 +12301,10 @@ function registerModelsCommands(parent) {
12099
12301
  }
12100
12302
 
12101
12303
  // src/commands/push.ts
12102
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
12304
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
12103
12305
  import { resolve as resolve12 } from "path";
12104
12306
  function persistRemoteId(filePath, id) {
12105
- const raw = JSON.parse(readFileSync12(filePath, "utf-8"));
12307
+ const raw = JSON.parse(readFileSync13(filePath, "utf-8"));
12106
12308
  raw.id = id;
12107
12309
  writeFileSync6(filePath, JSON.stringify(raw, null, 2) + "\n");
12108
12310
  }
@@ -12185,8 +12387,20 @@ function registerUsageCommand(parent) {
12185
12387
  });
12186
12388
  }
12187
12389
 
12390
+ // src/commands/upgrade.ts
12391
+ import { spawn as spawn5 } from "child_process";
12392
+ import { accessSync, constants as constants3, existsSync as existsSync10, readFileSync as readFileSync15, realpathSync } from "fs";
12393
+ import { dirname as dirname13, join as join17 } from "path";
12394
+ import chalk14 from "chalk";
12395
+
12188
12396
  // src/update-check.ts
12397
+ import { spawn as spawn4 } from "child_process";
12398
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
12399
+ import { dirname as dirname12, join as join16 } from "path";
12400
+ import chalk13 from "chalk";
12401
+ var CLI_PACKAGE_NAME = "@tuned-tensor/cli";
12189
12402
  var PACKAGE_LATEST_URL = "https://registry.npmjs.org/@tuned-tensor%2fcli/latest";
12403
+ var DEFAULT_CACHE_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
12190
12404
  function parseVersion(version) {
12191
12405
  const match = version.trim().match(
12192
12406
  /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
@@ -12230,11 +12444,50 @@ function compareVersions(left, right) {
12230
12444
  }
12231
12445
  return comparePrerelease(left.prerelease, right.prerelease);
12232
12446
  }
12233
- async function checkForCliUpdate(currentVersion, options = {}) {
12447
+ function getCliUpdateCacheFile(env = process.env) {
12448
+ return join16(getTunedTensorHome(env), "update-check.json");
12449
+ }
12450
+ function cliUpdateChecksDisabled(env = process.env) {
12451
+ return [env.TT_NO_UPDATE_CHECK, env.NO_UPDATE_NOTIFIER, env.CI].some((value) => value !== void 0 && value !== "" && value !== "0" && value !== "false");
12452
+ }
12453
+ function readUpdateCache(file) {
12454
+ try {
12455
+ const payload = JSON.parse(readFileSync14(file, "utf8"));
12456
+ if (typeof payload.checkedAt !== "number" || typeof payload.latestVersion !== "string" || !parseVersion(payload.latestVersion)) return null;
12457
+ return { checkedAt: payload.checkedAt, latestVersion: payload.latestVersion };
12458
+ } catch {
12459
+ return null;
12460
+ }
12461
+ }
12462
+ function writeUpdateCache(file, cache) {
12463
+ try {
12464
+ mkdirSync5(dirname12(file), { recursive: true });
12465
+ writeFileSync7(file, `${JSON.stringify(cache)}
12466
+ `);
12467
+ } catch {
12468
+ }
12469
+ }
12470
+ function recordLatestCliVersion(cacheFile, latestVersion, now = Date.now()) {
12471
+ writeUpdateCache(cacheFile, { checkedAt: now, latestVersion });
12472
+ }
12473
+ function compareCliVersions(left, right) {
12474
+ const parsedLeft = parseVersion(left);
12475
+ const parsedRight = parseVersion(right);
12476
+ if (!parsedLeft || !parsedRight) return null;
12477
+ return compareVersions(parsedLeft, parsedRight);
12478
+ }
12479
+ function newerStableRelease(currentVersion, latestVersion) {
12480
+ if (!latestVersion) return null;
12234
12481
  const current = parseVersion(currentVersion);
12235
- if (!current) return null;
12482
+ const latest = parseVersion(latestVersion);
12483
+ if (!current || !latest || latest.prerelease.length > 0 || compareVersions(latest, current) <= 0) return null;
12484
+ return { currentVersion, latestVersion };
12485
+ }
12486
+ async function fetchLatestCliVersion(options = {}) {
12236
12487
  const controller = new AbortController();
12237
- const timeoutMs = options.timeoutMs ?? 750;
12488
+ const abort = () => controller.abort();
12489
+ options.signal?.addEventListener("abort", abort, { once: true });
12490
+ const timeoutMs = options.timeoutMs ?? 1500;
12238
12491
  let timeout;
12239
12492
  const request2 = (async () => {
12240
12493
  try {
@@ -12246,11 +12499,7 @@ async function checkForCliUpdate(currentVersion, options = {}) {
12246
12499
  const payload = await response.json();
12247
12500
  if (typeof payload.version !== "string") return null;
12248
12501
  const latest = parseVersion(payload.version);
12249
- if (!latest || latest.prerelease.length > 0 || compareVersions(latest, current) <= 0) return null;
12250
- return {
12251
- currentVersion,
12252
- latestVersion: payload.version
12253
- };
12502
+ return latest && latest.prerelease.length === 0 ? payload.version : null;
12254
12503
  } catch {
12255
12504
  return null;
12256
12505
  }
@@ -12265,15 +12514,211 @@ async function checkForCliUpdate(currentVersion, options = {}) {
12265
12514
  return await Promise.race([request2, deadline]);
12266
12515
  } finally {
12267
12516
  if (timeout) clearTimeout(timeout);
12517
+ options.signal?.removeEventListener("abort", abort);
12518
+ }
12519
+ }
12520
+ function cachedCliUpdate(currentVersion, cacheFile) {
12521
+ return newerStableRelease(currentVersion, readUpdateCache(cacheFile)?.latestVersion);
12522
+ }
12523
+ function cliUpdateCacheIsStale(cacheFile, options = {}) {
12524
+ const cache = readUpdateCache(cacheFile);
12525
+ const now = (options.now ?? Date.now)();
12526
+ return !cache || now - cache.checkedAt >= (options.cacheMaxAgeMs ?? DEFAULT_CACHE_MAX_AGE_MS);
12527
+ }
12528
+ async function refreshCliUpdateCache(cacheFile, options = {}) {
12529
+ const latest = await fetchLatestCliVersion(options);
12530
+ if (latest) recordLatestCliVersion(cacheFile, latest, (options.now ?? Date.now)());
12531
+ return latest;
12532
+ }
12533
+ var REFRESH_SCRIPT = `
12534
+ const [url, file] = process.argv.slice(1);
12535
+ const fs = require("node:fs");
12536
+ const path = require("node:path");
12537
+ fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10000) })
12538
+ .then((response) => response.ok ? response.json() : null)
12539
+ .then((payload) => {
12540
+ if (!payload || typeof payload.version !== "string") return;
12541
+ fs.mkdirSync(path.dirname(file), { recursive: true });
12542
+ fs.writeFileSync(file, JSON.stringify({ checkedAt: Date.now(), latestVersion: payload.version }) + "\\n");
12543
+ })
12544
+ .catch(() => {});
12545
+ `;
12546
+ function spawnCliUpdateRefresh(cacheFile) {
12547
+ const cached = readUpdateCache(cacheFile);
12548
+ if (cached) recordLatestCliVersion(cacheFile, cached.latestVersion);
12549
+ try {
12550
+ const child = spawn4(
12551
+ process.execPath,
12552
+ ["-e", REFRESH_SCRIPT, PACKAGE_LATEST_URL, cacheFile],
12553
+ { detached: true, stdio: "ignore", windowsHide: true }
12554
+ );
12555
+ child.on("error", () => {
12556
+ });
12557
+ child.unref();
12558
+ } catch {
12268
12559
  }
12269
12560
  }
12561
+ async function checkForCliUpdate(currentVersion, options = {}) {
12562
+ if (!parseVersion(currentVersion)) return null;
12563
+ const { cacheFile } = options;
12564
+ if (cacheFile && !cliUpdateCacheIsStale(cacheFile, options)) {
12565
+ return cachedCliUpdate(currentVersion, cacheFile);
12566
+ }
12567
+ const latest = cacheFile ? await refreshCliUpdateCache(cacheFile, options) : await fetchLatestCliVersion(options);
12568
+ return newerStableRelease(
12569
+ currentVersion,
12570
+ latest ?? (cacheFile ? readUpdateCache(cacheFile)?.latestVersion : void 0)
12571
+ );
12572
+ }
12270
12573
  function formatCliUpdateNotice(update) {
12271
12574
  return [
12272
- `Update available: tt ${update.currentVersion} \u2192 ${update.latestVersion}`,
12273
- "Upgrade with: npm install -g @tuned-tensor/cli@latest"
12575
+ chalk13.yellow(`Update available: tt ${update.currentVersion} \u2192 ${chalk13.bold(update.latestVersion)}`),
12576
+ `Run ${chalk13.bold("tt upgrade")} to install it.`
12274
12577
  ].join("\n");
12275
12578
  }
12276
12579
 
12580
+ // src/commands/upgrade.ts
12581
+ function resolveGlobalInstall(entrypoint, platform = process.platform) {
12582
+ if (!entrypoint) return null;
12583
+ let script;
12584
+ try {
12585
+ script = realpathSync(entrypoint);
12586
+ } catch {
12587
+ return null;
12588
+ }
12589
+ if (/[\\/]_npx[\\/]/.test(script)) return null;
12590
+ const match = platform === "win32" ? script.match(/^(.*)\\node_modules\\@tuned-tensor\\cli\\dist\\index\.js$/i) : script.match(/^(.*)\/lib\/node_modules\/@tuned-tensor\/cli\/dist\/index\.js$/);
12591
+ if (!match?.[1]) return null;
12592
+ return { prefix: match[1], packageDir: dirname13(dirname13(script)) };
12593
+ }
12594
+ function prefixIsWritable(prefix, platform) {
12595
+ const modules = platform === "win32" ? join17(prefix, "node_modules") : join17(prefix, "lib", "node_modules");
12596
+ try {
12597
+ accessSync(existsSync10(modules) ? modules : prefix, constants3.W_OK);
12598
+ return true;
12599
+ } catch {
12600
+ return false;
12601
+ }
12602
+ }
12603
+ function installedVersion(packageDir) {
12604
+ try {
12605
+ const manifest = JSON.parse(readFileSync15(join17(packageDir, "package.json"), "utf8"));
12606
+ return typeof manifest.version === "string" ? manifest.version : void 0;
12607
+ } catch {
12608
+ return void 0;
12609
+ }
12610
+ }
12611
+ function quoteForCmd(arg) {
12612
+ return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
12613
+ }
12614
+ function defaultNpmRunner(platform) {
12615
+ return async (args2) => await new Promise((resolve13, reject) => {
12616
+ const windows = platform === "win32";
12617
+ const child = spawn5(
12618
+ windows ? "npm.cmd" : "npm",
12619
+ windows ? args2.map(quoteForCmd) : args2,
12620
+ {
12621
+ shell: windows,
12622
+ // Keep stdout clean for --json; npm progress goes to stderr.
12623
+ stdio: ["ignore", isJsonMode() ? 2 : "inherit", "inherit"]
12624
+ }
12625
+ );
12626
+ child.once("error", reject);
12627
+ child.once("close", (code2) => resolve13(code2 ?? 1));
12628
+ });
12629
+ }
12630
+ function formatCommand(args2) {
12631
+ return ["npm", ...args2].map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ");
12632
+ }
12633
+ function registerUpgradeCommand(program, deps) {
12634
+ program.command("upgrade").alias("update").description("Upgrade tt to the latest release").argument("[version]", "Version or npm dist-tag to install (default: latest)").option("--check", "Only report whether an update is available").addHelpText(
12635
+ "after",
12636
+ `
12637
+ Examples:
12638
+ tt upgrade Install the latest stable release
12639
+ tt upgrade --check Report whether a newer release exists
12640
+ tt upgrade beta Opt in to the beta channel
12641
+ tt upgrade 0.21.0 Install a specific version
12642
+
12643
+ Set TT_NO_UPDATE_CHECK=1 to silence update notices.
12644
+ `
12645
+ ).action(async (requested, options) => {
12646
+ const platform = deps.platform ?? process.platform;
12647
+ const cacheFile = getCliUpdateCacheFile(deps.env);
12648
+ const current = deps.version;
12649
+ const target = requested?.trim() || "latest";
12650
+ let latest;
12651
+ if (target === "latest") {
12652
+ latest = await fetchLatestCliVersion({ fetchImpl: deps.fetchImpl, timeoutMs: 1e4 }) ?? void 0;
12653
+ if (!latest) {
12654
+ throw new Error(
12655
+ "Could not reach the npm registry to look up the latest tt release. Check your connection and try again."
12656
+ );
12657
+ }
12658
+ recordLatestCliVersion(cacheFile, latest);
12659
+ const comparison = compareCliVersions(latest, current);
12660
+ if (comparison !== null && comparison <= 0) {
12661
+ if (isJsonMode()) {
12662
+ printJson({ current_version: current, latest_version: latest, update_available: false });
12663
+ } else {
12664
+ printSuccess(`tt ${current} is the latest version.`);
12665
+ }
12666
+ return;
12667
+ }
12668
+ }
12669
+ if (options.check) {
12670
+ if (isJsonMode()) {
12671
+ printJson({ current_version: current, latest_version: latest ?? null, update_available: Boolean(latest) });
12672
+ } else if (latest) {
12673
+ console.log(`Update available: tt ${current} \u2192 ${chalk14.bold(latest)}`);
12674
+ console.log(`Run ${chalk14.bold("tt upgrade")} to install it.`);
12675
+ } else {
12676
+ console.log(`tt ${current} is installed. Run ${chalk14.bold(`tt upgrade ${target}`)} to install ${target}.`);
12677
+ }
12678
+ return;
12679
+ }
12680
+ const install = resolveGlobalInstall(deps.entrypoint, platform);
12681
+ const packageSpec = `${CLI_PACKAGE_NAME}@${target}`;
12682
+ if (!install) {
12683
+ throw new Error(
12684
+ `This tt (${deps.entrypoint ?? "unknown path"}) was not installed with npm -g, so it can't upgrade itself.
12685
+ Upgrade it the way you installed it, for example:
12686
+ npm install -g --ignore-scripts ${packageSpec}`
12687
+ );
12688
+ }
12689
+ const npmArgs = ["install", "-g", "--ignore-scripts", "--prefix", install.prefix, packageSpec];
12690
+ if (!prefixIsWritable(install.prefix, platform)) {
12691
+ throw new Error(
12692
+ `tt is installed in ${install.prefix}, which this user can't write to. Run:
12693
+ sudo ${formatCommand(npmArgs)}`
12694
+ );
12695
+ }
12696
+ if (!isJsonMode()) {
12697
+ console.log(`Upgrading tt ${current} \u2192 ${chalk14.bold(latest ?? target)}`);
12698
+ console.log(chalk14.dim(`$ ${formatCommand(npmArgs)}`));
12699
+ }
12700
+ let code2;
12701
+ try {
12702
+ code2 = await (deps.runNpm ?? defaultNpmRunner(platform))(npmArgs);
12703
+ } catch (error) {
12704
+ throw new Error(
12705
+ `Could not run npm (${error instanceof Error ? error.message : String(error)}). Run it yourself:
12706
+ ${formatCommand(npmArgs)}`
12707
+ );
12708
+ }
12709
+ if (code2 !== 0) {
12710
+ throw new Error(`npm exited with code ${code2}. Run it yourself to see details:
12711
+ ${formatCommand(npmArgs)}`);
12712
+ }
12713
+ const installed = installedVersion(install.packageDir) ?? latest ?? target;
12714
+ if (isJsonMode()) {
12715
+ printJson({ previous_version: current, version: installed, prefix: install.prefix });
12716
+ } else {
12717
+ printSuccess(`Upgraded tt ${current} \u2192 ${installed}. Restart any open tt sessions to use it.`);
12718
+ }
12719
+ });
12720
+ }
12721
+
12277
12722
  // src/cli.ts
12278
12723
  function createAccountToolApi(opts) {
12279
12724
  return {
@@ -12384,7 +12829,7 @@ async function runSelfCommand(args2, options = {}) {
12384
12829
  throw new Error("Cannot locate the tt CLI entrypoint.");
12385
12830
  }
12386
12831
  return await new Promise((resolve13, reject) => {
12387
- const child = spawn4(process.execPath, [entrypoint, ...args2], {
12832
+ const child = spawn6(process.execPath, [entrypoint, ...args2], {
12388
12833
  cwd: options.cwd,
12389
12834
  env: options.env ?? process.env,
12390
12835
  stdio: "inherit",
@@ -12486,7 +12931,7 @@ function createProgram(version, runtime = {}) {
12486
12931
  if (root.json) setJsonMode(true);
12487
12932
  if (root.color === false) {
12488
12933
  process.env.FORCE_COLOR = "0";
12489
- chalk13.level = 0;
12934
+ chalk15.level = 0;
12490
12935
  }
12491
12936
  });
12492
12937
  registerLocalCommands(program, {
@@ -12505,6 +12950,11 @@ function createProgram(version, runtime = {}) {
12505
12950
  registerUsageCommand(program);
12506
12951
  registerPublishCommand(program);
12507
12952
  registerCloudCommands(program);
12953
+ registerUpgradeCommand(program, {
12954
+ version,
12955
+ entrypoint: runtime.argv?.[1] ?? process.argv[1],
12956
+ env
12957
+ });
12508
12958
  registerAgentCommands(program, {
12509
12959
  get env() {
12510
12960
  return childEnvironment2(program.opts(), env);
@@ -12524,12 +12974,12 @@ function createProgram(version, runtime = {}) {
12524
12974
  `);
12525
12975
  return;
12526
12976
  }
12527
- output.write(`${chalk13.bold.hex("#8B5CF6")("Tuned Tensor status")}
12977
+ output.write(`${chalk15.bold.hex("#8B5CF6")("Tuned Tensor status")}
12528
12978
  `);
12529
12979
  output.write(`${formatShellStatus(context).join("\n")}
12530
12980
 
12531
12981
  `);
12532
- output.write(`${chalk13.bold("Context")}
12982
+ output.write(`${chalk15.bold("Context")}
12533
12983
  `);
12534
12984
  output.write(`${formatShellContext(context).join("\n")}
12535
12985
  `);
@@ -12585,7 +13035,11 @@ async function runCli(version, runtime = {}) {
12585
13035
  const error = runtime.stderr ?? process.stderr;
12586
13036
  let update = null;
12587
13037
  try {
12588
- update = await (runtime.checkForUpdate ?? checkForCliUpdate)(version);
13038
+ if (runtime.checkForUpdate) {
13039
+ update = await runtime.checkForUpdate(version);
13040
+ } else if (!cliUpdateChecksDisabled(env)) {
13041
+ update = await checkForCliUpdate(version, { cacheFile: getCliUpdateCacheFile(env) });
13042
+ }
12589
13043
  } catch {
12590
13044
  }
12591
13045
  if (update) {
@@ -12626,11 +13080,29 @@ async function runCli(version, runtime = {}) {
12626
13080
  return;
12627
13081
  }
12628
13082
  if (argv.includes("--json")) setJsonMode(true);
12629
- await createProgram(version, { ...runtime, argv }).parseAsync(argv);
13083
+ const notifier = startCommandUpdateNotifier(version, args2, runtime, env);
13084
+ try {
13085
+ await createProgram(version, { ...runtime, argv }).parseAsync(argv);
13086
+ } finally {
13087
+ notifier?.();
13088
+ }
13089
+ }
13090
+ function startCommandUpdateNotifier(version, args2, runtime, env) {
13091
+ const error = runtime.stderr ?? process.stderr;
13092
+ if (runtime.checkForUpdate || error.isTTY !== true || cliUpdateChecksDisabled(env) || args2.some((arg) => ["--json", "--version", "-V", "--help", "-h"].includes(arg)) || ["upgrade", "update", "help"].includes(args2[0] ?? "")) return null;
13093
+ const cacheFile = getCliUpdateCacheFile(env);
13094
+ if (cliUpdateCacheIsStale(cacheFile)) spawnCliUpdateRefresh(cacheFile);
13095
+ return () => {
13096
+ if (isJsonMode()) return;
13097
+ const update = cachedCliUpdate(version, cacheFile);
13098
+ if (update) error.write(`
13099
+ ${formatCliUpdateNotice(update)}
13100
+ `);
13101
+ };
12630
13102
  }
12631
13103
 
12632
13104
  // src/index.ts
12633
- runCli("0.21.0").catch((err) => {
13105
+ runCli("0.22.1").catch((err) => {
12634
13106
  reportError(err);
12635
13107
  process.exit(1);
12636
13108
  });