storebound/cli/lib/csv.ts
CRT 3f12452e3f TestFlight through the App Store Connect API: apple testflight status | groups | testers | what-to-test | info | distribute
Beta groups from apple.testflight.groups (created when missing, never changed), testers from a CSV in .storebound/
(masked in logs, confirmation before Apple emails invitations, --remove / --invite), What to Test per locale from the
App Store listing (whatToTest or whatsNew), Test Information and Beta App Review details, builds by exact number to
groups, Beta App Review submission behind --submit-review and a confirmation. Pure planners in cli/lib/testflight.ts,
shared CSV / table / JSON:API helpers. MCP: apple_testflight_status, _groups, _what_to_test, _info, _distribute
(internal groups only). Skill testflight.
2026-10-10 23:28:10 +08:00

43 lines
1.6 KiB
TypeScript

// Small RFC 4180 CSV reader / writer (quotes, doubled quotes, CR LF, a UTF-8 BOM). Pure. Used for tester lists,
// price sheets and the Play statistics exports.
import { csvCell } from './data-safety';
/** Rows of cells. Empty lines are dropped. */
export function parseCsv(text: string, sep = ','): string[][] {
const rows: string[][] = [];
let row: string[] = [];
let cell = '';
let quoted = false;
const s = text.replace(/^/, '');
for (let i = 0; i < s.length; i++) {
const ch = s[i]!;
if (quoted) {
if (ch === '"') { if (s[i + 1] === '"') { cell += '"'; i++; } else quoted = false; }
else cell += ch;
continue;
}
if (ch === '"' && cell === '') { quoted = true; continue; }
if (ch === sep) { row.push(cell); cell = ''; continue; }
if (ch === '\n' || ch === '\r') {
if (ch === '\r' && s[i + 1] === '\n') i++;
row.push(cell); cell = '';
if (row.some(c => c !== '')) rows.push(row);
row = [];
continue;
}
cell += ch;
}
row.push(cell);
if (row.some(c => c !== '')) rows.push(row);
return rows;
}
/** Header row -> objects keyed by the lower-cased, trimmed header (spaces and dashes become "_"). */
export function parseCsvRecords(text: string, sep = ','): Record<string, string>[] {
const [head, ...rest] = parseCsv(text, sep);
if (!head) return [];
const keys = head.map(h => h.trim().toLowerCase().replace(/[\s-]+/g, '_'));
return rest.map(r => Object.fromEntries(keys.map((k, i) => [k, (r[i] ?? '').trim()])));
}
export const toCsv = (rows: (string | number)[][]): string => rows.map(r => r.map(c => csvCell(String(c))).join(',')).join('\n') + '\n';