@hellix-ai/cubes 0.1.2 → 0.1.3
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 +6 -2
- package/dist/cli.js +11 -5
- package/dist/sessions.js +79 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Terminal.app, anything) with your own font, colors and keyboard shortcuts.
|
|
|
7
7
|
npm install -g @hellix-ai/cubes # needs Node.js 22.4+
|
|
8
8
|
|
|
9
9
|
cubes login --host cubes.hellix.ai # prints a code; approve it in the browser
|
|
10
|
-
cubes ls #
|
|
10
|
+
cubes ls # sessions, under a heading per group (--mine, --live)
|
|
11
11
|
cubes attach # pick a running one
|
|
12
12
|
cubes attach 7ff4 # or name one: an id prefix, or part of its title
|
|
13
13
|
```
|
|
@@ -42,11 +42,15 @@ cubes login [--host URL] [--org ALIAS] [--no-browser]
|
|
|
42
42
|
cubes logout
|
|
43
43
|
cubes whoami
|
|
44
44
|
cubes org [ALIAS]
|
|
45
|
-
cubes ls [--all] [--json]
|
|
45
|
+
cubes ls [--mine] [--live] [--all] [--json]
|
|
46
46
|
cubes attach [SESSION] [--view] [--no-resume]
|
|
47
47
|
cubes upgrade [--check]
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`cubes ls` lists sessions the way the web's Sessions page does: a section per package, then per group
|
|
51
|
+
(A-Z), then Ungrouped, live ones first in each. `--mine` keeps the ones you started, `--live` the ones
|
|
52
|
+
running now; they combine. `--all` adds sessions other sessions spawned; `--json` prints the rows.
|
|
53
|
+
|
|
50
54
|
`--org ALIAS` works on any command. If you belong to several organizations, `cubes org` lists them and
|
|
51
55
|
`cubes org <alias>` changes the default.
|
|
52
56
|
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { readClipboardImage } from "./clipboard.js";
|
|
|
14
14
|
import { CubeSpinner } from "./cube.js";
|
|
15
15
|
import { clearLogin, configPath, loadLogin, saveLogin } from "./config.js";
|
|
16
16
|
import { parseArgs } from "./args.js";
|
|
17
|
-
import {
|
|
17
|
+
import { formatSections, isUuid, listSessions, LIVE, resolveSession, sectionize, stateOf } from "./sessions.js";
|
|
18
18
|
import { upgrade } from "./upgrade.js";
|
|
19
19
|
const PKG = createRequire(import.meta.url)("../package.json");
|
|
20
20
|
const VERSION = PKG.version;
|
|
@@ -25,7 +25,8 @@ Usage:
|
|
|
25
25
|
cubes logout sign out and revoke this computer's login
|
|
26
26
|
cubes whoami who, where, and which org
|
|
27
27
|
cubes org [ALIAS] list your orgs, or switch the default
|
|
28
|
-
cubes ls [--all] [--json]
|
|
28
|
+
cubes ls [--mine] [--live] [--all] [--json] list sessions by group (--mine: you started them,
|
|
29
|
+
--live: running now, --all: include spawned ones)
|
|
29
30
|
cubes attach [SESSION] [--view] [--no-resume] attach to a session
|
|
30
31
|
cubes upgrade [--check] install the newest cubes (--check only reports)
|
|
31
32
|
|
|
@@ -160,16 +161,21 @@ async function cmdOrg(a) {
|
|
|
160
161
|
}
|
|
161
162
|
async function cmdLs(a) {
|
|
162
163
|
const api = makeApi(currentSession({ org: str(a.flags, "org") }));
|
|
163
|
-
const
|
|
164
|
+
const mine = a.flags.has("mine");
|
|
165
|
+
const live = a.flags.has("live");
|
|
166
|
+
// Both narrow, and they compose: `--mine --live` is "my sessions that are running now".
|
|
167
|
+
const rows = (await listSessions(api, { all: a.flags.has("all") }))
|
|
168
|
+
.filter((r) => (!mine || r.created_by_me === true) && (!live || LIVE.has(r.status)));
|
|
164
169
|
if (a.flags.has("json")) {
|
|
165
170
|
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
166
171
|
return;
|
|
167
172
|
}
|
|
168
173
|
if (!rows.length) {
|
|
169
|
-
process.stderr.write(
|
|
174
|
+
process.stderr.write(`No ${live ? "live " : ""}sessions${mine ? " of yours" : ""}.\n`);
|
|
170
175
|
return;
|
|
171
176
|
}
|
|
172
|
-
process.stdout.
|
|
177
|
+
const color = !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
178
|
+
process.stdout.write(formatSections(sectionize(rows), process.stdout.columns ?? 120, { color }) + "\n");
|
|
173
179
|
}
|
|
174
180
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
175
181
|
/** Bring a stopped/hibernated session back and wait until its terminal can be attached. */
|
package/dist/sessions.js
CHANGED
|
@@ -64,20 +64,85 @@ function cut(s, w) {
|
|
|
64
64
|
return "";
|
|
65
65
|
return s.length <= w ? s : s.slice(0, w - 1) + "…";
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
s.title || "(untitled)",
|
|
74
|
-
s.group_name ?? "",
|
|
75
|
-
]);
|
|
67
|
+
/** Column layout shared by every table: widths come from ALL the rows, so the sections of one listing
|
|
68
|
+
* line up with each other. */
|
|
69
|
+
function layout(rows, width, now) {
|
|
70
|
+
const head = ["ID", "STATE", "AGE", "TITLE"];
|
|
71
|
+
const cells = (s) => [s.id.slice(0, 8), stateOf(s), age(s.last_attached_at ?? s.created_at, now), s.title || "(untitled)"];
|
|
72
|
+
const body = rows.map(cells);
|
|
76
73
|
const w = (i) => Math.max(head[i].length, ...body.map((r) => r[i].length));
|
|
77
74
|
const fixed = [w(0), w(1), w(2)];
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
const titleW = Math.max(10, Math.min(w(3), width - fixed.reduce((a, b) => a + b, 0) - 6));
|
|
76
|
+
const line = (r) => [r[0].padEnd(fixed[0]), r[1].padEnd(fixed[1]), r[2].padStart(fixed[2]), cut(r[3], titleW)].join(" ").trimEnd();
|
|
77
|
+
return { header: line(head), row: (s) => line(cells(s)), width: fixed.reduce((a, b) => a + b, 0) + titleW + 6 };
|
|
78
|
+
}
|
|
79
|
+
export function formatTable(rows, width, now = Date.now()) {
|
|
80
|
+
const l = layout(rows, width, now);
|
|
81
|
+
return [l.header, ...rows.map(l.row)].join("\n");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The web Sessions page's sections (web SessionsPage.tsx groupSections), so a listing reads the same in
|
|
85
|
+
* both places: one per PACKAGE (most recently started first), then one per GROUP A-Z, then Ungrouped.
|
|
86
|
+
* Inside each, live sessions first, then the most recently started. Unlike the page, an empty group is
|
|
87
|
+
* not shown - there is nothing to do with an empty heading in a terminal.
|
|
88
|
+
*/
|
|
89
|
+
export function sectionize(rows) {
|
|
90
|
+
const startOf = (r) => (r ? r.started_at ?? r.ended_at ?? r.created_at ?? "" : "");
|
|
91
|
+
const order = (a, b) => {
|
|
92
|
+
const la = LIVE.has(a.status);
|
|
93
|
+
const lb = LIVE.has(b.status);
|
|
94
|
+
if (la !== lb)
|
|
95
|
+
return la ? -1 : 1;
|
|
96
|
+
return startOf(b).localeCompare(startOf(a));
|
|
97
|
+
};
|
|
98
|
+
const pkgs = new Map();
|
|
99
|
+
const groups = new Map();
|
|
100
|
+
const ungrouped = [];
|
|
101
|
+
for (const r of rows) {
|
|
102
|
+
if (r.package_id) {
|
|
103
|
+
const sec = pkgs.get(r.package_id) ?? { title: r.package_name || "Package", rows: [] };
|
|
104
|
+
sec.rows.push(r);
|
|
105
|
+
pkgs.set(r.package_id, sec);
|
|
106
|
+
}
|
|
107
|
+
else if (r.group_id && r.group_name) {
|
|
108
|
+
const sec = groups.get(r.group_id) ?? { title: r.group_name, rows: [] };
|
|
109
|
+
sec.rows.push(r);
|
|
110
|
+
groups.set(r.group_id, sec);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
ungrouped.push(r);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const sec of [...pkgs.values(), ...groups.values()])
|
|
117
|
+
sec.rows.sort(order);
|
|
118
|
+
const out = [
|
|
119
|
+
...[...pkgs.values()].sort((a, b) => startOf(b.rows[0]).localeCompare(startOf(a.rows[0]))),
|
|
120
|
+
...[...groups.values()].sort((a, b) => a.title.localeCompare(b.title)),
|
|
121
|
+
];
|
|
122
|
+
if (ungrouped.length)
|
|
123
|
+
out.push({ title: "Ungrouped", rows: ungrouped.sort(order) });
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** The listing: one column header, then each section under a rule naming it and its count. A listing
|
|
127
|
+
* that is ALL ungrouped (an org that does not use groups) gets no rule at all - it would only say so. */
|
|
128
|
+
export function formatSections(sections, width, opts = {}) {
|
|
129
|
+
const all = sections.flatMap((s) => s.rows);
|
|
130
|
+
const l = layout(all, width, opts.now ?? Date.now());
|
|
131
|
+
const dim = (t) => (opts.color ? `\x1b[2m${t}\x1b[0m` : t);
|
|
132
|
+
const bold = (t) => (opts.color ? `\x1b[1m${t}\x1b[0m` : t);
|
|
133
|
+
const ruleW = Math.max(24, Math.min(width, l.width));
|
|
134
|
+
const out = [dim(l.header)];
|
|
135
|
+
const bare = sections.length === 1 && sections[0].title === "Ungrouped";
|
|
136
|
+
sections.forEach((sec, i) => {
|
|
137
|
+
if (!bare) {
|
|
138
|
+
const label = `${sec.title} (${sec.rows.length})`;
|
|
139
|
+
const fill = Math.max(2, ruleW - label.length - 4);
|
|
140
|
+
if (i > 0)
|
|
141
|
+
out.push("");
|
|
142
|
+
out.push(`${dim("──")} ${bold(sec.title)} ${dim(`(${sec.rows.length}) ${"─".repeat(fill)}`)}`);
|
|
143
|
+
}
|
|
144
|
+
for (const r of sec.rows)
|
|
145
|
+
out.push(l.row(r));
|
|
146
|
+
});
|
|
147
|
+
return out.join("\n");
|
|
83
148
|
}
|