orizu 0.0.9 → 0.1.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,91 @@
1
+ import { createReadStream } from 'fs';
2
+ import { createInterface } from 'readline';
3
+ export const JSONL_UPLOAD_CHUNK_MAX_BYTES = 2 * 1024 * 1024;
4
+ function isObjectRow(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function rowsPayloadByteLength(rowCount, rowsByteLength) {
8
+ return Buffer.byteLength('{"rows":[]}', 'utf8') +
9
+ rowsByteLength +
10
+ Math.max(0, rowCount - 1);
11
+ }
12
+ function formatMb(byteLength) {
13
+ return (byteLength / (1024 * 1024)).toFixed(1);
14
+ }
15
+ function mapReadError(filePath, error) {
16
+ const maybeError = error;
17
+ if (!maybeError.code && error instanceof Error) {
18
+ return error;
19
+ }
20
+ if (maybeError.code === 'ENOENT') {
21
+ return new Error(`File not found: ${filePath}. Check the path and filename, then retry.`);
22
+ }
23
+ if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
24
+ return new Error(`Cannot read file: ${filePath}. macOS may be blocking access to this folder (for example Downloads). Grant folder permission to your terminal app and retry.`);
25
+ }
26
+ return new Error(`Failed to read file ${filePath}: ${maybeError.message}`);
27
+ }
28
+ export function estimateRowsPayloadByteLength(rows) {
29
+ const rowsByteLength = rows.reduce((total, row) => {
30
+ return total + Buffer.byteLength(JSON.stringify(row), 'utf8');
31
+ }, 0);
32
+ return rowsPayloadByteLength(rows.length, rowsByteLength);
33
+ }
34
+ export async function* streamJsonlRowChunks(filePath, options = {}) {
35
+ const maxPayloadBytes = options.maxPayloadBytes ?? JSONL_UPLOAD_CHUNK_MAX_BYTES;
36
+ let chunk = [];
37
+ let chunkRowsByteLength = 0;
38
+ let parsedRowCount = 0;
39
+ let lineNumber = 0;
40
+ const stream = createReadStream(filePath, { encoding: 'utf8' });
41
+ const lines = createInterface({
42
+ input: stream,
43
+ crlfDelay: Infinity,
44
+ });
45
+ try {
46
+ for await (const line of lines) {
47
+ lineNumber += 1;
48
+ const trimmed = line.trim();
49
+ if (!trimmed) {
50
+ continue;
51
+ }
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(trimmed);
55
+ }
56
+ catch {
57
+ throw new Error(`Invalid JSONL at line ${lineNumber}`);
58
+ }
59
+ if (!isObjectRow(parsed)) {
60
+ throw new Error(`Row ${lineNumber} must be a JSON object`);
61
+ }
62
+ const jsonByteLength = Buffer.byteLength(JSON.stringify(parsed), 'utf8');
63
+ const singleRowPayloadBytes = rowsPayloadByteLength(1, jsonByteLength);
64
+ if (singleRowPayloadBytes > maxPayloadBytes) {
65
+ throw new Error(`Row ${lineNumber} is ${formatMb(singleRowPayloadBytes)} MB after JSON encoding, which exceeds the ${formatMb(maxPayloadBytes)} MB upload chunk limit`);
66
+ }
67
+ const nextPayloadBytes = rowsPayloadByteLength(chunk.length + 1, chunkRowsByteLength + jsonByteLength);
68
+ if (chunk.length > 0 && nextPayloadBytes > maxPayloadBytes) {
69
+ yield chunk.map(item => item.row);
70
+ chunk = [];
71
+ chunkRowsByteLength = 0;
72
+ }
73
+ chunk.push({ row: parsed, jsonByteLength });
74
+ chunkRowsByteLength += jsonByteLength;
75
+ parsedRowCount += 1;
76
+ }
77
+ }
78
+ catch (error) {
79
+ throw mapReadError(filePath, error);
80
+ }
81
+ finally {
82
+ lines.close();
83
+ stream.destroy();
84
+ }
85
+ if (chunk.length > 0) {
86
+ yield chunk.map(item => item.row);
87
+ }
88
+ if (parsedRowCount === 0) {
89
+ throw new Error('Dataset file contains no rows');
90
+ }
91
+ }
@@ -0,0 +1,60 @@
1
+ function createCliError(message, httpStatus, structuredPayload) {
2
+ const error = new Error(message);
3
+ error.httpStatus = httpStatus;
4
+ error.structuredPayload = structuredPayload;
5
+ return error;
6
+ }
7
+ export async function formatTaskCreateError(response) {
8
+ const contentType = response.headers.get('content-type') || '';
9
+ const rawBody = await response.text();
10
+ const httpStatus = response.status;
11
+ let parsedPayload;
12
+ if (!contentType.includes('application/json')) {
13
+ return createCliError(`Failed to create task: ${rawBody}`, httpStatus);
14
+ }
15
+ try {
16
+ const payload = JSON.parse(rawBody);
17
+ parsedPayload = payload;
18
+ if (Array.isArray(payload.invalidAssigneeErrors) &&
19
+ payload.invalidAssigneeErrors.length > 0) {
20
+ const details = payload.invalidAssigneeErrors
21
+ .filter(item => typeof item.assignee === 'string' &&
22
+ item.assignee.length > 0 &&
23
+ typeof item.reason === 'string' &&
24
+ item.reason.length > 0)
25
+ .map(item => ` - ${item.assignee}: ${item.reason}`)
26
+ .join('\n');
27
+ if (details) {
28
+ return createCliError(`Failed to create task: ${payload.error || 'Invalid assignees'}\n${details}`, httpStatus, parsedPayload);
29
+ }
30
+ }
31
+ if (Array.isArray(payload.missingRequiredFields) &&
32
+ payload.missingRequiredFields.length > 0) {
33
+ const lines = [
34
+ `Failed to create task: ${payload.error || 'Dataset incompatible'}`,
35
+ ];
36
+ if (payload.versionNum !== undefined) {
37
+ lines.push(` Version: v${payload.versionNum} (${payload.versionId || 'unknown'})`);
38
+ }
39
+ lines.push(` Missing required fields: ${payload.missingRequiredFields.join(', ')}`);
40
+ if (typeof payload.incompatibleRowCount === 'number') {
41
+ lines.push(` Incompatible rows: ${payload.incompatibleRowCount}`);
42
+ }
43
+ if (payload.missingRequiredFieldCounts &&
44
+ Object.keys(payload.missingRequiredFieldCounts).length > 0) {
45
+ const fieldCounts = Object.entries(payload.missingRequiredFieldCounts)
46
+ .map(([field, count]) => ` ${field}: ${count} rows`)
47
+ .join('\n');
48
+ lines.push(` Missing field counts:\n${fieldCounts}`);
49
+ }
50
+ return createCliError(lines.join('\n'), httpStatus, parsedPayload);
51
+ }
52
+ if (typeof payload.error === 'string' && payload.error.length > 0) {
53
+ return createCliError(`Failed to create task: ${payload.error}`, httpStatus, parsedPayload);
54
+ }
55
+ }
56
+ catch {
57
+ return createCliError(`Failed to create task: ${rawBody}`, httpStatus);
58
+ }
59
+ return createCliError(`Failed to create task: ${rawBody}`, httpStatus, parsedPayload);
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.0.9",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,7 +82,15 @@ export function loadCredentialsConfig(): StoredCredentialsV2 | null {
82
82
  }
83
83
 
84
84
  const raw = readFileSync(path, 'utf-8')
85
- const parsed = JSON.parse(raw) as unknown
85
+
86
+ let parsed: unknown
87
+ try {
88
+ parsed = JSON.parse(raw)
89
+ } catch {
90
+ console.warn('Warning: credentials file contains invalid JSON — please re-login with `orizu login`')
91
+ return null
92
+ }
93
+
86
94
  if (isStoredCredentialsV2(parsed)) {
87
95
  return parsed
88
96
  }
@@ -91,7 +99,8 @@ export function loadCredentialsConfig(): StoredCredentialsV2 | null {
91
99
  return migrateToV2(parsed)
92
100
  }
93
101
 
94
- throw new Error('Invalid credentials file format.')
102
+ console.warn('Warning: credentials file has unrecognized format — please re-login with `orizu login`')
103
+ return null
95
104
  }
96
105
 
97
106
  export function getServerCredentials(baseUrl: string): ServerCredentials | null {
@@ -0,0 +1,27 @@
1
+ interface ErrorResponsePayload {
2
+ error?: string
3
+ }
4
+
5
+ export async function extractErrorMessage(response: Response): Promise<string> {
6
+ const contentType = response.headers.get('content-type') || ''
7
+ const rawBody = await response.text()
8
+
9
+ if (contentType.includes('application/json')) {
10
+ try {
11
+ const payload = JSON.parse(rawBody) as ErrorResponsePayload | null
12
+ if (
13
+ payload &&
14
+ typeof payload === 'object' &&
15
+ !Array.isArray(payload) &&
16
+ typeof payload.error === 'string' &&
17
+ payload.error.length > 0
18
+ ) {
19
+ return payload.error
20
+ }
21
+ } catch {
22
+ // Fall through to raw body
23
+ }
24
+ }
25
+
26
+ return rawBody
27
+ }