@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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * A small TOML reader, enough for the config files `oalo migrate` reads
3
+ * (fly.toml, railway.toml): tables, arrays of tables, dotted and quoted keys,
4
+ * strings (basic, literal and multi-line), numbers, booleans, dates (kept as
5
+ * text), arrays (over several lines too) and inline tables.
6
+ *
7
+ * Throws an Error naming the line when it meets something it can't read.
8
+ */
9
+ export function parseToml(text) {
10
+ const p = new Reader(String(text).replace(/^/, ""));
11
+ const root = {};
12
+ let table = root;
13
+
14
+ for (;;) {
15
+ p.skipBlank();
16
+ if (p.done()) break;
17
+
18
+ if (p.peek() === "[") {
19
+ const array = p.text.startsWith("[[", p.i);
20
+ p.i += array ? 2 : 1;
21
+ const path = p.keyPath();
22
+ p.expect(array ? "]]" : "]");
23
+ p.endOfLine();
24
+ table = array ? pushTable(root, path, p) : openTable(root, path, p);
25
+ continue;
26
+ }
27
+
28
+ const path = p.keyPath();
29
+ p.skipSpaces();
30
+ p.expect("=");
31
+ p.skipSpaces();
32
+ const value = p.value();
33
+ p.endOfLine();
34
+ assign(table, path, value, p);
35
+ }
36
+ return root;
37
+ }
38
+
39
+ class Reader {
40
+ constructor(text) {
41
+ this.text = text;
42
+ this.i = 0;
43
+ }
44
+ done() {
45
+ return this.i >= this.text.length;
46
+ }
47
+ peek() {
48
+ return this.text[this.i];
49
+ }
50
+ line() {
51
+ return this.text.slice(0, this.i).split("\n").length;
52
+ }
53
+ fail(msg) {
54
+ throw new Error(`line ${this.line()}: ${msg}`);
55
+ }
56
+ expect(s) {
57
+ if (!this.text.startsWith(s, this.i)) this.fail(`expected ${s}`);
58
+ this.i += s.length;
59
+ }
60
+ skipSpaces() {
61
+ while (this.peek() === " " || this.peek() === "\t") this.i++;
62
+ }
63
+ skipComment() {
64
+ if (this.peek() === "#") while (!this.done() && this.peek() !== "\n") this.i++;
65
+ }
66
+ /** Spaces, newlines and comments. */
67
+ skipBlank() {
68
+ for (;;) {
69
+ this.skipSpaces();
70
+ this.skipComment();
71
+ if (this.peek() === "\n" || this.peek() === "\r") this.i++;
72
+ else return;
73
+ }
74
+ }
75
+ endOfLine() {
76
+ this.skipSpaces();
77
+ this.skipComment();
78
+ if (this.done()) return;
79
+ if (this.peek() === "\r") this.i++;
80
+ if (this.peek() !== "\n") this.fail("expected the end of the line");
81
+ this.i++;
82
+ }
83
+
84
+ keyPath() {
85
+ const keys = [];
86
+ for (;;) {
87
+ this.skipSpaces();
88
+ keys.push(this.key());
89
+ this.skipSpaces();
90
+ if (this.peek() !== ".") return keys;
91
+ this.i++;
92
+ }
93
+ }
94
+ key() {
95
+ const c = this.peek();
96
+ if (c === '"') return this.basicString();
97
+ if (c === "'") return this.literalString();
98
+ const m = this.text.slice(this.i).match(/^[A-Za-z0-9_-]+/);
99
+ if (!m) this.fail("expected a key");
100
+ this.i += m[0].length;
101
+ return m[0];
102
+ }
103
+
104
+ value() {
105
+ const c = this.peek();
106
+ if (this.text.startsWith('"""', this.i)) return this.multiString('"""');
107
+ if (this.text.startsWith("'''", this.i)) return this.multiString("'''");
108
+ if (c === '"') return this.basicString();
109
+ if (c === "'") return this.literalString();
110
+ if (c === "[") return this.array();
111
+ if (c === "{") return this.inlineTable();
112
+ const m = this.text.slice(this.i).match(/^[^\s,\]}#]+/);
113
+ if (!m) this.fail("expected a value");
114
+ this.i += m[0].length;
115
+ return scalar(m[0], this);
116
+ }
117
+
118
+ basicString() {
119
+ this.i++;
120
+ let out = "";
121
+ while (!this.done()) {
122
+ const c = this.text[this.i++];
123
+ if (c === '"') return out;
124
+ if (c === "\n") this.fail("a string isn't closed on its line");
125
+ if (c === "\\") out += this.escape();
126
+ else out += c;
127
+ }
128
+ this.fail("a string isn't closed");
129
+ }
130
+ escape() {
131
+ const c = this.text[this.i++];
132
+ const simple = { n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\", b: "\b", f: "\f" };
133
+ if (c in simple) return simple[c];
134
+ if (c === "u" || c === "U") {
135
+ const len = c === "u" ? 4 : 8;
136
+ const hex = this.text.slice(this.i, this.i + len);
137
+ this.i += len;
138
+ return String.fromCodePoint(parseInt(hex, 16));
139
+ }
140
+ this.fail(`unknown escape \\${c}`);
141
+ }
142
+ literalString() {
143
+ const end = this.text.indexOf("'", this.i + 1);
144
+ const nl = this.text.indexOf("\n", this.i + 1);
145
+ if (end < 0 || (nl >= 0 && nl < end)) this.fail("a string isn't closed on its line");
146
+ const s = this.text.slice(this.i + 1, end);
147
+ this.i = end + 1;
148
+ return s;
149
+ }
150
+ multiString(q) {
151
+ this.i += 3;
152
+ if (this.peek() === "\r") this.i++;
153
+ if (this.peek() === "\n") this.i++;
154
+ const end = this.text.indexOf(q, this.i);
155
+ if (end < 0) this.fail(`${q} isn't closed`);
156
+ let s = this.text.slice(this.i, end);
157
+ this.i = end + 3;
158
+ if (q === '"""') {
159
+ s = s.replace(/\\\r?\n\s*/g, "").replace(/\\(.)/g, (_, ch) => ({ n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" })[ch] ?? ch);
160
+ }
161
+ return s;
162
+ }
163
+ array() {
164
+ this.i++;
165
+ const out = [];
166
+ for (;;) {
167
+ this.skipBlank();
168
+ if (this.peek() === "]") {
169
+ this.i++;
170
+ return out;
171
+ }
172
+ out.push(this.value());
173
+ this.skipBlank();
174
+ if (this.peek() === ",") this.i++;
175
+ else if (this.peek() !== "]") this.fail("expected , or ] in an array");
176
+ }
177
+ }
178
+ inlineTable() {
179
+ this.i++;
180
+ const out = {};
181
+ this.skipSpaces();
182
+ if (this.peek() === "}") {
183
+ this.i++;
184
+ return out;
185
+ }
186
+ for (;;) {
187
+ const path = this.keyPath();
188
+ this.skipSpaces();
189
+ this.expect("=");
190
+ this.skipSpaces();
191
+ assign(out, path, this.value(), this);
192
+ this.skipSpaces();
193
+ if (this.peek() === ",") {
194
+ this.i++;
195
+ this.skipSpaces();
196
+ } else if (this.peek() === "}") {
197
+ this.i++;
198
+ return out;
199
+ } else this.fail("expected , or } in an inline table");
200
+ }
201
+ }
202
+ }
203
+
204
+ function scalar(word, p) {
205
+ if (word === "true") return true;
206
+ if (word === "false") return false;
207
+ if (/^[+-]?(\d[\d_]*)(\.\d[\d_]*)?([eE][+-]?\d+)?$/.test(word)) return Number(word.replace(/_/g, ""));
208
+ if (/^0x[0-9a-fA-F_]+$/.test(word)) return parseInt(word.slice(2).replace(/_/g, ""), 16);
209
+ // Dates and times are kept as text: nothing here needs them as dates.
210
+ if (/^\d{4}-\d{2}-\d{2}/.test(word) || /^\d{2}:\d{2}/.test(word)) return word;
211
+ if (/^[+-]?(inf|nan)$/.test(word)) return Number(word.replace("inf", "Infinity").replace("nan", "NaN"));
212
+ p.fail(`can't read the value ${word}`);
213
+ }
214
+
215
+ function walk(root, path, p) {
216
+ let t = root;
217
+ for (const k of path) {
218
+ if (t[k] === undefined) t[k] = {};
219
+ t = Array.isArray(t[k]) ? t[k][t[k].length - 1] : t[k];
220
+ if (typeof t !== "object" || t === null) p.fail(`${path.join(".")} is already a value`);
221
+ }
222
+ return t;
223
+ }
224
+
225
+ function openTable(root, path, p) {
226
+ return walk(root, path, p);
227
+ }
228
+
229
+ function pushTable(root, path, p) {
230
+ const parent = walk(root, path.slice(0, -1), p);
231
+ const k = path[path.length - 1];
232
+ if (parent[k] === undefined) parent[k] = [];
233
+ if (!Array.isArray(parent[k])) p.fail(`${path.join(".")} is already a table`);
234
+ const t = {};
235
+ parent[k].push(t);
236
+ return t;
237
+ }
238
+
239
+ function assign(table, path, value, p) {
240
+ const t = walk(table, path.slice(0, -1), p);
241
+ t[path[path.length - 1]] = value;
242
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * A small YAML reader, enough for render.yaml: block mappings and sequences by
3
+ * indentation, a sequence item that starts a mapping (`- type: web`), plain,
4
+ * single- and double-quoted scalars, block scalars (`|`, `>`), simple flow
5
+ * sequences (`[a, b]`) and comments.
6
+ *
7
+ * Anchors, aliases, tags and several documents in one file aren't supported:
8
+ * it throws an Error naming the line, so the caller can say to copy the
9
+ * commands by hand instead.
10
+ */
11
+ export function parseYaml(text) {
12
+ const lines = String(text).replace(/^/, "").split(/\r?\n/);
13
+ const r = new Reader(lines);
14
+ r.skip();
15
+ if (r.atEnd()) return null;
16
+ const value = r.block(r.indent());
17
+ r.skip();
18
+ if (!r.atEnd()) r.fail("unexpected indentation");
19
+ return value;
20
+ }
21
+
22
+ class Reader {
23
+ constructor(lines) {
24
+ this.lines = lines;
25
+ this.i = 0;
26
+ }
27
+ fail(msg) {
28
+ throw new Error(`line ${this.i + 1}: ${msg}`);
29
+ }
30
+ atEnd() {
31
+ return this.i >= this.lines.length;
32
+ }
33
+ /** Past blank lines, comments and a leading document marker. */
34
+ skip() {
35
+ while (!this.atEnd()) {
36
+ const t = this.lines[this.i].trim();
37
+ if (t === "" || t.startsWith("#") || (t === "---" && this.i === 0)) this.i++;
38
+ else if (t === "---" || t === "...") this.fail("several YAML documents in one file aren't supported");
39
+ else return;
40
+ }
41
+ }
42
+ indent() {
43
+ return this.lines[this.i].match(/^ */)[0].length;
44
+ }
45
+ content() {
46
+ return this.lines[this.i].slice(this.indent());
47
+ }
48
+
49
+ block(indent) {
50
+ const c = this.content();
51
+ return c === "-" || c.startsWith("- ") ? this.sequence(indent) : this.mapping(indent);
52
+ }
53
+
54
+ mapping(indent) {
55
+ const out = {};
56
+ for (;;) {
57
+ this.skip();
58
+ if (this.atEnd() || this.indent() < indent) return out;
59
+ if (this.indent() > indent) this.fail("unexpected indentation");
60
+ const c = this.content();
61
+ if (c === "-" || c.startsWith("- ")) return out;
62
+
63
+ const m = c.match(/^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^\s"'#][^:#]*?)\s*:(?:\s+|$)(.*)$/);
64
+ if (!m) this.fail("expected key: value");
65
+ const key = unquote(m[1].trim(), this);
66
+ const rest = stripComment(m[2]);
67
+ this.i++;
68
+ out[key] = this.valueAfterKey(rest, indent);
69
+ }
70
+ }
71
+
72
+ valueAfterKey(rest, indent) {
73
+ if (/^[|>][-+]?$/.test(rest)) return this.blockScalar(rest, indent);
74
+ if (rest !== "") return scalar(rest, this);
75
+ this.skip();
76
+ if (this.atEnd()) return null;
77
+ const next = this.indent();
78
+ const c = this.content();
79
+ if (next > indent) return this.block(next);
80
+ // `key:` followed by `- item` at the same indentation is a sequence.
81
+ if (next === indent && (c === "-" || c.startsWith("- "))) return this.sequence(indent);
82
+ return null;
83
+ }
84
+
85
+ sequence(indent) {
86
+ const out = [];
87
+ for (;;) {
88
+ this.skip();
89
+ if (this.atEnd() || this.indent() !== indent) return out;
90
+ const c = this.content();
91
+ if (!(c === "-" || c.startsWith("- "))) return out;
92
+
93
+ const after = c.slice(1);
94
+ const pad = after.match(/^ */)[0].length;
95
+ const item = after.slice(pad);
96
+ if (item === "" || item.startsWith("#")) {
97
+ this.i++;
98
+ this.skip();
99
+ out.push(!this.atEnd() && this.indent() > indent ? this.block(this.indent()) : null);
100
+ } else if (/^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^\s"'#\[{][^:#]*?)\s*:(\s|$)/.test(item)) {
101
+ // `- key: value` starts a mapping whose keys sit at the item's column.
102
+ const col = indent + 1 + pad;
103
+ this.lines[this.i] = " ".repeat(col) + item;
104
+ out.push(this.mapping(col));
105
+ } else {
106
+ this.i++;
107
+ out.push(scalar(stripComment(item), this));
108
+ }
109
+ }
110
+ }
111
+
112
+ blockScalar(head, indent) {
113
+ const fold = head[0] === ">";
114
+ const chomp = head[1];
115
+ const body = [];
116
+ let col = null;
117
+ while (!this.atEnd()) {
118
+ const line = this.lines[this.i];
119
+ if (line.trim() === "") {
120
+ body.push("");
121
+ this.i++;
122
+ continue;
123
+ }
124
+ const ind = line.match(/^ */)[0].length;
125
+ if (ind <= indent) break;
126
+ if (col === null) col = ind;
127
+ if (ind < col) break;
128
+ body.push(line.slice(col));
129
+ this.i++;
130
+ }
131
+ while (body.length && body[body.length - 1] === "" && chomp !== "+") body.pop();
132
+ let s = fold ? body.join("\n").replace(/([^\n])\n(?=[^\n])/g, "$1 ") : body.join("\n");
133
+ if (chomp !== "-" && s !== "") s += "\n";
134
+ return s;
135
+ }
136
+ }
137
+
138
+ function stripComment(s) {
139
+ if (s.startsWith('"') || s.startsWith("'")) return s.trim();
140
+ return s.replace(/\s+#.*$/, "").trim();
141
+ }
142
+
143
+ function unquote(s, r) {
144
+ if (s.startsWith('"')) {
145
+ try {
146
+ return JSON.parse(s);
147
+ } catch {
148
+ r.fail(`can't read the string ${s}`);
149
+ }
150
+ }
151
+ if (s.startsWith("'")) return s.slice(1, -1).replace(/''/g, "'");
152
+ return s;
153
+ }
154
+
155
+ function scalar(s, r) {
156
+ if (s.startsWith("&") || s.startsWith("*") || s.startsWith("!")) r.fail("anchors, aliases and tags aren't supported");
157
+ if (s.startsWith('"')) {
158
+ if (!/"$/.test(s) || s.length < 2) r.fail("a string isn't closed on its line");
159
+ return unquote(s, r);
160
+ }
161
+ if (s.startsWith("'")) {
162
+ if (!/'$/.test(s) || s.length < 2) r.fail("a string isn't closed on its line");
163
+ return unquote(s, r);
164
+ }
165
+ if (s.startsWith("[")) {
166
+ if (!s.endsWith("]")) r.fail("a [ list isn't closed on its line");
167
+ const inner = s.slice(1, -1).trim();
168
+ return inner === "" ? [] : inner.split(",").map((x) => scalar(x.trim(), r));
169
+ }
170
+ if (s.startsWith("{")) {
171
+ if (s === "{}") return {};
172
+ r.fail("{ } maps on one line aren't supported");
173
+ }
174
+ if (s === "~" || s === "null" || s === "Null" || s === "NULL") return null;
175
+ if (/^(true|True|TRUE)$/.test(s)) return true;
176
+ if (/^(false|False|FALSE)$/.test(s)) return false;
177
+ if (/^[+-]?\d+(\.\d+)?$/.test(s)) return Number(s);
178
+ return s;
179
+ }
package/src/server.js CHANGED
@@ -66,7 +66,7 @@ export async function requireFeature(url, feature, what) {
66
66
  if (old) throw new Error(old);
67
67
  if (!info.features.includes(feature)) {
68
68
  throw new Error(
69
- `${url} runs an older OALO than this CLI, so \`${what}\` isn't available there yet. It will be once the workspace is updated.`,
69
+ `${url} runs an older Oalo than this CLI, so \`${what}\` isn't available there yet. It will be once the workspace is updated.`,
70
70
  );
71
71
  }
72
72
  return info;