@devstationlabs/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/devstation.js +385 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,6 +46,15 @@ devstation config # what it will use, and where each value came fro
46
46
  Exporting `ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY` still works, and is what a
47
47
  server or CI job should do.
48
48
 
49
+ ## Upgrade
50
+
51
+ ```sh
52
+ devstation upgrade
53
+ ```
54
+
55
+ It upgrades however it was installed: through npm for an npm install, or by
56
+ downloading and verifying the new binary for the standalone one.
57
+
49
58
  ## Use it
50
59
 
51
60
  ```sh
package/devstation.js CHANGED
@@ -12510,7 +12510,7 @@ import { existsSync as existsSync12 } from "fs";
12510
12510
 
12511
12511
  // src/lib/agent/cli/args.ts
12512
12512
  var CLI_NAME = "devstation";
12513
- var VERSION = "0.1.2";
12513
+ var VERSION = "0.1.4";
12514
12514
  var COMMANDS = new Set([
12515
12515
  "chat",
12516
12516
  "run",
@@ -12528,6 +12528,7 @@ var COMMANDS = new Set([
12528
12528
  "config",
12529
12529
  "login",
12530
12530
  "logout",
12531
+ "upgrade",
12531
12532
  "doctor",
12532
12533
  "version",
12533
12534
  "help"
@@ -12542,6 +12543,7 @@ var OFFLINE_COMMANDS = new Set([
12542
12543
  "config",
12543
12544
  "login",
12544
12545
  "logout",
12546
+ "upgrade",
12545
12547
  "doctor",
12546
12548
  "version",
12547
12549
  "help",
@@ -12558,6 +12560,7 @@ function parseArgs(argv, cwd = process.cwd()) {
12558
12560
  json: false,
12559
12561
  sandbox: (process.env.DEVSTATION_SANDBOX ?? "").toLowerCase() !== "off",
12560
12562
  project: false,
12563
+ check: false,
12561
12564
  root: cwd
12562
12565
  };
12563
12566
  const words = [];
@@ -12585,6 +12588,9 @@ function parseArgs(argv, cwd = process.cwd()) {
12585
12588
  case "--project":
12586
12589
  parsed.project = true;
12587
12590
  break;
12591
+ case "--check":
12592
+ parsed.check = true;
12593
+ break;
12588
12594
  case "-h":
12589
12595
  case "--help":
12590
12596
  parsed.command = "help";
@@ -12675,6 +12681,7 @@ var HELP = `DevStation, the coding agent.
12675
12681
  ${CLI_NAME} tools list the tools it can use, and which ones ask first
12676
12682
  ${CLI_NAME} login [provider] store an API key and choose a model
12677
12683
  ${CLI_NAME} logout [provider] remove stored API keys
12684
+ ${CLI_NAME} upgrade [--check] update to the latest version (--check only reports)
12678
12685
  ${CLI_NAME} config show the settings a run would use, and where each came from
12679
12686
  ${CLI_NAME} config set <key> <value> [--project]
12680
12687
  set provider, model or baseUrl
@@ -12696,6 +12703,7 @@ Options
12696
12703
  --json JSON from config, sessions, checkpoints and tools
12697
12704
  --no-sandbox run commands on this machine instead of in a container
12698
12705
  --project with config set/unset: write this workspace's config, not the global one
12706
+ --check with upgrade: say whether a newer version exists, change nothing
12699
12707
  -h, --help this
12700
12708
  -v, --version the version
12701
12709
 
@@ -12723,7 +12731,7 @@ var SESSION_HELP = ` /undo rewind the last checkpoint
12723
12731
  import { accessSync, constants as constants2, existsSync as existsSync10 } from "fs";
12724
12732
 
12725
12733
  // src/lib/agent/git.ts
12726
- import { existsSync } from "fs";
12734
+ import { existsSync, readFileSync, statSync } from "fs";
12727
12735
  import { join } from "path";
12728
12736
 
12729
12737
  // src/lib/agent/shell.ts
@@ -12941,7 +12949,16 @@ function cloneTargetProblem(target) {
12941
12949
  // src/lib/agent/git.ts
12942
12950
  var CHECKPOINT_PREFIX = "agent checkpoint:";
12943
12951
  function isRepo(root) {
12944
- return existsSync(join(root, ".git"));
12952
+ const dotGit = join(root, ".git");
12953
+ if (!existsSync(dotGit))
12954
+ return false;
12955
+ try {
12956
+ if (statSync(dotGit).isFile())
12957
+ return readFileSync(dotGit, "utf8").startsWith("gitdir:");
12958
+ } catch {
12959
+ return false;
12960
+ }
12961
+ return existsSync(join(dotGit, "HEAD"));
12945
12962
  }
12946
12963
  function quote(value) {
12947
12964
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -13066,7 +13083,7 @@ import {
13066
13083
  cpSync,
13067
13084
  existsSync as existsSync3,
13068
13085
  mkdirSync as mkdirSync2,
13069
- readFileSync as readFileSync2,
13086
+ readFileSync as readFileSync3,
13070
13087
  readdirSync as readdirSync2,
13071
13088
  rmSync,
13072
13089
  writeFileSync as writeFileSync2
@@ -13077,10 +13094,10 @@ import { dirname as dirname2, join as join3 } from "path";
13077
13094
  import {
13078
13095
  existsSync as existsSync2,
13079
13096
  mkdirSync,
13080
- readFileSync,
13097
+ readFileSync as readFileSync2,
13081
13098
  readdirSync,
13082
13099
  realpathSync,
13083
- statSync,
13100
+ statSync as statSync2,
13084
13101
  writeFileSync
13085
13102
  } from "fs";
13086
13103
  import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
@@ -13157,10 +13174,10 @@ class Workspace {
13157
13174
  if (!existsSync2(resolved.absolute)) {
13158
13175
  return { ok: false, reason: `There is no file at ${relativePath}.` };
13159
13176
  }
13160
- if (statSync(resolved.absolute).isDirectory()) {
13177
+ if (statSync2(resolved.absolute).isDirectory()) {
13161
13178
  return { ok: false, reason: `${relativePath} is a directory, not a file.` };
13162
13179
  }
13163
- const buffer = readFileSync(resolved.absolute);
13180
+ const buffer = readFileSync2(resolved.absolute);
13164
13181
  if (looksBinary(buffer)) {
13165
13182
  return {
13166
13183
  ok: false,
@@ -13180,10 +13197,10 @@ class Workspace {
13180
13197
  if (!resolved.ok)
13181
13198
  return { ok: false, reason: resolved.reason };
13182
13199
  if (existsSync2(resolved.absolute)) {
13183
- if (statSync(resolved.absolute).isDirectory()) {
13200
+ if (statSync2(resolved.absolute).isDirectory()) {
13184
13201
  return { ok: false, reason: `${relativePath} is a directory.` };
13185
13202
  }
13186
- if (looksBinary(readFileSync(resolved.absolute))) {
13203
+ if (looksBinary(readFileSync2(resolved.absolute))) {
13187
13204
  return { ok: false, reason: `${relativePath} is a binary file and was not overwritten.` };
13188
13205
  }
13189
13206
  }
@@ -13195,7 +13212,7 @@ class Workspace {
13195
13212
  const resolved = this.resolve(relativePath);
13196
13213
  return resolved.ok && existsSync2(resolved.absolute);
13197
13214
  }
13198
- list(subdir = ".") {
13215
+ list(subdir = ".", limit = Number.POSITIVE_INFINITY) {
13199
13216
  const base = subdir === "." || subdir === "" ? { ok: true, absolute: this.root } : this.resolve(subdir);
13200
13217
  if (!base.ok || !existsSync2(base.absolute))
13201
13218
  return [];
@@ -13204,6 +13221,8 @@ class Workspace {
13204
13221
  const out = [];
13205
13222
  const walk = (dir) => {
13206
13223
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
13224
+ if (out.length >= limit)
13225
+ return;
13207
13226
  if (entry.name.startsWith(".") && skip.has(entry.name))
13208
13227
  continue;
13209
13228
  if (skip.has(entry.name))
@@ -13236,7 +13255,7 @@ function listSnapshots(root) {
13236
13255
  if (!existsSync3(manifest))
13237
13256
  continue;
13238
13257
  try {
13239
- found.push(JSON.parse(readFileSync2(manifest, "utf8")));
13258
+ found.push(JSON.parse(readFileSync3(manifest, "utf8")));
13240
13259
  } catch {}
13241
13260
  }
13242
13261
  return found.sort((a, b) => b.id.localeCompare(a.id));
@@ -18114,9 +18133,9 @@ import {
18114
18133
  chmodSync,
18115
18134
  existsSync as existsSync4,
18116
18135
  mkdirSync as mkdirSync3,
18117
- readFileSync as readFileSync3,
18136
+ readFileSync as readFileSync4,
18118
18137
  renameSync,
18119
- statSync as statSync2,
18138
+ statSync as statSync3,
18120
18139
  writeFileSync as writeFileSync3
18121
18140
  } from "fs";
18122
18141
  import { dirname as dirname4, join as join5 } from "path";
@@ -18151,11 +18170,12 @@ function readSettingsFile(path4, problems = []) {
18151
18170
  if (!existsSync4(path4))
18152
18171
  return {};
18153
18172
  try {
18154
- const raw = JSON.parse(readFileSync3(path4, "utf8"));
18173
+ const raw = JSON.parse(readFileSync4(path4, "utf8"));
18155
18174
  const out = {};
18156
18175
  if (raw.provider !== undefined) {
18157
- if (isProvider(raw.provider))
18158
- out.provider = raw.provider;
18176
+ const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
18177
+ if (isProvider(named))
18178
+ out.provider = named;
18159
18179
  else
18160
18180
  problems.push(`${path4}: unknown provider "${String(raw.provider)}".`);
18161
18181
  }
@@ -18195,11 +18215,11 @@ function readCredentials(home, problems = []) {
18195
18215
  if (!existsSync4(path4))
18196
18216
  return {};
18197
18217
  try {
18198
- const mode = statSync2(path4).mode & 511;
18218
+ const mode = statSync3(path4).mode & 511;
18199
18219
  if (mode & 63) {
18200
18220
  problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
18201
18221
  }
18202
- const raw = JSON.parse(readFileSync3(path4, "utf8"));
18222
+ const raw = JSON.parse(readFileSync4(path4, "utf8"));
18203
18223
  const out = {};
18204
18224
  for (const id of PROVIDER_IDS) {
18205
18225
  if (typeof raw[id] === "string" && raw[id].trim())
@@ -18228,8 +18248,9 @@ function resolveSettings(opts) {
18228
18248
  let provider = null;
18229
18249
  let providerSource = "not set";
18230
18250
  if (env2.DEVSTATION_PROVIDER) {
18231
- if (isProvider(env2.DEVSTATION_PROVIDER)) {
18232
- provider = env2.DEVSTATION_PROVIDER;
18251
+ const named = env2.DEVSTATION_PROVIDER.toLowerCase();
18252
+ if (isProvider(named)) {
18253
+ provider = named;
18233
18254
  providerSource = "DEVSTATION_PROVIDER";
18234
18255
  } else {
18235
18256
  warnings.push(`DEVSTATION_PROVIDER="${env2.DEVSTATION_PROVIDER}" is not a provider, so it was ignored.`);
@@ -18446,7 +18467,7 @@ async function embedMissing(store, provider, options = {}) {
18446
18467
  import { join as join7 } from "path";
18447
18468
 
18448
18469
  // src/lib/agent/repo-session.ts
18449
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
18470
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
18450
18471
  import { join as join6, relative as relative2, sep as sep2 } from "path";
18451
18472
  var SKIP_DIRS = new Set([
18452
18473
  ".git",
@@ -18490,9 +18511,9 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
18490
18511
  }
18491
18512
  if (!item.isFile())
18492
18513
  continue;
18493
- if (statSync3(full).size > maxFileBytes)
18514
+ if (statSync4(full).size > maxFileBytes)
18494
18515
  continue;
18495
- const buffer = readFileSync4(full);
18516
+ const buffer = readFileSync5(full);
18496
18517
  if (looksBinary(buffer))
18497
18518
  continue;
18498
18519
  files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
@@ -18501,6 +18522,69 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
18501
18522
  walk3(root);
18502
18523
  return files;
18503
18524
  }
18525
+ var INDEX_SKIP_DIRS = new Set([
18526
+ ...SKIP_DIRS,
18527
+ "target",
18528
+ "coverage",
18529
+ "__pycache__",
18530
+ ".venv",
18531
+ "venv",
18532
+ ".gradle",
18533
+ "out"
18534
+ ]);
18535
+ var HIDDEN_KEEP = new Set([".github"]);
18536
+ function readWorkspaceBounded(root, opts = {}) {
18537
+ const maxFiles = opts.maxFiles ?? 20000;
18538
+ const until = Date.now() + (opts.deadlineMs ?? 1e4);
18539
+ const maxBytes = opts.maxFileBytes ?? 1024 * 1024;
18540
+ const files = {};
18541
+ let seen = 0;
18542
+ let stopped = null;
18543
+ const walk3 = (dir) => {
18544
+ let entries;
18545
+ try {
18546
+ entries = readdirSync3(dir, { withFileTypes: true });
18547
+ } catch {
18548
+ return;
18549
+ }
18550
+ for (const item of entries) {
18551
+ if (stopped)
18552
+ return;
18553
+ if (item.isSymbolicLink())
18554
+ continue;
18555
+ const full = join6(dir, item.name);
18556
+ if (item.isDirectory()) {
18557
+ if (INDEX_SKIP_DIRS.has(item.name))
18558
+ continue;
18559
+ if (item.name.startsWith(".") && !HIDDEN_KEEP.has(item.name))
18560
+ continue;
18561
+ walk3(full);
18562
+ continue;
18563
+ }
18564
+ if (!item.isFile())
18565
+ continue;
18566
+ if (seen >= maxFiles) {
18567
+ stopped = "max-files";
18568
+ return;
18569
+ }
18570
+ if ((seen & 63) === 0 && Date.now() > until) {
18571
+ stopped = "deadline";
18572
+ return;
18573
+ }
18574
+ seen++;
18575
+ try {
18576
+ if (statSync4(full).size > maxBytes)
18577
+ continue;
18578
+ const buffer = readFileSync5(full);
18579
+ if (looksBinary(buffer))
18580
+ continue;
18581
+ files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
18582
+ } catch {}
18583
+ }
18584
+ };
18585
+ walk3(root);
18586
+ return { files, stopped };
18587
+ }
18504
18588
  var MAX_TITLE = 72;
18505
18589
  function proposalFor(goal, result, files) {
18506
18590
  const firstLine = (goal || result.summary).split(`
@@ -18869,7 +18953,7 @@ class MemoryStore {
18869
18953
  this.db.run("DELETE FROM chunks WHERE path = ?", [path4]);
18870
18954
  this.db.run("DELETE FROM files WHERE path = ?", [path4]);
18871
18955
  }
18872
- reindex(files) {
18956
+ reindex(files, options = {}) {
18873
18957
  const result = { scanned: 0, reindexed: 0, removed: 0, chunks: 0 };
18874
18958
  this.db.run("BEGIN");
18875
18959
  try {
@@ -18882,9 +18966,11 @@ class MemoryStore {
18882
18966
  result.chunks += this.replaceFile(path4, content);
18883
18967
  result.reindexed++;
18884
18968
  }
18885
- for (const gone of known) {
18886
- this.removeFile(gone);
18887
- result.removed++;
18969
+ if (!options.partial) {
18970
+ for (const gone of known) {
18971
+ this.removeFile(gone);
18972
+ result.removed++;
18973
+ }
18888
18974
  }
18889
18975
  this.db.run("COMMIT");
18890
18976
  } catch (error2) {
@@ -18969,8 +19055,27 @@ function openStore(root) {
18969
19055
  return new MemoryStore(storePath(root));
18970
19056
  }
18971
19057
  async function indexWorkspace(root, options = {}) {
19058
+ const started = Date.now();
19059
+ const home = options.home ?? process.env.HOME ?? "";
19060
+ const strip2 = (p) => p.replace(/\/+$/, "");
19061
+ if (home && strip2(root) === strip2(home)) {
19062
+ return {
19063
+ scanned: 0,
19064
+ reindexed: 0,
19065
+ removed: 0,
19066
+ chunks: 0,
19067
+ embedded: 0,
19068
+ embeddingModel: null,
19069
+ stopped: "home",
19070
+ ms: 0
19071
+ };
19072
+ }
18972
19073
  const store = options.store ?? openStore(root);
18973
- const result = store.reindex(readWorkspace(root));
19074
+ const read = readWorkspaceBounded(root, {
19075
+ maxFiles: options.maxFiles,
19076
+ deadlineMs: options.deadlineMs
19077
+ });
19078
+ const result = store.reindex(read.files, { partial: read.stopped !== null });
18974
19079
  let embedded = 0;
18975
19080
  if (options.embeddings) {
18976
19081
  try {
@@ -18979,11 +19084,17 @@ async function indexWorkspace(root, options = {}) {
18979
19084
  embedded = 0;
18980
19085
  }
18981
19086
  }
18982
- return { ...result, embedded, embeddingModel: options.embeddings?.model ?? null };
19087
+ return {
19088
+ ...result,
19089
+ embedded,
19090
+ embeddingModel: options.embeddings?.model ?? null,
19091
+ stopped: read.stopped,
19092
+ ms: Date.now() - started
19093
+ };
18983
19094
  }
18984
19095
 
18985
19096
  // src/lib/agent/memory/project-memory.ts
18986
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
19097
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
18987
19098
  import { dirname as dirname6, join as join8 } from "path";
18988
19099
  var DEFAULT_FILE = "PROJECT_MEMORY.md";
18989
19100
  var HEADER = `# Project memory
@@ -19002,7 +19113,7 @@ function readMemory(root) {
19002
19113
  const path4 = memoryPath(root);
19003
19114
  if (!existsSync5(path4))
19004
19115
  return [];
19005
- return parseMemory(readFileSync5(path4, "utf8"));
19116
+ return parseMemory(readFileSync6(path4, "utf8"));
19006
19117
  }
19007
19118
  var ENTRY = /^- \[([^\]]+)\](?:\s*\(([^)]*)\))?\s+([\s\S]*)$/;
19008
19119
  function parseMemory(text) {
@@ -19026,7 +19137,7 @@ function remember(root, note, tag = null, now = new Date) {
19026
19137
  if (!text) {
19027
19138
  return { ok: false, path: path4, message: "There was nothing to remember." };
19028
19139
  }
19029
- const existing = existsSync5(path4) ? readFileSync5(path4, "utf8") : "";
19140
+ const existing = existsSync5(path4) ? readFileSync6(path4, "utf8") : "";
19030
19141
  const entries = parseMemory(existing);
19031
19142
  const normal = (value) => value.toLowerCase().replace(/\s+/g, " ").trim();
19032
19143
  if (entries.some((entry) => normal(entry.note) === normal(text))) {
@@ -19062,7 +19173,7 @@ function renderMemory(entries) {
19062
19173
 
19063
19174
  // src/lib/agent/mcp.ts
19064
19175
  import { spawn as spawn3 } from "child_process";
19065
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
19176
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
19066
19177
  import { join as join9 } from "path";
19067
19178
  function configPaths(root, home = process.env.HOME ?? "") {
19068
19179
  return [
@@ -19077,7 +19188,7 @@ function loadConfig(root, home = process.env.HOME ?? "") {
19077
19188
  if (!existsSync6(path4))
19078
19189
  continue;
19079
19190
  try {
19080
- const parsed = JSON.parse(readFileSync6(path4, "utf8"));
19191
+ const parsed = JSON.parse(readFileSync7(path4, "utf8"));
19081
19192
  for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
19082
19193
  if (server && typeof server.command === "string")
19083
19194
  merged.mcpServers[name] = server;
@@ -19337,11 +19448,11 @@ function failedResult(message) {
19337
19448
 
19338
19449
  // src/lib/agent/sandbox-exec.ts
19339
19450
  import { randomBytes } from "crypto";
19340
- import { existsSync as existsSync8, statSync as statSync5, unlinkSync } from "fs";
19451
+ import { existsSync as existsSync8, statSync as statSync6, unlinkSync } from "fs";
19341
19452
  import { isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
19342
19453
 
19343
19454
  // src/lib/agent/project.ts
19344
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
19455
+ import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
19345
19456
  import { dirname as dirname7, join as join10, relative as relative3, sep as sep3 } from "path";
19346
19457
  var SKIP = new Set([
19347
19458
  "node_modules",
@@ -19368,7 +19479,7 @@ var MANIFESTS = [
19368
19479
  var MAX_DEPTH = 2;
19369
19480
  function readScripts(absolute) {
19370
19481
  try {
19371
- const parsed = JSON.parse(readFileSync7(absolute, "utf8"));
19482
+ const parsed = JSON.parse(readFileSync8(absolute, "utf8"));
19372
19483
  return parsed.scripts ?? {};
19373
19484
  } catch {
19374
19485
  return {};
@@ -19401,7 +19512,7 @@ function detectManifests(root) {
19401
19512
  continue;
19402
19513
  const child = join10(dir, entry);
19403
19514
  try {
19404
- if (statSync4(child).isDirectory())
19515
+ if (statSync5(child).isDirectory())
19405
19516
  scan(child, depth + 1);
19406
19517
  } catch {}
19407
19518
  }
@@ -19542,7 +19653,7 @@ var NEEDS = {
19542
19653
  go: ["go"]
19543
19654
  };
19544
19655
  async function probeImage(workspace, image, runtime) {
19545
- const ecosystems = [...new Set(detectManifests(workspace).map((m) => m.ecosystem))];
19656
+ const ecosystems = gatingEcosystems(detectManifests(workspace));
19546
19657
  const wanted = [...new Set(ecosystems.flatMap((e) => NEEDS[e]))];
19547
19658
  if (wanted.length === 0)
19548
19659
  return { ok: true, missing: [], ecosystems };
@@ -19554,10 +19665,13 @@ async function probeImage(workspace, image, runtime) {
19554
19665
  `).map((l) => l.trim()).filter(Boolean);
19555
19666
  return { ok: missing.length === 0, missing, ecosystems };
19556
19667
  }
19668
+ function gatingEcosystems(manifests) {
19669
+ return [...new Set(manifests.filter((m) => m.dir === "").map((m) => m.ecosystem))];
19670
+ }
19557
19671
  function probeProblem(probe, image) {
19558
19672
  if (probe.ok)
19559
19673
  return null;
19560
- return `This is a ${probe.ecosystems.join(" and ")} project, and the sandbox image ${image} has no ` + `${probe.missing.join(", ")}. Build an image that does and set DEVSTATION_SANDBOX_IMAGE, ` + "or run with --no-sandbox.";
19674
+ return `The sandbox image ${image} has no ${probe.missing.join(", ")}, which this ${probe.ecosystems.join(" and ")} ` + "project uses, so commands that need it will fail inside the sandbox. Build an image that has it " + "and set DEVSTATION_SANDBOX_IMAGE, or run with --no-sandbox.";
19561
19675
  }
19562
19676
  function quote2(value) {
19563
19677
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -19672,7 +19786,7 @@ function sandboxExecutor(options) {
19672
19786
  return "The sandbox could not write to the workspace. Check the mount and try --no-sandbox.";
19673
19787
  }
19674
19788
  try {
19675
- const stat2 = statSync5(path4);
19789
+ const stat2 = statSync6(path4);
19676
19790
  const [uid, gid] = user.split(":").map(Number);
19677
19791
  if (stat2.uid !== uid || stat2.gid !== gid) {
19678
19792
  return `The sandbox writes files as ${stat2.uid}:${stat2.gid} but this account is ${user}. ` + "Files it creates would not be yours to edit, and git would stop trusting the " + "repository, so it was not started. Run with --no-sandbox, or set " + "DEVSTATION_SANDBOX_USER.";
@@ -19940,8 +20054,20 @@ function fail(reason) {
19940
20054
  function executeFileTool(workspace, name, args) {
19941
20055
  switch (name) {
19942
20056
  case "list_files": {
19943
- const files = workspace.list(String(args.path ?? "."));
19944
- return files.length === 0 ? { ok: true, output: "The workspace is empty." } : { ok: true, output: files.join(`
20057
+ const LIMIT = 2000;
20058
+ const files = workspace.list(String(args.path ?? "."), LIMIT + 1);
20059
+ if (files.length === 0)
20060
+ return { ok: true, output: "The workspace is empty." };
20061
+ if (files.length > LIMIT) {
20062
+ return {
20063
+ ok: true,
20064
+ output: `${files.slice(0, LIMIT).join(`
20065
+ `)}
20066
+
20067
+ ` + `(Stopped at ${LIMIT} files: this directory is very large. List a subdirectory, or search for what you need.)`
20068
+ };
20069
+ }
20070
+ return { ok: true, output: files.join(`
19945
20071
  `) };
19946
20072
  }
19947
20073
  case "read_file": {
@@ -19978,7 +20104,10 @@ function executeFileTool(workspace, name, args) {
19978
20104
  return fail("No search query was given.");
19979
20105
  const hits = [];
19980
20106
  let truncated = false;
19981
- for (const path4 of workspace.list()) {
20107
+ const MAX_SCAN = 20000;
20108
+ const scanned = workspace.list(".", MAX_SCAN + 1);
20109
+ const scanLimited = scanned.length > MAX_SCAN;
20110
+ for (const path4 of scanned.slice(0, MAX_SCAN)) {
19982
20111
  const file = workspace.read(path4);
19983
20112
  if (!file.ok)
19984
20113
  continue;
@@ -19997,14 +20126,18 @@ function executeFileTool(workspace, name, args) {
19997
20126
  if (truncated)
19998
20127
  break;
19999
20128
  }
20000
- if (hits.length === 0)
20001
- return { ok: true, output: `No match for "${args.query}".` };
20129
+ const scanNote = scanLimited ? `
20130
+
20131
+ (Searched the first ${MAX_SCAN} files only: this workspace is very large. Search a subdirectory.)` : "";
20132
+ if (hits.length === 0) {
20133
+ return { ok: true, output: `No match for "${args.query}".${scanNote}` };
20134
+ }
20002
20135
  return {
20003
20136
  ok: true,
20004
- output: truncated ? `${hits.join(`
20137
+ output: `${truncated ? `${hits.join(`
20005
20138
  `)}
20006
20139
  \u2026 more matches not shown` : hits.join(`
20007
- `)
20140
+ `)}${scanNote}`
20008
20141
  };
20009
20142
  }
20010
20143
  default:
@@ -20725,10 +20858,10 @@ import {
20725
20858
  appendFileSync,
20726
20859
  existsSync as existsSync9,
20727
20860
  mkdirSync as mkdirSync6,
20728
- readFileSync as readFileSync8,
20861
+ readFileSync as readFileSync9,
20729
20862
  readdirSync as readdirSync6,
20730
20863
  renameSync as renameSync2,
20731
- statSync as statSync6,
20864
+ statSync as statSync7,
20732
20865
  writeFileSync as writeFileSync5
20733
20866
  } from "fs";
20734
20867
  import { join as join12 } from "path";
@@ -20791,7 +20924,7 @@ class SessionStore {
20791
20924
  if (!existsSync9(path4))
20792
20925
  return null;
20793
20926
  try {
20794
- return JSON.parse(readFileSync8(path4, "utf8"));
20927
+ return JSON.parse(readFileSync9(path4, "utf8"));
20795
20928
  } catch {
20796
20929
  return null;
20797
20930
  }
@@ -20813,10 +20946,10 @@ class SessionStore {
20813
20946
  const path4 = this.eventPath(id);
20814
20947
  if (!existsSync9(path4))
20815
20948
  return { events: [], offset: 0 };
20816
- const size = statSync6(path4).size;
20949
+ const size = statSync7(path4).size;
20817
20950
  if (size <= fromByte)
20818
20951
  return { events: [], offset: size };
20819
- const text = readFileSync8(path4, "utf8").slice(fromByte);
20952
+ const text = readFileSync9(path4, "utf8").slice(fromByte);
20820
20953
  const events = [];
20821
20954
  let consumed = 0;
20822
20955
  for (const line of text.split(`
@@ -20911,7 +21044,8 @@ function renderSessions(records) {
20911
21044
  }
20912
21045
 
20913
21046
  // src/lib/agent/cli/commands.ts
20914
- async function buildExecutor(root, sandbox) {
21047
+ async function buildExecutor(root, sandbox, warn = (message) => process.stderr.write(`warning: ${message}
21048
+ `)) {
20915
21049
  if (!sandbox)
20916
21050
  return { executor: hostExecutor() };
20917
21051
  const readiness = await sandboxReadiness();
@@ -20921,7 +21055,7 @@ async function buildExecutor(root, sandbox) {
20921
21055
  const probe = await probeImage(root, readiness.imageName, readiness.runtimeName);
20922
21056
  const mismatch = probeProblem(probe, readiness.imageName);
20923
21057
  if (mismatch)
20924
- return { problem: mismatch };
21058
+ warn(mismatch);
20925
21059
  return { executor: sandboxExecutor({ workspace: root }) };
20926
21060
  }
20927
21061
  function isYes(answer) {
@@ -20959,9 +21093,21 @@ async function runCommand(context, goal, options = {}) {
20959
21093
  executor = built.executor;
20960
21094
  }
20961
21095
  const embeddings = embeddingsFromEnv();
21096
+ const firstIndex = !existsSync10(storePath(context.root));
21097
+ if (firstIndex && !options.quiet)
21098
+ terminal.out("Indexing the workspace so the agent can search it\u2026");
20962
21099
  const memory = openStore(context.root);
20963
21100
  try {
20964
- await indexWorkspace(context.root, { store: memory, embeddings });
21101
+ const indexed = await indexWorkspace(context.root, { store: memory, embeddings });
21102
+ if (!options.quiet) {
21103
+ if (indexed.stopped === "home") {
21104
+ terminal.err("Not indexing your home directory. The agent can still read and list files; cd into a project for a real index.");
21105
+ } else if (indexed.stopped) {
21106
+ terminal.err(`Indexed ${indexed.scanned} files and stopped at the ${indexed.stopped === "deadline" ? "time" : "file"} limit: this workspace is very large. cd into the project you mean.`);
21107
+ } else if (firstIndex || indexed.ms > 1500) {
21108
+ terminal.out(`Indexed ${indexed.scanned} files in ${(indexed.ms / 1000).toFixed(1)}s.`);
21109
+ }
21110
+ }
20965
21111
  } catch {}
20966
21112
  const mcp = await McpHub.start(loadConfig(context.root));
20967
21113
  for (const status of mcp.status) {
@@ -21254,6 +21400,14 @@ ${r.problem}`);
21254
21400
  function home() {
21255
21401
  return process.env.HOME ?? "";
21256
21402
  }
21403
+ function homeDirectoryWarning(root, homeDir = process.env.HOME ?? "") {
21404
+ if (!homeDir)
21405
+ return null;
21406
+ const strip2 = (p) => p.replace(/\/+$/, "");
21407
+ if (strip2(root) !== strip2(homeDir))
21408
+ return null;
21409
+ return "You are in your home directory, so the agent treats everything under it as one project, " + "every repository inside it included. cd into the project you mean first.";
21410
+ }
21257
21411
  function configEditCommand(context, rest, opts = {}) {
21258
21412
  const [action, key, ...valueWords] = rest.trim().split(/\s+/);
21259
21413
  const value = valueWords.join(" ").trim();
@@ -21289,7 +21443,7 @@ function configEditCommand(context, rest, opts = {}) {
21289
21443
  context.terminal.err(`Give it a value: devstation config set ${name} <value>`);
21290
21444
  return 2;
21291
21445
  }
21292
- if (name === "provider" && !PROVIDER_IDS.includes(value)) {
21446
+ if (name === "provider" && !PROVIDER_IDS.includes(value.toLowerCase())) {
21293
21447
  context.terminal.err(`Unknown provider "${value}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21294
21448
  return 2;
21295
21449
  }
@@ -21297,7 +21451,7 @@ function configEditCommand(context, rest, opts = {}) {
21297
21451
  context.terminal.err("baseUrl must start with http:// or https://.");
21298
21452
  return 2;
21299
21453
  }
21300
- current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : value;
21454
+ current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : name === "provider" ? value.toLowerCase() : value;
21301
21455
  writeSettingsFile(path4, current);
21302
21456
  context.terminal.out(`Set ${name} = ${current[name]} in ${path4}.`);
21303
21457
  return 0;
@@ -21308,13 +21462,13 @@ function configEditCommand(context, rest, opts = {}) {
21308
21462
  async function loginCommand(context, rest) {
21309
21463
  const t = context.terminal;
21310
21464
  const secret = t.askSecret ? (q) => t.askSecret(q) : (q) => t.ask(q);
21311
- let provider = rest.trim().split(/\s+/)[0];
21465
+ let provider = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
21312
21466
  if (!provider) {
21313
21467
  t.out("Which provider?");
21314
21468
  t.out(" anthropic Claude, directly (prompt caching, native tool use)");
21315
21469
  t.out(" openrouter one key for Claude, GPT, Gemini, DeepSeek and more");
21316
21470
  t.out(" openai OpenAI, or any compatible server: Ollama, LM Studio, Groq, Together");
21317
- provider = (await t.ask("provider [anthropic]: ")).trim() || "anthropic";
21471
+ provider = (await t.ask("provider [anthropic]: ")).trim().toLowerCase() || "anthropic";
21318
21472
  }
21319
21473
  if (!PROVIDER_IDS.includes(provider)) {
21320
21474
  t.err(`Unknown provider "${provider}". Providers: ${PROVIDER_IDS.join(", ")}.`);
@@ -21372,7 +21526,7 @@ async function loginCommand(context, rest) {
21372
21526
  }
21373
21527
  function logoutCommand(context, rest) {
21374
21528
  const h = home();
21375
- const target = rest.trim().split(/\s+/)[0];
21529
+ const target = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
21376
21530
  if (target && !PROVIDER_IDS.includes(target)) {
21377
21531
  context.terminal.err(`Unknown provider "${target}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21378
21532
  return 2;
@@ -22135,6 +22289,169 @@ function lineReader(rl, write) {
22135
22289
  };
22136
22290
  }
22137
22291
 
22292
+ // src/lib/agent/cli/upgrade.ts
22293
+ import { spawnSync } from "child_process";
22294
+ import { createHash as createHash2 } from "crypto";
22295
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
22296
+ import { dirname as dirname8, join as join14 } from "path";
22297
+ var PACKAGE = "@devstationlabs/cli";
22298
+ var REPO = "linoxbt/dev-shipyard";
22299
+ var DAY_MS = 24 * 60 * 60 * 1000;
22300
+ function installMethod(execPath = process.execPath, script = process.argv[1] ?? "") {
22301
+ const slash = (p) => p.replace(/\\/g, "/");
22302
+ const marker = `node_modules/${PACKAGE}/`;
22303
+ if (slash(execPath).includes(marker) || slash(script).includes(marker))
22304
+ return "npm";
22305
+ if (/\.(ts|tsx)$/.test(script) && /(^|\/)bun(\.exe)?$/.test(slash(execPath)))
22306
+ return "source";
22307
+ return "binary";
22308
+ }
22309
+ function compareVersions(a, b) {
22310
+ const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
22311
+ const [x, y] = [parts(a), parts(b)];
22312
+ for (let i = 0;i < Math.max(x.length, y.length); i++) {
22313
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
22314
+ if (d !== 0)
22315
+ return d > 0 ? 1 : -1;
22316
+ }
22317
+ return 0;
22318
+ }
22319
+ async function latestVersion(fetchImpl = fetch, timeoutMs = 4000) {
22320
+ try {
22321
+ const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`, {
22322
+ headers: { accept: "application/json" },
22323
+ signal: AbortSignal.timeout(timeoutMs)
22324
+ });
22325
+ if (!res.ok)
22326
+ return null;
22327
+ const body = await res.json();
22328
+ return typeof body.version === "string" ? body.version : null;
22329
+ } catch {
22330
+ return null;
22331
+ }
22332
+ }
22333
+ function targetFor(platform = process.platform, arch = process.arch) {
22334
+ const map = {
22335
+ "linux-x64": "devstation-linux-x64",
22336
+ "linux-arm64": "devstation-linux-arm64",
22337
+ "darwin-arm64": "devstation-darwin-arm64",
22338
+ "darwin-x64": "devstation-darwin-x64",
22339
+ "win32-x64": "devstation-windows-x64.exe"
22340
+ };
22341
+ return map[`${platform}-${arch}`] ?? null;
22342
+ }
22343
+ function runInherit(command, args) {
22344
+ const result = spawnSync(command, args, { stdio: "inherit" });
22345
+ if (result.error)
22346
+ return 127;
22347
+ return result.status ?? 1;
22348
+ }
22349
+ async function upgradeCommand(context, opts = {}) {
22350
+ const t = context.terminal;
22351
+ const fetchImpl = opts.fetchImpl ?? fetch;
22352
+ const latest = await latestVersion(fetchImpl);
22353
+ if (!latest) {
22354
+ t.err("Could not reach the npm registry to check for a newer version. Nothing was changed.");
22355
+ return 1;
22356
+ }
22357
+ if (compareVersions(latest, VERSION) <= 0) {
22358
+ t.out(`devstation ${VERSION} is the latest version.`);
22359
+ return 0;
22360
+ }
22361
+ t.out(`devstation ${latest} is available (you have ${VERSION}).`);
22362
+ const method = installMethod(opts.execPath, opts.script);
22363
+ if (opts.check) {
22364
+ t.out(method === "source" ? "This is a source checkout: update it with git pull." : "Upgrade with: devstation upgrade");
22365
+ return 0;
22366
+ }
22367
+ switch (method) {
22368
+ case "npm": {
22369
+ t.out(`Installed through npm, so upgrading through npm: npm install -g ${PACKAGE}@${latest}`);
22370
+ const code = (opts.run ?? runInherit)("npm", ["install", "-g", `${PACKAGE}@${latest}`]);
22371
+ if (code !== 0) {
22372
+ t.err(`npm exited with ${code}. Nothing else was changed. Run it yourself: npm install -g ${PACKAGE}@latest`);
22373
+ return code === 0 ? 1 : code;
22374
+ }
22375
+ t.out(`Upgraded to ${latest}. If your shell still runs the old one, run: hash -r`);
22376
+ return 0;
22377
+ }
22378
+ case "source":
22379
+ t.out("This is running from a source checkout. Update it with git pull, not upgrade.");
22380
+ return 0;
22381
+ case "binary":
22382
+ return replaceBinary(t, latest, opts.execPath ?? process.execPath, fetchImpl);
22383
+ }
22384
+ }
22385
+ async function replaceBinary(t, latest, execPath, fetchImpl) {
22386
+ const target = targetFor();
22387
+ if (!target) {
22388
+ t.err(`There is no prebuilt binary for ${process.platform}-${process.arch}.`);
22389
+ return 1;
22390
+ }
22391
+ if (process.platform === "win32") {
22392
+ t.err(`A running .exe cannot replace itself on Windows. Download ${target} from https://github.com/${REPO}/releases/latest and swap it in.`);
22393
+ return 1;
22394
+ }
22395
+ const base = `https://github.com/${REPO}/releases/download/v${latest}`;
22396
+ t.out(`Downloading ${target} ${latest}\u2026`);
22397
+ try {
22398
+ const sums = await fetchImpl(`${base}/SHA256SUMS`, { redirect: "follow" });
22399
+ if (!sums.ok)
22400
+ throw new Error(`${sums.status} fetching the checksums`);
22401
+ const expected = (await sums.text()).split(`
22402
+ `).map((line) => line.trim().split(/\s+/)).find(([, name]) => name === target)?.[0];
22403
+ if (!expected)
22404
+ throw new Error(`no checksum is published for ${target}`);
22405
+ const bin = await fetchImpl(`${base}/${target}`, { redirect: "follow" });
22406
+ if (!bin.ok)
22407
+ throw new Error(`${bin.status} downloading ${target}`);
22408
+ const body = Buffer.from(await bin.arrayBuffer());
22409
+ const actual = createHash2("sha256").update(body).digest("hex");
22410
+ if (actual !== expected) {
22411
+ throw new Error(`the download does not match its checksum (expected ${expected}, got ${actual})`);
22412
+ }
22413
+ const partial = `${execPath}.partial`;
22414
+ writeFileSync7(partial, body);
22415
+ chmodSync2(partial, 493);
22416
+ renameSync3(partial, execPath);
22417
+ } catch (error2) {
22418
+ const message2 = error2 instanceof Error ? error2.message : String(error2);
22419
+ const permission = /EACCES|EPERM/.test(message2) ? " Run it with sudo, since the binary is in a system directory." : "";
22420
+ t.err(`Upgrade failed and the installed version was left untouched: ${message2}.${permission}`);
22421
+ return 1;
22422
+ }
22423
+ t.out(`Upgraded ${execPath} to ${latest}.`);
22424
+ return 0;
22425
+ }
22426
+ function notifyIfOutdated(terminal, opts = {}) {
22427
+ const env2 = opts.env ?? process.env;
22428
+ if (env2.DEVSTATION_NO_UPDATE_CHECK === "1" || env2.CI)
22429
+ return Promise.resolve();
22430
+ const homeDir = opts.home ?? env2.HOME ?? "";
22431
+ if (!homeDir)
22432
+ return Promise.resolve();
22433
+ const path4 = join14(homeDir, ".devstation", "update-check.json");
22434
+ let cached = {};
22435
+ try {
22436
+ cached = JSON.parse(readFileSync10(path4, "utf8"));
22437
+ } catch {}
22438
+ if (cached.latest && compareVersions(cached.latest, VERSION) > 0) {
22439
+ terminal.err(`devstation ${cached.latest} is available (you have ${VERSION}). Run: devstation upgrade`);
22440
+ }
22441
+ const now = opts.now ?? Date.now();
22442
+ if (cached.checkedAt && now - cached.checkedAt < DAY_MS)
22443
+ return Promise.resolve();
22444
+ return latestVersion(opts.fetchImpl ?? fetch, 2500).then((latest) => {
22445
+ if (!latest)
22446
+ return;
22447
+ try {
22448
+ mkdirSync8(dirname8(path4), { recursive: true, mode: 448 });
22449
+ writeFileSync7(path4, `${JSON.stringify({ checkedAt: now, latest })}
22450
+ `);
22451
+ } catch {}
22452
+ });
22453
+ }
22454
+
22138
22455
  // src/lib/agent/cli/index.ts
22139
22456
  async function main(argv) {
22140
22457
  const parsed = parseArgs(argv);
@@ -22222,6 +22539,8 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
22222
22539
  return await loginCommand(offline, parsed.rest);
22223
22540
  case "logout":
22224
22541
  return logoutCommand(offline, parsed.rest);
22542
+ case "upgrade":
22543
+ return await upgradeCommand(offline, { check: parsed.check });
22225
22544
  case "doctor":
22226
22545
  return await doctorCommand(offline);
22227
22546
  case "index":
@@ -22242,6 +22561,10 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
22242
22561
  terminal.err(`Run \`${CLI_NAME} doctor\` to see what else is missing.`);
22243
22562
  return 2;
22244
22563
  }
22564
+ const atHome = homeDirectoryWarning(root);
22565
+ if (atHome)
22566
+ terminal.err(`warning: ${atHome}`);
22567
+ notifyIfOutdated(terminal);
22245
22568
  const ctx = context(root, terminal, parsed, provider);
22246
22569
  ctx.signal = controller.signal;
22247
22570
  if (parsed.command === "repo") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstationlabs/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The DevStation coding agent, in your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",