@oalo-ai/cli 0.1.0 → 0.2.0

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.
@@ -16,7 +16,7 @@ import { readValue } from "./env.js";
16
16
  * oalo deploy --new [<name>] a new app from this repo, its history kept
17
17
  * oalo deploy --new [<name>] --from <url> a new app from a Git URL, cloned by the Base
18
18
  * oalo deploy --app <slug> --env prod build and swap the live release
19
- * oalo deploy --app <slug> --pull fetch the app's code from OALO as oalo/<slug>
19
+ * oalo deploy --app <slug> --pull fetch the app's code from Oalo as oalo/<slug>
20
20
  * oalo deploy --app <slug> --replace make this branch the app's code (old code is tagged)
21
21
  * oalo deploy --app <slug> --history the live releases it has had
22
22
  * oalo deploy --app <slug> --rollback [--to <sha>] make an earlier release live again
@@ -118,7 +118,7 @@ export async function deploy(flags) {
118
118
  if (result.error) {
119
119
  const e = result.error;
120
120
  console.error(`deploy failed: ${e.message}`);
121
- for (const c of e.box_only || []) console.error(dim(` on OALO only: ${c}`));
121
+ for (const c of e.box_only || []) console.error(dim(` on Oalo only: ${c}`));
122
122
  process.exitCode = 1;
123
123
  return;
124
124
  }
@@ -323,7 +323,7 @@ async function pull(url, token, app, root) {
323
323
  const ref = `refs/remotes/oalo/${app}`;
324
324
  git(["fetch", "--quiet", "--no-tags", file, `+HEAD:${ref}`], root);
325
325
  const sha = git(["rev-parse", ref], root);
326
- console.log(`fetched ${app} as it is on OALO (${sha.slice(0, 8)}) into oalo/${app}.`);
326
+ console.log(`fetched ${app} as it is on Oalo (${sha.slice(0, 8)}) into oalo/${app}.`);
327
327
  console.log(dim(`Merge it, then deploy again:\n git merge oalo/${app}\n oalo deploy --app ${app}`));
328
328
  } finally {
329
329
  rmSync(dir, { recursive: true, force: true });
@@ -381,7 +381,7 @@ function fail(message, code) {
381
381
  * `--history` lists the live releases; `--rollback` launches the one before the
382
382
  * current release (or `--to <sha>`). A rollback builds and checks that commit
383
383
  * like any live deploy, so a failing one leaves the current release live. It
384
- * doesn't touch the app's code on OALO: the next deploy carries on from there.
384
+ * doesn't touch the app's code on Oalo: the next deploy carries on from there.
385
385
  */
386
386
  async function releases(url, token, app, flags, opts = {}) {
387
387
  const { OaloChannel } = await import("../channel.js");
@@ -6,7 +6,7 @@ import { dim } from "../events.js";
6
6
  import { parseDotenv, parseAssignment } from "../envfile.js";
7
7
 
8
8
  /**
9
- * `oalo env`: an app's environment variables on OALO, per env.
9
+ * `oalo env`: an app's environment variables on Oalo, per env.
10
10
  *
11
11
  * oalo env [--app <slug>] [--env dev|prod] [--json] list them
12
12
  * oalo env set NAME=value [NAME=value ...] [--secret | --public]
@@ -22,7 +22,7 @@ import { parseDotenv, parseAssignment } from "../envfile.js";
22
22
  * the live app at the commit it already runs (no new code), unless
23
23
  * --no-restart, to batch several.
24
24
  *
25
- * A server app also gets OALO_URL and OALO_SYNC_TOKEN from OALO itself, a
25
+ * A server app also gets OALO_URL and OALO_SYNC_TOKEN from Oalo itself, a
26
26
  * token for its own env's data. `rotate-token` replaces it: the old one stops
27
27
  * working at once, and the preview restarts, or the live app's current commit
28
28
  * is relaunched.
@@ -98,15 +98,15 @@ export async function link(flags) {
98
98
  console.log(` token ${credPath} (never committed)`);
99
99
  if (addedRule) console.log(`\n added .oalo/credentials* to .gitignore`);
100
100
 
101
- // The coding agent in this project learns OALO from the skill, the one that
101
+ // The coding agent in this project learns Oalo from the skill, the one that
102
102
  // matches this CLI. --no-skill leaves the project alone.
103
103
  if (!flags["no-skill"]) {
104
104
  try {
105
105
  const had = installedSkillVersion();
106
106
  const r = installSkill();
107
- console.log(`\n ${had ? "refreshed" : "installed"} the OALO skill for coding agents: ${r.path}${r.agents ? ` (and AGENTS.md)` : ""}`);
107
+ console.log(`\n ${had ? "refreshed" : "installed"} the Oalo skill for coding agents: ${r.path}${r.agents ? ` (and AGENTS.md)` : ""}`);
108
108
  } catch (e) {
109
- console.error(`\n couldn't install the OALO skill (${e.message}); \`oalo skill\` tries again`);
109
+ console.error(`\n couldn't install the Oalo skill (${e.message}); \`oalo skill\` tries again`);
110
110
  }
111
111
  }
112
112
  // A Flutter app from Oalo's template takes Dart, not TypeScript.
@@ -10,7 +10,7 @@ import { dim } from "../events.js";
10
10
  * oalo logs [--app <slug>] [--env dev|prod] [--process web|<worker>] [--limit N] [--follow] [--json]
11
11
  *
12
12
  * The workspace keeps the last 2,000 lines of each env in memory, so a restart
13
- * of the workspace empties them. `oalo` lines are OALO's own notes: starts,
13
+ * of the workspace empties them. `oalo` lines are Oalo's own notes: starts,
14
14
  * exits and their status.
15
15
  */
16
16
  export async function logs(flags, rest, opts = {}) {
@@ -0,0 +1,321 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { execFile } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { OaloChannel } from "../channel.js";
5
+ import { resolve as resolveConfig, controlPlane, HOW_TO_CONNECT } from "../config.js";
6
+ import { requireFeature } from "../server.js";
7
+ import { dim } from "../events.js";
8
+ import { buildPlan, PLATFORMS } from "../migrate/plan.js";
9
+
10
+ /**
11
+ * `oalo migrate <heroku|railway|render|fly|primcloud>`: what an app on another
12
+ * platform becomes on Oalo, from the repo's files and that platform's own CLI.
13
+ *
14
+ * oalo migrate heroku [--from-app shop] the plan; nothing changes
15
+ * oalo migrate heroku --apply write the Procfile and .oalo/app.json,
16
+ * and set the variables on the live app
17
+ *
18
+ * It reads fly.toml, railway.json/railway.toml, render.yaml, app.json and the
19
+ * Procfile; asks the platform's CLI for variables (Heroku, Railway) or secret
20
+ * names (Fly) and dyno sizes (Heroku); drops what Oalo or the old platform
21
+ * sets, renames DATABASE_URL to EXTERNAL_DATABASE_URL, and says what doesn't
22
+ * carry over. Values are never printed or written to a file: they go straight
23
+ * to the app's prod variables. An existing Procfile is never overwritten, and
24
+ * .oalo/app.json only gains keys it lacks.
25
+ */
26
+ export async function migrate(flags, rest, opts = {}) {
27
+ const platform = rest[1];
28
+ if (!platform || !PLATFORMS[platform]) {
29
+ return fail(`usage: oalo migrate <${Object.keys(PLATFORMS).join("|")}> [--from-app <name>] [--env-file <file>] [--apply]`, 2);
30
+ }
31
+ const dir = opts.cwd || process.cwd();
32
+ const run = opts.exec || execCli;
33
+ const connect = opts.channel || ((o) => new OaloChannel(o));
34
+ const fromApp = typeof flags["from-app"] === "string" ? flags["from-app"] : null;
35
+
36
+ let envFile = null;
37
+ if (typeof flags["env-file"] === "string") {
38
+ try {
39
+ envFile = readFileSync(flags["env-file"], "utf8");
40
+ } catch (e) {
41
+ return fail(`can't read ${flags["env-file"]}: ${e.code || e.message}`, 2);
42
+ }
43
+ }
44
+
45
+ const files = readFiles(dir);
46
+ const remote = flags["no-cli"] ? { notes: [] } : await askPlatform(platform, fromApp, files, run);
47
+
48
+ let plan;
49
+ try {
50
+ plan = buildPlan(platform, { files, remote, envFile, fromApp });
51
+ } catch (e) {
52
+ return fail(e.message, 1);
53
+ }
54
+
55
+ const cfg = resolveConfig(flags, dir);
56
+ const app = typeof flags.app === "string" ? flags.app : cfg.appId;
57
+
58
+ if (!flags.apply) {
59
+ if (flags.json) return void console.log(JSON.stringify(publicPlan(plan, app), null, 2));
60
+ printPlan(plan, app, dir);
61
+ return;
62
+ }
63
+
64
+ // --apply: files first (never overwriting), then the variables.
65
+ const wrote = [];
66
+ if (plan.procfile.action === "write" && !existsSync(join(dir, "Procfile"))) {
67
+ writeFileSync(join(dir, "Procfile"), plan.procfile.text);
68
+ wrote.push("Procfile");
69
+ }
70
+ if (plan.manifest.action === "write" || plan.manifest.action === "merge") {
71
+ mkdirSync(join(dir, ".oalo"), { recursive: true });
72
+ writeFileSync(join(dir, ".oalo", "app.json"), plan.manifest.text);
73
+ wrote.push(".oalo/app.json");
74
+ }
75
+
76
+ let varsResult = null;
77
+ if (plan.vars.set.length) {
78
+ if (!app) {
79
+ varsResult = { skipped: "no app" };
80
+ } else {
81
+ const { url, token } = controlPlane(cfg);
82
+ if (!url || !token) return fail(`setting the variables needs a CLI token.\n\n${HOW_TO_CONNECT}`, 2);
83
+ try {
84
+ await requireFeature(url, "env", "oalo migrate");
85
+ } catch (e) {
86
+ return fail(e.message, 1);
87
+ }
88
+ const ch = connect({ url, token });
89
+ try {
90
+ await ch.connect();
91
+ const r = await ch.call("env:set", {
92
+ app,
93
+ env: "prod",
94
+ vars: plan.vars.set.map(({ name, value }) => ({ name, value })),
95
+ restart: false,
96
+ });
97
+ varsResult = { app: r.app, saved: r.saved.length, relaunch: !!(r.relaunching || r.next_release) };
98
+ } catch (e) {
99
+ return fail(`the files are written, but the variables weren't set: ${e.message}`, 1);
100
+ } finally {
101
+ ch.close();
102
+ }
103
+ }
104
+ }
105
+
106
+ if (flags.json) return void console.log(JSON.stringify({ ...publicPlan(plan, app), wrote, variables: varsResult }, null, 2));
107
+
108
+ console.log(`Moving from ${plan.label}.`);
109
+ if (wrote.length) console.log(`wrote ${wrote.join(" and ")}: review them, then commit`);
110
+ else console.log(dim("no files to write"));
111
+ if (varsResult?.saved != null) {
112
+ console.log(`set ${varsResult.saved} variable${varsResult.saved === 1 ? "" : "s"} on ${varsResult.app} (prod)`);
113
+ if (varsResult.relaunch) console.log(dim("the live app gets them at its next release"));
114
+ } else if (varsResult?.skipped) {
115
+ console.log(`variables not set yet: this folder isn't linked to an Oalo app. After \`oalo deploy --new\`, run \`oalo migrate ${platform} --apply\` again${fromApp ? ` --from-app ${fromApp}` : ""}${flags["env-file"] ? ` --env-file ${flags["env-file"]}` : ""}.`);
116
+ }
117
+ printNeeds(plan);
118
+ printNext(plan, app, platform, true);
119
+ }
120
+
121
+ // ── reading ───────────────────────────────────────────────────────────
122
+
123
+ const LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb", "npm-shrinkwrap.json"];
124
+
125
+ export function readFiles(dir) {
126
+ const read = (name) => {
127
+ try {
128
+ return readFileSync(join(dir, name), "utf8");
129
+ } catch {
130
+ return null;
131
+ }
132
+ };
133
+ return {
134
+ procfile: read("Procfile"),
135
+ appJson: read("app.json"),
136
+ flyToml: read("fly.toml"),
137
+ railwayJson: read("railway.json"),
138
+ railwayToml: read("railway.toml"),
139
+ renderYaml: read("render.yaml"),
140
+ dockerfile: read("Dockerfile"),
141
+ aptfile: existsSync(join(dir, "Aptfile")),
142
+ packageJson: existsSync(join(dir, "package.json")),
143
+ lockfile: LOCKFILES.some((f) => existsSync(join(dir, f))),
144
+ oaloAppJson: read(join(".oalo", "app.json")),
145
+ };
146
+ }
147
+
148
+ /** What the platform's own CLI can tell: variables, secret names, dyno sizes. */
149
+ async function askPlatform(platform, fromApp, files, run) {
150
+ const notes = [];
151
+ const out = { notes, vars: null, secretNames: null, dynos: null };
152
+ const flyApp = () => {
153
+ if (fromApp) return fromApp;
154
+ const m = files.flyToml?.match(/^\s*app\s*=\s*["']([^"']+)["']/m);
155
+ return m ? m[1] : null;
156
+ };
157
+
158
+ if (platform === "heroku") {
159
+ const a = fromApp ? ["-a", fromApp] : [];
160
+ const cfg = await run("heroku", ["config", "--json", ...a]);
161
+ if (cfg.ok) out.vars = asObject(cfg.stdout);
162
+ else notes.push(cliNote("heroku", cfg, "config vars", "heroku config --shell"));
163
+ const ps = await run("heroku", ["ps", "--json", ...a]);
164
+ if (ps.ok) out.dynos = asArray(ps.stdout).map((d) => ({ type: d.type, size: d.size }));
165
+ } else if (platform === "railway") {
166
+ const a = fromApp ? ["--service", fromApp] : [];
167
+ const r = await run("railway", ["variable", "list", "--json", ...a]);
168
+ if (r.ok) out.vars = asObject(r.stdout);
169
+ else notes.push(cliNote("railway", r, "variables", "railway variable list --kv"));
170
+ } else if (platform === "fly") {
171
+ const name = flyApp();
172
+ const a = name ? ["-a", name] : [];
173
+ let r = await run("fly", ["secrets", "list", "--json", ...a]);
174
+ if (!r.ok && r.missing) r = await run("flyctl", ["secrets", "list", "--json", ...a]);
175
+ if (r.ok) out.secretNames = asArray(r.stdout).map((s) => s.Name || s.name).filter(Boolean);
176
+ else notes.push(cliNote("fly", r, "secret names", "fly secrets list"));
177
+ }
178
+ return out;
179
+ }
180
+
181
+ function cliNote(cli, r, what, manual) {
182
+ if (r.missing) return `The ${cli} CLI isn't installed here, so its ${what} weren't read. Install it and sign in, or run \`${manual}\` elsewhere, save the values to a .env file and pass --env-file.`;
183
+ const why = firstLine(r.stderr) || "it failed";
184
+ return `\`${cli}\` couldn't list the ${what} (${why}). Sign in and link the project, or pass --from-app, or put them in a .env file and pass --env-file.`;
185
+ }
186
+
187
+ function execCli(cmd, args) {
188
+ return new Promise((resolve) => {
189
+ execFile(cmd, args, { timeout: 30_000, maxBuffer: 8 * 1024 * 1024, env: process.env }, (err, stdout, stderr) => {
190
+ if (err && err.code === "ENOENT") return resolve({ ok: false, missing: true, stdout: "", stderr: "" });
191
+ resolve({ ok: !err, missing: false, stdout: String(stdout || ""), stderr: String(stderr || "") });
192
+ });
193
+ });
194
+ }
195
+
196
+ function asObject(text) {
197
+ try {
198
+ const j = JSON.parse(text);
199
+ if (Array.isArray(j)) return Object.fromEntries(j.map((x) => [x.name ?? x.key, x.value]).filter(([k]) => k));
200
+ return j && typeof j === "object" ? j : {};
201
+ } catch {
202
+ return {};
203
+ }
204
+ }
205
+
206
+ function asArray(text) {
207
+ try {
208
+ const j = JSON.parse(text);
209
+ return Array.isArray(j) ? j : [];
210
+ } catch {
211
+ return [];
212
+ }
213
+ }
214
+
215
+ // Only a short reason, and never anything that looks like a URL with a password.
216
+ function firstLine(s) {
217
+ const line = String(s || "").split(/\r?\n/).map((x) => x.trim()).find(Boolean) || "";
218
+ return line.replace(/\/\/[^\s/@]+@/g, "//***@").slice(0, 160);
219
+ }
220
+
221
+ // ── printing ──────────────────────────────────────────────────────────
222
+
223
+ /** The plan without any values: what --json prints. */
224
+ function publicPlan(plan, app) {
225
+ return {
226
+ platform: plan.platform,
227
+ app: app || null,
228
+ procfile: { action: plan.procfile.action, lines: plan.procfile.lines.map(([name, command]) => ({ name, command })) },
229
+ manifest: { action: plan.manifest.action, sizes: plan.manifest.sizes, health: plan.manifest.health },
230
+ variables: {
231
+ set: plan.vars.set.map((v) => ({ name: v.name, from: v.from })),
232
+ renamed: plan.vars.renamed,
233
+ dropped: plan.vars.dropped,
234
+ missing: plan.vars.missing,
235
+ },
236
+ needs: plan.warnings,
237
+ };
238
+ }
239
+
240
+ function printPlan(plan, app, dir) {
241
+ console.log(`Moving from ${plan.label}. Nothing changes until you add --apply.\n`);
242
+
243
+ const pf = plan.procfile;
244
+ if (pf.action === "keep") console.log(`Procfile keep yours (${pf.lines.map(([n]) => n).join(", ") || "no lines"})`);
245
+ else if (pf.action === "write") {
246
+ console.log("Procfile write:");
247
+ for (const [n, c] of pf.lines) console.log(` ${n}: ${c}`);
248
+ } else console.log(`Procfile none ${dim("(web starts the usual way for its language, or from the Dockerfile)")}`);
249
+
250
+ const m = plan.manifest;
251
+ if (m.action === "write" || m.action === "merge") {
252
+ const parts = Object.entries(m.sizes).map(([p, s]) => `${p} ${s.size} (${s.from})`);
253
+ if (m.health) parts.push(`health ${m.health}`);
254
+ console.log(`.oalo/app.json ${m.action === "merge" ? "add" : "write"}: ${parts.join(", ")}`);
255
+ } else {
256
+ console.log(`.oalo/app.json ${m.action === "keep" ? "keep yours" : "none"} ${dim("(every process gets small)")}`);
257
+ }
258
+
259
+ const v = plan.vars;
260
+ console.log("");
261
+ if (v.set.length) {
262
+ console.log(`Variables ${v.set.length} to set on the live app${app ? ` (${app})` : ""}, values not shown:`);
263
+ console.log(wrap(v.set.map((x) => x.name).join(", "), 18, 0));
264
+ } else {
265
+ console.log("Variables none read");
266
+ }
267
+ for (const r of v.renamed) console.log(` ${r.to} from ${r.from} ${dim("(on Oalo, DATABASE_URL is your Base's own Postgres)")}`);
268
+ if (v.dropped.length) console.log(dim(` left out: ${v.dropped.map((d) => `${d.name} (${d.why})`).join(", ")}`));
269
+ if (v.missing.length) {
270
+ console.log(" still needed, values not available here:");
271
+ for (const x of v.missing) console.log(` ${x.name}: ${x.why}`);
272
+ }
273
+
274
+ printNeeds(plan);
275
+ printNext(plan, app, plan.platform, false);
276
+ if (!existsSync(join(dir, ".git"))) console.log(dim("\nThis folder isn't a git repo: oalo deploy --new sends commits, so run this in the app's repo."));
277
+ }
278
+
279
+ function printNeeds(plan) {
280
+ if (!plan.warnings.length) return;
281
+ console.log("\nNeeds you");
282
+ for (const w of plan.warnings) console.log(wrap(`- ${w}`, 2, 2));
283
+ }
284
+
285
+ function printNext(plan, app, platform, applied) {
286
+ console.log("\nNext");
287
+ const steps = [];
288
+ const files = plan.procfile.action === "write" || plan.manifest.action === "write" || plan.manifest.action === "merge";
289
+ const later = !app && plan.vars.set.length;
290
+ if (!applied) steps.push(`oalo migrate ${platform} --apply${later ? " (writes the files; the variables wait until the app exists)" : ""}`);
291
+ if (files) steps.push('git add Procfile .oalo/app.json && git commit -m "Move to Oalo"');
292
+ if (!app) steps.push("oalo deploy --new");
293
+ if (later) steps.push(`oalo migrate ${platform} --apply (now it sets the variables)`);
294
+ if (plan.vars.missing.length) steps.push("oalo env set NAME --secret --env prod (for each one still needed)");
295
+ steps.push("oalo env set NAME --env dev (test values for the preview)");
296
+ steps.push("your data: keep the database and set EXTERNAL_DATABASE_URL, or copy it in with oalo db restore");
297
+ steps.push("oalo deploy --env prod");
298
+ steps.push("move the domain: the app's Custom Domain panel in the dashboard");
299
+ steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
300
+ console.log(dim(`\nThe whole walk-through: oalo docs migrating-from-${platform}`));
301
+ }
302
+
303
+ function wrap(text, indent, hang = indent) {
304
+ const width = Math.max(40, (process.stdout.columns || 100) - 2);
305
+ const words = text.split(" ");
306
+ const lines = [];
307
+ let line = " ".repeat(indent);
308
+ for (const w of words) {
309
+ if (line.trim() && line.length + 1 + w.length > width) {
310
+ lines.push(line);
311
+ line = " ".repeat(indent + hang) + w;
312
+ } else line += (line.trim() ? " " : "") + w;
313
+ }
314
+ if (line.trim()) lines.push(line);
315
+ return lines.join("\n");
316
+ }
317
+
318
+ function fail(message, code) {
319
+ console.error(message);
320
+ process.exitCode = code;
321
+ }
@@ -4,7 +4,7 @@ import { requireFeature } from "../server.js";
4
4
  import { dim } from "../events.js";
5
5
 
6
6
  /**
7
- * `oalo secrets`: keys and tokens OALO found committed to an app's code (on
7
+ * `oalo secrets`: keys and tokens Oalo found committed to an app's code (on
8
8
  * import, and in each push or deploy's new commits). Only where each one is
9
9
  * and what it looks like: no value is ever kept.
10
10
  *
@@ -7,7 +7,7 @@ import { CLI_VERSION } from "../server.js";
7
7
  import { dim } from "../events.js";
8
8
 
9
9
  /**
10
- * `oalo skill`: teach the coding agent in this project how to work on OALO.
10
+ * `oalo skill`: teach the coding agent in this project how to work on Oalo.
11
11
  *
12
12
  * oalo skill install .claude/skills/oalo/SKILL.md (Claude Code loads it)
13
13
  * oalo skill --agents also add the same rules to AGENTS.md, for other agents
@@ -124,9 +124,9 @@ export function skillNotice(cwd = process.cwd()) {
124
124
  } catch {
125
125
  /* a read-only home: say it every time rather than never */
126
126
  }
127
- if (sameVersion) return "This project's OALO skill (.claude/skills/oalo/SKILL.md) is older than the one this CLI carries. Run `oalo skill` to update it.";
127
+ if (sameVersion) return "This project's Oalo skill (.claude/skills/oalo/SKILL.md) is older than the one this CLI carries. Run `oalo skill` to update it.";
128
128
  const from = installed === "old" ? "an older CLI" : `CLI ${installed}`;
129
- return `This project's OALO skill (.claude/skills/oalo/SKILL.md) is from ${from}; you have ${CLI_VERSION}. Run \`oalo skill\` to update it.`;
129
+ return `This project's Oalo skill (.claude/skills/oalo/SKILL.md) is from ${from}; you have ${CLI_VERSION}. Run \`oalo skill\` to update it.`;
130
130
  }
131
131
 
132
132
  export async function skill(flags) {
@@ -137,6 +137,6 @@ export async function skill(flags) {
137
137
 
138
138
  const r = installSkill(process.cwd(), { agents: !!flags.agents });
139
139
  console.log(`installed ${r.path}`);
140
- if (r.agents) console.log(`${r.agents} the OALO section in AGENTS.md`);
140
+ if (r.agents) console.log(`${r.agents} the Oalo section in AGENTS.md`);
141
141
  console.log(dim(`From CLI ${CLI_VERSION}. Run \`oalo skill\` again after updating the CLI.`));
142
142
  }
@@ -109,7 +109,7 @@ function renderFields(name, fields) {
109
109
  }
110
110
 
111
111
  // The workflows an app can call: their types, and `runWorkflow` to call one
112
- // from the app's server with the token OALO gives it.
112
+ // from the app's server with the token Oalo gives it.
113
113
  function renderWorkflows(workflows) {
114
114
  const list = [...workflows].sort((a, b) => a.name.localeCompare(b.name));
115
115
  const type = (w) => pascal(w.name);
@@ -172,7 +172,7 @@ function renderServerHelpers() {
172
172
  return [
173
173
  ``,
174
174
  `// ── Server-side helpers ───────────────────────────────────────────────────`,
175
- `// They use the OALO_URL and OALO_SYNC_TOKEN OALO gives a server app. Never`,
175
+ `// They use the OALO_URL and OALO_SYNC_TOKEN Oalo gives a server app. Never`,
176
176
  `// call them from a browser: the token must stay on the server.`,
177
177
  ``,
178
178
  `export interface OaloServerOptions {`,
@@ -186,7 +186,7 @@ function renderServerHelpers() {
186
186
  `async function oaloFetch(path: string, init: RequestInit, opts: OaloServerOptions): Promise<unknown> {`,
187
187
  ` const url = opts.url ?? oaloEnv("OALO_URL");`,
188
188
  ` const token = opts.token ?? oaloEnv("OALO_SYNC_TOKEN");`,
189
- ` if (!url || !token) throw new Error("OALO_URL and OALO_SYNC_TOKEN are not set: call OALO from the app's server");`,
189
+ ` if (!url || !token) throw new Error("OALO_URL and OALO_SYNC_TOKEN are not set: call Oalo from the app's server");`,
190
190
  ` const res = await fetch(url.replace(/\\/$/, "") + path, { ...init, headers: { Authorization: \`Bearer \${token}\`, "Content-Type": "application/json" } });`,
191
191
  ` const body = (await res.json().catch(() => null)) as { error?: string | { message?: string } } | null;`,
192
192
  ` const message = typeof body?.error === "string" ? body.error : body?.error?.message;`,
@@ -263,9 +263,9 @@ function renderServerHelpers() {
263
263
  `}`,
264
264
  ``,
265
265
  `/**`,
266
- ` * Check a request to a tool endpoint came from OALO, and for which user. Pass the`,
266
+ ` * Check a request to a tool endpoint came from Oalo, and for which user. Pass the`,
267
267
  ` * method, the path with its query as received, the headers and the RAW body (before`,
268
- ` * JSON parsing). Throws when it isn't OALO's, or is over 5 minutes old.`,
268
+ ` * JSON parsing). Throws when it isn't Oalo's, or is over 5 minutes old.`,
269
269
  ` */`,
270
270
  `export async function verifyOaloToolCall(`,
271
271
  ` req: { method: string; path: string; headers: Headers | Record<string, string | string[] | undefined>; body: string },`,
@@ -280,8 +280,8 @@ function renderServerHelpers() {
280
280
  ` const ts = header("x-oalo-timestamp");`,
281
281
  ` const sig = header("x-oalo-signature");`,
282
282
  ` const user = header("x-oalo-user") ?? "";`,
283
- ` if (!ts || !sig || !sig.startsWith("v1=")) throw new Error("not an OALO tool call");`,
284
- ` if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= (opts.maxAgeSeconds ?? 300))) throw new Error("the OALO tool call is too old");`,
283
+ ` if (!ts || !sig || !sig.startsWith("v1=")) throw new Error("not an Oalo tool call");`,
284
+ ` if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= (opts.maxAgeSeconds ?? 300))) throw new Error("the Oalo tool call is too old");`,
285
285
  ` const enc = new TextEncoder();`,
286
286
  ` const key = await crypto.subtle.importKey("raw", enc.encode(token), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);`,
287
287
  ` const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode([ts, req.method.toUpperCase(), req.path, user, req.body].join("\\n"))));`,
@@ -289,7 +289,7 @@ function renderServerHelpers() {
289
289
  ` const got = sig.slice(3);`,
290
290
  ` let diff = want.length ^ got.length;`,
291
291
  ` for (let i = 0; i < want.length; i++) diff |= want.charCodeAt(i) ^ (got.charCodeAt(i) || 0);`,
292
- ` if (diff !== 0) throw new Error("the OALO signature doesn't match");`,
292
+ ` if (diff !== 0) throw new Error("the Oalo signature doesn't match");`,
293
293
  ` return { tool: header("x-oalo-tool") ?? "", bot: header("x-oalo-bot") ?? "", conversation: header("x-oalo-conversation") || null, user: user || null };`,
294
294
  `}`,
295
295
  ``,
@@ -34,7 +34,7 @@ export async function whoami(flags) {
34
34
  } catch (e) {
35
35
  out.problems.push(
36
36
  e.code === "unknown_event"
37
- ? `${url} runs an older OALO, so it can't say who you are. Your CLI token does work.`
37
+ ? `${url} runs an older Oalo, so it can't say who you are. Your CLI token does work.`
38
38
  : `CLI token: ${e.message}`,
39
39
  );
40
40
  } finally {
package/src/config.js CHANGED
@@ -140,7 +140,7 @@ export function ensureGitignored(cwd = process.cwd()) {
140
140
  const rule = ".oalo/credentials*";
141
141
  const current = existsSync(gi) ? readFileSync(gi, "utf8") : "";
142
142
  if (current.includes(rule)) return false;
143
- appendFileSync(gi, `${current.endsWith("\n") || !current ? "" : "\n"}\n# OALO — never commit credentials\n${rule}\n`);
143
+ appendFileSync(gi, `${current.endsWith("\n") || !current ? "" : "\n"}\n# Oalo — never commit credentials\n${rule}\n`);
144
144
  return true;
145
145
  }
146
146