@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
package/bin/client.mjs ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The helpers every CLI command module shares: strict flag parsing, usage
3
+ * errors, the one output door (`emitJson`), the web pages the CLI points at,
4
+ * and writing the saved login.
5
+ *
6
+ * Talking to Terminus is bin/http.mjs's (the API base, the token, every
7
+ * request); errors are bin/errors.mjs's, and this module re-exports the two
8
+ * that command modules import from here. It knows nothing about command
9
+ * dispatch, terminal prompts, artifact packaging, or dev harnesses, so the
10
+ * executable entry point and its lazy command modules never import each
11
+ * other.
12
+ */
13
+
14
+ import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
15
+ import { randomUUID } from "node:crypto";
16
+ import path from "node:path";
17
+
18
+ import {
19
+ COMMAND_FLAGS,
20
+ COMMON_FLAGS,
21
+ closest,
22
+ commandNamed,
23
+ missingArgumentReason,
24
+ renderHelp,
25
+ subcommandNamed,
26
+ } from "./commands.mjs";
27
+ import { usageError } from "./errors.mjs";
28
+ import { normalizeBase, sessionPath } from "./http.mjs";
29
+
30
+ export { CliError, usageError } from "./errors.mjs";
31
+
32
+ /** Bad arguments for a known command, the way brew answers them: the
33
+ * command's whole help, then `Error: Invalid usage: …` naming what was wrong.
34
+ * `--json` carries only that last line. */
35
+ export function commandUsageError(name, { sub, reason } = {}) {
36
+ const command = commandNamed(name);
37
+ const entry = sub ? subcommandNamed(command, sub) : command;
38
+ const error = usageError(`Invalid usage: ${reason ?? missingArgumentReason(entry)}`);
39
+ error.help = renderHelp(sub ? [name, sub] : [name]);
40
+ return error;
41
+ }
42
+
43
+ /** Print `value` as JSON when `--json` was passed, otherwise run the human
44
+ * printer. The one door every command's output goes through. */
45
+ export function emitJson(flags, value, human) {
46
+ if (flags.json) {
47
+ console.log(JSON.stringify(value, null, 2));
48
+ } else if (human) {
49
+ human();
50
+ }
51
+ }
52
+
53
+ /** Strict flag parser: `--key`, `--key=value`, `--key value`; every option
54
+ * must be declared (by command name — see bin/commands.mjs — or an explicit
55
+ * spec) or parsing fails with a usage error. Keys come back snake_cased
56
+ * (`--use-first` → use_first). */
57
+ export function parseFlags(args, spec = {}) {
58
+ const declared = typeof spec === "string" ? COMMAND_FLAGS[spec] : spec;
59
+ if (!declared) throw new Error(`no flag spec for command '${spec}'`);
60
+ const booleans = new Set([...COMMON_FLAGS.booleans, ...(declared.booleans ?? [])]);
61
+ const strings = new Set([...COMMON_FLAGS.strings, ...(declared.strings ?? [])]);
62
+ const values = { _: [] };
63
+ // A command's own flag mistakes come back with its help, like brew's.
64
+ const [specName, specSub] = typeof spec === "string" ? spec.split(" ") : [];
65
+ const invalid = (message) => (specName && commandNamed(specName)
66
+ ? commandUsageError(specName, { sub: specSub, reason: message })
67
+ : usageError(message));
68
+ for (let index = 0; index < args.length; index += 1) {
69
+ const arg = args[index];
70
+ if (!arg.startsWith("--")) {
71
+ values._.push(arg);
72
+ continue;
73
+ }
74
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
75
+ const key = rawKey.replaceAll("-", "_");
76
+ if (booleans.has(rawKey)) {
77
+ if (inlineValue !== undefined) throw invalid(`--${rawKey} does not take a value.`);
78
+ values[key] = true;
79
+ continue;
80
+ }
81
+ if (!strings.has(rawKey)) {
82
+ const known = [...(declared.booleans ?? []), ...(declared.strings ?? [])];
83
+ const suggestion = closest(rawKey, known);
84
+ throw invalid(`unknown option --${rawKey}${suggestion ? `; did you mean --${suggestion}?` : "."}`);
85
+ }
86
+ const value = inlineValue ?? args[++index];
87
+ if (value == null || value.startsWith("--")) {
88
+ throw invalid(`--${rawKey} requires a value.`);
89
+ }
90
+ values[key] = value;
91
+ }
92
+ return values;
93
+ }
94
+
95
+ const DEFAULT_TERMINUS_WEB_BASE = "https://www.terminus.build";
96
+
97
+ /** The web portal: where creations are made, forked, and published. The CLI
98
+ * only moves files between a folder and a creation that already exists. */
99
+ export function webBase(flags = {}) {
100
+ return normalizeBase(flags.web_base ?? process.env.TERMINUS_WEB_BASE ?? DEFAULT_TERMINUS_WEB_BASE);
101
+ }
102
+
103
+ /** The web page that makes a new creation (and shows its address). */
104
+ export function newCreationUrl(flags = {}) {
105
+ return `${webBase(flags)}/os?open=create`;
106
+ }
107
+
108
+ /** One creation's page on the web, where it is published. Apps, agents, and
109
+ * services are keyed by their app id, skills by their uid. */
110
+ export function creationPageUrl(flags, id) {
111
+ return `${webBase(flags)}/os?open=creation:${encodeURIComponent(id)}`;
112
+ }
113
+
114
+ /** A catalog artifact's public page — `/apps/@maker/notes` without the `@`,
115
+ * the way the web links every kind; the official tier is one segment. */
116
+ export function artifactPageUrl(flags, artifact) {
117
+ const kind = artifact.kind ?? "skill";
118
+ const [handle, slug] = String(artifact.address ?? "").replace(/^@/, "").split("/");
119
+ const route = artifact.is_official
120
+ ? `/${kind}s/${encodeURIComponent(slug ?? handle)}`
121
+ : `/${kind}s/${encodeURIComponent(handle)}/${encodeURIComponent(slug ?? "")}`;
122
+ return `${webBase(flags)}${route}`;
123
+ }
124
+
125
+ /** What to do with a folder that is not connected to a creation yet. */
126
+ export function notConnectedHint(flags = {}) {
127
+ return `create it on the web (${newCreationUrl(flags)}), then run \`terminus remote add <address>\` here`;
128
+ }
129
+
130
+ export async function writePrivateJson(file, value) {
131
+ const directory = path.dirname(file);
132
+ await mkdir(directory, { recursive: true, mode: 0o700 });
133
+ try {
134
+ await chmod(directory, 0o700);
135
+ } catch {
136
+ // Best effort on platforms/filesystems that do not support POSIX modes.
137
+ }
138
+ const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
139
+ try {
140
+ await writeFile(temporary, JSON.stringify(value, null, 2), {
141
+ flag: "wx",
142
+ mode: 0o600,
143
+ });
144
+ try {
145
+ await chmod(temporary, 0o600);
146
+ } catch {
147
+ // Best effort on platforms/filesystems that do not support POSIX modes.
148
+ }
149
+ // Same-directory rename keeps readers from observing a partially written
150
+ // bearer token and avoids following a pre-existing session-file symlink.
151
+ await rename(temporary, file);
152
+ } finally {
153
+ await rm(temporary, { force: true });
154
+ }
155
+ }
156
+
157
+ export async function saveSession(session) {
158
+ await writePrivateJson(sessionPath(), {
159
+ version: 2,
160
+ ...session,
161
+ saved_at: new Date().toISOString(),
162
+ });
163
+ }
164
+
165
+ export function sessionRecordFromBrowserLogin(base, login, previous = {}) {
166
+ const profile = login.profile ?? {};
167
+ const user = login.user ?? {};
168
+ const avatarUrl = Object.hasOwn(profile, "avatar_url")
169
+ ? profile.avatar_url
170
+ : Object.hasOwn(user, "avatar_url")
171
+ ? user.avatar_url
172
+ : previous.avatar_url;
173
+ return {
174
+ version: 3,
175
+ api_base: base,
176
+ auth_method: "browser",
177
+ token: login.token,
178
+ expires_at: login.expires_at,
179
+ user_id: profile.id ?? user.id ?? previous.user_id,
180
+ email: profile.email ?? user.email ?? login.email ?? previous.email,
181
+ display_name: profile.display_name ?? login.display_name ?? previous.display_name,
182
+ username: profile.username ?? login.username ?? user.username ?? previous.username,
183
+ publisher_handle: profile.publisher_handle ?? previous.publisher_handle,
184
+ avatar_url: avatarUrl,
185
+ device_name: login.device_name ?? previous.device_name,
186
+ platform: login.platform ?? previous.platform,
187
+ client_version: login.client_version ?? previous.client_version,
188
+ };
189
+ }