@hellix-ai/cubes 0.1.2 → 0.1.4

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 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 # your sessions
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 { formatTable, isUuid, listSessions, LIVE, resolveSession, stateOf } from "./sessions.js";
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] list sessions (--all includes spawned ones)
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 rows = await listSessions(api, { all: a.flags.has("all") });
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("No sessions.\n");
174
+ process.stderr.write(`No ${live ? "live " : ""}sessions${mine ? " of yours" : ""}.\n`);
170
175
  return;
171
176
  }
172
- process.stdout.write(formatTable(rows, process.stdout.columns ?? 120) + "\n");
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
- export function formatTable(rows, width, now = Date.now()) {
68
- const head = ["ID", "STATE", "AGE", "TITLE", "GROUP"];
69
- const body = rows.map((s) => [
70
- s.id.slice(0, 8),
71
- stateOf(s),
72
- age(s.last_attached_at ?? s.created_at, now),
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 groupW = Math.min(w(4), 32);
79
- const titleW = Math.max(10, Math.min(w(3), width - fixed.reduce((a, b) => a + b, 0) - groupW - 8));
80
- const line = (r) => [r[0].padEnd(fixed[0]), r[1].padEnd(fixed[1]), r[2].padStart(fixed[2]),
81
- cut(r[3], titleW).padEnd(titleW), cut(r[4], groupW)].join(" ").trimEnd();
82
- return [line(head), ...body.map(line)].join("\n");
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
  }
package/dist/upgrade.js CHANGED
@@ -37,12 +37,19 @@ export function installerFor(scriptPath) {
37
37
  return "yarn";
38
38
  return "npm";
39
39
  }
40
+ /**
41
+ * ⚠ FORCE A FRESH LOOK AT THE REGISTRY. npm keeps a local copy of a package's version list and trusts
42
+ * it for the registry's `max-age=300`, so within five minutes of any earlier install it does not know a
43
+ * release we just read from `/latest` exists: measured as `ETARGET No matching version found for
44
+ * @hellix-ai/cubes@0.1.3` four minutes after 0.1.3 was published. `--prefer-online` makes npm revalidate
45
+ * that copy; bun's `--no-cache` does the same for its manifest cache. pnpm and yarn revalidate already.
46
+ */
40
47
  export function installCommand(installer, spec) {
41
48
  switch (installer) {
42
49
  case "pnpm": return ["pnpm", "add", "-g", spec];
43
- case "bun": return ["bun", "add", "-g", spec];
50
+ case "bun": return ["bun", "add", "-g", "--no-cache", spec];
44
51
  case "yarn": return ["yarn", "global", "add", spec];
45
- default: return ["npm", "install", "-g", spec];
52
+ default: return ["npm", "install", "-g", "--prefer-online", spec];
46
53
  }
47
54
  }
48
55
  export async function latestVersion(pkg, fetchImpl = fetch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellix-ai/cubes",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Attach to your Cubes sessions from your own terminal (Warp, iTerm, Ghostty, anything).",
5
5
  "type": "module",
6
6
  "bin": {