@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 +1 -1
- package/bin/app-sdk.mjs +128 -0
- package/bin/appdev.mjs +8 -2
- package/bin/apps.mjs +8 -6
- package/bin/terminus.js +1 -1
- package/package.json +1 -1
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.
|
|
770
|
+
Terminus v0.0.2, created by Terminus Intelligence
|
|
771
771
|
|
|
772
772
|
Account:
|
|
773
773
|
terminus login Open your browser and sign in
|
package/bin/app-sdk.mjs
ADDED
|
@@ -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
|
|
169
|
-
|
|
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.
|
|
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",
|