@terminus-ai/cli 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -767,7 +767,7 @@ Bare `terminus` (or `terminus help`) prints its version and the everyday command
767
767
  with what it does on one line (`<word>` is a placeholder, `[ ]` is optional):
768
768
 
769
769
  ```text
770
- Terminus v0.0.1, created by Terminus Intelligence
770
+ Terminus v0.0.2, created by Terminus Intelligence
771
771
 
772
772
  Account:
773
773
  terminus login Open your browser and sign in
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The app SDK as the CLI sees it: the package apps depend on, the range
3
+ * `terminus init` writes, and whether an app's installed copy has fallen
4
+ * behind the newest one on npm.
5
+ *
6
+ * During the beta every app should run the newest SDK, but npm keeps whatever
7
+ * an app's lockfile holds until someone updates it — a cloned app, or one set
8
+ * up from the docs, can sit on an old SDK indefinitely. So `terminus push`
9
+ * and `terminus dev` ask `appSdkUpdate` and say so, with the command that
10
+ * fixes it.
11
+ *
12
+ * The check never fails or holds up a command: it asks the npm registry at
13
+ * most once a day (the answer is kept in ~/.terminus/app-sdk-latest.json),
14
+ * gives up after 1.5 s, and answers null — say nothing — offline, in CI, for
15
+ * a folder that does not use the SDK, or when anything is unexpected.
16
+ */
17
+
18
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+
22
+ import { compareReleaseVersions, parseReleaseVersion } from "./versioning.mjs";
23
+
24
+ export const APP_SDK_PACKAGE = "@terminus-ai/app-sdk";
25
+
26
+ // "latest" during the beta: a new app starts on the newest SDK and
27
+ // `npm update` keeps moving it there, breaking releases included. When the
28
+ // beta ends, scaffolds go back to a normal range.
29
+ export const APP_SDK_VERSION = "latest";
30
+
31
+ const DAY_MS = 24 * 60 * 60 * 1000;
32
+ const REGISTRY_TIMEOUT_MS = 1500;
33
+
34
+ async function readJson(file) {
35
+ try {
36
+ return JSON.parse(await readFile(file, "utf8"));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** What the app's package.json asks for, or null when it does not use the SDK. */
43
+ async function declaredRange(dir) {
44
+ const manifest = await readJson(path.join(dir, "package.json"));
45
+ const range = manifest?.dependencies?.[APP_SDK_PACKAGE] ?? manifest?.devDependencies?.[APP_SDK_PACKAGE];
46
+ return typeof range === "string" ? range : null;
47
+ }
48
+
49
+ /** The installed SDK's version, found the way Node resolves packages — up
50
+ * the node_modules chain — without going through the package's `exports`,
51
+ * which older builds did not open to package.json. */
52
+ async function installedVersion(dir) {
53
+ for (let current = path.resolve(dir); ; current = path.dirname(current)) {
54
+ const manifest = await readJson(path.join(current, "node_modules", APP_SDK_PACKAGE, "package.json"));
55
+ if (typeof manifest?.version === "string") return manifest.version;
56
+ if (path.dirname(current) === current) return null;
57
+ }
58
+ }
59
+
60
+ function registryBase() {
61
+ const base = process.env.TERMINUS_NPM_REGISTRY || process.env.npm_config_registry || "https://registry.npmjs.org";
62
+ return base.replace(/\/+$/, "");
63
+ }
64
+
65
+ /** The newest published SDK version: from today's cache, else the registry. */
66
+ async function newestVersion() {
67
+ const cacheFile = path.join(os.homedir(), ".terminus", "app-sdk-latest.json");
68
+ const cached = await readJson(cacheFile);
69
+ const age = Date.now() - Date.parse(cached?.checked_at);
70
+ if (typeof cached?.version === "string" && age >= 0 && age < DAY_MS) return cached.version;
71
+ let version;
72
+ try {
73
+ const response = await fetch(`${registryBase()}/${APP_SDK_PACKAGE.replace("/", "%2f")}/latest`, {
74
+ headers: { accept: "application/json" },
75
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
76
+ });
77
+ if (!response.ok) return null;
78
+ ({ version } = await response.json());
79
+ } catch {
80
+ return null;
81
+ }
82
+ if (typeof version !== "string") return null;
83
+ try {
84
+ await mkdir(path.dirname(cacheFile), { recursive: true });
85
+ await writeFile(cacheFile, `${JSON.stringify({ version, checked_at: new Date().toISOString() })}\n`);
86
+ } catch {
87
+ // Unwritable home: ask again next time.
88
+ }
89
+ return version;
90
+ }
91
+
92
+ function inCi() {
93
+ const ci = process.env.CI;
94
+ return Boolean(ci) && ci !== "false" && ci !== "0";
95
+ }
96
+
97
+ /**
98
+ * Whether the app in `dir` runs an older SDK than the newest on npm:
99
+ * `{ package, installed, newest, command }`, or null. Never throws.
100
+ *
101
+ * The command follows the app's range: under "latest" (what `terminus init`
102
+ * writes) `npm update` reaches the newest; any other range may not let it, so
103
+ * the answer is `npm install …@latest`, which works from anywhere.
104
+ */
105
+ export async function appSdkUpdate(dir) {
106
+ try {
107
+ if (inCi()) return null;
108
+ const range = await declaredRange(dir);
109
+ if (range === null) return null;
110
+ const installed = parseReleaseVersion(await installedVersion(dir));
111
+ if (!installed) return null;
112
+ const newest = parseReleaseVersion(await newestVersion());
113
+ if (!newest || compareReleaseVersions(installed, newest) >= 0) return null;
114
+ return {
115
+ package: APP_SDK_PACKAGE,
116
+ installed: installed.canonical,
117
+ newest: newest.canonical,
118
+ command: range === "latest" ? `npm update ${APP_SDK_PACKAGE}` : `npm install ${APP_SDK_PACKAGE}@latest`,
119
+ };
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /** The line `terminus push` and `terminus dev` print for an update. */
126
+ export function appSdkUpdateLine(update) {
127
+ return `${update.package} ${update.newest} is out; this app has ${update.installed}. Update it: ${update.command}`;
128
+ }
package/bin/appdev.mjs CHANGED
@@ -46,6 +46,7 @@ import { injectAppRouteScript } from "./app-route-script.mjs";
46
46
  import { CliError, parseFlags, usageError } from "./client.mjs";
47
47
  import { Api, connect } from "./http.mjs";
48
48
  import { packageKind, readAppPackage, resolveCreation } from "./apps.mjs";
49
+ import { appSdkUpdate, appSdkUpdateLine } from "./app-sdk.mjs";
49
50
  import { resolveTemplate } from "./devtriggers.mjs";
50
51
  import { DEV_DIRECTORY, ensureDevDirectory, sha256 } from "./files.mjs";
51
52
  import { nextCronAfter } from "./schedules.mjs";
@@ -4355,9 +4356,12 @@ export async function devCommand(args) {
4355
4356
  const { serviceDevCommand } = await import("./servicedev.mjs");
4356
4357
  return serviceDevCommand(dir, flags, args);
4357
4358
  }
4359
+ // A stale SDK is told after the banner, whenever the answer comes; the
4360
+ // harness never waits for it.
4361
+ const sdkUpdate = appSdkUpdate(dir);
4358
4362
  const pkg = await readDevPackage(dir);
4359
4363
  requireBuiltDevBundle(pkg);
4360
- if (flags.remote) return remoteDevCommand(dir, flags, pkg, args);
4364
+ if (flags.remote) return remoteDevCommand(dir, flags, pkg, args, sdkUpdate);
4361
4365
  let iconRemote = null;
4362
4366
  let platform = null;
4363
4367
  try {
@@ -4410,12 +4414,13 @@ export async function devCommand(args) {
4410
4414
  if (harness.guestPort !== null) {
4411
4415
  console.log("The guest is someone who is not signed in; the app's sign-in (session.signIn()) picks a member there.");
4412
4416
  }
4417
+ sdkUpdate.then((update) => update && console.log(appSdkUpdateLine(update)));
4413
4418
  await new Promise(() => {}); // runs until Ctrl-C
4414
4419
  }
4415
4420
 
4416
4421
  /** `dev --remote`: the local bundle over the production runtime. Opt-in —
4417
4422
  * the SQLite harness stays the default. */
4418
- async function remoteDevCommand(dir, flags, pkg, commandArgs) {
4423
+ async function remoteDevCommand(dir, flags, pkg, commandArgs, sdkUpdate) {
4419
4424
  for (const flag of ["members", "profiles", "guest", "fresh"]) {
4420
4425
  if (flags[flag] !== undefined) throw usageError(`--${flag} applies to the local harness, not --remote`);
4421
4426
  }
@@ -4442,5 +4447,6 @@ async function remoteDevCommand(dir, flags, pkg, commandArgs) {
4442
4447
  console.log(`Runtime: ${api.base}/app-runtime/* as the app session of ${minted.webHost}`);
4443
4448
  if (minted.expiresAt) console.log(`App session expires: ${minted.expiresAt}`);
4444
4449
  console.log(` http://localhost:${harness.port}/`);
4450
+ sdkUpdate.then((update) => update && console.log(appSdkUpdateLine(update)));
4445
4451
  await new Promise(() => {}); // runs until Ctrl-C
4446
4452
  }
package/bin/apps.mjs CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  } from "./client.mjs";
78
78
  import { CliError, usageError } from "./errors.mjs";
79
79
  import { connect, send } from "./http.mjs";
80
+ import { APP_SDK_PACKAGE, APP_SDK_VERSION, appSdkUpdate, appSdkUpdateLine } from "./app-sdk.mjs";
80
81
  import {
81
82
  exists,
82
83
  globToRegExp,
@@ -165,12 +166,8 @@ function isSecretBasename(name) {
165
166
  return !/^\.env\.(example|sample|template)$/.test(name);
166
167
  }
167
168
 
168
- // The npm package scaffolds depend on. `~` takes every later 0.0.x (`^` on a
169
- // 0.0.x version takes that one version only), so SDK fixes reach new apps and
170
- // `npm update` without a CLI release; a breaking SDK release is 0.1.0, and
171
- // moves this range to `^0.1.0`. 0.0.2 is the SDK's first public version.
172
- export const APP_SDK_PACKAGE = "@terminus-ai/app-sdk";
173
- export const APP_SDK_VERSION = "~0.0.2";
169
+ // The npm package scaffolds depend on, and the range they write (see app-sdk.mjs).
170
+ export { APP_SDK_PACKAGE, APP_SDK_VERSION };
174
171
  const AUTOMATION_ACTIONS = new Set([
175
172
  "collection.put",
176
173
  "collection.delete",
@@ -4261,6 +4258,8 @@ export async function secretsCommand(args) {
4261
4258
  export async function pushAppCommand(args) {
4262
4259
  const flags = parseFlags(args, "push");
4263
4260
  const dir = path.resolve(flags._[0] ?? ".");
4261
+ // Asked now, answered by the end: a stale SDK is worth a line, never a wait.
4262
+ const sdkUpdate = appSdkUpdate(dir);
4264
4263
  const record = await readSyncRecord(dir);
4265
4264
  if (record?.source === "release") throw new CliError(readOnlyPushMessage(record.address));
4266
4265
  // The commit door caps a message at 500 characters and refuses a longer
@@ -4301,6 +4300,8 @@ export async function pushAppCommand(args) {
4301
4300
  version_problem: spentVersionWarning(manifest.version, pushed.releaseLabel),
4302
4301
  publish_url: creationPageUrl(flags, pushed.appId),
4303
4302
  };
4303
+ const sdk = await sdkUpdate;
4304
+ if (sdk) result.sdk_update = sdk;
4304
4305
  emitJson(flags, result, () => {
4305
4306
  if (!result.changed) {
4306
4307
  console.log(`Everything up to date: ${result.address} already matches this folder.`);
@@ -4319,6 +4320,7 @@ export async function pushAppCommand(args) {
4319
4320
  console.log("This agent runs code: `terminus dev --remote` tries it on Terminus's own engine first.");
4320
4321
  }
4321
4322
  console.log(`Publish it on the web when it is ready: ${result.publish_url}`);
4323
+ if (result.sdk_update) console.log(appSdkUpdateLine(result.sdk_update));
4322
4324
  });
4323
4325
  }
4324
4326
 
package/bin/terminus.js CHANGED
@@ -199,7 +199,7 @@ async function versionCommand(args) {
199
199
  const { APP_RUNTIME_API_VERSION } = await import("./app-runtime-contract.mjs");
200
200
  emitJson(flags, {
201
201
  version,
202
- app_sdk: { package: APP_SDK_PACKAGE, version: APP_SDK_VERSION, install: `npm install ${APP_SDK_PACKAGE}` },
202
+ app_sdk: { package: APP_SDK_PACKAGE, version: APP_SDK_VERSION, install: `npm install ${APP_SDK_PACKAGE}@latest` },
203
203
  runtime_api_version: APP_RUNTIME_API_VERSION,
204
204
  });
205
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminus-ai/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Terminus CLI (`terminus`): search and use skills, and develop, run, and publish apps, services, and agents on the Terminus platform.",
5
5
  "license": "MIT",
6
6
  "type": "module",