@infrarates/cli 0.0.0-stage → 0.1.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/commands.mjs ADDED
@@ -0,0 +1,183 @@
1
+ // Customer routes only. Each command declares its authentication and options.
2
+ // The website console (src/ui/console.ts) keeps a copy; test/console.test.ts fails when they drift.
3
+ export const commands = [
4
+ [
5
+ "prices history",
6
+ "GET",
7
+ "/v1/prices/history",
8
+ "key",
9
+ ["providers", "alias", "as-of", "limit", "cursor", "all", "format"],
10
+ ],
11
+ [
12
+ "prices latest",
13
+ "GET",
14
+ "/api/prices/latest",
15
+ "key",
16
+ [
17
+ "provider",
18
+ "resource",
19
+ "category",
20
+ "dimension",
21
+ "tier",
22
+ "limit",
23
+ "offset",
24
+ "sort",
25
+ "dir",
26
+ "prefer",
27
+ "still-listed",
28
+ ],
29
+ ],
30
+ [
31
+ "prices resource-history",
32
+ "GET",
33
+ "/api/prices/history",
34
+ "key",
35
+ [
36
+ "provider",
37
+ "resource",
38
+ "source",
39
+ "category",
40
+ "dimension",
41
+ "tier",
42
+ "unit",
43
+ "from",
44
+ "to",
45
+ "as-of",
46
+ ],
47
+ ],
48
+ ["terms latest", "GET", "/api/tos/latest", "key", ["provider", "doc"]],
49
+ ["terms history", "GET", "/api/tos/history", "key", ["provider", "doc"]],
50
+ ["terms diff", "GET", "/api/tos/diff", "key", ["provider", "doc", "a", "b"]],
51
+ [
52
+ "terms content <key>",
53
+ "GET",
54
+ "/api/snapshots/:key/content",
55
+ "key",
56
+ ["prefix", "name"],
57
+ ],
58
+ ["status", "GET", "/api/health", "key", []],
59
+ ["providers list", "GET", "/v1/providers", "key", []],
60
+ ["catalog", "GET", "/v1/catalog", "key", ["provider"]],
61
+ ["activity", "GET", "/v1/activity", "key", []],
62
+ ["compare resources", "GET", "/v1/comparison/resources", "key", []],
63
+ [
64
+ "compare prices",
65
+ "GET",
66
+ "/v1/comparison/prices",
67
+ "key",
68
+ ["resource", "conversion"],
69
+ ],
70
+ ["compare aliases", "GET", "/v1/comparison/aliases", "key", []],
71
+ [
72
+ "compare proposals list",
73
+ "GET",
74
+ "/v1/comparison/alias-proposals",
75
+ "session",
76
+ ["after"],
77
+ ],
78
+ [
79
+ "compare proposals create",
80
+ "POST",
81
+ "/v1/comparison/alias-proposals",
82
+ "session",
83
+ ["data"],
84
+ ],
85
+ ["me", "GET", "/v1/me", "session", []],
86
+ ["organizations get", "GET", "/v1/organization", "session", []],
87
+ ["organizations create", "POST", "/v1/organizations", "session", ["data"]],
88
+ [
89
+ "organizations switch",
90
+ "POST",
91
+ "/v1/session/organization",
92
+ "session",
93
+ ["data"],
94
+ ],
95
+ ["webhooks list", "GET", "/v1/webhooks", "session", []],
96
+ ["webhooks get <id>", "GET", "/v1/webhooks/:id", "session", []],
97
+ ["webhooks create", "POST", "/v1/webhooks", "session", ["data"]],
98
+ ["webhooks update <id>", "PUT", "/v1/webhooks/:id", "session", ["data"]],
99
+ ["webhooks delete <id>", "DELETE", "/v1/webhooks/:id", "session", []],
100
+ ["webhooks secret <id>", "GET", "/v1/webhooks/:id/secret", "session", []],
101
+ [
102
+ "webhooks rotate-secret <id>",
103
+ "POST",
104
+ "/v1/webhooks/:id/secret/rotate",
105
+ "session",
106
+ [],
107
+ ],
108
+ ["webhooks test <id>", "POST", "/v1/webhooks/:id/test", "session", []],
109
+ [
110
+ "webhooks deliveries <id>",
111
+ "GET",
112
+ "/v1/webhooks/:id/deliveries",
113
+ "session",
114
+ [],
115
+ ],
116
+ [
117
+ "webhooks resend <id> <eventId>",
118
+ "POST",
119
+ "/v1/webhooks/:id/deliveries/:eventId/resend",
120
+ "session",
121
+ [],
122
+ ],
123
+ ["members list", "GET", "/v1/members", "session", []],
124
+ ["members update <id>", "PATCH", "/v1/members/:id", "session", ["data"]],
125
+ ["members delete <id>", "DELETE", "/v1/members/:id", "session", []],
126
+ ["invitations create", "POST", "/v1/invitations", "session", ["data"]],
127
+ ["invitations delete <id>", "DELETE", "/v1/invitations/:id", "session", []],
128
+ ["api-keys list", "GET", "/v1/api-keys", "session", []],
129
+ ["api-keys create", "POST", "/v1/api-keys", "session", ["data"]],
130
+ ["api-keys delete <id>", "DELETE", "/v1/api-keys/:id", "session", []],
131
+ ["sources list", "GET", "/v1/sources", "session", []],
132
+ ["incidents list", "GET", "/v1/incidents", "session", []],
133
+ ["preferences get", "GET", "/v1/preferences", "session", []],
134
+ ["preferences update", "PUT", "/v1/preferences", "session", ["data"]],
135
+ [
136
+ "provider-requests list",
137
+ "GET",
138
+ "/v1/provider-requests",
139
+ "session",
140
+ ["after"],
141
+ ],
142
+ [
143
+ "provider-requests get <id>",
144
+ "GET",
145
+ "/v1/provider-requests/:id",
146
+ "session",
147
+ [],
148
+ ],
149
+ [
150
+ "provider-requests create",
151
+ "POST",
152
+ "/v1/provider-requests",
153
+ "session",
154
+ ["data"],
155
+ ],
156
+ ];
157
+
158
+ export function resolveCommand(positionals) {
159
+ for (const [name, method, template, auth, options] of commands) {
160
+ const parts = name.split(" ");
161
+ if (
162
+ parts.length !== positionals.length ||
163
+ !parts.every((p, i) => p.startsWith("<") || p === positionals[i])
164
+ )
165
+ continue;
166
+ let path = template;
167
+ parts.forEach((p, i) => {
168
+ if (!p.startsWith("<")) return;
169
+ const value = positionals[i];
170
+ if (
171
+ !/^[a-zA-Z0-9_.-]{1,200}$/.test(value) ||
172
+ value === "." ||
173
+ value === ".."
174
+ )
175
+ throw new Error("Invalid resource ID.");
176
+ if (p === "<key>" && !/^tos-[a-z0-9-]+$/.test(value))
177
+ throw new Error("Expected a terms snapshot key: tos-<provider>-<doc>.");
178
+ path = path.replace(`:${p.slice(1, -1)}`, encodeURIComponent(value));
179
+ });
180
+ return { name, method, path, auth, options };
181
+ }
182
+ throw new Error("Unknown command. Use --help for available commands.");
183
+ }
@@ -0,0 +1,162 @@
1
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
2
+ const characters = (text) =>
3
+ [...segmenter.segment(text)].map((part) => part.segment);
4
+ const charWidth = (char) =>
5
+ /[\p{Extended_Pictographic}\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6]/u.test(
6
+ char,
7
+ )
8
+ ? 2
9
+ : 1;
10
+ const width = (text) =>
11
+ characters(text).reduce((total, char) => total + charWidth(char), 0);
12
+ // API values are untrusted terminal text; render controls visibly, including OSC/CSI escapes.
13
+ const visible = (char) =>
14
+ `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
15
+ const text = (value) =>
16
+ value === null || value === undefined || value === ""
17
+ ? "-"
18
+ : String(value).replace(
19
+ /[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,
20
+ visible,
21
+ );
22
+ /** A multi-line document for a terminal: line breaks and tabs stay, other controls show escaped. */
23
+ export const terminalText = (value) =>
24
+ value
25
+ .replace(/\r\n/g, "\n")
26
+ .replace(
27
+ /[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,
28
+ visible,
29
+ );
30
+ const quote = (value) => `'${text(value).replaceAll("'", "'\\''")}'`;
31
+ function wrap(value, size) {
32
+ const lines = [];
33
+ let line = "",
34
+ used = 0;
35
+ for (const char of characters(value)) {
36
+ const next = charWidth(char);
37
+ if (used + next > size && line) {
38
+ lines.push(line);
39
+ line = "";
40
+ used = 0;
41
+ }
42
+ line += char;
43
+ used += next;
44
+ }
45
+ lines.push(line);
46
+ return lines;
47
+ }
48
+ function detailsLines(details, size) {
49
+ const lines = [];
50
+ let line = "";
51
+ for (const detail of details) {
52
+ if (line && width(`${line} | ${detail}`) > size) {
53
+ lines.push(line);
54
+ line = "";
55
+ }
56
+ if (width(detail) > size) lines.push(...wrap(detail, size));
57
+ else line = line ? `${line} | ${detail}` : detail;
58
+ }
59
+ if (line) lines.push(line);
60
+ return lines;
61
+ }
62
+
63
+ /** A display-only projection: exact prices and currencies retain their native units. */
64
+ export function formatHistory(
65
+ history,
66
+ { columns = 100, baseUrl, limit, config } = {},
67
+ ) {
68
+ const available =
69
+ Number.isSafeInteger(columns) && columns > 0 ? columns : 100;
70
+ const rows = history.prices.map((row) => ({
71
+ cells: [
72
+ text(row?.provider),
73
+ text(row?.resource),
74
+ text(row?.dimension),
75
+ `${text(row?.price)} ${text(row?.currency)}`,
76
+ text(row?.unit),
77
+ ],
78
+ details: [
79
+ `Tier: ${text(row?.pricingTier)}`,
80
+ `Source: ${text(row?.source)}`,
81
+ `Recorded: ${text(row?.recordedAt)}`,
82
+ ...(row?.effectiveDate ? [`Effective: ${text(row.effectiveDate)}`] : []),
83
+ ...(row?.renamedTo ? [`Renamed to: ${text(row.renamedTo)}`] : []),
84
+ ],
85
+ }));
86
+ const headers = ["PROVIDER", "RESOURCE", "DIMENSION", "PRICE", "UNIT"];
87
+ const maximum = [16, 40, 24, Infinity, 28];
88
+ const minimum = [8, 12, 9, 5, 10];
89
+ const sizes = headers.map((header, index) =>
90
+ Math.min(
91
+ maximum[index],
92
+ rows.reduce(
93
+ (largest, row) => Math.max(largest, width(row.cells[index])),
94
+ width(header),
95
+ ),
96
+ ),
97
+ );
98
+ // Keep the full rate together; shrink descriptive columns and wrap their contents.
99
+ const total = () => sizes.reduce((sum, size) => sum + size, 0) + 8;
100
+ for (const index of [1, 4, 2, 0]) {
101
+ sizes[index] = Math.max(
102
+ Math.min(minimum[index], sizes[index]),
103
+ sizes[index] - Math.max(0, total() - available),
104
+ );
105
+ }
106
+ const output = [
107
+ `Price history (${rows.length} ${rows.length === 1 ? "observation" : "observations"})`,
108
+ `As of: ${text(history.asOf)}`,
109
+ `Providers: ${history.providers.map(text).join(", ")}`,
110
+ ...(history.alias
111
+ ? [
112
+ `Alias: ${text(history.alias.name)} (revision ${text(history.alias.revision)}, ${history.alias.members.length} members)`,
113
+ ]
114
+ : []),
115
+ "",
116
+ ];
117
+ if (!rows.length) output.push("No reviewed prices found for this snapshot.");
118
+ else if (total() > available) {
119
+ for (const row of rows) {
120
+ output.push(...wrap(`${row.cells[0]} / ${row.cells[1]}`, available));
121
+ output.push(...wrap(`Dimension: ${row.cells[2]}`, available));
122
+ output.push(`Price: ${row.cells[3]}`);
123
+ output.push(...wrap(`Unit: ${row.cells[4]}`, available));
124
+ for (const detail of row.details) output.push(...wrap(detail, available));
125
+ output.push("");
126
+ }
127
+ } else {
128
+ const line = (cells) =>
129
+ cells
130
+ .map(
131
+ (cell, i) => cell + " ".repeat(Math.max(0, sizes[i] - width(cell))),
132
+ )
133
+ .join(" ")
134
+ .trimEnd();
135
+ output.push(
136
+ line(headers),
137
+ sizes.map((size) => "─".repeat(size)).join(" "),
138
+ );
139
+ for (const row of rows) {
140
+ const cells = row.cells.map((cell, i) => wrap(cell, sizes[i]));
141
+ for (
142
+ let index = 0;
143
+ index < Math.max(...cells.map((cell) => cell.length));
144
+ index++
145
+ )
146
+ output.push(line(cells.map((cell) => cell[index] ?? "")));
147
+ for (const detail of detailsLines(
148
+ row.details,
149
+ Math.max(1, available - 2),
150
+ ))
151
+ output.push(` ${detail}`);
152
+ output.push("");
153
+ }
154
+ }
155
+ if (history.nextCursor)
156
+ output.push(
157
+ "More history is available. Continue with:",
158
+ `oimo prices history --cursor ${quote(history.nextCursor)}${baseUrl ? ` --base-url ${quote(baseUrl)}` : ""}${limit ? ` --limit ${quote(limit)}` : ""}${config ? ` --config ${quote(config)}` : ""}`,
159
+ "Use --all to fetch every page.",
160
+ );
161
+ return `${output.join("\n").trimEnd()}\n`;
162
+ }
package/oimo.d.mts ADDED
@@ -0,0 +1,15 @@
1
+ import type { browserLogin } from "./browser-login.mjs";
2
+
3
+ export function run(
4
+ args: string[],
5
+ options?: {
6
+ env?: Record<string, string | undefined>;
7
+ fetcher?: (url: URL, init: RequestInit) => Promise<Response>;
8
+ write?: (text: string) => void;
9
+ readInput?: (hidden?: boolean) => Promise<string>;
10
+ loginBrowser?: typeof browserLogin;
11
+ report?: (text: string) => void;
12
+ isTTY?: boolean;
13
+ columns?: number;
14
+ },
15
+ ): Promise<void>;