@terminus-ai/cli 0.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. package/package.json +41 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Validate-on-write rules — the `terminus dev` port of the backend evaluator
3
+ * (terminus-backend src/routes/app_write_rules.rs, capability doors C2), so
4
+ * an app meets its 422s locally instead of first in production. Semantics
5
+ * are pinned by the shared conformance vector
6
+ * test/contracts/app-runtime-v1/write-rules.json: same operators, same
7
+ * publish-time caps, same anchored full-match `matches`, same shared step
8
+ * budget, and the same `validate[{index}]: {message}` 422 envelope with
9
+ * error code "unprocessable". Resets and platform migrations bypass rules
10
+ * by design — they restore invariants, not break them.
11
+ */
12
+
13
+ import { CliError } from "./client.mjs";
14
+
15
+ export const MAX_WRITE_RULES_PER_COLLECTION = 32;
16
+ export const MAX_WRITE_RULE_NODES = 64;
17
+ export const MAX_WRITE_RULE_DEPTH = 4;
18
+ export const MAX_WRITE_RULE_PATTERN_BYTES = 128;
19
+ export const MAX_WRITE_RULE_MESSAGE_BYTES = 200;
20
+ export const MAX_WRITE_RULE_PATH_DEPTH = 4;
21
+ /** Shared per-write evaluation budget across all of a collection's rules. */
22
+ export const MAX_WRITE_RULE_EVAL_STEPS = 4096;
23
+
24
+ export const WRITE_RULE_OPERATORS = [
25
+ "eq", "neq", "lt", "lte", "gt", "gte", "in",
26
+ "required", "forbidChange", "typeOf", "matches",
27
+ "anyOf", "allOf", "not",
28
+ ];
29
+ const OPERATOR_SET = new Set(WRITE_RULE_OPERATORS);
30
+
31
+ function unprocessable(message) {
32
+ const error = new CliError(message);
33
+ error.status = 422;
34
+ // `code` is CliError's process-exit concept; the production error
35
+ // envelope's code rides separately so only coded API errors carry one.
36
+ error.apiCode = "unprocessable";
37
+ return error;
38
+ }
39
+
40
+ function isPlainObject(value) {
41
+ return value !== null && typeof value === "object" && !Array.isArray(value);
42
+ }
43
+
44
+ function soleEntry(value) {
45
+ if (!isPlainObject(value)) return null;
46
+ const entries = Object.entries(value);
47
+ return entries.length === 1 ? entries[0] : null;
48
+ }
49
+
50
+ function validFieldPath(path) {
51
+ const segments = String(path).split(".");
52
+ return segments.length >= 1
53
+ && segments.length <= MAX_WRITE_RULE_PATH_DEPTH
54
+ && segments.every((segment) => /^[A-Za-z0-9_-]{1,64}$/.test(segment));
55
+ }
56
+
57
+ /** Anchored full-match, mirroring the backend's `\A(?:pattern)\z` wrapper. */
58
+ function compilePattern(pattern) {
59
+ try {
60
+ return new RegExp(`^(?:${pattern})$`);
61
+ } catch (error) {
62
+ throw new CliError(`invalid validate pattern: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ /** Count AST nodes while validating shape; the count is the publish-time
67
+ * cost bound. Increment-then-check order matches the backend exactly so
68
+ * both sides accept and reject the same rule at the 64-node boundary. */
69
+ function validateOperandNode(operand, counter) {
70
+ counter.nodes += 1;
71
+ if (counter.nodes > MAX_WRITE_RULE_NODES) {
72
+ throw new CliError(`validate rules hold at most ${MAX_WRITE_RULE_NODES} nodes`);
73
+ }
74
+ if (isPlainObject(operand)) {
75
+ const entries = Object.entries(operand);
76
+ if (!entries.length) throw new CliError("validate operands must not be empty objects");
77
+ if (entries.length > 1) throw new CliError("validate operands carry exactly one key");
78
+ const [key, value] = entries[0];
79
+ if (["field", "prior", "len"].includes(key)) {
80
+ if (typeof value !== "string" || !validFieldPath(value)) {
81
+ throw new CliError(
82
+ `validate ${key} paths are dot-paths of at most ${MAX_WRITE_RULE_PATH_DEPTH} short segments`,
83
+ );
84
+ }
85
+ return;
86
+ }
87
+ if (key === "actor") {
88
+ if (value !== "user_id") {
89
+ throw new CliError("validate actor operands support only 'user_id'");
90
+ }
91
+ return;
92
+ }
93
+ throw new CliError(`unknown validate operand '${key}'`);
94
+ }
95
+ if (Array.isArray(operand)) {
96
+ if (operand.length > 16) {
97
+ throw new CliError("validate literal arrays hold at most 16 values");
98
+ }
99
+ for (const value of operand) {
100
+ counter.nodes += 1;
101
+ if (value !== null && !["string", "number", "boolean"].includes(typeof value)) {
102
+ throw new CliError("validate literal arrays hold scalars only");
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function validateRuleNode(rule, counter, depth) {
109
+ counter.nodes += 1;
110
+ if (counter.nodes > MAX_WRITE_RULE_NODES) {
111
+ throw new CliError(`validate rules hold at most ${MAX_WRITE_RULE_NODES} nodes`);
112
+ }
113
+ const entry = soleEntry(rule);
114
+ if (!entry) throw new CliError("validate rules are single-operator objects");
115
+ const [operator, operand] = entry;
116
+ if (!OPERATOR_SET.has(operator)) {
117
+ throw new CliError(`unknown validate operator '${operator}'`);
118
+ }
119
+ if (["anyOf", "allOf", "not"].includes(operator)) {
120
+ if (depth >= MAX_WRITE_RULE_DEPTH) {
121
+ throw new CliError(`validate combinators nest at most ${MAX_WRITE_RULE_DEPTH} deep`);
122
+ }
123
+ if (operator === "not") {
124
+ validateRuleNode(operand, counter, depth + 1);
125
+ return;
126
+ }
127
+ if (!Array.isArray(operand) || !operand.length) {
128
+ throw new CliError(`validate ${operator} takes a non-empty array`);
129
+ }
130
+ for (const nested of operand) validateRuleNode(nested, counter, depth + 1);
131
+ return;
132
+ }
133
+ if (["required", "forbidChange"].includes(operator)) {
134
+ if (!Array.isArray(operand) || !operand.length) {
135
+ throw new CliError(`validate ${operator} takes a non-empty array of field paths`);
136
+ }
137
+ if (operand.length > 16) {
138
+ throw new CliError(`validate ${operator} lists at most 16 fields`);
139
+ }
140
+ for (const field of operand) {
141
+ counter.nodes += 1;
142
+ if (typeof field !== "string" || !validFieldPath(field)) {
143
+ throw new CliError(`validate ${operator} entries are dot-paths`);
144
+ }
145
+ }
146
+ return;
147
+ }
148
+ if (operator === "typeOf") {
149
+ if (!Array.isArray(operand) || operand.length !== 2) {
150
+ throw new CliError("validate typeOf takes [operand, type]");
151
+ }
152
+ validateOperandNode(operand[0], counter);
153
+ if (!["string", "number", "boolean", "array", "object", "null"].includes(operand[1])) {
154
+ throw new CliError(
155
+ "validate typeOf types are string, number, boolean, array, object, or null",
156
+ );
157
+ }
158
+ return;
159
+ }
160
+ if (operator === "matches") {
161
+ if (!Array.isArray(operand) || operand.length !== 2) {
162
+ throw new CliError("validate matches takes [operand, pattern]");
163
+ }
164
+ validateOperandNode(operand[0], counter);
165
+ if (typeof operand[1] !== "string") {
166
+ throw new CliError("validate matches patterns are strings");
167
+ }
168
+ if (Buffer.byteLength(operand[1]) > MAX_WRITE_RULE_PATTERN_BYTES) {
169
+ throw new CliError(`validate patterns are at most ${MAX_WRITE_RULE_PATTERN_BYTES} bytes`);
170
+ }
171
+ compilePattern(operand[1]);
172
+ return;
173
+ }
174
+ if (operator === "in") {
175
+ if (!Array.isArray(operand) || operand.length !== 2) {
176
+ throw new CliError("validate in takes [operand, [values]]");
177
+ }
178
+ validateOperandNode(operand[0], counter);
179
+ if (!Array.isArray(operand[1])) {
180
+ throw new CliError("validate in takes a literal array of allowed values");
181
+ }
182
+ validateOperandNode(operand[1], counter);
183
+ return;
184
+ }
185
+ // Binary comparisons.
186
+ if (!Array.isArray(operand) || operand.length !== 2) {
187
+ throw new CliError(`validate ${operator} takes [left, right]`);
188
+ }
189
+ validateOperandNode(operand[0], counter);
190
+ validateOperandNode(operand[1], counter);
191
+ }
192
+
193
+ /** Normalize + bound a collection's `validate` section at definition time
194
+ * (the backend's `normalized_write_rules`). Returns undefined when there is
195
+ * nothing to enforce. */
196
+ export function normalizedWriteRules(raw) {
197
+ if (raw === undefined || raw === null) return undefined;
198
+ if (!Array.isArray(raw)) {
199
+ throw new CliError("collection validate must be an array of { rule, message }");
200
+ }
201
+ if (!raw.length) return undefined;
202
+ if (raw.length > MAX_WRITE_RULES_PER_COLLECTION) {
203
+ throw new CliError(
204
+ `a collection declares at most ${MAX_WRITE_RULES_PER_COLLECTION} validate rules`,
205
+ );
206
+ }
207
+ for (const entry of raw) {
208
+ if (!isPlainObject(entry)) {
209
+ throw new CliError("validate entries are { rule, message } objects");
210
+ }
211
+ for (const key of Object.keys(entry)) {
212
+ if (!["rule", "message"].includes(key)) {
213
+ throw new CliError(`unknown validate entry field '${key}'`);
214
+ }
215
+ }
216
+ const message = typeof entry.message === "string" ? entry.message : "";
217
+ if (!message || Buffer.byteLength(message) > MAX_WRITE_RULE_MESSAGE_BYTES) {
218
+ throw new CliError(`validate messages are 1..=${MAX_WRITE_RULE_MESSAGE_BYTES} characters`);
219
+ }
220
+ if (!("rule" in entry)) throw new CliError("validate entries need a rule");
221
+ validateRuleNode(entry.rule, { nodes: 0 }, 0);
222
+ }
223
+ return structuredClone(raw);
224
+ }
225
+
226
+ /** Dot-path lookup distinguishing missing (undefined) from explicit null,
227
+ * the way the backend's Option<&Value> does. */
228
+ function lookup(root, path) {
229
+ let current = root;
230
+ for (const segment of String(path).split(".")) {
231
+ if (!isPlainObject(current) || !Object.hasOwn(current, segment)) return undefined;
232
+ current = current[segment];
233
+ }
234
+ return current;
235
+ }
236
+
237
+ /** serde_json Value equality: deep, object-key-order-insensitive. */
238
+ function deepEqual(left, right) {
239
+ if (left === right) return true;
240
+ if (Array.isArray(left) && Array.isArray(right)) {
241
+ return left.length === right.length
242
+ && left.every((value, index) => deepEqual(value, right[index]));
243
+ }
244
+ if (isPlainObject(left) && isPlainObject(right)) {
245
+ const keys = Object.keys(left);
246
+ return keys.length === Object.keys(right).length
247
+ && keys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
248
+ }
249
+ return false;
250
+ }
251
+
252
+ /** Rust String ordering: by Unicode code point (equals UTF-8 byte order),
253
+ * not UTF-16 code units and not collation. */
254
+ function compareCodePoints(left, right) {
255
+ const a = [...left];
256
+ const b = [...right];
257
+ const shared = Math.min(a.length, b.length);
258
+ for (let index = 0; index < shared; index += 1) {
259
+ const delta = a[index].codePointAt(0) - b[index].codePointAt(0);
260
+ if (delta) return delta < 0 ? -1 : 1;
261
+ }
262
+ return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
263
+ }
264
+
265
+ function step(context) {
266
+ context.steps += 1;
267
+ if (context.steps > MAX_WRITE_RULE_EVAL_STEPS) {
268
+ throw unprocessable("validate rules exceeded their evaluation budget");
269
+ }
270
+ }
271
+
272
+ function resolveOperand(context, operand) {
273
+ step(context);
274
+ const entry = soleEntry(operand);
275
+ if (entry) {
276
+ const [key, value] = entry;
277
+ const path = typeof value === "string" ? value : "";
278
+ if (key === "field") return lookup(context.incoming, path) ?? null;
279
+ if (key === "prior") return lookup(context.prior, path) ?? null;
280
+ if (key === "len") {
281
+ const target = lookup(context.incoming, path);
282
+ if (typeof target === "string") return [...target].length;
283
+ if (Array.isArray(target)) return target.length;
284
+ return null;
285
+ }
286
+ if (key === "actor") return context.actorUserId;
287
+ return null;
288
+ }
289
+ return operand;
290
+ }
291
+
292
+ function holds(context, rule) {
293
+ step(context);
294
+ const entry = soleEntry(rule);
295
+ if (!entry) return false;
296
+ const [operator, operand] = entry;
297
+ if (operator === "anyOf") {
298
+ for (const nested of Array.isArray(operand) ? operand : []) {
299
+ if (holds(context, nested)) return true;
300
+ }
301
+ return false;
302
+ }
303
+ if (operator === "allOf") {
304
+ for (const nested of Array.isArray(operand) ? operand : []) {
305
+ if (!holds(context, nested)) return false;
306
+ }
307
+ return true;
308
+ }
309
+ if (operator === "not") return !holds(context, operand);
310
+ if (operator === "required") {
311
+ return (Array.isArray(operand) ? operand : []).every((field) => {
312
+ const value = lookup(context.incoming, typeof field === "string" ? field : "");
313
+ return value !== undefined && value !== null;
314
+ });
315
+ }
316
+ if (operator === "forbidChange") {
317
+ // Nothing stored yet: creation sets immutable fields.
318
+ if (context.prior === null) return true;
319
+ return (Array.isArray(operand) ? operand : []).every((field) => {
320
+ const path = typeof field === "string" ? field : "";
321
+ const incoming = lookup(context.incoming, path);
322
+ const prior = lookup(context.prior, path);
323
+ if (incoming === undefined || prior === undefined) return incoming === prior;
324
+ return deepEqual(incoming, prior);
325
+ });
326
+ }
327
+ if (operator === "typeOf") {
328
+ if (!Array.isArray(operand) || operand.length !== 2) return false;
329
+ const value = resolveOperand(context, operand[0]);
330
+ if (operand[1] === "string") return typeof value === "string";
331
+ if (operand[1] === "number") return typeof value === "number";
332
+ if (operand[1] === "boolean") return typeof value === "boolean";
333
+ if (operand[1] === "array") return Array.isArray(value);
334
+ if (operand[1] === "object") return isPlainObject(value);
335
+ if (operand[1] === "null") return value === null;
336
+ return false;
337
+ }
338
+ if (operator === "matches") {
339
+ if (!Array.isArray(operand) || operand.length !== 2) return false;
340
+ const value = resolveOperand(context, operand[0]);
341
+ if (typeof value !== "string" || typeof operand[1] !== "string") return false;
342
+ // Charged against the step budget by input size so a rule set cannot
343
+ // multiply large-string scans for free.
344
+ context.steps += Math.floor(Buffer.byteLength(value) / 64);
345
+ return compilePattern(operand[1]).test(value);
346
+ }
347
+ if (operator === "in") {
348
+ if (!Array.isArray(operand) || operand.length !== 2) return false;
349
+ const value = resolveOperand(context, operand[0]);
350
+ return Array.isArray(operand[1])
351
+ && operand[1].some((allowed) => deepEqual(allowed, value));
352
+ }
353
+ if (["eq", "neq", "lt", "lte", "gt", "gte"].includes(operator)) {
354
+ if (!Array.isArray(operand) || operand.length !== 2) return false;
355
+ const left = resolveOperand(context, operand[0]);
356
+ const right = resolveOperand(context, operand[1]);
357
+ if (operator === "eq") return deepEqual(left, right);
358
+ if (operator === "neq") return !deepEqual(left, right);
359
+ let order = null;
360
+ if (typeof left === "number" && typeof right === "number") {
361
+ order = left < right ? -1 : left > right ? 1 : 0;
362
+ } else if (typeof left === "string" && typeof right === "string") {
363
+ order = compareCodePoints(left, right);
364
+ }
365
+ if (order === null) return false;
366
+ if (operator === "lt") return order < 0;
367
+ if (operator === "lte") return order <= 0;
368
+ if (operator === "gt") return order > 0;
369
+ return order >= 0;
370
+ }
371
+ return false;
372
+ }
373
+
374
+ /** Enforce a collection's validate rules for one write (the backend's
375
+ * `enforce_write_rules`, called at every record-write door). `incoming` is
376
+ * the record as it will be stored; `prior` the stored record value, when
377
+ * any. Violations throw the production 422: code "unprocessable", message
378
+ * `validate[{index}]: {message}`. */
379
+ export function enforceWriteRules(declaration, actorUserId, prior, incoming) {
380
+ const rules = declaration?.validate;
381
+ if (!Array.isArray(rules)) return;
382
+ const context = {
383
+ incoming,
384
+ prior: prior ?? null,
385
+ actorUserId: String(actorUserId),
386
+ steps: 0,
387
+ };
388
+ for (const [index, entry] of rules.entries()) {
389
+ const rule = isPlainObject(entry) ? entry.rule : undefined;
390
+ if (rule === undefined) continue;
391
+ if (!holds(context, rule)) {
392
+ const message = typeof entry.message === "string"
393
+ ? entry.message
394
+ : "record violates a collection rule";
395
+ throw unprocessable(`validate[${index}]: ${message}`);
396
+ }
397
+ }
398
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@terminus-ai/cli",
3
+ "version": "0.0.1",
4
+ "description": "Terminus CLI (`terminus`): search and use skills, and develop, run, and publish apps, services, and agents on the Terminus platform.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://www.terminus.build",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Terminus-Intelligence/terminus-cli.git"
11
+ },
12
+ "bin": {
13
+ "terminus": "bin/terminus.js",
14
+ "terminus-cli": "bin/terminus.js"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "README.md"
19
+ ],
20
+ "exports": {
21
+ "./package.json": "./package.json"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org/",
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.16.0"
29
+ },
30
+ "scripts": {
31
+ "build": "npm pack --dry-run",
32
+ "prepublishOnly": "npm test",
33
+ "test": "node --import ./test/helpers/offline.mjs --test \"test/*.test.mjs\""
34
+ },
35
+ "optionalDependencies": {
36
+ "@terminus-ai/agentd-darwin-arm64": "0.0.1",
37
+ "@terminus-ai/agentd-darwin-x64": "0.0.1",
38
+ "@terminus-ai/agentd-linux-arm64": "0.0.1",
39
+ "@terminus-ai/agentd-linux-x64": "0.0.1"
40
+ }
41
+ }