@modelstatus/cli 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/LICENSE +21 -0
- package/README.md +124 -0
- package/package.json +55 -0
- package/src/api.js +85 -0
- package/src/auth.js +44 -0
- package/src/config.js +63 -0
- package/src/detect/core.js +58 -0
- package/src/index.js +337 -0
- package/src/openUrl.js +29 -0
- package/src/redact.js +28 -0
- package/src/registry/fetch.js +93 -0
- package/src/registry/local.js +34 -0
- package/src/registry/root-keys.js +12 -0
- package/src/registry/sign.js +17 -0
- package/src/registry/verify.js +67 -0
- package/src/scan.js +15 -0
- package/src/sources/aws.js +63 -0
- package/src/sources/configscan.js +88 -0
- package/src/sources/env.js +21 -0
- package/src/sources/filesystem.js +95 -0
- package/src/sources/helm.js +42 -0
- package/src/sources/index.js +63 -0
- package/src/sources/k8s.js +51 -0
- package/src/sources/shell.js +35 -0
- package/src/sources/sql.js +47 -0
- package/src/tui/app.js +139 -0
- package/src/tui/ui.js +47 -0
- package/src/tui/views/account.js +76 -0
- package/src/tui/views/add.js +84 -0
- package/src/tui/views/alerts.js +160 -0
- package/src/tui/views/inventory.js +102 -0
- package/src/tui/views/scan.js +125 -0
- package/src/tui/views/whatsnew.js +177 -0
- package/src/upgrade.js +29 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { h, useAsync, cell, clampCursor } from "../ui.js";
|
|
4
|
+
import { collectFrom } from "../../sources/index.js";
|
|
5
|
+
import { loadConfig, setConfigValue } from "../../config.js";
|
|
6
|
+
|
|
7
|
+
const TABS = ["Registry", "Alerts", "Drift"];
|
|
8
|
+
|
|
9
|
+
export function WhatsNewView({ client, dir, ui, active }) {
|
|
10
|
+
const [tab, setTab] = React.useState(0);
|
|
11
|
+
const [cursor, setCursor] = React.useState(0);
|
|
12
|
+
const lastSeen = loadConfig().lastEventsSeenAt || null;
|
|
13
|
+
|
|
14
|
+
const reg = useAsync(async () => {
|
|
15
|
+
const [ev, m, p] = await Promise.all([
|
|
16
|
+
client.registryEvents(),
|
|
17
|
+
client.registryModels({ limit: 1000 }),
|
|
18
|
+
client.raw("GET", "/registry/providers"),
|
|
19
|
+
]);
|
|
20
|
+
const models = new Map((m.data || []).map((x) => [x.id, x.display_name]));
|
|
21
|
+
const provs = new Map((p.data || []).map((x) => [x.id, x.name]));
|
|
22
|
+
return { events: ev.data || [], models, provs };
|
|
23
|
+
}, []);
|
|
24
|
+
const notif = useAsync(async () => (await client.listNotifications({})).data || [], []);
|
|
25
|
+
const [drift, setDrift] = React.useState(null);
|
|
26
|
+
|
|
27
|
+
async function runDrift() {
|
|
28
|
+
setDrift({ loading: true });
|
|
29
|
+
try {
|
|
30
|
+
const patterns = await client.detectionPatterns();
|
|
31
|
+
const cands = await collectFrom(["filesystem"], { root: dir }, patterns);
|
|
32
|
+
const uniq = [...new Set(cands.map((c) => c.model_string))];
|
|
33
|
+
const resolved = uniq.length ? (await client.resolve(uniq)).data || [] : [];
|
|
34
|
+
const idByStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r.model_id]));
|
|
35
|
+
const codeKeys = new Map();
|
|
36
|
+
for (const c of cands) {
|
|
37
|
+
const mid = idByStr.get(c.model_string.toLowerCase()) || null;
|
|
38
|
+
const ref = mid ? mid : "custom:" + c.model_string.toLowerCase();
|
|
39
|
+
const key = `${ref}|${c.source_path}`;
|
|
40
|
+
if (!codeKeys.has(key)) codeKeys.set(key, c);
|
|
41
|
+
}
|
|
42
|
+
const cliUsages = ((await client.listUsages({})).data || []).filter((u) => u.discovered_by === "cli");
|
|
43
|
+
const trackedKeys = new Map();
|
|
44
|
+
for (const u of cliUsages) {
|
|
45
|
+
const ref = u.model_id ? u.model_id : "custom:" + (u.custom_model_name || "").toLowerCase();
|
|
46
|
+
trackedKeys.set(`${ref}|${u.source_path}`, u);
|
|
47
|
+
}
|
|
48
|
+
const added = [...codeKeys.entries()].filter(([k]) => !trackedKeys.has(k)).map(([, c]) => c);
|
|
49
|
+
const gone = [...trackedKeys.entries()].filter(([k]) => !codeKeys.has(k)).map(([, u]) => u);
|
|
50
|
+
const present = [...codeKeys.keys()].filter((k) => trackedKeys.has(k)).length;
|
|
51
|
+
setDrift({ loading: false, added, gone, present });
|
|
52
|
+
setCursor(0);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
setDrift({ loading: false, error: e.message });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
useInput(
|
|
59
|
+
(input, key) => {
|
|
60
|
+
if (!active) return;
|
|
61
|
+
if (key.leftArrow) return (setTab((t) => (t + TABS.length - 1) % TABS.length), setCursor(0));
|
|
62
|
+
if (key.rightArrow) return (setTab((t) => (t + 1) % TABS.length), setCursor(0));
|
|
63
|
+
if (key.downArrow || input === "j") return setCursor((c) => c + 1);
|
|
64
|
+
if (key.upArrow || input === "k") return setCursor((c) => Math.max(0, c - 1));
|
|
65
|
+
|
|
66
|
+
if (tab === 0) {
|
|
67
|
+
if (input === "m") {
|
|
68
|
+
setConfigValue("lastEventsSeenAt", new Date().toISOString());
|
|
69
|
+
ui.showToast("marked all as seen");
|
|
70
|
+
}
|
|
71
|
+
} else if (tab === 1) {
|
|
72
|
+
const list = notif.data || [];
|
|
73
|
+
const cur = list[clampCursor(cursor, list.length)];
|
|
74
|
+
if (input === "o" && cur)
|
|
75
|
+
client.readNotification(cur.id).then(() => {
|
|
76
|
+
ui.showToast("marked read");
|
|
77
|
+
notif.reload();
|
|
78
|
+
}).catch((e) => ui.showToast(e.message, "red"));
|
|
79
|
+
} else if (tab === 2) {
|
|
80
|
+
if (input === "r") return runDrift();
|
|
81
|
+
const gone = drift?.gone || [];
|
|
82
|
+
const cur = gone[clampCursor(cursor, gone.length)];
|
|
83
|
+
if (input === "a" && cur)
|
|
84
|
+
client.deleteUsage(cur.id).then(() => {
|
|
85
|
+
ui.showToast("archived");
|
|
86
|
+
runDrift();
|
|
87
|
+
}).catch((e) => ui.showToast(e.message, "red"));
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{ isActive: active },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const tabBar = h(
|
|
94
|
+
Box,
|
|
95
|
+
{},
|
|
96
|
+
...TABS.map((t, i) =>
|
|
97
|
+
h(Text, { key: t, color: i === tab ? "black" : "gray", backgroundColor: i === tab ? "cyan" : undefined }, ` ${t} `),
|
|
98
|
+
),
|
|
99
|
+
h(Text, { color: "gray" }, " (←/→ switch)"),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let body, footer;
|
|
103
|
+
if (tab === 0) {
|
|
104
|
+
footer = "←/→ tab · ↑↓ scroll · m mark seen";
|
|
105
|
+
if (reg.loading) body = h(Text, { color: "gray" }, "Loading registry changes…");
|
|
106
|
+
else if (reg.error) body = h(Text, { color: "red" }, reg.error);
|
|
107
|
+
else {
|
|
108
|
+
const events = reg.data.events;
|
|
109
|
+
if (!events.length) body = h(Text, { color: "gray" }, "No registry changes recorded yet.");
|
|
110
|
+
else {
|
|
111
|
+
const start = clampCursor(cursor, events.length);
|
|
112
|
+
body = h(
|
|
113
|
+
Box,
|
|
114
|
+
{ flexDirection: "column" },
|
|
115
|
+
...events.slice(start, start + 12).map((e) => {
|
|
116
|
+
const name = e.model_id ? reg.data.models.get(e.model_id) : reg.data.provs.get(e.provider_id);
|
|
117
|
+
const isNew = lastSeen && e.published_at && e.published_at > lastSeen;
|
|
118
|
+
return h(
|
|
119
|
+
Text,
|
|
120
|
+
{ key: e.id, color: isNew ? "yellow" : "white" },
|
|
121
|
+
`${isNew ? "• " : " "}${cell(e.event_type, 18)} ${cell(name || "—", 26)} ${String(e.published_at || "").slice(0, 10)}`,
|
|
122
|
+
);
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if (tab === 1) {
|
|
128
|
+
footer = "←/→ tab · ↑↓ move · o mark read";
|
|
129
|
+
const list = notif.data || [];
|
|
130
|
+
if (notif.loading) body = h(Text, { color: "gray" }, "Loading alerts…");
|
|
131
|
+
else if (!list.length) body = h(Text, { color: "gray" }, "No alerts yet. Configure rules in the Alerts view.");
|
|
132
|
+
else
|
|
133
|
+
body = h(
|
|
134
|
+
Box,
|
|
135
|
+
{ flexDirection: "column" },
|
|
136
|
+
...list.slice(0, 12).map((n, i) =>
|
|
137
|
+
h(
|
|
138
|
+
Text,
|
|
139
|
+
{ key: n.id, inverse: i === clampCursor(cursor, list.length), color: n.unread ? "white" : "gray" },
|
|
140
|
+
`${n.unread ? "●" : "○"} ${cell(n.title, 40)} ${String(n.when || "").slice(0, 10)}`,
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
footer = "←/→ tab · r scan for drift · ↑↓ move · a archive (gone)";
|
|
146
|
+
if (!drift) body = h(Text, { color: "gray" }, `Press r to scan ${dir} and compare against tracked usages.`);
|
|
147
|
+
else if (drift.loading) body = h(Text, { color: "gray" }, "Scanning for drift…");
|
|
148
|
+
else if (drift.error) body = h(Text, { color: "red" }, drift.error);
|
|
149
|
+
else {
|
|
150
|
+
const gone = drift.gone || [];
|
|
151
|
+
body = h(
|
|
152
|
+
Box,
|
|
153
|
+
{ flexDirection: "column" },
|
|
154
|
+
h(
|
|
155
|
+
Text,
|
|
156
|
+
{},
|
|
157
|
+
h(Text, { color: "green" }, `${drift.added.length} new in code`),
|
|
158
|
+
h(Text, { color: "gray" }, ` · ${drift.present} still present · `),
|
|
159
|
+
h(Text, { color: "red" }, `${gone.length} gone from code`),
|
|
160
|
+
),
|
|
161
|
+
h(Text, {}, ""),
|
|
162
|
+
...drift.added.slice(0, 5).map((c, i) =>
|
|
163
|
+
h(Text, { key: "a" + i, color: "green" }, ` + ${cell(c.display || c.model_string, 24)} ${c.location_label}`),
|
|
164
|
+
),
|
|
165
|
+
...gone.slice(0, 7).map((u, i) =>
|
|
166
|
+
h(
|
|
167
|
+
Text,
|
|
168
|
+
{ key: "g" + i, inverse: i === clampCursor(cursor, gone.length), color: "red" },
|
|
169
|
+
` - ${cell(u.model_display || u.custom_model_name || "?", 24)} ${u.source_path || "—"}`,
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return h(Box, { flexDirection: "column" }, tabBar, h(Text, {}, ""), body, h(Text, {}, ""), h(Text, { color: "gray" }, footer));
|
|
177
|
+
}
|
package/src/upgrade.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { openUrl } from "./openUrl.js";
|
|
2
|
+
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
|
|
5
|
+
/** Open Stripe checkout and poll /me until the plan flips off "free". Mirrors
|
|
6
|
+
* the browser-login poll UX. Returns the new plan, or null on timeout. */
|
|
7
|
+
export async function upgradeViaBrowser({ client, log = console.error, onTick }) {
|
|
8
|
+
const { url } = await client.checkout();
|
|
9
|
+
if (!url) throw new Error("Could not start checkout.");
|
|
10
|
+
log(`\n Opening checkout in your browser…`);
|
|
11
|
+
log(` If it doesn't open, visit:\n ${url}\n`);
|
|
12
|
+
openUrl(url);
|
|
13
|
+
|
|
14
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
15
|
+
while (Date.now() < deadline) {
|
|
16
|
+
await sleep(4000);
|
|
17
|
+
try {
|
|
18
|
+
const me = await client.me();
|
|
19
|
+
onTick?.(me);
|
|
20
|
+
if (me?.account?.plan && me.account.plan !== "free") {
|
|
21
|
+
log(`\n ✓ Upgraded to ${me.account.plan}. Thank you!\n`);
|
|
22
|
+
return me.account.plan;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
/* keep polling */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|