@devstationlabs/cli 0.1.2 → 0.1.3

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 +205 -14
  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.3";
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
 
@@ -18154,8 +18162,9 @@ function readSettingsFile(path4, problems = []) {
18154
18162
  const raw = JSON.parse(readFileSync3(path4, "utf8"));
18155
18163
  const out = {};
18156
18164
  if (raw.provider !== undefined) {
18157
- if (isProvider(raw.provider))
18158
- out.provider = raw.provider;
18165
+ const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
18166
+ if (isProvider(named))
18167
+ out.provider = named;
18159
18168
  else
18160
18169
  problems.push(`${path4}: unknown provider "${String(raw.provider)}".`);
18161
18170
  }
@@ -18228,8 +18237,9 @@ function resolveSettings(opts) {
18228
18237
  let provider = null;
18229
18238
  let providerSource = "not set";
18230
18239
  if (env2.DEVSTATION_PROVIDER) {
18231
- if (isProvider(env2.DEVSTATION_PROVIDER)) {
18232
- provider = env2.DEVSTATION_PROVIDER;
18240
+ const named = env2.DEVSTATION_PROVIDER.toLowerCase();
18241
+ if (isProvider(named)) {
18242
+ provider = named;
18233
18243
  providerSource = "DEVSTATION_PROVIDER";
18234
18244
  } else {
18235
18245
  warnings.push(`DEVSTATION_PROVIDER="${env2.DEVSTATION_PROVIDER}" is not a provider, so it was ignored.`);
@@ -19542,7 +19552,7 @@ var NEEDS = {
19542
19552
  go: ["go"]
19543
19553
  };
19544
19554
  async function probeImage(workspace, image, runtime) {
19545
- const ecosystems = [...new Set(detectManifests(workspace).map((m) => m.ecosystem))];
19555
+ const ecosystems = gatingEcosystems(detectManifests(workspace));
19546
19556
  const wanted = [...new Set(ecosystems.flatMap((e) => NEEDS[e]))];
19547
19557
  if (wanted.length === 0)
19548
19558
  return { ok: true, missing: [], ecosystems };
@@ -19554,10 +19564,13 @@ async function probeImage(workspace, image, runtime) {
19554
19564
  `).map((l) => l.trim()).filter(Boolean);
19555
19565
  return { ok: missing.length === 0, missing, ecosystems };
19556
19566
  }
19567
+ function gatingEcosystems(manifests) {
19568
+ return [...new Set(manifests.filter((m) => m.dir === "").map((m) => m.ecosystem))];
19569
+ }
19557
19570
  function probeProblem(probe, image) {
19558
19571
  if (probe.ok)
19559
19572
  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.";
19573
+ 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
19574
  }
19562
19575
  function quote2(value) {
19563
19576
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -20911,7 +20924,8 @@ function renderSessions(records) {
20911
20924
  }
20912
20925
 
20913
20926
  // src/lib/agent/cli/commands.ts
20914
- async function buildExecutor(root, sandbox) {
20927
+ async function buildExecutor(root, sandbox, warn = (message) => process.stderr.write(`warning: ${message}
20928
+ `)) {
20915
20929
  if (!sandbox)
20916
20930
  return { executor: hostExecutor() };
20917
20931
  const readiness = await sandboxReadiness();
@@ -20921,7 +20935,7 @@ async function buildExecutor(root, sandbox) {
20921
20935
  const probe = await probeImage(root, readiness.imageName, readiness.runtimeName);
20922
20936
  const mismatch = probeProblem(probe, readiness.imageName);
20923
20937
  if (mismatch)
20924
- return { problem: mismatch };
20938
+ warn(mismatch);
20925
20939
  return { executor: sandboxExecutor({ workspace: root }) };
20926
20940
  }
20927
20941
  function isYes(answer) {
@@ -21254,6 +21268,14 @@ ${r.problem}`);
21254
21268
  function home() {
21255
21269
  return process.env.HOME ?? "";
21256
21270
  }
21271
+ function homeDirectoryWarning(root, homeDir = process.env.HOME ?? "") {
21272
+ if (!homeDir)
21273
+ return null;
21274
+ const strip2 = (p) => p.replace(/\/+$/, "");
21275
+ if (strip2(root) !== strip2(homeDir))
21276
+ return null;
21277
+ 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.";
21278
+ }
21257
21279
  function configEditCommand(context, rest, opts = {}) {
21258
21280
  const [action, key, ...valueWords] = rest.trim().split(/\s+/);
21259
21281
  const value = valueWords.join(" ").trim();
@@ -21289,7 +21311,7 @@ function configEditCommand(context, rest, opts = {}) {
21289
21311
  context.terminal.err(`Give it a value: devstation config set ${name} <value>`);
21290
21312
  return 2;
21291
21313
  }
21292
- if (name === "provider" && !PROVIDER_IDS.includes(value)) {
21314
+ if (name === "provider" && !PROVIDER_IDS.includes(value.toLowerCase())) {
21293
21315
  context.terminal.err(`Unknown provider "${value}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21294
21316
  return 2;
21295
21317
  }
@@ -21297,7 +21319,7 @@ function configEditCommand(context, rest, opts = {}) {
21297
21319
  context.terminal.err("baseUrl must start with http:// or https://.");
21298
21320
  return 2;
21299
21321
  }
21300
- current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : value;
21322
+ current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : name === "provider" ? value.toLowerCase() : value;
21301
21323
  writeSettingsFile(path4, current);
21302
21324
  context.terminal.out(`Set ${name} = ${current[name]} in ${path4}.`);
21303
21325
  return 0;
@@ -21308,13 +21330,13 @@ function configEditCommand(context, rest, opts = {}) {
21308
21330
  async function loginCommand(context, rest) {
21309
21331
  const t = context.terminal;
21310
21332
  const secret = t.askSecret ? (q) => t.askSecret(q) : (q) => t.ask(q);
21311
- let provider = rest.trim().split(/\s+/)[0];
21333
+ let provider = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
21312
21334
  if (!provider) {
21313
21335
  t.out("Which provider?");
21314
21336
  t.out(" anthropic Claude, directly (prompt caching, native tool use)");
21315
21337
  t.out(" openrouter one key for Claude, GPT, Gemini, DeepSeek and more");
21316
21338
  t.out(" openai OpenAI, or any compatible server: Ollama, LM Studio, Groq, Together");
21317
- provider = (await t.ask("provider [anthropic]: ")).trim() || "anthropic";
21339
+ provider = (await t.ask("provider [anthropic]: ")).trim().toLowerCase() || "anthropic";
21318
21340
  }
21319
21341
  if (!PROVIDER_IDS.includes(provider)) {
21320
21342
  t.err(`Unknown provider "${provider}". Providers: ${PROVIDER_IDS.join(", ")}.`);
@@ -21372,7 +21394,7 @@ async function loginCommand(context, rest) {
21372
21394
  }
21373
21395
  function logoutCommand(context, rest) {
21374
21396
  const h = home();
21375
- const target = rest.trim().split(/\s+/)[0];
21397
+ const target = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
21376
21398
  if (target && !PROVIDER_IDS.includes(target)) {
21377
21399
  context.terminal.err(`Unknown provider "${target}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21378
21400
  return 2;
@@ -22135,6 +22157,169 @@ function lineReader(rl, write) {
22135
22157
  };
22136
22158
  }
22137
22159
 
22160
+ // src/lib/agent/cli/upgrade.ts
22161
+ import { spawnSync } from "child_process";
22162
+ import { createHash as createHash2 } from "crypto";
22163
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
22164
+ import { dirname as dirname8, join as join14 } from "path";
22165
+ var PACKAGE = "@devstationlabs/cli";
22166
+ var REPO = "linoxbt/dev-shipyard";
22167
+ var DAY_MS = 24 * 60 * 60 * 1000;
22168
+ function installMethod(execPath = process.execPath, script = process.argv[1] ?? "") {
22169
+ const slash = (p) => p.replace(/\\/g, "/");
22170
+ const marker = `node_modules/${PACKAGE}/`;
22171
+ if (slash(execPath).includes(marker) || slash(script).includes(marker))
22172
+ return "npm";
22173
+ if (/\.(ts|tsx)$/.test(script) && /(^|\/)bun(\.exe)?$/.test(slash(execPath)))
22174
+ return "source";
22175
+ return "binary";
22176
+ }
22177
+ function compareVersions(a, b) {
22178
+ const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
22179
+ const [x, y] = [parts(a), parts(b)];
22180
+ for (let i = 0;i < Math.max(x.length, y.length); i++) {
22181
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
22182
+ if (d !== 0)
22183
+ return d > 0 ? 1 : -1;
22184
+ }
22185
+ return 0;
22186
+ }
22187
+ async function latestVersion(fetchImpl = fetch, timeoutMs = 4000) {
22188
+ try {
22189
+ const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`, {
22190
+ headers: { accept: "application/json" },
22191
+ signal: AbortSignal.timeout(timeoutMs)
22192
+ });
22193
+ if (!res.ok)
22194
+ return null;
22195
+ const body = await res.json();
22196
+ return typeof body.version === "string" ? body.version : null;
22197
+ } catch {
22198
+ return null;
22199
+ }
22200
+ }
22201
+ function targetFor(platform = process.platform, arch = process.arch) {
22202
+ const map = {
22203
+ "linux-x64": "devstation-linux-x64",
22204
+ "linux-arm64": "devstation-linux-arm64",
22205
+ "darwin-arm64": "devstation-darwin-arm64",
22206
+ "darwin-x64": "devstation-darwin-x64",
22207
+ "win32-x64": "devstation-windows-x64.exe"
22208
+ };
22209
+ return map[`${platform}-${arch}`] ?? null;
22210
+ }
22211
+ function runInherit(command, args) {
22212
+ const result = spawnSync(command, args, { stdio: "inherit" });
22213
+ if (result.error)
22214
+ return 127;
22215
+ return result.status ?? 1;
22216
+ }
22217
+ async function upgradeCommand(context, opts = {}) {
22218
+ const t = context.terminal;
22219
+ const fetchImpl = opts.fetchImpl ?? fetch;
22220
+ const latest = await latestVersion(fetchImpl);
22221
+ if (!latest) {
22222
+ t.err("Could not reach the npm registry to check for a newer version. Nothing was changed.");
22223
+ return 1;
22224
+ }
22225
+ if (compareVersions(latest, VERSION) <= 0) {
22226
+ t.out(`devstation ${VERSION} is the latest version.`);
22227
+ return 0;
22228
+ }
22229
+ t.out(`devstation ${latest} is available (you have ${VERSION}).`);
22230
+ const method = installMethod(opts.execPath, opts.script);
22231
+ if (opts.check) {
22232
+ t.out(method === "source" ? "This is a source checkout: update it with git pull." : "Upgrade with: devstation upgrade");
22233
+ return 0;
22234
+ }
22235
+ switch (method) {
22236
+ case "npm": {
22237
+ t.out(`Installed through npm, so upgrading through npm: npm install -g ${PACKAGE}@${latest}`);
22238
+ const code = (opts.run ?? runInherit)("npm", ["install", "-g", `${PACKAGE}@${latest}`]);
22239
+ if (code !== 0) {
22240
+ t.err(`npm exited with ${code}. Nothing else was changed. Run it yourself: npm install -g ${PACKAGE}@latest`);
22241
+ return code === 0 ? 1 : code;
22242
+ }
22243
+ t.out(`Upgraded to ${latest}. If your shell still runs the old one, run: hash -r`);
22244
+ return 0;
22245
+ }
22246
+ case "source":
22247
+ t.out("This is running from a source checkout. Update it with git pull, not upgrade.");
22248
+ return 0;
22249
+ case "binary":
22250
+ return replaceBinary(t, latest, opts.execPath ?? process.execPath, fetchImpl);
22251
+ }
22252
+ }
22253
+ async function replaceBinary(t, latest, execPath, fetchImpl) {
22254
+ const target = targetFor();
22255
+ if (!target) {
22256
+ t.err(`There is no prebuilt binary for ${process.platform}-${process.arch}.`);
22257
+ return 1;
22258
+ }
22259
+ if (process.platform === "win32") {
22260
+ t.err(`A running .exe cannot replace itself on Windows. Download ${target} from https://github.com/${REPO}/releases/latest and swap it in.`);
22261
+ return 1;
22262
+ }
22263
+ const base = `https://github.com/${REPO}/releases/download/v${latest}`;
22264
+ t.out(`Downloading ${target} ${latest}\u2026`);
22265
+ try {
22266
+ const sums = await fetchImpl(`${base}/SHA256SUMS`, { redirect: "follow" });
22267
+ if (!sums.ok)
22268
+ throw new Error(`${sums.status} fetching the checksums`);
22269
+ const expected = (await sums.text()).split(`
22270
+ `).map((line) => line.trim().split(/\s+/)).find(([, name]) => name === target)?.[0];
22271
+ if (!expected)
22272
+ throw new Error(`no checksum is published for ${target}`);
22273
+ const bin = await fetchImpl(`${base}/${target}`, { redirect: "follow" });
22274
+ if (!bin.ok)
22275
+ throw new Error(`${bin.status} downloading ${target}`);
22276
+ const body = Buffer.from(await bin.arrayBuffer());
22277
+ const actual = createHash2("sha256").update(body).digest("hex");
22278
+ if (actual !== expected) {
22279
+ throw new Error(`the download does not match its checksum (expected ${expected}, got ${actual})`);
22280
+ }
22281
+ const partial = `${execPath}.partial`;
22282
+ writeFileSync7(partial, body);
22283
+ chmodSync2(partial, 493);
22284
+ renameSync3(partial, execPath);
22285
+ } catch (error2) {
22286
+ const message2 = error2 instanceof Error ? error2.message : String(error2);
22287
+ const permission = /EACCES|EPERM/.test(message2) ? " Run it with sudo, since the binary is in a system directory." : "";
22288
+ t.err(`Upgrade failed and the installed version was left untouched: ${message2}.${permission}`);
22289
+ return 1;
22290
+ }
22291
+ t.out(`Upgraded ${execPath} to ${latest}.`);
22292
+ return 0;
22293
+ }
22294
+ function notifyIfOutdated(terminal, opts = {}) {
22295
+ const env2 = opts.env ?? process.env;
22296
+ if (env2.DEVSTATION_NO_UPDATE_CHECK === "1" || env2.CI)
22297
+ return Promise.resolve();
22298
+ const homeDir = opts.home ?? env2.HOME ?? "";
22299
+ if (!homeDir)
22300
+ return Promise.resolve();
22301
+ const path4 = join14(homeDir, ".devstation", "update-check.json");
22302
+ let cached = {};
22303
+ try {
22304
+ cached = JSON.parse(readFileSync9(path4, "utf8"));
22305
+ } catch {}
22306
+ if (cached.latest && compareVersions(cached.latest, VERSION) > 0) {
22307
+ terminal.err(`devstation ${cached.latest} is available (you have ${VERSION}). Run: devstation upgrade`);
22308
+ }
22309
+ const now = opts.now ?? Date.now();
22310
+ if (cached.checkedAt && now - cached.checkedAt < DAY_MS)
22311
+ return Promise.resolve();
22312
+ return latestVersion(opts.fetchImpl ?? fetch, 2500).then((latest) => {
22313
+ if (!latest)
22314
+ return;
22315
+ try {
22316
+ mkdirSync8(dirname8(path4), { recursive: true, mode: 448 });
22317
+ writeFileSync7(path4, `${JSON.stringify({ checkedAt: now, latest })}
22318
+ `);
22319
+ } catch {}
22320
+ });
22321
+ }
22322
+
22138
22323
  // src/lib/agent/cli/index.ts
22139
22324
  async function main(argv) {
22140
22325
  const parsed = parseArgs(argv);
@@ -22222,6 +22407,8 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
22222
22407
  return await loginCommand(offline, parsed.rest);
22223
22408
  case "logout":
22224
22409
  return logoutCommand(offline, parsed.rest);
22410
+ case "upgrade":
22411
+ return await upgradeCommand(offline, { check: parsed.check });
22225
22412
  case "doctor":
22226
22413
  return await doctorCommand(offline);
22227
22414
  case "index":
@@ -22242,6 +22429,10 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
22242
22429
  terminal.err(`Run \`${CLI_NAME} doctor\` to see what else is missing.`);
22243
22430
  return 2;
22244
22431
  }
22432
+ const atHome = homeDirectoryWarning(root);
22433
+ if (atHome)
22434
+ terminal.err(`warning: ${atHome}`);
22435
+ notifyIfOutdated(terminal);
22245
22436
  const ctx = context(root, terminal, parsed, provider);
22246
22437
  ctx.signal = controller.signal;
22247
22438
  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.3",
4
4
  "description": "The DevStation coding agent, in your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",