@atollhq/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,1090 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+
13
+ // src/lib/config.ts
14
+ var config_exports = {};
15
+ __export(config_exports, {
16
+ deleteConfig: () => deleteConfig,
17
+ getApiKey: () => getApiKey,
18
+ readConfig: () => readConfig,
19
+ writeConfig: () => writeConfig
20
+ });
21
+ function readConfig() {
22
+ if (!(0, import_node_fs.existsSync)(CONFIG_PATH)) return {};
23
+ try {
24
+ return JSON.parse((0, import_node_fs.readFileSync)(CONFIG_PATH, "utf-8"));
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+ function writeConfig(config) {
30
+ if (!(0, import_node_fs.existsSync)(CONFIG_DIR)) {
31
+ (0, import_node_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
32
+ }
33
+ (0, import_node_fs.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
34
+ }
35
+ function deleteConfig() {
36
+ if ((0, import_node_fs.existsSync)(CONFIG_PATH)) {
37
+ (0, import_node_fs.unlinkSync)(CONFIG_PATH);
38
+ }
39
+ }
40
+ function getApiKey() {
41
+ return process.env.ATOLL_API_KEY || readConfig().apiKey;
42
+ }
43
+ var import_node_fs, import_node_os, import_node_path, CONFIG_DIR, CONFIG_PATH;
44
+ var init_config = __esm({
45
+ "src/lib/config.ts"() {
46
+ "use strict";
47
+ import_node_fs = require("fs");
48
+ import_node_os = require("os");
49
+ import_node_path = require("path");
50
+ CONFIG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".atoll");
51
+ CONFIG_PATH = (0, import_node_path.join)(CONFIG_DIR, "config.json");
52
+ }
53
+ });
54
+
55
+ // src/index.ts
56
+ var import_commander8 = require("commander");
57
+
58
+ // src/commands/auth.ts
59
+ var import_commander = require("commander");
60
+ init_config();
61
+
62
+ // src/lib/client.ts
63
+ init_config();
64
+
65
+ // src/lib/output.ts
66
+ function isJsonMode() {
67
+ if (process.env.OUTPUT_FORMAT === "json") return true;
68
+ return !process.stdout.isTTY;
69
+ }
70
+ function output(data, humanReadable) {
71
+ if (isJsonMode()) {
72
+ process.stdout.write(JSON.stringify(data) + "\n");
73
+ } else {
74
+ console.log(humanReadable);
75
+ }
76
+ }
77
+ function outputError(message) {
78
+ if (isJsonMode()) {
79
+ process.stdout.write(JSON.stringify({ error: message }) + "\n");
80
+ } else {
81
+ console.error(`Error: ${message}`);
82
+ }
83
+ }
84
+
85
+ // src/lib/client.ts
86
+ var DEFAULT_BASE_URL = "https://atollhq.com";
87
+ var AtollClient = class {
88
+ constructor(opts) {
89
+ this.baseUrl = opts?.baseUrl || process.env.ATOLL_BASE_URL || DEFAULT_BASE_URL;
90
+ const key = opts?.apiKey || getApiKey();
91
+ if (!key) {
92
+ outputError("No API key found. Run `atoll auth login --key <API_KEY>` or set ATOLL_API_KEY.");
93
+ process.exit(1);
94
+ }
95
+ this.apiKey = key;
96
+ }
97
+ async request(path, init) {
98
+ const url = `${this.baseUrl}${path}`;
99
+ const res = await fetch(url, {
100
+ ...init,
101
+ headers: {
102
+ "Authorization": `Bearer ${this.apiKey}`,
103
+ "Content-Type": "application/json",
104
+ ...init?.headers
105
+ }
106
+ });
107
+ if (!res.ok) {
108
+ const body = await res.text();
109
+ throw new Error(`API ${res.status}: ${body}`);
110
+ }
111
+ return res.json();
112
+ }
113
+ async get(path) {
114
+ return this.request(path, { method: "GET" });
115
+ }
116
+ async post(path, body) {
117
+ return this.request(path, {
118
+ method: "POST",
119
+ body: body ? JSON.stringify(body) : void 0
120
+ });
121
+ }
122
+ async patch(path, body) {
123
+ return this.request(path, {
124
+ method: "PATCH",
125
+ body: body ? JSON.stringify(body) : void 0
126
+ });
127
+ }
128
+ async delete(path) {
129
+ return this.request(path, { method: "DELETE" });
130
+ }
131
+ };
132
+
133
+ // src/commands/auth.ts
134
+ var authCommand = new import_commander.Command("auth").description("Manage authentication");
135
+ authCommand.command("login").description("Save an API key to ~/.atoll/config.json").requiredOption("--key <API_KEY>", "API key to store").action((opts) => {
136
+ const config = readConfig();
137
+ config.apiKey = opts.key;
138
+ writeConfig(config);
139
+ output(
140
+ { status: "ok", message: "API key saved" },
141
+ "\u2713 API key saved to ~/.atoll/config.json"
142
+ );
143
+ });
144
+ authCommand.command("status").description("Show current auth status and user info").action(async () => {
145
+ const apiKey = getApiKey();
146
+ if (!apiKey) {
147
+ outputError("Not authenticated. Run `atoll auth login --key <API_KEY>` or set ATOLL_API_KEY.");
148
+ process.exit(1);
149
+ }
150
+ try {
151
+ const client = new AtollClient({ apiKey });
152
+ const me = await client.get("/api/auth/me");
153
+ output(
154
+ { status: "authenticated", ...me },
155
+ [
156
+ "\u2713 Authenticated",
157
+ me.user?.name ? ` User: ${me.user.name}` : null,
158
+ me.user?.email ? ` Email: ${me.user.email}` : null,
159
+ me.org?.name ? ` Org: ${me.org.name} (${me.org.slug})` : null
160
+ ].filter(Boolean).join("\n")
161
+ );
162
+ } catch (err) {
163
+ const msg = err.message || "";
164
+ if (/API (401|403):/.test(msg)) {
165
+ outputError("API key is invalid or expired");
166
+ process.exit(1);
167
+ }
168
+ outputError(`Auth check failed: ${msg}`);
169
+ process.exit(1);
170
+ }
171
+ });
172
+ authCommand.command("logout").description("Remove stored API key").action(() => {
173
+ const config = readConfig();
174
+ delete config.apiKey;
175
+ writeConfig(config);
176
+ output(
177
+ { status: "ok", message: "Logged out" },
178
+ "\u2713 Logged out \u2014 API key removed (org/team config preserved)"
179
+ );
180
+ });
181
+
182
+ // src/commands/issue.ts
183
+ var import_commander2 = require("commander");
184
+
185
+ // src/lib/colors.ts
186
+ function isTTY() {
187
+ return process.stdout.isTTY === true && process.env.OUTPUT_FORMAT !== "json";
188
+ }
189
+ function ansi(code, text) {
190
+ if (!isTTY()) return text;
191
+ return `\x1B[${code}m${text}\x1B[0m`;
192
+ }
193
+ var bold = (t) => ansi("1", t);
194
+ var dim = (t) => ansi("2", t);
195
+ var red = (t) => ansi("31", t);
196
+ var green = (t) => ansi("32", t);
197
+ var yellow = (t) => ansi("33", t);
198
+ var cyan = (t) => ansi("36", t);
199
+ var gray = (t) => ansi("90", t);
200
+ function statusColor(status, text) {
201
+ switch (status) {
202
+ case "done":
203
+ return green(text);
204
+ case "in_progress":
205
+ return yellow(text);
206
+ case "backlog":
207
+ case "todo":
208
+ return gray(text);
209
+ case "cancelled":
210
+ return red(text);
211
+ default:
212
+ return text;
213
+ }
214
+ }
215
+ function priorityIcon(priority) {
216
+ switch (priority) {
217
+ case 0:
218
+ return red("\u26A0\u26A0\u26A0");
219
+ // urgent
220
+ case 1:
221
+ return yellow("\u2584\u2586\u2588");
222
+ // high
223
+ case 2:
224
+ return cyan("\u2584\u2586");
225
+ // medium
226
+ case 3:
227
+ return dim("---");
228
+ // low
229
+ default:
230
+ return String(priority);
231
+ }
232
+ }
233
+ var success = (msg) => green(`\u2713 ${msg}`);
234
+ var RESET = isTTY() ? "\x1B[0m" : "";
235
+ var BOLD = isTTY() ? "\x1B[1m" : "";
236
+ var DIM = isTTY() ? "\x1B[2m" : "";
237
+
238
+ // src/commands/issue.ts
239
+ function handleApiError(err) {
240
+ const msg = err.message ?? String(err);
241
+ if (/API 401\b/.test(msg)) {
242
+ outputError("Session expired \u2014 run `atoll auth login` to re-authenticate.");
243
+ } else if (/API 403\b/.test(msg)) {
244
+ outputError("Permission denied. You do not have access to this resource.");
245
+ } else {
246
+ outputError(msg);
247
+ }
248
+ process.exit(1);
249
+ }
250
+ var VALID_STATUSES = ["backlog", "todo", "in_progress", "done", "cancelled"];
251
+ var PRIORITY_LABELS = { 0: "Urgent", 1: "High", 2: "Medium", 3: "Low" };
252
+ async function resolveOrgId(client, orgSlugOverride) {
253
+ const { orgs } = await client.get("/api/orgs");
254
+ if (!orgs || orgs.length === 0) {
255
+ outputError("No organizations found. Create one first.");
256
+ process.exit(1);
257
+ }
258
+ if (orgs.length === 1) return orgs[0].id;
259
+ const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
260
+ const config = readConfig2();
261
+ const slug = orgSlugOverride ?? process.env.ATOLL_ORG ?? config.orgSlug;
262
+ if (slug) {
263
+ const match = orgs.find((o) => o.slug === slug);
264
+ if (match) return match.id;
265
+ outputError(`Org "${slug}" not found. Available: ${orgs.map((o) => o.slug).join(", ")}`);
266
+ process.exit(1);
267
+ }
268
+ outputError(`Multiple orgs found. Use --org <slug> or run \`atoll config set-org <slug>\`. Available: ${orgs.map((o) => o.slug).join(", ")}`);
269
+ process.exit(1);
270
+ }
271
+ async function resolveIssueId(client, orgId, identifier) {
272
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier)) {
273
+ return identifier;
274
+ }
275
+ const match = identifier.match(/^(?:[A-Za-z]+-)?(\d+)$/);
276
+ if (!match) {
277
+ outputError(`Invalid identifier: "${identifier}". Use a UUID or PREFIX-NUMBER (e.g. ATOLL-42).`);
278
+ process.exit(2);
279
+ }
280
+ const num = parseInt(match[1], 10);
281
+ const { issues } = await client.get(
282
+ `/api/orgs/${orgId}/issues?number=${num}&limit=1`
283
+ );
284
+ if (issues.length > 0 && issues[0].number === num) {
285
+ return issues[0].id;
286
+ }
287
+ let offset = 0;
288
+ const pageSize = 100;
289
+ while (true) {
290
+ const page = await client.get(
291
+ `/api/orgs/${orgId}/issues?limit=${pageSize}&offset=${offset}`
292
+ );
293
+ const found = page.issues.find((i) => i.number === num);
294
+ if (found) return found.id;
295
+ offset += pageSize;
296
+ if (offset >= page.total) break;
297
+ }
298
+ outputError(`Issue #${num} not found.`);
299
+ process.exit(1);
300
+ }
301
+ function formatIdentifier(issue) {
302
+ return issue.number != null ? `ATOLL-${issue.number}` : issue.id.slice(0, 8);
303
+ }
304
+ function padEnd(str, len) {
305
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
306
+ }
307
+ var issueCommand = new import_commander2.Command("issue").description("Manage issues").addHelpText("after", `
308
+ Examples:
309
+ $ atoll issue list
310
+ $ atoll issue list --status in_progress --priority 1
311
+ $ atoll issue view ATOLL-42
312
+ $ atoll issue create --title "Fix login" --priority 1 --status todo
313
+ $ atoll issue update ATOLL-42 --status done
314
+ $ atoll issue assign ATOLL-42 --to <user-id>`);
315
+ issueCommand.command("list").description("List issues").option("--status <status>", `Filter by status (${VALID_STATUSES.join(", ")})`).option("--assignee <user>", "Filter by assignee ID").option("--priority <n>", "Filter by priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
316
+ try {
317
+ const client = new AtollClient();
318
+ const orgId = await resolveOrgId(client);
319
+ const params = new URLSearchParams();
320
+ if (opts.status) params.set("status", opts.status);
321
+ if (opts.assignee) params.set("assigneeId", opts.assignee);
322
+ if (opts.priority !== void 0) params.set("priority", String(opts.priority));
323
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
324
+ const qs = params.toString();
325
+ const data = await client.get(`/api/orgs/${orgId}/issues${qs ? `?${qs}` : ""}`);
326
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
327
+ for (const issue of data.issues) {
328
+ process.stdout.write(JSON.stringify(issue) + "\n");
329
+ }
330
+ return;
331
+ }
332
+ if (data.issues.length === 0) {
333
+ console.log("No issues found.");
334
+ return;
335
+ }
336
+ const header = bold(`${padEnd("ID", 10)} ${padEnd("STATUS", 14)} ${padEnd("PRI", 8)} TITLE`);
337
+ console.log(header);
338
+ console.log("\u2500".repeat(82));
339
+ for (const issue of data.issues) {
340
+ const id = formatIdentifier(issue);
341
+ const pri = PRIORITY_LABELS[issue.priority] ?? String(issue.priority);
342
+ const title = issue.title.length > 50 ? issue.title.slice(0, 47) + "\u2026" : issue.title;
343
+ const icon = priorityIcon(issue.priority);
344
+ console.log(`${padEnd(id, 10)} ${statusColor(issue.status, padEnd(issue.status, 14))} ${icon} ${padEnd(pri, 8)} ${title}`);
345
+ }
346
+ console.log(dim(`${data.issues.length} of ${data.total} issues`));
347
+ } catch (err) {
348
+ handleApiError(err);
349
+ }
350
+ });
351
+ issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(async (identifier) => {
352
+ try {
353
+ const client = new AtollClient();
354
+ const orgId = await resolveOrgId(client);
355
+ const issueId = await resolveIssueId(client, orgId, identifier);
356
+ const { issue } = await client.get(`/api/orgs/${orgId}/issues/${issueId}`);
357
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
358
+ process.stdout.write(JSON.stringify(issue) + "\n");
359
+ return;
360
+ }
361
+ const id = formatIdentifier(issue);
362
+ const pri = PRIORITY_LABELS[issue.priority] ?? String(issue.priority);
363
+ console.log(`${bold(id)} ${issue.title}`);
364
+ console.log(`Status: ${statusColor(issue.status, issue.status)} Priority: ${priorityIcon(issue.priority)} ${pri}`);
365
+ if (issue.assignee_id) console.log(`Assignee: ${issue.assignee_id}`);
366
+ if (issue.due_date) console.log(`Due: ${issue.due_date}`);
367
+ if (issue.description) {
368
+ console.log(`
369
+ ${issue.description}`);
370
+ }
371
+ if (issue.issue_labels && issue.issue_labels.length > 0) {
372
+ const labels = issue.issue_labels.map((l) => l.labels.name).join(", ");
373
+ console.log(`Labels: ${labels}`);
374
+ }
375
+ if (issue.sub_tasks && Array.isArray(issue.sub_tasks) && issue.sub_tasks.length > 0) {
376
+ console.log(`
377
+ Subtasks: ${issue.sub_tasks.length}`);
378
+ }
379
+ if (issue.created_at) console.log(dim(`Created: ${issue.created_at}`));
380
+ if (issue.updated_at) console.log(dim(`Updated: ${issue.updated_at}`));
381
+ } catch (err) {
382
+ handleApiError(err);
383
+ }
384
+ });
385
+ issueCommand.command("create").description("Create a new issue").requiredOption("--title <title>", "Issue title").option("--description <text>", "Issue description").option("--status <status>", `Status (${VALID_STATUSES.join(", ")})`).option("--priority <n>", "Priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).action(async (opts) => {
386
+ try {
387
+ if (opts.status && !VALID_STATUSES.includes(opts.status)) {
388
+ outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
389
+ process.exit(2);
390
+ }
391
+ if (opts.priority !== void 0 && ![0, 1, 2, 3].includes(opts.priority)) {
392
+ outputError("Priority must be 0 (urgent), 1 (high), 2 (medium), or 3 (low).");
393
+ process.exit(2);
394
+ }
395
+ const client = new AtollClient();
396
+ const orgId = await resolveOrgId(client);
397
+ const body = { title: opts.title };
398
+ if (opts.description !== void 0) body.description = opts.description;
399
+ if (opts.status) body.status = opts.status;
400
+ if (opts.priority !== void 0) body.priority = opts.priority;
401
+ const { issue } = await client.post(`/api/orgs/${orgId}/issues`, body);
402
+ output(
403
+ { issue },
404
+ success(`Created ${formatIdentifier(issue)}: ${issue.title}`)
405
+ );
406
+ } catch (err) {
407
+ handleApiError(err);
408
+ }
409
+ });
410
+ issueCommand.command("update <identifier>").description("Update an issue").option("--title <t>", "New title").option("--status <s>", `New status (${VALID_STATUSES.join(", ")})`).option("--priority <p>", "New priority (0-3)", parseInt).action(async (identifier, opts) => {
411
+ try {
412
+ if (opts.status && !VALID_STATUSES.includes(opts.status)) {
413
+ outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
414
+ process.exit(2);
415
+ }
416
+ if (opts.priority !== void 0 && ![0, 1, 2, 3].includes(opts.priority)) {
417
+ outputError("Priority must be 0-3.");
418
+ process.exit(2);
419
+ }
420
+ const body = {};
421
+ if (opts.title !== void 0) body.title = opts.title;
422
+ if (opts.status) body.status = opts.status;
423
+ if (opts.priority !== void 0) body.priority = opts.priority;
424
+ if (Object.keys(body).length === 0) {
425
+ outputError("No fields to update. Provide --title, --status, or --priority.");
426
+ process.exit(2);
427
+ }
428
+ const client = new AtollClient();
429
+ const orgId = await resolveOrgId(client);
430
+ const issueId = await resolveIssueId(client, orgId, identifier);
431
+ const { issue } = await client.patch(`/api/orgs/${orgId}/issues/${issueId}`, body);
432
+ output(
433
+ { issue },
434
+ success(`Updated ${formatIdentifier(issue)}: ${issue.title}`)
435
+ );
436
+ } catch (err) {
437
+ handleApiError(err);
438
+ }
439
+ });
440
+ issueCommand.command("delete <identifier>").description("Delete an issue (admin/owner only)").action(async (identifier) => {
441
+ try {
442
+ const client = new AtollClient();
443
+ const orgId = await resolveOrgId(client);
444
+ const issueId = await resolveIssueId(client, orgId, identifier);
445
+ await client.delete(`/api/orgs/${orgId}/issues/${issueId}`);
446
+ output(
447
+ { success: true, id: issueId },
448
+ success(`Deleted issue ${identifier}`)
449
+ );
450
+ } catch (err) {
451
+ handleApiError(err);
452
+ }
453
+ });
454
+ issueCommand.command("assign <identifier>").description("Assign an issue to a user or agent").requiredOption("--to <user>", 'User/agent ID or "self" for yourself').action(async (identifier, opts) => {
455
+ try {
456
+ const client = new AtollClient();
457
+ const orgId = await resolveOrgId(client);
458
+ const issueId = await resolveIssueId(client, orgId, identifier);
459
+ let assigneeId = opts.to;
460
+ if (assigneeId === "self") {
461
+ const me = await client.get("/api/auth/me");
462
+ const callerUserId = me.auth?.userId;
463
+ if (!callerUserId) {
464
+ outputError("Could not resolve your user ID.");
465
+ process.exit(1);
466
+ }
467
+ const { members } = await client.get(
468
+ `/api/orgs/${orgId}/members`
469
+ );
470
+ const member = members.find((m) => m.id === callerUserId || m.user_id === callerUserId);
471
+ if (!member) {
472
+ outputError("You are not a member of this organisation.");
473
+ process.exit(1);
474
+ }
475
+ assigneeId = member.id;
476
+ }
477
+ const { issue } = await client.patch(
478
+ `/api/orgs/${orgId}/issues/${issueId}`,
479
+ { assignee_id: assigneeId }
480
+ );
481
+ output(
482
+ { issue },
483
+ success(`Assigned ${formatIdentifier(issue)} to ${opts.to}`)
484
+ );
485
+ } catch (err) {
486
+ handleApiError(err);
487
+ }
488
+ });
489
+ issueCommand.command("unassign <identifier>").description("Remove assignee from an issue").action(async (identifier) => {
490
+ try {
491
+ const client = new AtollClient();
492
+ const orgId = await resolveOrgId(client);
493
+ const issueId = await resolveIssueId(client, orgId, identifier);
494
+ const { issue } = await client.patch(
495
+ `/api/orgs/${orgId}/issues/${issueId}`,
496
+ { assignee_id: null }
497
+ );
498
+ output(
499
+ { issue },
500
+ success(`Unassigned ${formatIdentifier(issue)}`)
501
+ );
502
+ } catch (err) {
503
+ handleApiError(err);
504
+ }
505
+ });
506
+
507
+ // src/commands/comment.ts
508
+ var import_commander3 = require("commander");
509
+ var BOLD2 = "\x1B[1m";
510
+ var DIM2 = "\x1B[2m";
511
+ var RESET2 = "\x1B[0m";
512
+ var CYAN = "\x1B[36m";
513
+ function formatAuthor(comment) {
514
+ const name = comment.author?.display_name ?? comment.author_id;
515
+ const badge = (comment.author?.type ?? comment.author_type) === "agent" ? " [agent]" : "";
516
+ return `${name}${badge}`;
517
+ }
518
+ function formatTimestamp(ts) {
519
+ if (!ts) return "";
520
+ const d = new Date(ts);
521
+ return d.toLocaleString();
522
+ }
523
+ var commentCommand = new import_commander3.Command("comment").description("Manage issue comments");
524
+ commentCommand.command("list <identifier>").description("List comments on an issue (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(async (identifier) => {
525
+ try {
526
+ const client = new AtollClient();
527
+ const orgId = await resolveOrgId(client);
528
+ const issueId = await resolveIssueId(client, orgId, identifier);
529
+ const data = await client.get(
530
+ `/api/orgs/${orgId}/issues/${issueId}/comments`
531
+ );
532
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
533
+ for (const comment of data.comments) {
534
+ process.stdout.write(JSON.stringify(comment) + "\n");
535
+ }
536
+ return;
537
+ }
538
+ if (data.comments.length === 0) {
539
+ console.log("No comments found.");
540
+ return;
541
+ }
542
+ for (const comment of data.comments) {
543
+ const author = formatAuthor(comment);
544
+ const time = formatTimestamp(comment.created_at);
545
+ console.log(`${BOLD2}${CYAN}${author}${RESET2} ${DIM2}${time}${RESET2}`);
546
+ console.log(comment.body);
547
+ console.log();
548
+ }
549
+ console.log(`${DIM2}${data.comments.length} comment(s)${RESET2}`);
550
+ } catch (err) {
551
+ handleApiError(err);
552
+ }
553
+ });
554
+ commentCommand.command("add <identifier>").description("Add a comment to an issue").requiredOption("--body <text>", "Comment body").action(async (identifier, opts) => {
555
+ try {
556
+ const client = new AtollClient();
557
+ const orgId = await resolveOrgId(client);
558
+ const issueId = await resolveIssueId(client, orgId, identifier);
559
+ const { comment } = await client.post(
560
+ `/api/orgs/${orgId}/issues/${issueId}/comments`,
561
+ { body: opts.body }
562
+ );
563
+ output(
564
+ { comment },
565
+ `\u2713 Comment added to ${identifier}`
566
+ );
567
+ } catch (err) {
568
+ handleApiError(err);
569
+ }
570
+ });
571
+ commentCommand.command("update <comment-id>").description("Update a comment").requiredOption("--body <text>", "New comment body").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").action(async (commentId, opts) => {
572
+ try {
573
+ const client = new AtollClient();
574
+ const orgId = await resolveOrgId(client);
575
+ const issueId = await resolveIssueId(client, orgId, opts.issue);
576
+ const { comment } = await client.patch(
577
+ `/api/orgs/${orgId}/issues/${issueId}/comments/${commentId}`,
578
+ { body: opts.body }
579
+ );
580
+ output(
581
+ { comment },
582
+ `\u2713 Comment ${commentId} updated`
583
+ );
584
+ } catch (err) {
585
+ handleApiError(err);
586
+ }
587
+ });
588
+ commentCommand.command("delete <comment-id>").description("Delete a comment").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").action(async (commentId, opts) => {
589
+ try {
590
+ const client = new AtollClient();
591
+ const orgId = await resolveOrgId(client);
592
+ const issueId = await resolveIssueId(client, orgId, opts.issue);
593
+ await client.delete(`/api/orgs/${orgId}/issues/${issueId}/comments/${commentId}`);
594
+ output(
595
+ { success: true, id: commentId },
596
+ `\u2713 Comment ${commentId} deleted`
597
+ );
598
+ } catch (err) {
599
+ handleApiError(err);
600
+ }
601
+ });
602
+
603
+ // src/commands/project.ts
604
+ var import_commander4 = require("commander");
605
+ function progressBar(progress, width = 20) {
606
+ const filled = Math.round(progress / 100 * width);
607
+ const empty = width - filled;
608
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${progress}%`;
609
+ }
610
+ var projectCommand = new import_commander4.Command("project").description("Manage projects").addHelpText("after", `
611
+ Examples:
612
+ $ atoll project list
613
+ $ atoll project create --name "My Project" --icon \u{1F680}
614
+ $ atoll project view <project-id>`);
615
+ projectCommand.command("list").description("List all projects with progress").action(async () => {
616
+ const client = new AtollClient();
617
+ try {
618
+ const orgId = await resolveOrgId(client);
619
+ const data = await client.get(`/api/orgs/${orgId}/projects`);
620
+ const projects = data.projects ?? [];
621
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
622
+ for (const p of projects) {
623
+ process.stdout.write(JSON.stringify(p) + "\n");
624
+ }
625
+ return;
626
+ }
627
+ if (projects.length === 0) {
628
+ console.log(dim("No projects found."));
629
+ return;
630
+ }
631
+ for (const p of projects) {
632
+ const progress = p.progress ?? 0;
633
+ const bar = progressBar(progress);
634
+ console.log(`${bold(`${p.icon} ${p.name}`)} ${dim(`(${p.id})`)}`);
635
+ if (p.description) console.log(` ${dim(p.description)}`);
636
+ console.log(` ${cyan(bar)} ${p.issueCount ?? 0} issues, ${p.completedCount ?? 0} done`);
637
+ if (p.status === "archived") console.log(` ${dim("[archived]")}`);
638
+ console.log("");
639
+ }
640
+ } catch (err) {
641
+ handleApiError(err);
642
+ }
643
+ });
644
+ projectCommand.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").option("--description <desc>", "Project description").option("--color <color>", "Project color (hex)", "#6366f1").option("--icon <icon>", "Project icon (emoji)", "\u{1F4C1}").action(async (opts) => {
645
+ const client = new AtollClient();
646
+ try {
647
+ const orgId = await resolveOrgId(client);
648
+ const data = await client.post(`/api/orgs/${orgId}/projects`, {
649
+ name: opts.name,
650
+ description: opts.description ?? null,
651
+ color: opts.color,
652
+ icon: opts.icon
653
+ });
654
+ output(
655
+ { project: data.project },
656
+ success(`Created project ${data.project.icon} ${data.project.name} (${dim(data.project.id)})`)
657
+ );
658
+ } catch (err) {
659
+ handleApiError(err);
660
+ }
661
+ });
662
+ projectCommand.command("view <projectId>").description("View project details and issues").action(async (projectId) => {
663
+ const client = new AtollClient();
664
+ try {
665
+ const orgId = await resolveOrgId(client);
666
+ const data = await client.get(
667
+ `/api/orgs/${orgId}/projects/${projectId}`
668
+ );
669
+ const p = data.project;
670
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
671
+ process.stdout.write(JSON.stringify(p) + "\n");
672
+ return;
673
+ }
674
+ const progress = p.progress ?? 0;
675
+ console.log(bold(`${p.icon} ${p.name}`));
676
+ if (p.description) console.log(` ${p.description}`);
677
+ console.log(` ${cyan(progressBar(progress))}`);
678
+ console.log(` ${p.issueCount ?? 0} issues \xB7 ${p.completedCount ?? 0} completed`);
679
+ console.log("");
680
+ const issues = p.issues ?? [];
681
+ if (issues.length === 0) {
682
+ console.log(dim("No issues in this project."));
683
+ return;
684
+ }
685
+ console.log(bold("Issues"));
686
+ for (const issue of issues) {
687
+ const num = issue.number ? `#${issue.number}` : issue.id.slice(0, 8);
688
+ console.log(` ${dim(num)} ${issue.title} ${dim(`[${issue.status}]`)}`);
689
+ }
690
+ } catch (err) {
691
+ handleApiError(err);
692
+ }
693
+ });
694
+
695
+ // src/commands/milestone.ts
696
+ var import_commander5 = require("commander");
697
+ function progressBar2(progress, width = 20) {
698
+ const filled = Math.round(progress / 100 * width);
699
+ const empty = width - filled;
700
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${progress}%`;
701
+ }
702
+ var milestoneCommand = new import_commander5.Command("milestone").description("Manage milestones").addHelpText("after", `
703
+ Examples:
704
+ $ atoll milestone list --project <project-id>
705
+ $ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
706
+ milestoneCommand.command("list").description("List milestones for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
707
+ const client = new AtollClient();
708
+ try {
709
+ const orgId = await resolveOrgId(client);
710
+ const data = await client.get(
711
+ `/api/orgs/${orgId}/projects/${opts.project}/milestones`
712
+ );
713
+ const milestones = data.milestones ?? [];
714
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
715
+ for (const m of milestones) {
716
+ process.stdout.write(JSON.stringify(m) + "\n");
717
+ }
718
+ return;
719
+ }
720
+ if (milestones.length === 0) {
721
+ console.log(dim("No milestones found."));
722
+ return;
723
+ }
724
+ for (const m of milestones) {
725
+ const progress = m.progress ?? 0;
726
+ const bar = progressBar2(progress);
727
+ const overdue = m.isOverdue ? ` ${red("[OVERDUE]")}` : "";
728
+ console.log(`${bold(m.name)} ${dim(`(${m.id})`)}${overdue}`);
729
+ if (m.due_date) console.log(` Due: ${m.due_date}`);
730
+ if (m.description) console.log(` ${dim(m.description)}`);
731
+ console.log(` ${cyan(bar)} ${m.issueCount ?? 0} issues, ${m.completedCount ?? 0} done`);
732
+ if (m.status === "closed") console.log(` ${dim("[closed]")}`);
733
+ console.log("");
734
+ }
735
+ } catch (err) {
736
+ handleApiError(err);
737
+ }
738
+ });
739
+ milestoneCommand.command("create").description("Create a new milestone").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Milestone name").option("--date <YYYY-MM-DD>", "Due date").option("--description <desc>", "Description").action(async (opts) => {
740
+ const client = new AtollClient();
741
+ try {
742
+ const orgId = await resolveOrgId(client);
743
+ const data = await client.post(
744
+ `/api/orgs/${orgId}/projects/${opts.project}/milestones`,
745
+ {
746
+ name: opts.name,
747
+ description: opts.description ?? null,
748
+ dueDate: opts.date ?? null
749
+ }
750
+ );
751
+ output(
752
+ { milestone: data.milestone },
753
+ success(`Milestone created: ${bold(data.milestone.name)} ${dim(`(${data.milestone.id})`)}`)
754
+ );
755
+ if (data.milestone.due_date) {
756
+ console.log(` Due: ${data.milestone.due_date}`);
757
+ }
758
+ } catch (err) {
759
+ handleApiError(err);
760
+ }
761
+ });
762
+
763
+ // src/commands/config.ts
764
+ var import_commander6 = require("commander");
765
+ init_config();
766
+ var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, API key)").addHelpText("after", `
767
+ Examples:
768
+ $ atoll config show
769
+ $ atoll config set-org my-org
770
+ $ atoll config set-team team-abc123`);
771
+ configCommand.command("show").description("Display current configuration").action(() => {
772
+ const cfg = readConfig();
773
+ const apiKeySet = !!(process.env.ATOLL_API_KEY || cfg.apiKey);
774
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
775
+ output(
776
+ {
777
+ orgSlug: cfg.orgSlug ?? null,
778
+ defaultTeam: cfg.defaultTeam ?? null,
779
+ apiKeySet
780
+ },
781
+ ""
782
+ );
783
+ return;
784
+ }
785
+ console.log(`${bold("Atoll CLI Configuration")}`);
786
+ console.log(` ${dim("Org slug:")} ${cfg.orgSlug ? bold(cfg.orgSlug) : gray("(not set)")}`);
787
+ console.log(` ${dim("Default team:")} ${cfg.defaultTeam ? bold(cfg.defaultTeam) : gray("(not set)")}`);
788
+ console.log(` ${dim("API key:")} ${apiKeySet ? bold("set") : gray("not set \u2014 run `atoll auth login`")}`);
789
+ });
790
+ configCommand.command("set-org <slug>").description("Set the default organisation slug").action((slug) => {
791
+ const cfg = readConfig();
792
+ cfg.orgSlug = slug;
793
+ writeConfig(cfg);
794
+ output(
795
+ { orgSlug: slug },
796
+ success(`Default org set to "${slug}"`)
797
+ );
798
+ });
799
+ configCommand.command("set-team <team>").description("Set the default team slug or ID").action((team) => {
800
+ const cfg = readConfig();
801
+ cfg.defaultTeam = team;
802
+ writeConfig(cfg);
803
+ output(
804
+ { defaultTeam: team },
805
+ success(`Default team set to "${team}"`)
806
+ );
807
+ });
808
+
809
+ // src/commands/webhook.ts
810
+ var import_commander7 = require("commander");
811
+ function handleApiError2(err) {
812
+ const msg = err.message ?? String(err);
813
+ if (/API 401\b/.test(msg)) {
814
+ outputError("Session expired \u2014 run `atoll auth login` to re-authenticate.");
815
+ } else if (/API 403\b/.test(msg)) {
816
+ outputError("Permission denied. You do not have access to this resource.");
817
+ } else {
818
+ outputError(msg);
819
+ }
820
+ process.exit(1);
821
+ }
822
+ async function resolveOrgId2(client, orgSlugOverride) {
823
+ const { orgs } = await client.get("/api/orgs");
824
+ if (!orgs || orgs.length === 0) {
825
+ outputError("No organizations found.");
826
+ process.exit(1);
827
+ }
828
+ if (orgs.length === 1) return orgs[0].id;
829
+ const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
830
+ const config = readConfig2();
831
+ const slug = orgSlugOverride ?? process.env.ATOLL_ORG ?? config.orgSlug;
832
+ if (slug) {
833
+ const match = orgs.find((o) => o.slug === slug);
834
+ if (match) return match.id;
835
+ outputError(`Org "${slug}" not found. Available: ${orgs.map((o) => o.slug).join(", ")}`);
836
+ process.exit(1);
837
+ }
838
+ outputError(`Multiple orgs found. Use --org <slug>.`);
839
+ process.exit(1);
840
+ }
841
+ function padEnd2(str, len) {
842
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
843
+ }
844
+ var webhookCommand = new import_commander7.Command("webhook").description("Manage outbound webhooks").addHelpText("after", `
845
+ Examples:
846
+ $ atoll webhook list
847
+ $ atoll webhook create --url https://example.com/hook --events issue.created,issue.updated
848
+ $ atoll webhook delete <id>`);
849
+ webhookCommand.command("list").description("List webhooks for the current org").action(async () => {
850
+ try {
851
+ const client = new AtollClient();
852
+ const orgId = await resolveOrgId2(client);
853
+ const data = await client.get(`/api/webhooks?orgId=${orgId}`);
854
+ if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
855
+ for (const wh of data.webhooks) {
856
+ process.stdout.write(JSON.stringify(wh) + "\n");
857
+ }
858
+ return;
859
+ }
860
+ if (data.webhooks.length === 0) {
861
+ console.log("No webhooks configured.");
862
+ return;
863
+ }
864
+ const header = bold(`${padEnd2("ID", 38)} ${padEnd2("URL", 40)} ${padEnd2("EVENTS", 20)} ACTIVE`);
865
+ console.log(header);
866
+ console.log("\u2500".repeat(100));
867
+ for (const wh of data.webhooks) {
868
+ const evts = wh.events.length === 0 ? "all" : wh.events.join(",");
869
+ const active = wh.enabled ? green("yes") : gray("no");
870
+ const url = wh.url.length > 38 ? wh.url.slice(0, 35) + "\u2026" : wh.url;
871
+ const evtStr = evts.length > 18 ? evts.slice(0, 15) + "\u2026" : evts;
872
+ console.log(`${padEnd2(wh.id.slice(0, 36), 38)} ${padEnd2(url, 40)} ${padEnd2(evtStr, 20)} ${active}`);
873
+ }
874
+ console.log(dim(`${data.webhooks.length} webhook(s)`));
875
+ } catch (err) {
876
+ handleApiError2(err);
877
+ }
878
+ });
879
+ webhookCommand.command("create").description("Create a new webhook").requiredOption("--url <url>", "Payload URL (HTTPS)").option("--events <events>", "Comma-separated event types (default: all)", "").action(async (opts) => {
880
+ try {
881
+ if (!opts.url.startsWith("https://")) {
882
+ outputError("URL must use HTTPS.");
883
+ process.exit(2);
884
+ }
885
+ const events = opts.events ? opts.events.split(",").map((e) => e.trim()).filter(Boolean) : [];
886
+ const client = new AtollClient();
887
+ const orgId = await resolveOrgId2(client);
888
+ const data = await client.post(
889
+ `/api/webhooks?orgId=${orgId}`,
890
+ { url: opts.url, events }
891
+ );
892
+ output(
893
+ { webhook: data.webhook, secret: data.secret },
894
+ [
895
+ success(`Created webhook ${data.webhook.id}`),
896
+ "",
897
+ bold("Signing secret (save this \u2014 shown only once):"),
898
+ ` ${data.secret}`
899
+ ].join("\n")
900
+ );
901
+ } catch (err) {
902
+ handleApiError2(err);
903
+ }
904
+ });
905
+ webhookCommand.command("delete <id>").description("Delete a webhook").action(async (id) => {
906
+ try {
907
+ const client = new AtollClient();
908
+ await client.delete(`/api/webhooks/${id}`);
909
+ output(
910
+ { success: true, id },
911
+ success(`Deleted webhook ${id}`)
912
+ );
913
+ } catch (err) {
914
+ handleApiError2(err);
915
+ }
916
+ });
917
+
918
+ // src/index.ts
919
+ var program = new import_commander8.Command("atoll").description("Atoll CLI \u2014 project management from the terminal").version("0.1.0").addHelpText("after", `
920
+ Examples:
921
+ $ atoll issue list
922
+ $ atoll issue create --title "Fix login bug" --priority 1
923
+ $ atoll issue view ATOLL-42
924
+ $ atoll project list
925
+ $ atoll milestone list --project <id>
926
+ $ atoll config show`).option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").hook("preAction", (thisCommand) => {
927
+ const opts = thisCommand.opts();
928
+ if (opts.org) process.env.ATOLL_ORG = opts.org;
929
+ if (opts.team) process.env.ATOLL_TEAM = opts.team;
930
+ });
931
+ program.addCommand(authCommand);
932
+ program.addCommand(issueCommand);
933
+ program.addCommand(commentCommand);
934
+ program.addCommand(projectCommand);
935
+ program.addCommand(milestoneCommand);
936
+ program.addCommand(configCommand);
937
+ program.addCommand(webhookCommand);
938
+ var completionCommand = new import_commander8.Command("completion").description("Output shell completion scripts").addHelpText("after", `
939
+ Examples:
940
+ $ atoll completion bash >> ~/.bashrc
941
+ $ atoll completion zsh >> ~/.zshrc`);
942
+ completionCommand.command("bash").description("Output bash completion script").action(() => {
943
+ process.stdout.write(bashCompletion());
944
+ });
945
+ completionCommand.command("zsh").description("Output zsh completion script").action(() => {
946
+ process.stdout.write(zshCompletion());
947
+ });
948
+ program.addCommand(completionCommand);
949
+ program.parse();
950
+ function bashCompletion() {
951
+ return `# Atoll CLI bash completion
952
+ # Add to ~/.bashrc: source <(atoll completion bash)
953
+
954
+ _atoll_completions() {
955
+ local cur prev words cword
956
+ _init_completion || return
957
+
958
+ local commands="auth issue comment project milestone config webhook completion"
959
+ local issue_cmds="list view create update delete assign unassign"
960
+ local project_cmds="list create view"
961
+ local milestone_cmds="list create"
962
+ local config_cmds="show set-org set-team"
963
+ local auth_cmds="login logout status"
964
+ local webhook_cmds="list create delete"
965
+ local completion_cmds="bash zsh"
966
+
967
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
968
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
969
+ return 0
970
+ fi
971
+
972
+ case "\${words[1]}" in
973
+ issue)
974
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
975
+ COMPREPLY=( $(compgen -W "\${issue_cmds}" -- "\${cur}") )
976
+ fi
977
+ ;;
978
+ project)
979
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
980
+ COMPREPLY=( $(compgen -W "\${project_cmds}" -- "\${cur}") )
981
+ fi
982
+ ;;
983
+ milestone)
984
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
985
+ COMPREPLY=( $(compgen -W "\${milestone_cmds}" -- "\${cur}") )
986
+ fi
987
+ ;;
988
+ config)
989
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
990
+ COMPREPLY=( $(compgen -W "\${config_cmds}" -- "\${cur}") )
991
+ fi
992
+ ;;
993
+ auth)
994
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
995
+ COMPREPLY=( $(compgen -W "\${auth_cmds}" -- "\${cur}") )
996
+ fi
997
+ ;;
998
+ webhook)
999
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
1000
+ COMPREPLY=( $(compgen -W "\${webhook_cmds}" -- "\${cur}") )
1001
+ fi
1002
+ ;;
1003
+ completion)
1004
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
1005
+ COMPREPLY=( $(compgen -W "\${completion_cmds}" -- "\${cur}") )
1006
+ fi
1007
+ ;;
1008
+ esac
1009
+ }
1010
+
1011
+ complete -F _atoll_completions atoll
1012
+ `;
1013
+ }
1014
+ function zshCompletion() {
1015
+ return `#compdef atoll
1016
+ # Atoll CLI zsh completion
1017
+ # Add to ~/.zshrc: source <(atoll completion zsh)
1018
+
1019
+ _atoll() {
1020
+ local state
1021
+
1022
+ _arguments \\
1023
+ '1: :->command' \\
1024
+ '*: :->args'
1025
+
1026
+ case $state in
1027
+ command)
1028
+ _values 'command' \\
1029
+ 'auth[Manage authentication]' \\
1030
+ 'issue[Manage issues]' \\
1031
+ 'comment[Manage comments]' \\
1032
+ 'project[Manage projects]' \\
1033
+ 'milestone[Manage milestones]' \\
1034
+ 'config[Manage CLI configuration]' \\
1035
+ 'webhook[Manage outbound webhooks]' \\
1036
+ 'completion[Output shell completion scripts]'
1037
+ ;;
1038
+ args)
1039
+ case $words[2] in
1040
+ issue)
1041
+ _values 'subcommand' \\
1042
+ 'list[List issues]' \\
1043
+ 'view[View issue details]' \\
1044
+ 'create[Create a new issue]' \\
1045
+ 'update[Update an issue]' \\
1046
+ 'delete[Delete an issue]' \\
1047
+ 'assign[Assign an issue]' \\
1048
+ 'unassign[Remove assignee]'
1049
+ ;;
1050
+ project)
1051
+ _values 'subcommand' \\
1052
+ 'list[List projects]' \\
1053
+ 'create[Create a project]' \\
1054
+ 'view[View project details]'
1055
+ ;;
1056
+ milestone)
1057
+ _values 'subcommand' \\
1058
+ 'list[List milestones]' \\
1059
+ 'create[Create a milestone]'
1060
+ ;;
1061
+ config)
1062
+ _values 'subcommand' \\
1063
+ 'show[Show configuration]' \\
1064
+ 'set-org[Set default org slug]' \\
1065
+ 'set-team[Set default team]'
1066
+ ;;
1067
+ auth)
1068
+ _values 'subcommand' \\
1069
+ 'login[Log in]' \\
1070
+ 'logout[Log out]' \\
1071
+ 'status[Show auth status]'
1072
+ ;;
1073
+ webhook)
1074
+ _values 'subcommand' \\
1075
+ 'list[List webhooks]' \\
1076
+ 'create[Create a webhook]' \\
1077
+ 'delete[Delete a webhook]'
1078
+ ;;
1079
+ completion)
1080
+ _values 'shell' 'bash' 'zsh'
1081
+ ;;
1082
+ esac
1083
+ ;;
1084
+ esac
1085
+ }
1086
+
1087
+ _atoll "$@"
1088
+ `;
1089
+ }
1090
+ //# sourceMappingURL=index.js.map