@aitherium/shell-cli 1.11.1 → 1.12.1

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,250 @@
1
+ /**
2
+ * image-preview.ts — Render raster images INLINE in the terminal as Unicode
3
+ * half-blocks (▀) with 24-bit colour.
4
+ *
5
+ * Why half-blocks instead of sixel / iTerm2 / kitty graphics: those protocols
6
+ * each work in only SOME terminals (sixel needs Windows Terminal ≥1.22, iTerm2's
7
+ * OSC-1337 needs iTerm2/WezTerm, kitty needs kitty) and several need the image
8
+ * re-encoded. The half-block trick — print '▀' with the FOREGROUND set to the top
9
+ * pixel and the BACKGROUND to the bottom pixel, so one character cell shows TWO
10
+ * vertical pixels — works in EVERY 24-bit-colour terminal, including the Windows
11
+ * Terminal the fleet runs on. Decoding uses node's built-in zlib (no new deps),
12
+ * so PNG (the format ComfyUI/AitherCanvas emit) renders for real; other formats
13
+ * fall back to the OS viewer at the call site.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { inflateSync } from 'node:zlib';
17
+ /** Paeth predictor (PNG filter type 4). */
18
+ function paeth(a, b, c) {
19
+ const p = a + b - c;
20
+ const pa = Math.abs(p - a);
21
+ const pb = Math.abs(p - b);
22
+ const pc = Math.abs(p - c);
23
+ if (pa <= pb && pa <= pc)
24
+ return a;
25
+ if (pb <= pc)
26
+ return b;
27
+ return c;
28
+ }
29
+ /**
30
+ * Minimal PNG decoder — enough for the 8-bit images the fleet produces:
31
+ * colour types 0 (grey), 2 (RGB), 3 (palette), 6 (RGBA), and all 5 scanline
32
+ * filters. Returns null for anything it can't handle (interlaced, 16-bit, etc.)
33
+ * so the caller can fall back to the OS viewer.
34
+ */
35
+ export function decodePNG(buf) {
36
+ // Signature.
37
+ const SIG = [137, 80, 78, 71, 13, 10, 26, 10];
38
+ for (let i = 0; i < 8; i++)
39
+ if (buf[i] !== SIG[i])
40
+ return null;
41
+ let pos = 8;
42
+ let width = 0, height = 0, bitDepth = 0, colorType = 0, interlace = 0;
43
+ let palette = null;
44
+ let trns = null;
45
+ const idat = [];
46
+ while (pos < buf.length) {
47
+ const len = buf.readUInt32BE(pos);
48
+ pos += 4;
49
+ const type = buf.toString('ascii', pos, pos + 4);
50
+ pos += 4;
51
+ const data = buf.subarray(pos, pos + len);
52
+ pos += len;
53
+ pos += 4; // CRC — skip.
54
+ if (type === 'IHDR') {
55
+ width = data.readUInt32BE(0);
56
+ height = data.readUInt32BE(4);
57
+ bitDepth = data[8];
58
+ colorType = data[9];
59
+ interlace = data[12];
60
+ }
61
+ else if (type === 'PLTE') {
62
+ palette = new Uint8Array(data);
63
+ }
64
+ else if (type === 'tRNS') {
65
+ trns = new Uint8Array(data);
66
+ }
67
+ else if (type === 'IDAT') {
68
+ idat.push(Buffer.from(data));
69
+ }
70
+ else if (type === 'IEND') {
71
+ break;
72
+ }
73
+ }
74
+ if (!width || !height)
75
+ return null;
76
+ if (bitDepth !== 8)
77
+ return null; // 1/2/4/16-bit unsupported (keep it small)
78
+ if (interlace !== 0)
79
+ return null; // Adam7 unsupported
80
+ const channels = colorType === 2 ? 3 : colorType === 6 ? 4 : colorType === 0 ? 1 : colorType === 3 ? 1 : 0;
81
+ if (channels === 0)
82
+ return null;
83
+ let raw;
84
+ try {
85
+ raw = inflateSync(Buffer.concat(idat));
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ const stride = width * channels;
91
+ const out = new Uint8Array(width * height * 4);
92
+ const line = new Uint8Array(stride);
93
+ const prev = new Uint8Array(stride);
94
+ let rp = 0;
95
+ for (let y = 0; y < height; y++) {
96
+ const filter = raw[rp++];
97
+ for (let x = 0; x < stride; x++) {
98
+ const rawByte = raw[rp++];
99
+ const a = x >= channels ? line[x - channels] : 0;
100
+ const b = prev[x];
101
+ const c = x >= channels ? prev[x - channels] : 0;
102
+ let val;
103
+ switch (filter) {
104
+ case 0:
105
+ val = rawByte;
106
+ break;
107
+ case 1:
108
+ val = rawByte + a;
109
+ break;
110
+ case 2:
111
+ val = rawByte + b;
112
+ break;
113
+ case 3:
114
+ val = rawByte + ((a + b) >> 1);
115
+ break;
116
+ case 4:
117
+ val = rawByte + paeth(a, b, c);
118
+ break;
119
+ default:
120
+ val = rawByte;
121
+ break;
122
+ }
123
+ line[x] = val & 0xff;
124
+ }
125
+ // Expand the scanline into RGBA.
126
+ for (let x = 0; x < width; x++) {
127
+ const o = (y * width + x) * 4;
128
+ if (colorType === 2) { // RGB
129
+ out[o] = line[x * 3];
130
+ out[o + 1] = line[x * 3 + 1];
131
+ out[o + 2] = line[x * 3 + 2];
132
+ out[o + 3] = 255;
133
+ }
134
+ else if (colorType === 6) { // RGBA
135
+ out[o] = line[x * 4];
136
+ out[o + 1] = line[x * 4 + 1];
137
+ out[o + 2] = line[x * 4 + 2];
138
+ out[o + 3] = line[x * 4 + 3];
139
+ }
140
+ else if (colorType === 0) { // grey
141
+ const g = line[x];
142
+ out[o] = g;
143
+ out[o + 1] = g;
144
+ out[o + 2] = g;
145
+ out[o + 3] = 255;
146
+ }
147
+ else if (colorType === 3 && palette) { // palette
148
+ const idx = line[x];
149
+ out[o] = palette[idx * 3];
150
+ out[o + 1] = palette[idx * 3 + 1];
151
+ out[o + 2] = palette[idx * 3 + 2];
152
+ out[o + 3] = trns && idx < trns.length ? trns[idx] : 255;
153
+ }
154
+ }
155
+ prev.set(line);
156
+ }
157
+ return { width, height, rgba: out };
158
+ }
159
+ /** Nearest-neighbour sample of the source image at normalised (u,v) → RGBA. */
160
+ function sample(img, u, v) {
161
+ const x = Math.min(img.width - 1, Math.max(0, Math.floor(u * img.width)));
162
+ const y = Math.min(img.height - 1, Math.max(0, Math.floor(v * img.height)));
163
+ const o = (y * img.width + x) * 4;
164
+ return [img.rgba[o], img.rgba[o + 1], img.rgba[o + 2], img.rgba[o + 3]];
165
+ }
166
+ /** Composite a pixel over a dark checkerboard so transparency reads correctly. */
167
+ function overBg(r, g, b, a, dark) {
168
+ if (a >= 255)
169
+ return [r, g, b];
170
+ const bg = dark ? 30 : 45;
171
+ const af = a / 255;
172
+ return [
173
+ Math.round(r * af + bg * (1 - af)),
174
+ Math.round(g * af + bg * (1 - af)),
175
+ Math.round(b * af + bg * (1 - af)),
176
+ ];
177
+ }
178
+ /**
179
+ * Render a decoded image as half-block ANSI lines scaled to fit `cols` columns
180
+ * (each line is one terminal row = two image rows). Preserves aspect ratio;
181
+ * terminal cells are ~2× tall as wide, which the half-block already accounts for
182
+ * (2 pixels per cell vertically).
183
+ */
184
+ export function renderHalfBlock(img, cols, maxRows = 40) {
185
+ const aspect = img.height / img.width;
186
+ let outCols = Math.max(1, Math.min(cols, img.width));
187
+ // rows of CELLS; each cell = 2 vertical pixels, and a cell is ~2× tall as wide.
188
+ let outRows = Math.max(1, Math.round((outCols * aspect) / 2));
189
+ if (outRows > maxRows) {
190
+ outRows = maxRows;
191
+ outCols = Math.max(1, Math.round((outRows * 2) / aspect));
192
+ }
193
+ outCols = Math.min(outCols, cols);
194
+ const lines = [];
195
+ for (let ry = 0; ry < outRows; ry++) {
196
+ let line = '';
197
+ for (let cx = 0; cx < outCols; cx++) {
198
+ const u = (cx + 0.5) / outCols;
199
+ const vTop = (ry * 2 + 0.5) / (outRows * 2);
200
+ const vBot = (ry * 2 + 1.5) / (outRows * 2);
201
+ const [tr, tg, tb, ta] = sample(img, u, vTop);
202
+ const [br, bg, bb, ba] = sample(img, u, vBot);
203
+ const [TR, TG, TB] = overBg(tr, tg, tb, ta, true);
204
+ const [BR, BG, BB] = overBg(br, bg, bb, ba, true);
205
+ // '▀' upper half-block: fg = top pixel, bg = bottom pixel.
206
+ line += `\x1b[38;2;${TR};${TG};${TB}m\x1b[48;2;${BR};${BG};${BB}m▀`;
207
+ }
208
+ line += '\x1b[0m';
209
+ lines.push(line);
210
+ }
211
+ return lines;
212
+ }
213
+ const PNG_EXTS = new Set(['.png', '.apng']);
214
+ /**
215
+ * Read an image file and render it inline as half-block ANSI lines, scaled to
216
+ * `cols`. Returns null when the format isn't decodable here (JPEG/GIF/WEBP/…),
217
+ * so the caller can fall back to the OS viewer. Never throws.
218
+ */
219
+ export function imageToAnsi(absPath, cols, maxRows = 40) {
220
+ try {
221
+ const dot = absPath.lastIndexOf('.');
222
+ const ext = dot >= 0 ? absPath.slice(dot).toLowerCase() : '';
223
+ if (!PNG_EXTS.has(ext))
224
+ return null; // only PNG decodes without a dep
225
+ const buf = readFileSync(absPath);
226
+ const img = decodePNG(buf);
227
+ if (!img)
228
+ return null;
229
+ return renderHalfBlock(img, cols, maxRows);
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ /** Cheap dimensions probe for the metadata header (PNG only; null otherwise). */
236
+ export function imageDimensions(absPath) {
237
+ try {
238
+ const buf = readFileSync(absPath, { flag: 'r' });
239
+ if (buf.length < 24)
240
+ return null;
241
+ // PNG: IHDR width/height at bytes 16..24.
242
+ const isPng = buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47;
243
+ if (isPng)
244
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
245
+ return null;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AitherShell Installer Wizard
3
+ * ===========================
4
+ *
5
+ * Non-technical user flow: one-click installer via interactive prompts.
6
+ * Guides the user through device-flow login, license provisioning,
7
+ * endpoint registration, and adk quickstart bootstrap.
8
+ *
9
+ * Endpoints called:
10
+ * POST /v1/licenses/issue (Bearer token auth)
11
+ * POST /v1/endpoints/register (X-License-Key header)
12
+ * POST /v1/workspace/sync (X-License-Key header)
13
+ */
14
+ import type { GenesisClient } from './client.js';
15
+ import type { ShellConfig } from './config.js';
16
+ interface SystemInfo {
17
+ cpu_count: number;
18
+ gpu_name?: string;
19
+ gpu_vram?: number;
20
+ models_present: string[];
21
+ }
22
+ interface LicenseIssueResponse {
23
+ license_key: string;
24
+ tier: string;
25
+ entitlements: Record<string, unknown>;
26
+ expires_at: number;
27
+ sync_interval: number;
28
+ }
29
+ interface EndpointRegisterResponse {
30
+ endpoint_id: string;
31
+ tenant_id: string;
32
+ workspace_id: string;
33
+ sync_interval: number;
34
+ }
35
+ interface WorkspaceSyncResponse {
36
+ agent_roster: Record<string, unknown>[];
37
+ provider_key_refs: string[];
38
+ settings: Record<string, unknown>;
39
+ cache_ttl: number;
40
+ }
41
+ export declare class OnboardingClient {
42
+ private portalUrl;
43
+ private authToken;
44
+ constructor(portalUrl?: string);
45
+ setAuthToken(token: string): void;
46
+ /**
47
+ * POST /v1/licenses/issue — Mint a license.
48
+ * Requires: Bearer token in Authorization header
49
+ * Returns: license_key, tier, entitlements, expires_at
50
+ */
51
+ issueLicense(tierRequest: string, hardwareId?: string): Promise<LicenseIssueResponse>;
52
+ /**
53
+ * POST /v1/endpoints/register — Register a compute endpoint.
54
+ * Requires: license_key in request body
55
+ * Returns: endpoint_id, tenant_id, workspace_id, sync_interval
56
+ */
57
+ registerEndpoint(licenseKey: string, nodeId: string, hostname: string, systemInfo: SystemInfo): Promise<EndpointRegisterResponse>;
58
+ /**
59
+ * POST /v1/workspace/sync — Sync workspace configuration.
60
+ * Requires: X-License-Key header
61
+ * Returns: agent_roster, provider_key_refs, settings, cache_ttl
62
+ */
63
+ syncWorkspace(licenseKey: string, workspaceId: string): Promise<WorkspaceSyncResponse>;
64
+ }
65
+ /**
66
+ * Collect basic system info for the portal.
67
+ */
68
+ export declare function collectSystemInfo(): SystemInfo;
69
+ /**
70
+ * Check if Docker is running (best-effort).
71
+ * Returns true if 'docker ps' succeeds, false otherwise.
72
+ */
73
+ export declare function isDockerRunning(): boolean;
74
+ /**
75
+ * Check if 'adk' command is on PATH.
76
+ */
77
+ export declare function isAdkAvailable(): boolean;
78
+ /**
79
+ * Run the interactive installation wizard.
80
+ *
81
+ * Flow:
82
+ * 1. Welcome banner
83
+ * 2. Check Docker (optional)
84
+ * 3. Device-flow portal login
85
+ * 4. Request license tier
86
+ * 5. Detect system info
87
+ * 6. Register endpoint
88
+ * 7. Sync workspace
89
+ * 8. Run 'adk quickstart-local' (bootstrap Ollama + model)
90
+ * 9. Persist workspace config
91
+ * 10. Print success message
92
+ */
93
+ export declare function runWizard(client: GenesisClient, config: ShellConfig): Promise<void>;
94
+ export {};