@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.
- package/README.md +1 -1
- package/man/oalo.1 +78 -17
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/skills/oalo/SKILL.md +23 -0
- package/src/channel.js +1 -1
- package/src/commands/db.js +336 -4
- package/src/commands/deploy.js +4 -4
- package/src/commands/env.js +2 -2
- package/src/commands/link.js +3 -3
- package/src/commands/logs.js +1 -1
- package/src/commands/migrate.js +321 -0
- package/src/commands/secrets.js +1 -1
- package/src/commands/skill.js +4 -4
- package/src/commands/typegen.js +8 -8
- package/src/commands/whoami.js +1 -1
- package/src/config.js +1 -1
- package/src/help.js +57 -17
- package/src/index.js +4 -0
- package/src/migrate/plan.js +478 -0
- package/src/migrate/toml.js +242 -0
- package/src/migrate/yaml.js +179 -0
- package/src/server.js +1 -1
package/src/commands/deploy.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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");
|
package/src/commands/env.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
package/src/commands/link.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
package/src/commands/logs.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/commands/secrets.js
CHANGED
|
@@ -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
|
|
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
|
*
|
package/src/commands/skill.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/commands/typegen.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
284
|
-
` if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= (opts.maxAgeSeconds ?? 300))) throw new Error("the
|
|
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
|
|
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
|
``,
|
package/src/commands/whoami.js
CHANGED
|
@@ -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
|
|
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#
|
|
143
|
+
appendFileSync(gi, `${current.endsWith("\n") || !current ? "" : "\n"}\n# Oalo — never commit credentials\n${rule}\n`);
|
|
144
144
|
return true;
|
|
145
145
|
}
|
|
146
146
|
|