@koderlabs/tasks-cli 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1011 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/index.ts
6
+ import { Command } from "commander";
7
+ import pc8 from "picocolors";
8
+
9
+ // src/api.ts
10
+ var ApiError = class extends Error {
11
+ static {
12
+ __name(this, "ApiError");
13
+ }
14
+ status;
15
+ body;
16
+ constructor(status, message, body) {
17
+ super(message), this.status = status, this.body = body;
18
+ this.name = "ApiError";
19
+ }
20
+ };
21
+ var ApiClient = class {
22
+ static {
23
+ __name(this, "ApiClient");
24
+ }
25
+ apiUrl;
26
+ key;
27
+ fetchImpl;
28
+ maxRetries;
29
+ retryBaseMs;
30
+ constructor(opts) {
31
+ this.apiUrl = opts.apiUrl.replace(/\/+$/, "");
32
+ this.key = opts.key;
33
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
34
+ this.maxRetries = opts.maxRetries ?? 3;
35
+ this.retryBaseMs = opts.retryBaseMs ?? 500;
36
+ if (!this.fetchImpl) {
37
+ throw new Error("No fetch implementation available. Use Node >= 18.");
38
+ }
39
+ }
40
+ /** Upload one or more source-map artifacts to a release. */
41
+ async uploadSourceMaps(version, environment, files) {
42
+ const url = `${this.apiUrl}/sdk/source-maps?version=${encodeURIComponent(version)}&environment=${encodeURIComponent(environment)}`;
43
+ const form = new FormData();
44
+ for (const f of files) {
45
+ const bytes = toUint8(f.content);
46
+ const blob = new Blob([
47
+ bytes
48
+ ], {
49
+ type: "application/json"
50
+ });
51
+ form.append("files", blob, f.filename);
52
+ }
53
+ return this.request("POST", url, form);
54
+ }
55
+ /** Create (or upsert) a release without uploading any artifacts. */
56
+ async createRelease(version, environment) {
57
+ return this.uploadSourceMaps(version, environment, []);
58
+ }
59
+ async request(method, url, body) {
60
+ let attempt = 0;
61
+ let lastErr;
62
+ while (attempt <= this.maxRetries) {
63
+ try {
64
+ const res = await this.fetchImpl(url, {
65
+ method,
66
+ headers: {
67
+ authorization: `Bearer ${this.key}`
68
+ },
69
+ body
70
+ });
71
+ if (res.status === 401 || res.status === 403) {
72
+ const txt = await safeText(res);
73
+ throw new ApiError(res.status, `Authentication failed (HTTP ${res.status}). Check your management key. ${txt}`.trim(), txt);
74
+ }
75
+ if (res.status === 429 || res.status >= 500) {
76
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
77
+ if (attempt < this.maxRetries) {
78
+ const wait2 = retryAfter ?? this.retryBaseMs * Math.pow(2, attempt);
79
+ await sleep(wait2);
80
+ attempt++;
81
+ continue;
82
+ }
83
+ const txt = await safeText(res);
84
+ throw new ApiError(res.status, `HTTP ${res.status} after ${attempt + 1} attempts. ${txt}`.trim(), txt);
85
+ }
86
+ if (!res.ok) {
87
+ const txt = await safeText(res);
88
+ throw new ApiError(res.status, `HTTP ${res.status}: ${txt}`.trim(), txt);
89
+ }
90
+ const json = await res.json();
91
+ return json && typeof json === "object" && "data" in json ? json.data : json;
92
+ } catch (err) {
93
+ if (err instanceof ApiError && err.status !== 429 && err.status < 500) throw err;
94
+ lastErr = err;
95
+ if (attempt >= this.maxRetries) throw err;
96
+ await sleep(this.retryBaseMs * Math.pow(2, attempt));
97
+ attempt++;
98
+ }
99
+ }
100
+ throw lastErr ?? new Error("Unreachable");
101
+ }
102
+ };
103
+ function toUint8(content) {
104
+ if (content instanceof Uint8Array) return content;
105
+ return new Uint8Array(content);
106
+ }
107
+ __name(toUint8, "toUint8");
108
+ async function safeText(res) {
109
+ try {
110
+ return await res.text();
111
+ } catch {
112
+ return "";
113
+ }
114
+ }
115
+ __name(safeText, "safeText");
116
+ function parseRetryAfter(header) {
117
+ if (!header) return null;
118
+ const n = Number(header);
119
+ if (Number.isFinite(n) && n >= 0) return n * 1e3;
120
+ const date = Date.parse(header);
121
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
122
+ return null;
123
+ }
124
+ __name(parseRetryAfter, "parseRetryAfter");
125
+ function sleep(ms) {
126
+ return new Promise((r) => setTimeout(r, ms));
127
+ }
128
+ __name(sleep, "sleep");
129
+
130
+ // src/config.ts
131
+ import { existsSync, readFileSync } from "fs";
132
+ import * as path from "path";
133
+ var DEFAULT_API_URL = "http://localhost:11002/api/v1";
134
+ var RC_FILENAME = ".tasksrc.json";
135
+ function findRcFile(start) {
136
+ let dir = path.resolve(start);
137
+ while (true) {
138
+ const candidate = path.join(dir, RC_FILENAME);
139
+ if (existsSync(candidate)) return candidate;
140
+ const parent = path.dirname(dir);
141
+ if (parent === dir) return void 0;
142
+ dir = parent;
143
+ }
144
+ }
145
+ __name(findRcFile, "findRcFile");
146
+ function loadRc(start) {
147
+ const file = findRcFile(start);
148
+ if (!file) return {
149
+ data: {}
150
+ };
151
+ try {
152
+ const data = JSON.parse(readFileSync(file, "utf-8"));
153
+ return {
154
+ file,
155
+ data
156
+ };
157
+ } catch {
158
+ return {
159
+ file,
160
+ data: {}
161
+ };
162
+ }
163
+ }
164
+ __name(loadRc, "loadRc");
165
+ function resolveConfig(input = {}, env = process.env) {
166
+ const { file: rcFile, data: rc } = loadRc(input.cwd ?? process.cwd());
167
+ const apiUrl = (input.apiUrl ?? env.INSTANTTASKS_ENDPOINT ?? env.INSTANTTASKS_API_URL ?? rc.endpoint ?? rc.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
168
+ const key = input.key ?? env.INSTANTTASKS_INGEST_KEY ?? env.INSTANTTASKS_MANAGEMENT_KEY ?? rc.ingestKey ?? rc.key ?? "";
169
+ const projectId = input.projectId ?? env.INSTANTTASKS_PROJECT_ID ?? rc.projectId;
170
+ if (!key && !input.optionalKey) {
171
+ throw new Error("Missing key. Pass --key sk_live_... / pk_live_..., set INSTANTTASKS_INGEST_KEY, or add it to .tasksrc.json.");
172
+ }
173
+ if (key && !/^(sk|pk)_/.test(key)) {
174
+ throw new Error(`Invalid key format: expected sk_live_... / pk_live_..., got "${key.slice(0, 6)}\u2026".`);
175
+ }
176
+ return {
177
+ apiUrl,
178
+ key,
179
+ projectId,
180
+ rcFile
181
+ };
182
+ }
183
+ __name(resolveConfig, "resolveConfig");
184
+
185
+ // src/commands/upload-sourcemaps.ts
186
+ import { promises as fs } from "fs";
187
+ import * as path2 from "path";
188
+ import pc from "picocolors";
189
+ async function uploadSourcemaps(opts) {
190
+ const log = opts.log ?? ((s) => console.log(s));
191
+ const env = opts.environment ?? "production";
192
+ const root = path2.resolve(opts.dir);
193
+ const maps = await walkForMaps(root);
194
+ if (maps.length === 0) {
195
+ log(pc.yellow(`No .js.map files found under ${root}`));
196
+ return 0;
197
+ }
198
+ log(pc.cyan(`Uploading ${maps.length} source map(s) to release ${opts.release} (${env})\u2026`));
199
+ const files = [];
200
+ for (const absPath of maps) {
201
+ const rel = path2.relative(root, absPath).split(path2.sep).join("/");
202
+ const url = opts.urlPrefix ? joinUrl(opts.urlPrefix, rel.replace(/\.map$/, "")) : void 0;
203
+ const content = await fs.readFile(absPath);
204
+ files.push({
205
+ filename: path2.basename(absPath),
206
+ url,
207
+ content
208
+ });
209
+ log(` ${pc.dim("\u2192")} ${rel}${url ? pc.dim(` (${url})`) : ""}`);
210
+ }
211
+ const result = await opts.apiClient.uploadSourceMaps(opts.release, env, files);
212
+ log(pc.green(`Done. Release ${result.release.version} now has ${result.uploaded.length} artifact(s).`));
213
+ return result.uploaded.length;
214
+ }
215
+ __name(uploadSourcemaps, "uploadSourcemaps");
216
+ async function walkForMaps(dir) {
217
+ const out = [];
218
+ let entries;
219
+ try {
220
+ entries = await fs.readdir(dir, {
221
+ withFileTypes: true
222
+ });
223
+ } catch (err) {
224
+ throw new Error(`Cannot read directory ${dir}: ${err.message}`);
225
+ }
226
+ for (const entry of entries) {
227
+ const full = path2.join(dir, entry.name);
228
+ if (entry.isDirectory()) {
229
+ const nested = await walkForMaps(full);
230
+ out.push(...nested);
231
+ } else if (entry.isFile() && entry.name.endsWith(".js.map")) {
232
+ out.push(full);
233
+ }
234
+ }
235
+ return out.sort();
236
+ }
237
+ __name(walkForMaps, "walkForMaps");
238
+ function joinUrl(prefix, rel) {
239
+ const left = prefix.replace(/\/+$/, "");
240
+ const right = rel.replace(/^\/+/, "");
241
+ return `${left}/${right}`;
242
+ }
243
+ __name(joinUrl, "joinUrl");
244
+
245
+ // src/commands/releases.ts
246
+ import pc2 from "picocolors";
247
+ async function createRelease(opts) {
248
+ const log = opts.log ?? ((s) => console.log(s));
249
+ const env = opts.environment ?? "production";
250
+ log(pc2.cyan(`Creating release ${opts.name} (${env})\u2026`));
251
+ const result = await opts.apiClient.createRelease(opts.name, env);
252
+ log(pc2.green(`Release ${result.release.version} ready (id=${result.release.id}).`));
253
+ return result.release.id;
254
+ }
255
+ __name(createRelease, "createRelease");
256
+
257
+ // src/commands/doctor.ts
258
+ import pc3 from "picocolors";
259
+ async function doctor(opts = {}) {
260
+ const log = opts.log ?? ((s) => console.log(s));
261
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
262
+ const checks = [];
263
+ let cfg;
264
+ try {
265
+ cfg = resolveConfig({
266
+ apiUrl: opts.apiUrl,
267
+ key: opts.key,
268
+ projectId: opts.projectId
269
+ });
270
+ checks.push({
271
+ name: "config",
272
+ ok: true,
273
+ detail: `endpoint=${cfg.apiUrl}${cfg.rcFile ? ` (rc=${cfg.rcFile})` : ""}`
274
+ });
275
+ } catch (err) {
276
+ checks.push({
277
+ name: "config",
278
+ ok: false,
279
+ detail: err.message
280
+ });
281
+ return finish(checks, log);
282
+ }
283
+ if (!cfg.projectId) {
284
+ checks.push({
285
+ name: "projectId",
286
+ ok: false,
287
+ detail: "INSTANTTASKS_PROJECT_ID not set (some commands require it)."
288
+ });
289
+ } else {
290
+ checks.push({
291
+ name: "projectId",
292
+ ok: true,
293
+ detail: cfg.projectId
294
+ });
295
+ }
296
+ const configUrl = `${cfg.apiUrl}/sdk/v1/config`;
297
+ try {
298
+ const res = await fetchImpl(configUrl, {
299
+ method: "GET",
300
+ headers: {
301
+ authorization: `Bearer ${cfg.key}`
302
+ }
303
+ });
304
+ if (res.ok) {
305
+ checks.push({
306
+ name: "endpoint",
307
+ ok: true,
308
+ detail: `${configUrl} \u2192 ${res.status}`
309
+ });
310
+ checks.push({
311
+ name: "ingestKey",
312
+ ok: true,
313
+ detail: "authenticated"
314
+ });
315
+ } else if (res.status === 401 || res.status === 403) {
316
+ checks.push({
317
+ name: "endpoint",
318
+ ok: true,
319
+ detail: `${configUrl} reachable (${res.status})`
320
+ });
321
+ checks.push({
322
+ name: "ingestKey",
323
+ ok: false,
324
+ detail: `key rejected (HTTP ${res.status}) \u2014 check INSTANTTASKS_INGEST_KEY`
325
+ });
326
+ } else {
327
+ checks.push({
328
+ name: "endpoint",
329
+ ok: false,
330
+ detail: `${configUrl} \u2192 HTTP ${res.status}`
331
+ });
332
+ }
333
+ } catch (err) {
334
+ checks.push({
335
+ name: "endpoint",
336
+ ok: false,
337
+ detail: `cannot reach ${configUrl}: ${err.message}`
338
+ });
339
+ }
340
+ if (cfg.key.startsWith("pk_")) {
341
+ checks.push({
342
+ name: "sourcemapPermission",
343
+ ok: false,
344
+ detail: "pk_ keys cannot upload source maps \u2014 use sk_live_... for CI uploads"
345
+ });
346
+ } else if (cfg.key.startsWith("sk_")) {
347
+ checks.push({
348
+ name: "sourcemapPermission",
349
+ ok: true,
350
+ detail: "sk_ key has upload role"
351
+ });
352
+ }
353
+ return finish(checks, log);
354
+ }
355
+ __name(doctor, "doctor");
356
+ function finish(checks, log) {
357
+ for (const c of checks) {
358
+ const mark = c.ok ? pc3.green("\u2713") : pc3.red("\u2717");
359
+ log(` ${mark} ${pc3.bold(c.name.padEnd(20))} ${c.ok ? pc3.dim(c.detail) : pc3.yellow(c.detail)}`);
360
+ }
361
+ const ok = checks.every((c) => c.ok);
362
+ log("");
363
+ log(ok ? pc3.green("All checks passed.") : pc3.red("One or more checks failed."));
364
+ return {
365
+ ok,
366
+ checks
367
+ };
368
+ }
369
+ __name(finish, "finish");
370
+
371
+ // src/commands/sourcemap-upload.ts
372
+ import { promises as fs2 } from "fs";
373
+ import * as path3 from "path";
374
+ import pc4 from "picocolors";
375
+ var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
376
+ var MAX_RETRIES = 3;
377
+ var RETRY_BASE_MS = 500;
378
+ async function sourcemapUpload(opts) {
379
+ const log = opts.log ?? ((s) => console.log(s));
380
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
381
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
382
+ const strict = opts.strict !== false;
383
+ const root = path3.resolve(opts.dist);
384
+ const maps = await walk(root);
385
+ if (maps.length === 0) {
386
+ log(pc4.yellow(`No .map files found under ${root}`));
387
+ return {
388
+ uploaded: 0,
389
+ failed: 0,
390
+ skipped: 0
391
+ };
392
+ }
393
+ log(pc4.cyan(`Uploading ${maps.length} source map(s)${opts.release ? ` for release ${opts.release}` : ""}\u2026`));
394
+ let uploaded = 0;
395
+ let failed = 0;
396
+ let skipped = 0;
397
+ for (const absPath of maps) {
398
+ const safe = safeRelative(root, absPath);
399
+ if (!safe) {
400
+ log(pc4.red(` \u2717 ${absPath} \u2014 path escapes dist root`));
401
+ failed++;
402
+ continue;
403
+ }
404
+ const stat = await fs2.stat(absPath);
405
+ if (stat.size > maxBytes) {
406
+ log(pc4.yellow(` \u26A0 ${safe} \u2014 ${stat.size} bytes exceeds cap ${maxBytes}, skipping`));
407
+ skipped++;
408
+ continue;
409
+ }
410
+ const content = await fs2.readFile(absPath, "utf-8");
411
+ const ok = await postWithRetry(fetchImpl, opts.apiUrl, opts.key, {
412
+ path: safe,
413
+ content,
414
+ release: opts.release,
415
+ environment: opts.environment
416
+ });
417
+ if (ok.ok) {
418
+ log(` ${pc4.green("\u2713")} ${safe}`);
419
+ uploaded++;
420
+ } else {
421
+ log(pc4.red(` \u2717 ${safe} \u2014 ${ok.detail}`));
422
+ failed++;
423
+ }
424
+ }
425
+ log("");
426
+ log(`${pc4.bold("Summary:")} ${pc4.green(`${uploaded} uploaded`)}, ${failed ? pc4.red(`${failed} failed`) : `${failed} failed`}, ${skipped} skipped.`);
427
+ if (failed > 0 && strict) {
428
+ throw new Error(`${failed} source-map upload(s) failed (use --no-strict to ignore).`);
429
+ }
430
+ return {
431
+ uploaded,
432
+ failed,
433
+ skipped
434
+ };
435
+ }
436
+ __name(sourcemapUpload, "sourcemapUpload");
437
+ function safeRelative(root, abs) {
438
+ const resolvedRoot = path3.resolve(root);
439
+ const resolved = path3.resolve(abs);
440
+ if (!resolved.startsWith(resolvedRoot + path3.sep) && resolved !== resolvedRoot) return null;
441
+ const rel = path3.relative(resolvedRoot, resolved);
442
+ if (!rel || rel.startsWith("..") || path3.isAbsolute(rel)) return null;
443
+ return rel.split(path3.sep).join("/");
444
+ }
445
+ __name(safeRelative, "safeRelative");
446
+ async function walk(dir) {
447
+ const out = [];
448
+ let entries;
449
+ try {
450
+ entries = await fs2.readdir(dir, {
451
+ withFileTypes: true
452
+ });
453
+ } catch {
454
+ return out;
455
+ }
456
+ for (const entry of entries) {
457
+ if (entry.isSymbolicLink()) continue;
458
+ const full = path3.join(dir, entry.name);
459
+ if (entry.isDirectory()) {
460
+ out.push(...await walk(full));
461
+ } else if (entry.isFile() && entry.name.endsWith(".map")) {
462
+ out.push(full);
463
+ }
464
+ }
465
+ return out.sort();
466
+ }
467
+ __name(walk, "walk");
468
+ async function postWithRetry(fetchImpl, apiUrl, key, body) {
469
+ const url = `${apiUrl.replace(/\/+$/, "")}/sdk/source-maps`;
470
+ let lastDetail = "unknown";
471
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
472
+ try {
473
+ const res = await fetchImpl(url, {
474
+ method: "POST",
475
+ headers: {
476
+ "content-type": "application/json",
477
+ authorization: `Bearer ${key}`
478
+ },
479
+ body: JSON.stringify(body)
480
+ });
481
+ if (res.ok) return {
482
+ ok: true,
483
+ detail: `HTTP ${res.status}`
484
+ };
485
+ lastDetail = `HTTP ${res.status}`;
486
+ if (res.status !== 429 && (res.status < 500 || res.status >= 600)) {
487
+ return {
488
+ ok: false,
489
+ detail: lastDetail
490
+ };
491
+ }
492
+ } catch (err) {
493
+ lastDetail = err.message;
494
+ }
495
+ if (attempt < MAX_RETRIES) {
496
+ await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt)));
497
+ }
498
+ }
499
+ return {
500
+ ok: false,
501
+ detail: lastDetail
502
+ };
503
+ }
504
+ __name(postWithRetry, "postWithRetry");
505
+
506
+ // src/commands/symbols-upload.ts
507
+ import { promises as fs3 } from "fs";
508
+ import * as path4 from "path";
509
+ import pc5 from "picocolors";
510
+ var NATIVE_SYMBOLS_PATH = "/sdk/native-symbols";
511
+ async function uploadIos(dsymPath, opts) {
512
+ const log = opts.log ?? ((s) => console.log(s));
513
+ const root = path4.resolve(dsymPath);
514
+ if (!root.endsWith(".dSYM")) {
515
+ log(pc5.yellow(`Warning: ${root} does not end in .dSYM \u2014 proceeding anyway.`));
516
+ }
517
+ const dwarfRoot = path4.join(root, "Contents", "Resources", "DWARF");
518
+ const files = await listFiles(dwarfRoot);
519
+ if (files.length === 0) {
520
+ log(pc5.yellow(`No DWARF files found under ${dwarfRoot}`));
521
+ return {
522
+ uploaded: 0,
523
+ failed: 0,
524
+ endpointMissing: false
525
+ };
526
+ }
527
+ log(pc5.cyan(`Uploading ${files.length} iOS symbol file(s)\u2026`));
528
+ return uploadAll(files, "ios", opts, log);
529
+ }
530
+ __name(uploadIos, "uploadIos");
531
+ async function uploadAndroid(mappingPath, opts) {
532
+ const log = opts.log ?? ((s) => console.log(s));
533
+ const resolved = path4.resolve(mappingPath);
534
+ const stat = await fs3.stat(resolved).catch(() => null);
535
+ if (!stat?.isFile()) {
536
+ throw new Error(`mapping.txt not found at ${resolved}`);
537
+ }
538
+ log(pc5.cyan(`Uploading Android mapping ${resolved}\u2026`));
539
+ return uploadAll([
540
+ resolved
541
+ ], "android", opts, log);
542
+ }
543
+ __name(uploadAndroid, "uploadAndroid");
544
+ async function uploadAll(files, platform, opts, log) {
545
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
546
+ const url = `${opts.apiUrl.replace(/\/+$/, "")}${NATIVE_SYMBOLS_PATH}`;
547
+ let uploaded = 0;
548
+ let failed = 0;
549
+ let endpointMissing = false;
550
+ for (const file of files) {
551
+ const content = await fs3.readFile(file);
552
+ const body = {
553
+ platform,
554
+ filename: path4.basename(file),
555
+ release: opts.release,
556
+ // Base64-encoded so binary DWARFs survive JSON transport.
557
+ contentBase64: content.toString("base64")
558
+ };
559
+ try {
560
+ const res = await fetchImpl(url, {
561
+ method: "POST",
562
+ headers: {
563
+ "content-type": "application/json",
564
+ authorization: `Bearer ${opts.key}`
565
+ },
566
+ body: JSON.stringify(body)
567
+ });
568
+ if (res.status === 404) {
569
+ endpointMissing = true;
570
+ log(pc5.yellow(` \u26A0 ${path4.basename(file)} \u2014 endpoint pending (${NATIVE_SYMBOLS_PATH} returned 404)`));
571
+ } else if (res.ok) {
572
+ log(` ${pc5.green("\u2713")} ${path4.basename(file)}`);
573
+ uploaded++;
574
+ } else {
575
+ log(pc5.red(` \u2717 ${path4.basename(file)} \u2014 HTTP ${res.status}`));
576
+ failed++;
577
+ }
578
+ } catch (err) {
579
+ log(pc5.red(` \u2717 ${path4.basename(file)} \u2014 ${err.message}`));
580
+ failed++;
581
+ }
582
+ }
583
+ if (endpointMissing) {
584
+ log("");
585
+ log(pc5.yellow(`Backend resolver pending: ${NATIVE_SYMBOLS_PATH} is not yet implemented. Symbol files were prepared but not stored server-side.`));
586
+ }
587
+ return {
588
+ uploaded,
589
+ failed,
590
+ endpointMissing
591
+ };
592
+ }
593
+ __name(uploadAll, "uploadAll");
594
+ async function listFiles(dir) {
595
+ const out = [];
596
+ let entries;
597
+ try {
598
+ entries = await fs3.readdir(dir, {
599
+ withFileTypes: true
600
+ });
601
+ } catch {
602
+ return out;
603
+ }
604
+ for (const entry of entries) {
605
+ if (entry.isSymbolicLink()) continue;
606
+ const full = path4.join(dir, entry.name);
607
+ if (entry.isDirectory()) {
608
+ out.push(...await listFiles(full));
609
+ } else if (entry.isFile()) {
610
+ out.push(full);
611
+ }
612
+ }
613
+ return out;
614
+ }
615
+ __name(listFiles, "listFiles");
616
+
617
+ // src/commands/init.ts
618
+ import { promises as fs4 } from "fs";
619
+ import * as path5 from "path";
620
+ import pc6 from "picocolors";
621
+ var SUPPORTED_PLATFORMS = [
622
+ "web",
623
+ "react",
624
+ "nextjs",
625
+ "vue",
626
+ "rn",
627
+ "nestjs"
628
+ ];
629
+ var RC_TEMPLATE = `{
630
+ "endpoint": "https://tasks.koderlabs.net/api/v1",
631
+ "ingestKey": "pk_live_REPLACE_ME",
632
+ "projectId": "REPLACE_WITH_PROJECT_UUID"
633
+ }
634
+ `;
635
+ var README_SNIPPET = `
636
+ ## InstantTasks SDK
637
+
638
+ Run \`npx @koderlabs/tasks-cli doctor\` to verify your setup.
639
+
640
+ Upload source maps in CI:
641
+
642
+ \`\`\`sh
643
+ npx @koderlabs/tasks-cli sourcemap upload ./dist --release "$GIT_SHA"
644
+ \`\`\`
645
+ `;
646
+ function planInit(platform) {
647
+ const files = [
648
+ {
649
+ filepath: ".tasksrc.json",
650
+ contents: RC_TEMPLATE
651
+ },
652
+ {
653
+ filepath: "INSTANTTASKS.md",
654
+ contents: README_SNIPPET.trim() + "\n"
655
+ }
656
+ ];
657
+ files.push({
658
+ filepath: snippetPath(platform),
659
+ contents: snippetFor(platform)
660
+ });
661
+ return files;
662
+ }
663
+ __name(planInit, "planInit");
664
+ function snippetPath(platform) {
665
+ switch (platform) {
666
+ case "web":
667
+ return "src/tasks-sdk.ts";
668
+ case "react":
669
+ return "src/tasks-sdk.ts";
670
+ case "nextjs":
671
+ return "instrumentation.ts";
672
+ case "vue":
673
+ return "src/tasks-sdk.ts";
674
+ case "rn":
675
+ return "src/tasks-sdk.ts";
676
+ case "nestjs":
677
+ return "src/tasks-sdk.ts";
678
+ }
679
+ }
680
+ __name(snippetPath, "snippetPath");
681
+ function snippetFor(platform) {
682
+ switch (platform) {
683
+ case "web":
684
+ case "react":
685
+ case "vue":
686
+ return `import { init } from '@koderlabs/tasks-sdk-browser';
687
+
688
+ init({
689
+ endpoint: import.meta.env.VITE_TASKS_ENDPOINT,
690
+ ingestKey: import.meta.env.VITE_TASKS_INGEST_KEY,
691
+ release: import.meta.env.VITE_RELEASE,
692
+ });
693
+ `;
694
+ case "nextjs":
695
+ return `// Next.js 13+ instrumentation hook \u2014 auto-loaded once per process.
696
+ export async function register() {
697
+ const { init } = await import('@koderlabs/tasks-sdk-nextjs');
698
+ init({
699
+ endpoint: process.env.NEXT_PUBLIC_TASKS_ENDPOINT!,
700
+ ingestKey: process.env.NEXT_PUBLIC_TASKS_INGEST_KEY!,
701
+ release: process.env.NEXT_PUBLIC_RELEASE,
702
+ });
703
+ }
704
+ `;
705
+ case "rn":
706
+ return `import { init } from '@koderlabs/tasks-sdk-rn';
707
+
708
+ init({
709
+ endpoint: process.env.EXPO_PUBLIC_TASKS_ENDPOINT!,
710
+ ingestKey: process.env.EXPO_PUBLIC_TASKS_INGEST_KEY!,
711
+ release: process.env.EXPO_PUBLIC_RELEASE,
712
+ });
713
+ `;
714
+ case "nestjs":
715
+ return `import { init } from '@koderlabs/tasks-sdk-node';
716
+
717
+ init({
718
+ endpoint: process.env.INSTANTTASKS_ENDPOINT!,
719
+ ingestKey: process.env.INSTANTTASKS_INGEST_KEY!,
720
+ release: process.env.RELEASE,
721
+ });
722
+ `;
723
+ }
724
+ }
725
+ __name(snippetFor, "snippetFor");
726
+ async function initScaffold(opts) {
727
+ const log = opts.log ?? ((s) => console.log(s));
728
+ const cwd = path5.resolve(opts.cwd ?? process.cwd());
729
+ const files = planInit(opts.platform);
730
+ const written = [];
731
+ const skipped = [];
732
+ log(pc6.cyan(`Scaffolding InstantTasks for ${pc6.bold(opts.platform)}\u2026`));
733
+ for (const f of files) {
734
+ const abs = path5.join(cwd, f.filepath);
735
+ const exists = await fs4.stat(abs).then(() => true).catch(() => false);
736
+ if (opts.dryRun) {
737
+ log(pc6.dim(` [dry-run] would write ${f.filepath}${exists ? " (overwrite)" : ""}`));
738
+ log(pc6.dim(" \u250C\u2500"));
739
+ for (const line of f.contents.split("\n")) log(pc6.dim(` \u2502 ${line}`));
740
+ log(pc6.dim(" \u2514\u2500"));
741
+ continue;
742
+ }
743
+ if (exists && !opts.force) {
744
+ log(pc6.yellow(` \u26A0 ${f.filepath} exists \u2014 skipping (pass --force to overwrite)`));
745
+ skipped.push(f.filepath);
746
+ continue;
747
+ }
748
+ await fs4.mkdir(path5.dirname(abs), {
749
+ recursive: true
750
+ });
751
+ await fs4.writeFile(abs, f.contents, "utf-8");
752
+ log(` ${pc6.green("\u2713")} ${f.filepath}`);
753
+ written.push(f.filepath);
754
+ }
755
+ if (!opts.dryRun) {
756
+ log("");
757
+ log(pc6.green("Done. Edit .tasksrc.json with your project credentials, then run:"));
758
+ log(` ${pc6.bold("npx @koderlabs/tasks-cli doctor")}`);
759
+ }
760
+ return {
761
+ files,
762
+ written,
763
+ skipped
764
+ };
765
+ }
766
+ __name(initScaffold, "initScaffold");
767
+
768
+ // src/commands/events-tail.ts
769
+ import pc7 from "picocolors";
770
+ async function eventsTail(opts) {
771
+ const log = opts.log ?? ((s) => console.log(s));
772
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
773
+ const interval = opts.intervalMs ?? 5e3;
774
+ let since = opts.since ?? Date.now();
775
+ log(pc7.cyan(`Tailing events (poll every ${interval}ms). Ctrl-C to exit.`));
776
+ while (!opts.signal?.aborted) {
777
+ const params = new URLSearchParams({
778
+ since: String(since)
779
+ });
780
+ if (opts.projectId) params.set("projectId", opts.projectId);
781
+ for (const [k, v] of Object.entries(opts.filter ?? {})) params.set(k, v);
782
+ const url = `${opts.apiUrl.replace(/\/+$/, "")}/sdk/v1/events?${params.toString()}`;
783
+ try {
784
+ const res = await fetchImpl(url, {
785
+ method: "GET",
786
+ headers: {
787
+ authorization: `Bearer ${opts.key}`
788
+ },
789
+ signal: opts.signal
790
+ });
791
+ if (res.status === 404) {
792
+ log(pc7.yellow(`events endpoint not available yet (404). Backend needs to ship /sdk/v1/events \u2014 exiting tail loop.`));
793
+ return;
794
+ }
795
+ if (res.ok) {
796
+ const body = await res.json();
797
+ const rows = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : [];
798
+ for (const row of rows) {
799
+ const ts = typeof row.ts === "number" ? new Date(row.ts).toISOString() : String(row.ts);
800
+ const kind = row.kind ?? "event";
801
+ const message = row.message ?? "";
802
+ const url2 = row.url ? pc7.dim(` ${row.url}`) : "";
803
+ log(`${pc7.dim(ts)} | ${pc7.bold(kind.padEnd(12))} | ${message}${url2}`);
804
+ if (typeof row.ts === "number") since = Math.max(since, row.ts + 1);
805
+ }
806
+ if (rows.length === 0) {
807
+ since = Date.now();
808
+ }
809
+ } else {
810
+ log(pc7.yellow(`poll failed: HTTP ${res.status}`));
811
+ }
812
+ } catch (err) {
813
+ if (err.name === "AbortError") return;
814
+ log(pc7.yellow(`poll error: ${err.message}`));
815
+ }
816
+ await wait(interval, opts.signal);
817
+ }
818
+ }
819
+ __name(eventsTail, "eventsTail");
820
+ function wait(ms, signal) {
821
+ return new Promise((resolve6) => {
822
+ const t = setTimeout(resolve6, ms);
823
+ signal?.addEventListener("abort", () => {
824
+ clearTimeout(t);
825
+ resolve6();
826
+ });
827
+ });
828
+ }
829
+ __name(wait, "wait");
830
+ function parseSince(input, now = Date.now()) {
831
+ if (!input) return now;
832
+ const match = input.match(/^(\d+)([smhd])$/);
833
+ if (!match) {
834
+ const n = Number(input);
835
+ return Number.isFinite(n) ? n : now;
836
+ }
837
+ const value = parseInt(match[1], 10);
838
+ const unit = match[2];
839
+ const mult = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
840
+ return now - value * mult;
841
+ }
842
+ __name(parseSince, "parseSince");
843
+
844
+ // src/index.ts
845
+ var VERSION = "0.0.0";
846
+ function buildProgram() {
847
+ const program = new Command();
848
+ program.name("tasks").description("InstantTasks CLI \u2014 doctor, source maps, symbols, init, events tail").version(VERSION);
849
+ program.command("doctor").description("Validate config + connectivity (CI-friendly; non-zero on failure)").option("--api-url <url>", "API base URL (defaults to env / .tasksrc.json)").option("--key <key>", "Ingest or management key").option("--project <id>", "Project ID").action(async (opts) => {
850
+ const result = await doctor({
851
+ apiUrl: opts.apiUrl,
852
+ key: opts.key,
853
+ projectId: opts.project
854
+ });
855
+ if (!result.ok) process.exit(1);
856
+ });
857
+ const sourcemap = program.command("sourcemap").description("Source-map utilities");
858
+ sourcemap.command("upload <dist>").description("Recursively upload .map files from a dist directory").option("--release <sha>", "Release name / version (associates maps with a release)").option("--environment <env>", "Release environment", "production").option("--max-size <bytes>", "Per-file size cap in bytes", String(50 * 1024 * 1024)).option("--no-strict", "Do not exit non-zero on individual file failures").option("--api-url <url>", "API base URL").option("--key <key>", "Management key (sk_live_...)").action(async (dist, opts) => {
859
+ const cfg = resolveConfig({
860
+ apiUrl: opts.apiUrl,
861
+ key: opts.key
862
+ });
863
+ await sourcemapUpload({
864
+ dist,
865
+ apiUrl: cfg.apiUrl,
866
+ key: cfg.key,
867
+ release: opts.release,
868
+ environment: opts.environment,
869
+ maxBytes: Number(opts.maxSize),
870
+ strict: opts.strict !== false
871
+ });
872
+ });
873
+ const symbols = program.command("symbols").description("Native symbol uploads (iOS dSYM, Android ProGuard)");
874
+ symbols.command("upload-ios <dsym>").description("Upload DWARF files from a .dSYM bundle").option("--release <sha>", "Release name / version").option("--api-url <url>").option("--key <key>").action(async (dsym, opts) => {
875
+ const cfg = resolveConfig({
876
+ apiUrl: opts.apiUrl,
877
+ key: opts.key
878
+ });
879
+ await uploadIos(dsym, {
880
+ apiUrl: cfg.apiUrl,
881
+ key: cfg.key,
882
+ release: opts.release
883
+ });
884
+ });
885
+ symbols.command("upload-android <mapping>").description("Upload an Android ProGuard mapping.txt").option("--release <sha>", "Release name / version").option("--api-url <url>").option("--key <key>").action(async (mapping, opts) => {
886
+ const cfg = resolveConfig({
887
+ apiUrl: opts.apiUrl,
888
+ key: opts.key
889
+ });
890
+ await uploadAndroid(mapping, {
891
+ apiUrl: cfg.apiUrl,
892
+ key: cfg.key,
893
+ release: opts.release
894
+ });
895
+ });
896
+ program.command("init <platform>").description(`Scaffold .tasksrc.json + init snippet (platforms: ${SUPPORTED_PLATFORMS.join(", ")})`).option("--dry-run", "Print files that would be written but do not write them").option("--force", "Overwrite existing files").action(async (platform, opts) => {
897
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
898
+ throw new Error(`Unsupported platform "${platform}". Pick one of: ${SUPPORTED_PLATFORMS.join(", ")}`);
899
+ }
900
+ await initScaffold({
901
+ platform,
902
+ dryRun: opts.dryRun,
903
+ force: opts.force
904
+ });
905
+ });
906
+ const events = program.command("events").description("Event utilities");
907
+ events.command("tail").description("Stream recent SDK events (long-poll fallback until WebSocket lands)").option("--filter <kv...>", "Filter, e.g. project=<id>").option("--since <duration>", "Lookback window (e.g. 10m, 1h)").option("--interval <ms>", "Poll interval in ms", "5000").option("--api-url <url>").option("--key <key>").option("--project <id>").action(async (opts) => {
908
+ const cfg = resolveConfig({
909
+ apiUrl: opts.apiUrl,
910
+ key: opts.key,
911
+ projectId: opts.project
912
+ });
913
+ const filter = {};
914
+ const rawFilters = Array.isArray(opts.filter) ? opts.filter : opts.filter ? [
915
+ opts.filter
916
+ ] : [];
917
+ for (const kv of rawFilters) {
918
+ const [k, v] = String(kv).split("=");
919
+ if (k && v) filter[k] = v;
920
+ }
921
+ const controller = new AbortController();
922
+ const onSig = /* @__PURE__ */ __name(() => {
923
+ console.log(pc8.dim("\n (exiting)"));
924
+ controller.abort();
925
+ }, "onSig");
926
+ process.once("SIGINT", onSig);
927
+ process.once("SIGTERM", onSig);
928
+ await eventsTail({
929
+ apiUrl: cfg.apiUrl,
930
+ key: cfg.key,
931
+ projectId: cfg.projectId,
932
+ since: parseSince(opts.since),
933
+ intervalMs: Number(opts.interval) || 5e3,
934
+ filter,
935
+ signal: controller.signal
936
+ });
937
+ });
938
+ program.command("upload-sourcemaps").description("[legacy] Upload .js.map files in a directory to a release").requiredOption("--release <name>", "Release name / version").requiredOption("--dir <path>", "Directory to walk for *.js.map files").option("--environment <env>", "Release environment", "production").option("--url-prefix <prefix>", "Public URL prefix for source files").option("--project <projectId>").option("--api-url <url>").option("--key <key>").action(async (opts) => {
939
+ const cfg = resolveConfig({
940
+ apiUrl: opts.apiUrl,
941
+ key: opts.key,
942
+ projectId: opts.project
943
+ });
944
+ const client = new ApiClient({
945
+ apiUrl: cfg.apiUrl,
946
+ key: cfg.key
947
+ });
948
+ await uploadSourcemaps({
949
+ release: opts.release,
950
+ dir: opts.dir,
951
+ environment: opts.environment,
952
+ urlPrefix: opts.urlPrefix,
953
+ apiClient: client
954
+ });
955
+ });
956
+ program.command("create-release").description("Create (or upsert) a release on the server").requiredOption("--name <name>", "Release name / version").option("--environment <env>", "Release environment", "production").option("--project <projectId>").option("--api-url <url>").option("--key <key>").action(async (opts) => {
957
+ const cfg = resolveConfig({
958
+ apiUrl: opts.apiUrl,
959
+ key: opts.key,
960
+ projectId: opts.project
961
+ });
962
+ const client = new ApiClient({
963
+ apiUrl: cfg.apiUrl,
964
+ key: cfg.key
965
+ });
966
+ await createRelease({
967
+ name: opts.name,
968
+ environment: opts.environment,
969
+ apiClient: client
970
+ });
971
+ });
972
+ return program;
973
+ }
974
+ __name(buildProgram, "buildProgram");
975
+ async function main(argv = process.argv) {
976
+ const program = buildProgram();
977
+ program.exitOverride();
978
+ try {
979
+ await program.parseAsync(argv);
980
+ return 0;
981
+ } catch (err) {
982
+ if (err instanceof ApiError) {
983
+ console.error(pc8.red(`API error (${err.status}): ${err.message}`));
984
+ return 1;
985
+ }
986
+ const anyErr = err;
987
+ if (anyErr && typeof anyErr === "object" && "code" in anyErr) {
988
+ if (anyErr.code === "commander.helpDisplayed" || anyErr.code === "commander.version") return 0;
989
+ if (anyErr.code === "commander.help") return 0;
990
+ }
991
+ console.error(pc8.red(err.message || String(err)));
992
+ return 1;
993
+ }
994
+ }
995
+ __name(main, "main");
996
+ var isEntry = (() => {
997
+ try {
998
+ const argv1 = process.argv[1] ?? "";
999
+ return /(?:^|[\\/])(tasks|instanttasks-cli|index\.[mc]?js)$/.test(argv1);
1000
+ } catch {
1001
+ return false;
1002
+ }
1003
+ })();
1004
+ if (isEntry) {
1005
+ main().then((code) => process.exit(code));
1006
+ }
1007
+ export {
1008
+ buildProgram,
1009
+ main
1010
+ };
1011
+ //# sourceMappingURL=index.js.map