@oalo-ai/cli 0.0.0-stage → 0.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -2
  3. package/man/oalo.1 +1530 -0
  4. package/package.json +44 -4
  5. package/plugin/.claude-plugin/plugin.json +12 -0
  6. package/plugin/skills/oalo/SKILL.md +414 -0
  7. package/src/api.js +132 -0
  8. package/src/app-ref.js +11 -0
  9. package/src/args.js +67 -0
  10. package/src/bot-settings.json +468 -0
  11. package/src/channel.js +162 -0
  12. package/src/commands/apps.js +64 -0
  13. package/src/commands/bots.js +278 -0
  14. package/src/commands/call.js +179 -0
  15. package/src/commands/channels.js +110 -0
  16. package/src/commands/data.js +446 -0
  17. package/src/commands/db.js +58 -0
  18. package/src/commands/deploy.js +431 -0
  19. package/src/commands/dockerfile.js +81 -0
  20. package/src/commands/docs.js +70 -0
  21. package/src/commands/emit.js +124 -0
  22. package/src/commands/env.js +223 -0
  23. package/src/commands/graphs.js +570 -0
  24. package/src/commands/incidents.js +44 -0
  25. package/src/commands/link.js +150 -0
  26. package/src/commands/login.js +180 -0
  27. package/src/commands/logs.js +69 -0
  28. package/src/commands/mcp.js +125 -0
  29. package/src/commands/metrics.js +168 -0
  30. package/src/commands/products.js +169 -0
  31. package/src/commands/qa.js +98 -0
  32. package/src/commands/rehearse.js +100 -0
  33. package/src/commands/run.js +152 -0
  34. package/src/commands/secrets.js +68 -0
  35. package/src/commands/setup.js +96 -0
  36. package/src/commands/skill.js +142 -0
  37. package/src/commands/sources.js +377 -0
  38. package/src/commands/switches.js +156 -0
  39. package/src/commands/tail.js +84 -0
  40. package/src/commands/tokens.js +85 -0
  41. package/src/commands/typegen.js +505 -0
  42. package/src/commands/whoami.js +112 -0
  43. package/src/config.js +157 -0
  44. package/src/data.js +392 -0
  45. package/src/envfile.js +71 -0
  46. package/src/events.js +63 -0
  47. package/src/graphfiles.js +273 -0
  48. package/src/help.js +920 -0
  49. package/src/index.js +216 -0
  50. package/src/mcp/tools.js +773 -0
  51. package/src/server.js +73 -0
  52. package/src/workspace.js +44 -0
@@ -0,0 +1,124 @@
1
+ import { OaloChannel } from "../channel.js";
2
+ import { resolve as resolveConfig, controlPlane, HOW_TO_CONNECT } from "../config.js";
3
+ import { requireFeature } from "../server.js";
4
+ import { dim } from "../events.js";
5
+
6
+ /**
7
+ * `oalo emit <type>`: fire a domain event (form.submitted, order.created, ...)
8
+ * as if it really happened, to test that a loop's trigger is wired. `oalo run
9
+ * loop` skips the trigger; this goes through it.
10
+ *
11
+ * oalo emit form.submitted --payload '{"email":"a@b.co"}' [--contact <id>] [--dry-run]
12
+ * oalo emit --list
13
+ *
14
+ * The loops it fires do real things (email, WhatsApp, invoices), so the server
15
+ * allows it only for admins and members with Developer access, tags the event
16
+ * as coming from the CLI and posts it to the team feed. --dry-run lists the
17
+ * loops that would fire and changes nothing.
18
+ */
19
+ export async function emit(flags, rest) {
20
+ const type = rest[1];
21
+ if (!flags.list && !type) {
22
+ console.error("usage: oalo emit <event type> [--payload '<json>'] [--contact <id>] [--dry-run]\n oalo emit --list");
23
+ process.exitCode = 2;
24
+ return;
25
+ }
26
+
27
+ let payload = {};
28
+ if (typeof flags.payload === "string") {
29
+ try {
30
+ payload = JSON.parse(flags.payload);
31
+ } catch (e) {
32
+ console.error(`--payload must be valid JSON: ${e.message}`);
33
+ process.exitCode = 2;
34
+ return;
35
+ }
36
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
37
+ console.error("--payload must be a JSON object");
38
+ process.exitCode = 2;
39
+ return;
40
+ }
41
+ }
42
+
43
+ const cfg = resolveConfig(flags);
44
+ const { url, token } = controlPlane(cfg);
45
+ if (!url || !token) {
46
+ console.error(`oalo emit needs a CLI token.\n\n${HOW_TO_CONNECT}`);
47
+ process.exitCode = 2;
48
+ return;
49
+ }
50
+
51
+ try {
52
+ await requireFeature(url, "emit", "oalo emit");
53
+ } catch (e) {
54
+ console.error(e.message);
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+
59
+ const ch = new OaloChannel({ url, token });
60
+ try {
61
+ await ch.connect();
62
+
63
+ if (flags.list) {
64
+ const { events } = await ch.call("cli_api:event_types", {});
65
+ if (flags.json) return void console.log(JSON.stringify(events, null, 2));
66
+ printList(events);
67
+ return;
68
+ }
69
+
70
+ const params = { type, payload, dry_run: !!flags["dry-run"] };
71
+ if (typeof flags.contact === "string") params.contact_id = flags.contact;
72
+ const r = await ch.call("cli_api:emit", params);
73
+
74
+ if (flags.json) return void console.log(JSON.stringify(r, null, 2));
75
+
76
+ const names = r.loops.map((l) => l.name);
77
+ if (r.dry_run) {
78
+ console.log(
79
+ names.length
80
+ ? `${type} would fire ${names.length} loop${names.length > 1 ? "s" : ""}: ${names.join(", ")}`
81
+ : `no enabled loop listens for ${type}${Object.keys(payload).length ? " with this payload" : ""}. Nothing would fire.`,
82
+ );
83
+ console.log(dim("dry run: nothing was emitted"));
84
+ } else {
85
+ console.log(`emitted ${type} (event ${r.event_id}), tagged as a test from the CLI`);
86
+ console.log(
87
+ names.length
88
+ ? `firing ${names.length} loop${names.length > 1 ? "s" : ""}: ${names.join(", ")}`
89
+ : "no enabled loop listens for it, so nothing fired",
90
+ );
91
+ if (names.length) console.log(dim("watch the runs with: oalo tail"));
92
+ }
93
+ } catch (e) {
94
+ console.error(`emit failed: ${e.message}`);
95
+ process.exitCode = 1;
96
+ } finally {
97
+ ch.close();
98
+ }
99
+ }
100
+
101
+ function printList(events) {
102
+ const byCat = new Map();
103
+ for (const e of events) {
104
+ if (!byCat.has(e.category)) byCat.set(e.category, []);
105
+ byCat.get(e.category).push(e);
106
+ }
107
+ const w = Math.max(...events.map((e) => e.event.length));
108
+ for (const [cat, list] of byCat) {
109
+ console.log(`\n${cat}`);
110
+ for (const e of list) {
111
+ const loops = e.loops.length ? ` -> ${e.loops.join(", ")}` : "";
112
+ console.log(` ${e.event.padEnd(w)} ${firstSentence(e.description)}${loops}`);
113
+ if (e.payload_fields.length) console.log(dim(` ${" ".repeat(w)} payload: ${e.payload_fields.join(", ")}`));
114
+ }
115
+ }
116
+ console.log(dim("\n-> loops in this workspace that listen for the event now"));
117
+ }
118
+
119
+ // The catalog's descriptions run to a paragraph; the list shows the first
120
+ // sentence and --json has the rest.
121
+ function firstSentence(text = "") {
122
+ const m = text.match(/^.*?[.!?](\s|$)/);
123
+ return (m ? m[0] : text).trim();
124
+ }
@@ -0,0 +1,223 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { OaloChannel } from "../channel.js";
3
+ import { resolve as resolveConfig, controlPlane, HOW_TO_CONNECT } from "../config.js";
4
+ import { requireFeature } from "../server.js";
5
+ import { dim } from "../events.js";
6
+ import { parseDotenv, parseAssignment } from "../envfile.js";
7
+
8
+ /**
9
+ * `oalo env`: an app's environment variables on Oalo, per env.
10
+ *
11
+ * oalo env [--app <slug>] [--env dev|prod] [--json] list them
12
+ * oalo env set NAME=value [NAME=value ...] [--secret | --public]
13
+ * oalo env set NAME --secret asks for the value, hidden
14
+ * oalo env set --file .env.production --env prod every line of a .env file
15
+ * oalo env unset NAME [NAME ...]
16
+ * oalo env rotate-token [--env dev|prod] a new OALO_SYNC_TOKEN
17
+ *
18
+ * A variable is secret unless its name starts with a browser prefix (VITE_,
19
+ * EXPO_PUBLIC_, ...) or it's set with --public. Secret values can't be read
20
+ * back, and only server apps get them: a static app's bundle would show them to
21
+ * every visitor. A dev change restarts the preview; a prod change relaunches
22
+ * the live app at the commit it already runs (no new code), unless
23
+ * --no-restart, to batch several.
24
+ *
25
+ * A server app also gets OALO_URL and OALO_SYNC_TOKEN from Oalo itself, a
26
+ * token for its own env's data. `rotate-token` replaces it: the old one stops
27
+ * working at once, and the preview restarts, or the live app's current commit
28
+ * is relaunched.
29
+ */
30
+ export async function env(flags, rest, opts = {}) {
31
+ // Tests pass a stub channel and value reader; real runs use neither.
32
+ const connect = opts.channel || ((o) => new OaloChannel(o));
33
+ const read = opts.readValue || readValue;
34
+
35
+ const sub = rest[1] || "list";
36
+ const cfg = resolveConfig(flags);
37
+ const { url, token } = controlPlane(cfg);
38
+ if (!url || !token) return fail(`oalo env needs a CLI token.\n\n${HOW_TO_CONNECT}`, 2);
39
+
40
+ const app = typeof flags.app === "string" ? flags.app : cfg.appId;
41
+ if (!app) return fail("which app? Pass --app <slug> (`oalo apps` lists them), or save it with `oalo link --app <slug>`.", 2);
42
+
43
+ const target = flags.env || cfg.env || "dev";
44
+ if (!["dev", "prod"].includes(target)) return fail(`--env must be dev or prod (got ${target})`, 2);
45
+ if (sub !== "list" && target === "prod" && flags.env !== "prod") {
46
+ return fail("this project is linked to prod. Changing the live app's variables needs --env prod on the command line.", 2);
47
+ }
48
+ if (flags.secret && flags.public) return fail("pick one of --secret and --public", 2);
49
+
50
+ let vars;
51
+ if (sub === "set") {
52
+ try {
53
+ vars = await collect(flags, rest.slice(2), read);
54
+ } catch (e) {
55
+ return fail(e.message, 2);
56
+ }
57
+ if (!vars) return;
58
+ } else if (sub === "unset") {
59
+ if (rest.length < 3) return fail("usage: oalo env unset NAME [NAME ...]", 2);
60
+ } else if (sub !== "list" && sub !== "rotate-token") {
61
+ return fail(`unknown env command: ${sub}. Use list, set, unset or rotate-token.`, 2);
62
+ }
63
+
64
+ try {
65
+ await requireFeature(url, "env", "oalo env");
66
+ } catch (e) {
67
+ return fail(e.message, 1);
68
+ }
69
+
70
+ const ch = connect({ url, token });
71
+ try {
72
+ await ch.connect();
73
+ if (sub === "list") return await list(ch, app, target, flags);
74
+ if (sub === "set") return await set(ch, app, target, vars, flags);
75
+ if (sub === "rotate-token") return await rotateToken(ch, app, target, flags);
76
+ return await unset(ch, app, target, rest.slice(2), flags);
77
+ } catch (e) {
78
+ return fail(e.message, 1);
79
+ } finally {
80
+ ch.close();
81
+ }
82
+ }
83
+
84
+ async function list(ch, app, env, flags) {
85
+ const r = await ch.call("env:list", { app, env });
86
+ if (flags.json) return void console.log(JSON.stringify(r, null, 2));
87
+
88
+ const vars = r.vars || [];
89
+ console.log(dim(`${r.app} (${r.env}), ${vars.length} variable${vars.length === 1 ? "" : "s"}`));
90
+ const width = Math.max(0, ...vars.map((v) => v.name.length));
91
+ for (const v of vars) {
92
+ const when = v.updated_at ? `, set ${v.updated_at.slice(0, 10)}` : "";
93
+ if (v.secret) {
94
+ const unused = r.static ? ", not used: static apps only get public variables" : "";
95
+ console.log(` ${v.name.padEnd(width)} ${dim(`secret${when}${unused}`)}`);
96
+ } else {
97
+ console.log(` ${v.name.padEnd(width)} ${v.value}`);
98
+ }
99
+ }
100
+ if (!vars.length) console.log(dim(` none yet: oalo env set NAME=value --app ${r.app}${env === "prod" ? " --env prod" : ""}`));
101
+ }
102
+
103
+ async function set(ch, app, env, vars, flags) {
104
+ const params = { app, env, vars: vars.map(({ name, value, secret }) => ({ name, value, ...(secret === undefined ? {} : { secret }) })) };
105
+ if (flags["no-restart"]) params.restart = false;
106
+
107
+ const r = await ch.call("env:set", params);
108
+ if (flags.json) return void console.log(JSON.stringify(r, null, 2));
109
+
110
+ const names = r.saved.map((v) => `${v.name}${v.secret ? " (secret)" : ""}`);
111
+ console.log(`set ${names.join(", ")} on ${r.app} (${r.env})`);
112
+ report(r);
113
+
114
+ const secrets = r.saved.filter((v) => v.secret);
115
+ if (r.static && secrets.length) {
116
+ console.error(
117
+ dim(`${r.app} is a static app, so ${secrets.map((v) => v.name).join(", ")} won't reach it: what a static app's build sees ends up in the browser. Use a public name (VITE_, EXPO_PUBLIC_) or --public for values that are safe to show, and keep keys in a server app.`),
118
+ );
119
+ }
120
+ const typed = secrets.filter((v) => vars.find((x) => x.name === v.name)?.fromArgv);
121
+ if (typed.length) {
122
+ console.error(dim(`tip: a value typed on the command line stays in your shell history. \`oalo env set ${typed[0].name} --secret\` asks for it instead.`));
123
+ }
124
+ }
125
+
126
+ async function unset(ch, app, env, names, flags) {
127
+ const r = await ch.call("env:unset", { app, env, names, ...(flags["no-restart"] ? { restart: false } : {}) });
128
+ if (flags.json) return void console.log(JSON.stringify(r, null, 2));
129
+
130
+ if (r.removed.length) console.log(`removed ${r.removed.join(", ")} from ${r.app} (${r.env})`);
131
+ if (r.missing.length) console.log(dim(`not set, skipped: ${r.missing.join(", ")}`));
132
+ if (r.removed.length) report(r);
133
+ }
134
+
135
+ async function rotateToken(ch, app, env, flags) {
136
+ const r = await ch.call("env:rotate_token", { app, env });
137
+ if (flags.json) return void console.log(JSON.stringify(r, null, 2));
138
+
139
+ console.log(`new OALO_SYNC_TOKEN for ${r.app} (${r.env}); the old one no longer works`);
140
+ if (r.restarting) console.log(dim("restarting the preview so it picks it up"));
141
+ else if (r.relaunching) console.log(dim(`relaunching the live app at ${r.relaunching.slice(0, 7)} so it picks it up (a build; follow it with oalo logs --app ${r.app} --env prod)`));
142
+ else if (r.env === "prod") console.log(dim(`${r.app} isn't live yet: its first release gets the new token`));
143
+ else console.log(dim("the preview picks it up the next time it starts"));
144
+ }
145
+
146
+ function report(r) {
147
+ if (r.restarting) console.log(dim("restarting the preview so it picks them up"));
148
+ else if (r.relaunching) console.log(dim(`relaunching the live app at ${r.relaunching.slice(0, 7)}, the commit it already runs, so it picks them up (a build; follow it with oalo logs --app ${r.app} --env prod)`));
149
+ else if (r.next_release) console.log(dim("the live app gets this when it next relaunches: your next `oalo env` change without --no-restart, or a live deploy"));
150
+ else if (r.env === "prod") console.log(dim(`${r.app} isn't live yet: its first release gets this`));
151
+ else if (r.env === "dev") console.log(dim("the preview picks this up the next time it starts"));
152
+ }
153
+
154
+ // ── what to set ─────────────────────────────────────────────────────────
155
+
156
+ async function collect(flags, args, read) {
157
+ const secret = flags.secret ? true : flags.public ? false : undefined;
158
+ const vars = [];
159
+
160
+ if (typeof flags.file === "string") {
161
+ let text;
162
+ try {
163
+ text = readFileSync(flags.file, "utf8");
164
+ } catch (e) {
165
+ throw new Error(`can't read ${flags.file}: ${e.code || e.message}`);
166
+ }
167
+ for (const v of parseDotenv(text)) vars.push({ ...v, secret });
168
+ if (!vars.length) throw new Error(`${flags.file} has no NAME=value lines`);
169
+ }
170
+
171
+ for (const arg of args) {
172
+ const a = parseAssignment(arg);
173
+ if (a) {
174
+ vars.push({ ...a, secret, fromArgv: true });
175
+ } else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(arg)) {
176
+ vars.push({ name: arg, value: await read(arg), secret });
177
+ } else {
178
+ throw new Error(`${arg} isn't NAME=value or a NAME`);
179
+ }
180
+ }
181
+
182
+ if (!vars.length) throw new Error("usage: oalo env set NAME=value [...], oalo env set NAME --secret, or oalo env set --file .env");
183
+ return vars;
184
+ }
185
+
186
+ /** A value from stdin when piped, otherwise a prompt that doesn't echo. */
187
+ export async function readValue(name) {
188
+ const stdin = process.stdin;
189
+ if (!stdin.isTTY) {
190
+ let text = "";
191
+ for await (const chunk of stdin) text += chunk;
192
+ return text.replace(/\r?\n$/, "");
193
+ }
194
+
195
+ process.stderr.write(`${name} (hidden): `);
196
+ return new Promise((resolve, reject) => {
197
+ let value = "";
198
+ stdin.setRawMode(true);
199
+ stdin.setEncoding("utf8");
200
+ stdin.resume();
201
+ const done = (fn) => {
202
+ stdin.setRawMode(false);
203
+ stdin.pause();
204
+ stdin.removeListener("data", onData);
205
+ process.stderr.write("\n");
206
+ fn();
207
+ };
208
+ const onData = (chunk) => {
209
+ for (const ch of chunk) {
210
+ if (ch === "\r" || ch === "\n") return done(() => resolve(value));
211
+ if (ch === "\u0003") return done(() => reject(new Error("cancelled")));
212
+ if (ch === "\u007f" || ch === "\b") value = value.slice(0, -1);
213
+ else value += ch;
214
+ }
215
+ };
216
+ stdin.on("data", onData);
217
+ });
218
+ }
219
+
220
+ function fail(message, code) {
221
+ console.error(message);
222
+ process.exitCode = code;
223
+ }