@atollhq/cli 0.1.2 → 0.1.4
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/README.md +84 -0
- package/dist/index.js +652 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,7 +81,9 @@ var init_config = __esm({
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
// src/index.ts
|
|
84
|
-
var
|
|
84
|
+
var import_commander11 = require("commander");
|
|
85
|
+
var import_node_fs4 = require("fs");
|
|
86
|
+
var import_node_path4 = require("path");
|
|
85
87
|
|
|
86
88
|
// src/commands/auth.ts
|
|
87
89
|
var import_commander = require("commander");
|
|
@@ -95,6 +97,9 @@ function isJsonMode() {
|
|
|
95
97
|
if (process.env.OUTPUT_FORMAT === "json") return true;
|
|
96
98
|
return !process.stdout.isTTY;
|
|
97
99
|
}
|
|
100
|
+
function enableJsonMode() {
|
|
101
|
+
process.env.OUTPUT_FORMAT = "json";
|
|
102
|
+
}
|
|
98
103
|
function output(data, humanReadable) {
|
|
99
104
|
if (isJsonMode()) {
|
|
100
105
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
@@ -102,13 +107,34 @@ function output(data, humanReadable) {
|
|
|
102
107
|
console.log(humanReadable);
|
|
103
108
|
}
|
|
104
109
|
}
|
|
110
|
+
function outputJson(data) {
|
|
111
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
112
|
+
}
|
|
105
113
|
function outputError(message) {
|
|
106
114
|
if (isJsonMode()) {
|
|
107
|
-
process.
|
|
115
|
+
process.stderr.write(JSON.stringify({ error: message }) + "\n");
|
|
108
116
|
} else {
|
|
109
117
|
console.error(`Error: ${message}`);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
120
|
+
var DEFAULT_LIST_LIMIT = 25;
|
|
121
|
+
var MAX_LIST_LIMIT = 100;
|
|
122
|
+
function normalizeLimit(limit, defaultLimit = DEFAULT_LIST_LIMIT) {
|
|
123
|
+
if (limit === void 0 || Number.isNaN(limit)) return defaultLimit;
|
|
124
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
|
|
125
|
+
outputError(`--limit must be an integer from 1 to ${MAX_LIST_LIMIT} (got: "${limit}")`);
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
return limit;
|
|
129
|
+
}
|
|
130
|
+
function normalizeOffset(offset) {
|
|
131
|
+
if (offset === void 0 || Number.isNaN(offset)) return 0;
|
|
132
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
133
|
+
outputError(`--offset must be a non-negative integer (got: "${offset}")`);
|
|
134
|
+
process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
return offset;
|
|
137
|
+
}
|
|
112
138
|
|
|
113
139
|
// src/lib/client.ts
|
|
114
140
|
var DEFAULT_BASE_URL = "https://atollhq.com";
|
|
@@ -138,7 +164,10 @@ var AtollClient = class {
|
|
|
138
164
|
const body = await res.text();
|
|
139
165
|
throw new Error(`API ${res.status}: ${body}`);
|
|
140
166
|
}
|
|
141
|
-
|
|
167
|
+
if (res.status === 204) return void 0;
|
|
168
|
+
const text = await res.text();
|
|
169
|
+
if (!text) return void 0;
|
|
170
|
+
return JSON.parse(text);
|
|
142
171
|
}
|
|
143
172
|
async get(path) {
|
|
144
173
|
return this.request(path, { method: "GET" });
|
|
@@ -462,15 +491,26 @@ Examples:
|
|
|
462
491
|
$ atoll issue create --title "Fix login" --priority 1 --status todo
|
|
463
492
|
$ atoll issue update ATOLL-42 --status done
|
|
464
493
|
$ atoll issue assign ATOLL-42 --to <user-id>`);
|
|
465
|
-
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) => {
|
|
494
|
+
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).option("--offset <n>", "Results offset for pagination", parseInt).action(async (opts) => {
|
|
466
495
|
try {
|
|
496
|
+
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
497
|
+
outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
498
|
+
process.exit(2);
|
|
499
|
+
}
|
|
500
|
+
if (opts.priority !== void 0 && ![0, 1, 2, 3].includes(opts.priority)) {
|
|
501
|
+
outputError("Priority must be 0 (urgent), 1 (high), 2 (medium), or 3 (low).");
|
|
502
|
+
process.exit(2);
|
|
503
|
+
}
|
|
467
504
|
const client = new AtollClient();
|
|
468
505
|
const org = await resolveOrg(client);
|
|
506
|
+
const limit = normalizeLimit(opts.limit);
|
|
507
|
+
const offset = normalizeOffset(opts.offset);
|
|
469
508
|
const params = new URLSearchParams();
|
|
470
509
|
if (opts.status) params.set("status", opts.status);
|
|
471
510
|
if (opts.assignee) params.set("assigneeId", opts.assignee);
|
|
472
511
|
if (opts.priority !== void 0) params.set("priority", String(opts.priority));
|
|
473
|
-
|
|
512
|
+
params.set("limit", String(limit));
|
|
513
|
+
if (offset > 0) params.set("offset", String(offset));
|
|
474
514
|
const qs = params.toString();
|
|
475
515
|
const [data, projects] = await Promise.all([
|
|
476
516
|
client.get(`/api/orgs/${org.id}/issues${qs ? `?${qs}` : ""}`),
|
|
@@ -479,10 +519,17 @@ issueCommand.command("list").description("List issues").option("--status <status
|
|
|
479
519
|
const enriched = data.issues.map(
|
|
480
520
|
(issue) => attachIssueUrl(issue, client.baseUrl, org.slug, projects)
|
|
481
521
|
);
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
522
|
+
if (isJsonMode()) {
|
|
523
|
+
outputJson({
|
|
524
|
+
resource: "issues",
|
|
525
|
+
items: enriched,
|
|
526
|
+
total: data.total,
|
|
527
|
+
limit: data.limit,
|
|
528
|
+
offset: data.offset,
|
|
529
|
+
nextOffset: data.offset + data.limit < data.total ? data.offset + data.limit : null,
|
|
530
|
+
truncated: data.offset + data.limit < data.total,
|
|
531
|
+
hint: data.offset + data.limit < data.total ? "Use --offset with nextOffset to continue." : null
|
|
532
|
+
});
|
|
486
533
|
return;
|
|
487
534
|
}
|
|
488
535
|
if (enriched.length === 0) {
|
|
@@ -500,11 +547,14 @@ issueCommand.command("list").description("List issues").option("--status <status
|
|
|
500
547
|
console.log(`${padEnd(id, 10)} ${statusColor(issue.status, padEnd(issue.status, 14))} ${icon} ${padEnd(pri, 8)} ${title}`);
|
|
501
548
|
}
|
|
502
549
|
console.log(dim(`${enriched.length} of ${data.total} issues`));
|
|
550
|
+
if (data.offset + data.limit < data.total) {
|
|
551
|
+
console.log(dim(`Next: atoll issue list --offset ${data.offset + data.limit} --limit ${data.limit}`));
|
|
552
|
+
}
|
|
503
553
|
} catch (err) {
|
|
504
554
|
handleApiError(err);
|
|
505
555
|
}
|
|
506
556
|
});
|
|
507
|
-
|
|
557
|
+
async function viewIssue(identifier) {
|
|
508
558
|
try {
|
|
509
559
|
const client = new AtollClient();
|
|
510
560
|
const org = await resolveOrg(client);
|
|
@@ -514,8 +564,8 @@ issueCommand.command("view <identifier>").description("View issue details (UUID
|
|
|
514
564
|
fetchProjectMap(client, org.id)
|
|
515
565
|
]);
|
|
516
566
|
const enriched = attachIssueUrl(issue, client.baseUrl, org.slug, projects);
|
|
517
|
-
if (
|
|
518
|
-
|
|
567
|
+
if (isJsonMode()) {
|
|
568
|
+
outputJson(enriched);
|
|
519
569
|
return;
|
|
520
570
|
}
|
|
521
571
|
const id = formatIdentifier(enriched, projects);
|
|
@@ -542,7 +592,9 @@ Subtasks: ${enriched.sub_tasks.length}`);
|
|
|
542
592
|
} catch (err) {
|
|
543
593
|
handleApiError(err);
|
|
544
594
|
}
|
|
545
|
-
}
|
|
595
|
+
}
|
|
596
|
+
issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
|
|
597
|
+
issueCommand.command("get <identifier>").description("Get issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
|
|
546
598
|
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) => {
|
|
547
599
|
try {
|
|
548
600
|
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
@@ -603,11 +655,22 @@ issueCommand.command("update <identifier>").description("Update an issue").optio
|
|
|
603
655
|
handleApiError(err);
|
|
604
656
|
}
|
|
605
657
|
});
|
|
606
|
-
issueCommand.command("delete <identifier>").description("Delete an issue (admin/owner only)").action(async (identifier) => {
|
|
658
|
+
issueCommand.command("delete <identifier>").description("Delete an issue (admin/owner only)").option("--force", "Permanently delete the issue").option("--dry-run", "Show what would be deleted without changing anything").action(async (identifier, opts) => {
|
|
607
659
|
try {
|
|
660
|
+
if (!opts.dryRun && !opts.force) {
|
|
661
|
+
outputError("Permanent issue deletion requires --force. Prefer `atoll issue archive <identifier>` for reversible removal.");
|
|
662
|
+
process.exit(2);
|
|
663
|
+
}
|
|
608
664
|
const client = new AtollClient();
|
|
609
665
|
const orgId = await resolveOrgId(client);
|
|
610
666
|
const issueId = await resolveIssueId(client, orgId, identifier);
|
|
667
|
+
if (opts.dryRun) {
|
|
668
|
+
output(
|
|
669
|
+
{ dryRun: true, wouldDelete: { type: "issue", id: issueId, identifier } },
|
|
670
|
+
`Would permanently delete issue ${identifier} (${issueId})`
|
|
671
|
+
);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
611
674
|
await client.delete(`/api/orgs/${orgId}/issues/${issueId}`);
|
|
612
675
|
output(
|
|
613
676
|
{ success: true, id: issueId },
|
|
@@ -617,6 +680,48 @@ issueCommand.command("delete <identifier>").description("Delete an issue (admin/
|
|
|
617
680
|
handleApiError(err);
|
|
618
681
|
}
|
|
619
682
|
});
|
|
683
|
+
issueCommand.command("archive <identifier>").description("Archive an issue (reversible soft delete)").option("--dry-run", "Show what would be archived without changing anything").action(async (identifier, opts) => {
|
|
684
|
+
try {
|
|
685
|
+
const client = new AtollClient();
|
|
686
|
+
const orgId = await resolveOrgId(client);
|
|
687
|
+
const issueId = await resolveIssueId(client, orgId, identifier);
|
|
688
|
+
if (opts.dryRun) {
|
|
689
|
+
output(
|
|
690
|
+
{ dryRun: true, wouldArchive: { type: "issue", id: issueId, identifier } },
|
|
691
|
+
`Would archive issue ${identifier} (${issueId})`
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
await client.post(`/api/orgs/${orgId}/issues/${issueId}/archive`);
|
|
696
|
+
output(
|
|
697
|
+
{ success: true, id: issueId, archived: true },
|
|
698
|
+
success(`Archived issue ${identifier}`)
|
|
699
|
+
);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
handleApiError(err);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
issueCommand.command("unarchive <identifier>").description("Unarchive an issue").option("--dry-run", "Show what would be unarchived without changing anything").action(async (identifier, opts) => {
|
|
705
|
+
try {
|
|
706
|
+
const client = new AtollClient();
|
|
707
|
+
const orgId = await resolveOrgId(client);
|
|
708
|
+
const issueId = await resolveIssueId(client, orgId, identifier);
|
|
709
|
+
if (opts.dryRun) {
|
|
710
|
+
output(
|
|
711
|
+
{ dryRun: true, wouldUnarchive: { type: "issue", id: issueId, identifier } },
|
|
712
|
+
`Would unarchive issue ${identifier} (${issueId})`
|
|
713
|
+
);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
await client.delete(`/api/orgs/${orgId}/issues/${issueId}/archive`);
|
|
717
|
+
output(
|
|
718
|
+
{ success: true, id: issueId, archived: false },
|
|
719
|
+
success(`Unarchived issue ${identifier}`)
|
|
720
|
+
);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
handleApiError(err);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
620
725
|
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) => {
|
|
621
726
|
try {
|
|
622
727
|
const client = new AtollClient();
|
|
@@ -691,32 +796,44 @@ function formatTimestamp(ts) {
|
|
|
691
796
|
return d.toLocaleString();
|
|
692
797
|
}
|
|
693
798
|
var commentCommand = new import_commander3.Command("comment").description("Manage issue comments");
|
|
694
|
-
commentCommand.command("list <identifier>").description("List comments on an issue (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(async (identifier) => {
|
|
799
|
+
commentCommand.command("list <identifier>").description("List comments on an issue (UUID or PREFIX-NUMBER e.g. ATOLL-42)").option("--limit <n>", "Max results (1-100)", parseInt).action(async (identifier, opts) => {
|
|
695
800
|
try {
|
|
801
|
+
const limit = normalizeLimit(opts.limit);
|
|
696
802
|
const client = new AtollClient();
|
|
697
803
|
const orgId = await resolveOrgId(client);
|
|
698
804
|
const issueId = await resolveIssueId(client, orgId, identifier);
|
|
699
805
|
const data = await client.get(
|
|
700
806
|
`/api/orgs/${orgId}/issues/${issueId}/comments`
|
|
701
807
|
);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
808
|
+
const allComments = data.comments ?? [];
|
|
809
|
+
const comments = allComments.slice(0, limit);
|
|
810
|
+
const truncated = allComments.length > comments.length;
|
|
811
|
+
if (isJsonMode()) {
|
|
812
|
+
outputJson({
|
|
813
|
+
resource: "comments",
|
|
814
|
+
items: comments,
|
|
815
|
+
total: allComments.length,
|
|
816
|
+
limit,
|
|
817
|
+
offset: 0,
|
|
818
|
+
nextOffset: null,
|
|
819
|
+
truncated,
|
|
820
|
+
hint: truncated ? "Increase --limit up to 100 to show more comments." : null
|
|
821
|
+
});
|
|
706
822
|
return;
|
|
707
823
|
}
|
|
708
|
-
if (
|
|
824
|
+
if (comments.length === 0) {
|
|
709
825
|
console.log("No comments found.");
|
|
710
826
|
return;
|
|
711
827
|
}
|
|
712
|
-
for (const comment of
|
|
828
|
+
for (const comment of comments) {
|
|
713
829
|
const author = formatAuthor(comment);
|
|
714
830
|
const time = formatTimestamp(comment.created_at);
|
|
715
831
|
console.log(`${BOLD2}${CYAN}${author}${RESET2} ${DIM2}${time}${RESET2}`);
|
|
716
832
|
console.log(comment.body);
|
|
717
833
|
console.log();
|
|
718
834
|
}
|
|
719
|
-
console.log(`${DIM2}${
|
|
835
|
+
console.log(`${DIM2}${comments.length} of ${allComments.length} comment(s)${RESET2}`);
|
|
836
|
+
if (truncated) console.log(`${DIM2}${allComments.length - comments.length} more comment(s) hidden; increase --limit to show more.${RESET2}`);
|
|
720
837
|
} catch (err) {
|
|
721
838
|
handleApiError(err);
|
|
722
839
|
}
|
|
@@ -755,11 +872,22 @@ commentCommand.command("update <comment-id>").description("Update a comment").re
|
|
|
755
872
|
handleApiError(err);
|
|
756
873
|
}
|
|
757
874
|
});
|
|
758
|
-
commentCommand.command("delete <comment-id>").description("Delete a comment").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").action(async (commentId, opts) => {
|
|
875
|
+
commentCommand.command("delete <comment-id>").description("Delete a comment").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").option("--force", "Delete the comment").option("--dry-run", "Show what would be deleted without changing anything").action(async (commentId, opts) => {
|
|
759
876
|
try {
|
|
877
|
+
if (!opts.dryRun && !opts.force) {
|
|
878
|
+
outputError("Comment deletion requires --force.");
|
|
879
|
+
process.exit(2);
|
|
880
|
+
}
|
|
760
881
|
const client = new AtollClient();
|
|
761
882
|
const orgId = await resolveOrgId(client);
|
|
762
883
|
const issueId = await resolveIssueId(client, orgId, opts.issue);
|
|
884
|
+
if (opts.dryRun) {
|
|
885
|
+
output(
|
|
886
|
+
{ dryRun: true, wouldDelete: { type: "comment", id: commentId, issueId } },
|
|
887
|
+
`Would delete comment ${commentId} on ${opts.issue}`
|
|
888
|
+
);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
763
891
|
await client.delete(`/api/orgs/${orgId}/issues/${issueId}/comments/${commentId}`);
|
|
764
892
|
output(
|
|
765
893
|
{ success: true, id: commentId },
|
|
@@ -785,16 +913,26 @@ Examples:
|
|
|
785
913
|
$ atoll project list
|
|
786
914
|
$ atoll project create --name "My Project" --icon \u{1F680}
|
|
787
915
|
$ atoll project view <project-id>`);
|
|
788
|
-
projectCommand.command("list").description("List all projects with progress").action(async () => {
|
|
916
|
+
projectCommand.command("list").description("List all projects with progress").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
|
|
789
917
|
const client = new AtollClient();
|
|
790
918
|
try {
|
|
919
|
+
const limit = normalizeLimit(opts.limit);
|
|
791
920
|
const org = await resolveOrg(client);
|
|
792
921
|
const data = await client.get(`/api/orgs/${org.id}/projects`);
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
922
|
+
const allProjects = (data.projects ?? []).map((p) => attachProjectUrl(p, client.baseUrl, org.slug));
|
|
923
|
+
const projects = allProjects.slice(0, limit);
|
|
924
|
+
const truncated = allProjects.length > projects.length;
|
|
925
|
+
if (isJsonMode()) {
|
|
926
|
+
outputJson({
|
|
927
|
+
resource: "projects",
|
|
928
|
+
items: projects,
|
|
929
|
+
total: allProjects.length,
|
|
930
|
+
limit,
|
|
931
|
+
offset: 0,
|
|
932
|
+
nextOffset: null,
|
|
933
|
+
truncated,
|
|
934
|
+
hint: truncated ? "Narrow the request in the API or increase --limit up to 100." : null
|
|
935
|
+
});
|
|
798
936
|
return;
|
|
799
937
|
}
|
|
800
938
|
if (projects.length === 0) {
|
|
@@ -811,6 +949,7 @@ projectCommand.command("list").description("List all projects with progress").ac
|
|
|
811
949
|
if (p.status === "archived") console.log(` ${dim("[archived]")}`);
|
|
812
950
|
console.log("");
|
|
813
951
|
}
|
|
952
|
+
if (truncated) console.log(dim(`${allProjects.length - projects.length} more project(s) hidden; increase --limit to show more.`));
|
|
814
953
|
} catch (err) {
|
|
815
954
|
handleApiError(err);
|
|
816
955
|
}
|
|
@@ -835,7 +974,7 @@ ${project.url}` : ""}`)
|
|
|
835
974
|
handleApiError(err);
|
|
836
975
|
}
|
|
837
976
|
});
|
|
838
|
-
|
|
977
|
+
async function viewProject(projectId) {
|
|
839
978
|
const client = new AtollClient();
|
|
840
979
|
try {
|
|
841
980
|
const org = await resolveOrg(client);
|
|
@@ -843,8 +982,8 @@ projectCommand.command("view <projectId>").description("View project details and
|
|
|
843
982
|
`/api/orgs/${org.id}/projects/${projectId}`
|
|
844
983
|
);
|
|
845
984
|
const p = attachProjectUrl(data.project, client.baseUrl, org.slug);
|
|
846
|
-
if (
|
|
847
|
-
|
|
985
|
+
if (isJsonMode()) {
|
|
986
|
+
outputJson(p);
|
|
848
987
|
return;
|
|
849
988
|
}
|
|
850
989
|
const progress = p.progress ?? 0;
|
|
@@ -867,7 +1006,9 @@ projectCommand.command("view <projectId>").description("View project details and
|
|
|
867
1006
|
} catch (err) {
|
|
868
1007
|
handleApiError(err);
|
|
869
1008
|
}
|
|
870
|
-
}
|
|
1009
|
+
}
|
|
1010
|
+
projectCommand.command("view <projectId>").description("View project details and issues").action(viewProject);
|
|
1011
|
+
projectCommand.command("get <projectId>").description("Get project details and issues").action(viewProject);
|
|
871
1012
|
|
|
872
1013
|
// src/commands/milestone.ts
|
|
873
1014
|
var import_commander5 = require("commander");
|
|
@@ -880,18 +1021,28 @@ var milestoneCommand = new import_commander5.Command("milestone").description("M
|
|
|
880
1021
|
Examples:
|
|
881
1022
|
$ atoll milestone list --project <project-id>
|
|
882
1023
|
$ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
|
|
883
|
-
milestoneCommand.command("list").description("List milestones for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
|
|
1024
|
+
milestoneCommand.command("list").description("List milestones for a project").requiredOption("--project <id>", "Project ID").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
|
|
884
1025
|
const client = new AtollClient();
|
|
885
1026
|
try {
|
|
1027
|
+
const limit = normalizeLimit(opts.limit);
|
|
886
1028
|
const orgId = await resolveOrgId(client);
|
|
887
1029
|
const data = await client.get(
|
|
888
1030
|
`/api/orgs/${orgId}/projects/${opts.project}/milestones`
|
|
889
1031
|
);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1032
|
+
const allMilestones = data.milestones ?? [];
|
|
1033
|
+
const milestones = allMilestones.slice(0, limit);
|
|
1034
|
+
const truncated = allMilestones.length > milestones.length;
|
|
1035
|
+
if (isJsonMode()) {
|
|
1036
|
+
outputJson({
|
|
1037
|
+
resource: "milestones",
|
|
1038
|
+
items: milestones,
|
|
1039
|
+
total: allMilestones.length,
|
|
1040
|
+
limit,
|
|
1041
|
+
offset: 0,
|
|
1042
|
+
nextOffset: null,
|
|
1043
|
+
truncated,
|
|
1044
|
+
hint: truncated ? "Narrow the request by project or increase --limit up to 100." : null
|
|
1045
|
+
});
|
|
895
1046
|
return;
|
|
896
1047
|
}
|
|
897
1048
|
if (milestones.length === 0) {
|
|
@@ -909,6 +1060,7 @@ milestoneCommand.command("list").description("List milestones for a project").re
|
|
|
909
1060
|
if (m.status === "closed") console.log(` ${dim("[closed]")}`);
|
|
910
1061
|
console.log("");
|
|
911
1062
|
}
|
|
1063
|
+
if (truncated) console.log(dim(`${allMilestones.length - milestones.length} more milestone(s) hidden; increase --limit to show more.`));
|
|
912
1064
|
} catch (err) {
|
|
913
1065
|
handleApiError(err);
|
|
914
1066
|
}
|
|
@@ -1057,32 +1209,44 @@ Examples:
|
|
|
1057
1209
|
$ atoll webhook list
|
|
1058
1210
|
$ atoll webhook create --url https://example.com/hook --events issue.created,issue.updated
|
|
1059
1211
|
$ atoll webhook delete <id>`);
|
|
1060
|
-
webhookCommand.command("list").description("List webhooks for the current org").action(async () => {
|
|
1212
|
+
webhookCommand.command("list").description("List webhooks for the current org").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
|
|
1061
1213
|
try {
|
|
1214
|
+
const limit = normalizeLimit(opts.limit);
|
|
1062
1215
|
const client = new AtollClient();
|
|
1063
1216
|
const orgId = await resolveOrgId2(client);
|
|
1064
1217
|
const data = await client.get(`/api/webhooks?orgId=${orgId}`);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1218
|
+
const allWebhooks = data.webhooks ?? [];
|
|
1219
|
+
const webhooks = allWebhooks.slice(0, limit);
|
|
1220
|
+
const truncated = allWebhooks.length > webhooks.length;
|
|
1221
|
+
if (isJsonMode()) {
|
|
1222
|
+
outputJson({
|
|
1223
|
+
resource: "webhooks",
|
|
1224
|
+
items: webhooks,
|
|
1225
|
+
total: allWebhooks.length,
|
|
1226
|
+
limit,
|
|
1227
|
+
offset: 0,
|
|
1228
|
+
nextOffset: null,
|
|
1229
|
+
truncated,
|
|
1230
|
+
hint: truncated ? "Increase --limit up to 100 to show more webhooks." : null
|
|
1231
|
+
});
|
|
1069
1232
|
return;
|
|
1070
1233
|
}
|
|
1071
|
-
if (
|
|
1234
|
+
if (webhooks.length === 0) {
|
|
1072
1235
|
console.log("No webhooks configured.");
|
|
1073
1236
|
return;
|
|
1074
1237
|
}
|
|
1075
1238
|
const header = bold(`${padEnd2("ID", 38)} ${padEnd2("URL", 40)} ${padEnd2("EVENTS", 20)} ACTIVE`);
|
|
1076
1239
|
console.log(header);
|
|
1077
1240
|
console.log("\u2500".repeat(100));
|
|
1078
|
-
for (const wh of
|
|
1241
|
+
for (const wh of webhooks) {
|
|
1079
1242
|
const evts = wh.events.length === 0 ? "all" : wh.events.join(",");
|
|
1080
1243
|
const active = wh.enabled ? green("yes") : gray("no");
|
|
1081
1244
|
const url = wh.url.length > 38 ? wh.url.slice(0, 35) + "\u2026" : wh.url;
|
|
1082
1245
|
const evtStr = evts.length > 18 ? evts.slice(0, 15) + "\u2026" : evts;
|
|
1083
1246
|
console.log(`${padEnd2(wh.id.slice(0, 36), 38)} ${padEnd2(url, 40)} ${padEnd2(evtStr, 20)} ${active}`);
|
|
1084
1247
|
}
|
|
1085
|
-
console.log(dim(`${
|
|
1248
|
+
console.log(dim(`${webhooks.length} of ${allWebhooks.length} webhook(s)`));
|
|
1249
|
+
if (truncated) console.log(dim(`${allWebhooks.length - webhooks.length} more webhook(s) hidden; increase --limit to show more.`));
|
|
1086
1250
|
} catch (err) {
|
|
1087
1251
|
handleApiError2(err);
|
|
1088
1252
|
}
|
|
@@ -1113,9 +1277,20 @@ webhookCommand.command("create").description("Create a new webhook").requiredOpt
|
|
|
1113
1277
|
handleApiError2(err);
|
|
1114
1278
|
}
|
|
1115
1279
|
});
|
|
1116
|
-
webhookCommand.command("delete <id>").description("Delete a webhook").action(async (id) => {
|
|
1280
|
+
webhookCommand.command("delete <id>").description("Delete a webhook").option("--force", "Delete the webhook").option("--dry-run", "Show what would be deleted without changing anything").action(async (id, opts) => {
|
|
1117
1281
|
try {
|
|
1282
|
+
if (!opts.dryRun && !opts.force) {
|
|
1283
|
+
outputError("Webhook deletion requires --force.");
|
|
1284
|
+
process.exit(2);
|
|
1285
|
+
}
|
|
1118
1286
|
const client = new AtollClient();
|
|
1287
|
+
if (opts.dryRun) {
|
|
1288
|
+
output(
|
|
1289
|
+
{ dryRun: true, wouldDelete: { type: "webhook", id } },
|
|
1290
|
+
`Would delete webhook ${id}`
|
|
1291
|
+
);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1119
1294
|
await client.delete(`/api/webhooks/${id}`);
|
|
1120
1295
|
output(
|
|
1121
1296
|
{ success: true, id },
|
|
@@ -1126,16 +1301,405 @@ webhookCommand.command("delete <id>").description("Delete a webhook").action(asy
|
|
|
1126
1301
|
}
|
|
1127
1302
|
});
|
|
1128
1303
|
|
|
1304
|
+
// src/commands/heartbeat.ts
|
|
1305
|
+
var import_commander8 = require("commander");
|
|
1306
|
+
var VALID_SEVERITIES = ["critical", "warning", "info"];
|
|
1307
|
+
var heartbeatCommand = new import_commander8.Command("heartbeat").description("Get the current agent heartbeat briefing").option("--signals-only", "Only show heartbeat signals").option("--severity <severity>", `Filter signals by severity (${VALID_SEVERITIES.join(", ")})`).action(async (opts) => {
|
|
1308
|
+
try {
|
|
1309
|
+
if (opts.severity && !VALID_SEVERITIES.includes(opts.severity)) {
|
|
1310
|
+
outputError(`Invalid severity "${opts.severity}". Must be one of: ${VALID_SEVERITIES.join(", ")}`);
|
|
1311
|
+
process.exit(2);
|
|
1312
|
+
}
|
|
1313
|
+
const client = new AtollClient();
|
|
1314
|
+
const org = await resolveOrg(client);
|
|
1315
|
+
const context = await client.get(`/api/orgs/${org.id}/heartbeat`);
|
|
1316
|
+
const signals = filterSignals(context.signals ?? [], opts.severity);
|
|
1317
|
+
if (isJsonMode()) {
|
|
1318
|
+
if (opts.signalsOnly) {
|
|
1319
|
+
outputJson({ signals });
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
outputJson({ ...context, signals });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
renderHeartbeat(context, signals, opts.signalsOnly ?? false);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
handleApiError(err);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
function filterSignals(signals, severity) {
|
|
1331
|
+
if (!severity) return signals;
|
|
1332
|
+
return signals.filter((signal) => signal.severity === severity);
|
|
1333
|
+
}
|
|
1334
|
+
function renderHeartbeat(context, signals, signalsOnly) {
|
|
1335
|
+
const agentName = context.agent?.display_name || context.agent?.id || "agent";
|
|
1336
|
+
console.log(bold(`Heartbeat for ${agentName}`));
|
|
1337
|
+
if (context.timestamp) console.log(dim(context.timestamp));
|
|
1338
|
+
console.log("");
|
|
1339
|
+
renderSignals(signals);
|
|
1340
|
+
if (signalsOnly) return;
|
|
1341
|
+
const assigned = context.assigned_issues ?? [];
|
|
1342
|
+
if (assigned.length > 0) {
|
|
1343
|
+
console.log("");
|
|
1344
|
+
console.log(bold("Assigned"));
|
|
1345
|
+
for (const issue of assigned.slice(0, 10)) {
|
|
1346
|
+
const id = issue.number == null ? issue.id.slice(0, 8) : `ATOLL-${issue.number}`;
|
|
1347
|
+
console.log(` ${pad(id, 10)} ${pad(issue.status, 13)} P${issue.priority} ${issue.title}`);
|
|
1348
|
+
}
|
|
1349
|
+
if (assigned.length > 10) console.log(dim(` ${assigned.length - 10} more assigned issue(s) hidden`));
|
|
1350
|
+
}
|
|
1351
|
+
const goals = context.goals ?? [];
|
|
1352
|
+
if (goals.length > 0) {
|
|
1353
|
+
console.log("");
|
|
1354
|
+
console.log(bold("Goals"));
|
|
1355
|
+
for (const goal of goals.slice(0, 5)) {
|
|
1356
|
+
const title = goal.goal?.title ?? "(untitled goal)";
|
|
1357
|
+
const days = goal.days_remaining == null ? "no deadline" : `${goal.days_remaining} days left`;
|
|
1358
|
+
const kpis = goal.kpis ?? [];
|
|
1359
|
+
const initiatives = goal.initiatives ?? [];
|
|
1360
|
+
const offPace = kpis.filter((kpi) => kpi.is_off_pace).length;
|
|
1361
|
+
const stale = kpis.filter((kpi) => kpi.is_stale).length;
|
|
1362
|
+
const stalledOrBlocked = initiatives.filter(
|
|
1363
|
+
(initiative) => (initiative.stalled_issues ?? 0) > 0 || (initiative.blocked_issues ?? 0) > 0
|
|
1364
|
+
).length;
|
|
1365
|
+
console.log(` ${title} ${dim(days)}`);
|
|
1366
|
+
console.log(dim(` KPIs: ${offPace} off pace, ${stale} stale`));
|
|
1367
|
+
console.log(dim(` Initiatives: ${stalledOrBlocked} stalled/blocked`));
|
|
1368
|
+
}
|
|
1369
|
+
if (goals.length > 5) console.log(dim(` ${goals.length - 5} more goal(s) hidden`));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function renderSignals(signals) {
|
|
1373
|
+
if (signals.length === 0) {
|
|
1374
|
+
console.log(gray("No heartbeat signals."));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const groups = [
|
|
1378
|
+
["critical", "Critical"],
|
|
1379
|
+
["warning", "Warnings"],
|
|
1380
|
+
["info", "Info"]
|
|
1381
|
+
];
|
|
1382
|
+
for (const [severity, label] of groups) {
|
|
1383
|
+
const group = signals.filter((signal) => signal.severity === severity);
|
|
1384
|
+
if (group.length === 0) continue;
|
|
1385
|
+
console.log(formatSeverityHeading(severity, label));
|
|
1386
|
+
for (const signal of group) {
|
|
1387
|
+
console.log(` ${pad(signal.type, 18)} ${signal.message}`);
|
|
1388
|
+
}
|
|
1389
|
+
if (severity !== "info") console.log("");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function formatSeverityHeading(severity, label) {
|
|
1393
|
+
if (severity === "critical") return red(bold(label));
|
|
1394
|
+
if (severity === "warning") return yellow(bold(label));
|
|
1395
|
+
return cyan(bold(label));
|
|
1396
|
+
}
|
|
1397
|
+
function pad(value, length) {
|
|
1398
|
+
return value.length >= length ? value.slice(0, length) : value + " ".repeat(length - value.length);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/commands/agent-context.ts
|
|
1402
|
+
var import_node_fs2 = require("fs");
|
|
1403
|
+
var import_node_path2 = require("path");
|
|
1404
|
+
var import_commander9 = require("commander");
|
|
1405
|
+
init_config();
|
|
1406
|
+
var ISSUE_STATUSES = ["backlog", "todo", "in_progress", "done", "cancelled"];
|
|
1407
|
+
var PRIORITIES = [0, 1, 2, 3];
|
|
1408
|
+
var SEVERITIES = ["critical", "warning", "info"];
|
|
1409
|
+
var SIGNAL_TYPES = [
|
|
1410
|
+
"kpi_off_pace",
|
|
1411
|
+
"kpi_stale",
|
|
1412
|
+
"issue_stale",
|
|
1413
|
+
"issue_blocked",
|
|
1414
|
+
"milestone_overdue",
|
|
1415
|
+
"initiative_stalled",
|
|
1416
|
+
"webhook_failing"
|
|
1417
|
+
];
|
|
1418
|
+
var agentContextCommand = new import_commander9.Command("agent-context").description("Output machine-readable CLI context for agents").action(() => {
|
|
1419
|
+
const config = readConfig();
|
|
1420
|
+
const resolved = resolveConfig();
|
|
1421
|
+
const profiles = Object.entries(config.profiles ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
1422
|
+
name,
|
|
1423
|
+
active: name === config.activeProfile,
|
|
1424
|
+
apiKeySet: Boolean(profile.apiKey),
|
|
1425
|
+
orgSlug: profile.orgSlug ?? null,
|
|
1426
|
+
defaultTeam: profile.defaultTeam ?? null,
|
|
1427
|
+
baseUrl: profile.baseUrl ?? null
|
|
1428
|
+
}));
|
|
1429
|
+
outputJson({
|
|
1430
|
+
schema_version: "1",
|
|
1431
|
+
cli: {
|
|
1432
|
+
name: "atoll",
|
|
1433
|
+
json_flag: "--json",
|
|
1434
|
+
output: {
|
|
1435
|
+
stdout: "data",
|
|
1436
|
+
stderr: "diagnostics_and_errors",
|
|
1437
|
+
list_shape: "{ resource, items, total, limit, offset, nextOffset, truncated, hint }"
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
selected_context: {
|
|
1441
|
+
activeProfile: config.activeProfile ?? null,
|
|
1442
|
+
selectedProfile: resolved.profile ?? null,
|
|
1443
|
+
orgSlug: resolved.orgSlug ?? null,
|
|
1444
|
+
defaultTeam: resolved.defaultTeam ?? null,
|
|
1445
|
+
baseUrl: resolved.baseUrl ?? null,
|
|
1446
|
+
apiKeySet: Boolean(resolved.apiKey)
|
|
1447
|
+
},
|
|
1448
|
+
available_profiles: profiles,
|
|
1449
|
+
commands: buildCommandContext(),
|
|
1450
|
+
skill_manifests: findSkillManifests()
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
var skillPathCommand = new import_commander9.Command("skill-path").description("Output local Atoll skill manifest paths when available").action(() => {
|
|
1454
|
+
outputJson({ skill_manifests: findSkillManifests() });
|
|
1455
|
+
});
|
|
1456
|
+
function buildCommandContext() {
|
|
1457
|
+
return {
|
|
1458
|
+
heartbeat: {
|
|
1459
|
+
description: "Primary agent orientation command. Reads current goals, KPI pace, initiative health, assigned work, and prioritized signals.",
|
|
1460
|
+
subcommands: {},
|
|
1461
|
+
flags: {
|
|
1462
|
+
"--signals-only": { type: "boolean", default: false },
|
|
1463
|
+
"--severity": { type: "enum", values: SEVERITIES },
|
|
1464
|
+
"--json": { type: "boolean", default: false }
|
|
1465
|
+
},
|
|
1466
|
+
signal_types: SIGNAL_TYPES
|
|
1467
|
+
},
|
|
1468
|
+
issue: {
|
|
1469
|
+
subcommands: {
|
|
1470
|
+
list: {
|
|
1471
|
+
flags: {
|
|
1472
|
+
"--status": { type: "enum", values: ISSUE_STATUSES },
|
|
1473
|
+
"--assignee": { type: "string" },
|
|
1474
|
+
"--priority": { type: "enum", values: PRIORITIES },
|
|
1475
|
+
"--limit": { type: "integer", min: 1, max: 100, default: 25 },
|
|
1476
|
+
"--offset": { type: "integer", min: 0, default: 0 },
|
|
1477
|
+
"--json": { type: "boolean", default: false }
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
get: { alias_for: "view", args: ["identifier"], flags: { "--json": { type: "boolean", default: false } } },
|
|
1481
|
+
view: { args: ["identifier"], flags: { "--json": { type: "boolean", default: false } } },
|
|
1482
|
+
create: {
|
|
1483
|
+
flags: {
|
|
1484
|
+
"--title": { type: "string", required: true },
|
|
1485
|
+
"--description": { type: "string" },
|
|
1486
|
+
"--status": { type: "enum", values: ISSUE_STATUSES },
|
|
1487
|
+
"--priority": { type: "enum", values: PRIORITIES },
|
|
1488
|
+
"--json": { type: "boolean", default: false }
|
|
1489
|
+
}
|
|
1490
|
+
},
|
|
1491
|
+
update: {
|
|
1492
|
+
args: ["identifier"],
|
|
1493
|
+
flags: {
|
|
1494
|
+
"--title": { type: "string" },
|
|
1495
|
+
"--status": { type: "enum", values: ISSUE_STATUSES },
|
|
1496
|
+
"--priority": { type: "enum", values: PRIORITIES },
|
|
1497
|
+
"--json": { type: "boolean", default: false }
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
archive: { args: ["identifier"], flags: { "--dry-run": { type: "boolean" }, "--json": { type: "boolean" } } },
|
|
1501
|
+
unarchive: { args: ["identifier"], flags: { "--dry-run": { type: "boolean" }, "--json": { type: "boolean" } } },
|
|
1502
|
+
delete: {
|
|
1503
|
+
args: ["identifier"],
|
|
1504
|
+
flags: {
|
|
1505
|
+
"--force": { type: "boolean", required: true },
|
|
1506
|
+
"--dry-run": { type: "boolean" },
|
|
1507
|
+
"--json": { type: "boolean", default: false }
|
|
1508
|
+
}
|
|
1509
|
+
},
|
|
1510
|
+
assign: { args: ["identifier"], flags: { "--to": { type: "string", required: true }, "--json": { type: "boolean" } } },
|
|
1511
|
+
unassign: { args: ["identifier"], flags: { "--json": { type: "boolean" } } }
|
|
1512
|
+
}
|
|
1513
|
+
},
|
|
1514
|
+
project: {
|
|
1515
|
+
subcommands: {
|
|
1516
|
+
list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
|
|
1517
|
+
get: { alias_for: "view", args: ["projectId"], flags: { "--json": { type: "boolean" } } },
|
|
1518
|
+
view: { args: ["projectId"], flags: { "--json": { type: "boolean" } } },
|
|
1519
|
+
create: {
|
|
1520
|
+
flags: {
|
|
1521
|
+
"--name": { type: "string", required: true },
|
|
1522
|
+
"--description": { type: "string" },
|
|
1523
|
+
"--color": { type: "string", default: "#6366f1" },
|
|
1524
|
+
"--icon": { type: "string", default: "folder" },
|
|
1525
|
+
"--json": { type: "boolean" }
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
comment: {
|
|
1531
|
+
subcommands: {
|
|
1532
|
+
list: { args: ["identifier"], flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
|
|
1533
|
+
add: { args: ["identifier"], flags: { "--body": { type: "string", required: true }, "--json": { type: "boolean" } } },
|
|
1534
|
+
update: {
|
|
1535
|
+
args: ["comment-id"],
|
|
1536
|
+
flags: { "--issue": { type: "string", required: true }, "--body": { type: "string", required: true }, "--json": { type: "boolean" } }
|
|
1537
|
+
},
|
|
1538
|
+
delete: {
|
|
1539
|
+
args: ["comment-id"],
|
|
1540
|
+
flags: {
|
|
1541
|
+
"--issue": { type: "string", required: true },
|
|
1542
|
+
"--force": { type: "boolean", required: true },
|
|
1543
|
+
"--dry-run": { type: "boolean" },
|
|
1544
|
+
"--json": { type: "boolean" }
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
feedback: {
|
|
1550
|
+
description: "Record CLI or platform friction locally and optionally submit it upstream.",
|
|
1551
|
+
subcommands: {
|
|
1552
|
+
add: {
|
|
1553
|
+
alias: "feedback <text>",
|
|
1554
|
+
flags: {
|
|
1555
|
+
"--type": { type: "enum", values: ["bug", "feature"], default: "bug" },
|
|
1556
|
+
"--url": { type: "string" },
|
|
1557
|
+
"--send": { type: "boolean", default: false },
|
|
1558
|
+
"--json": { type: "boolean" }
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } }
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function findSkillManifests() {
|
|
1567
|
+
const repoRoot = (0, import_node_path2.resolve)(__dirname, "..", "..", "..");
|
|
1568
|
+
const candidates = [
|
|
1569
|
+
["codex", (0, import_node_path2.join)(repoRoot, "packages", "skill-codex", "skill", "SKILL.md")],
|
|
1570
|
+
["claude", (0, import_node_path2.join)(repoRoot, "packages", "skill-claude", "skill", "SKILL.md")],
|
|
1571
|
+
["gemini", (0, import_node_path2.join)(repoRoot, "packages", "skill-gemini", "skill", "SKILL.md")],
|
|
1572
|
+
["clawhub", (0, import_node_path2.join)(repoRoot, "clawhub", "atoll-api", "SKILL.md")]
|
|
1573
|
+
];
|
|
1574
|
+
return candidates.map(([name, path]) => ({ name, path, exists: (0, import_node_fs2.existsSync)(path) }));
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/commands/feedback.ts
|
|
1578
|
+
var import_node_fs3 = require("fs");
|
|
1579
|
+
var import_node_os2 = require("os");
|
|
1580
|
+
var import_node_path3 = require("path");
|
|
1581
|
+
var import_commander10 = require("commander");
|
|
1582
|
+
init_config();
|
|
1583
|
+
var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".atoll");
|
|
1584
|
+
var FEEDBACK_PATH = (0, import_node_path3.join)(CONFIG_DIR2, "feedback.jsonl");
|
|
1585
|
+
var VALID_TYPES = ["bug", "feature"];
|
|
1586
|
+
var DEFAULT_BASE_URL2 = "https://atollhq.com";
|
|
1587
|
+
var feedbackCommand = new import_commander10.Command("feedback").description("Record CLI or platform feedback").argument("[text...]", "Feedback text").option("--type <type>", `Feedback type (${VALID_TYPES.join(", ")})`, "bug").option("--url <url>", "Related page or endpoint URL").option("--send", "Also submit to the configured Atoll feedback endpoint").action(async (text, opts) => {
|
|
1588
|
+
if (text.length === 0) {
|
|
1589
|
+
outputError('Feedback text is required. Usage: atoll feedback "what went wrong"');
|
|
1590
|
+
process.exit(2);
|
|
1591
|
+
}
|
|
1592
|
+
await recordFeedback(text.join(" "), opts);
|
|
1593
|
+
});
|
|
1594
|
+
feedbackCommand.command("add <text...>").description("Record feedback").option("--type <type>", `Feedback type (${VALID_TYPES.join(", ")})`, "bug").option("--url <url>", "Related page or endpoint URL").option("--send", "Also submit to the configured Atoll feedback endpoint").action(async (text, opts) => {
|
|
1595
|
+
await recordFeedback(text.join(" "), opts);
|
|
1596
|
+
});
|
|
1597
|
+
feedbackCommand.command("list").description("List locally recorded feedback").option("--limit <n>", "Max results (1-100)", parseInt).action((opts) => {
|
|
1598
|
+
const limit = normalizeLimit(opts.limit);
|
|
1599
|
+
const entries = readFeedbackEntries().slice(-limit).reverse();
|
|
1600
|
+
if (isJsonMode()) {
|
|
1601
|
+
outputJson({
|
|
1602
|
+
resource: "feedback",
|
|
1603
|
+
items: entries,
|
|
1604
|
+
total: readFeedbackEntries().length,
|
|
1605
|
+
limit,
|
|
1606
|
+
offset: 0,
|
|
1607
|
+
nextOffset: null,
|
|
1608
|
+
truncated: readFeedbackEntries().length > entries.length,
|
|
1609
|
+
hint: null
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (entries.length === 0) {
|
|
1614
|
+
console.log("No local feedback recorded.");
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
for (const entry of entries) {
|
|
1618
|
+
const sent = entry.sent_upstream ? `sent:${entry.upstream_status}` : "local";
|
|
1619
|
+
console.log(`${entry.created_at} ${entry.type} ${sent}`);
|
|
1620
|
+
console.log(` ${entry.description}`);
|
|
1621
|
+
if (entry.url) console.log(dim(` ${entry.url}`));
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
async function recordFeedback(description, opts) {
|
|
1625
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
1626
|
+
outputError(`Invalid feedback type "${opts.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1627
|
+
process.exit(2);
|
|
1628
|
+
}
|
|
1629
|
+
const entry = {
|
|
1630
|
+
id: makeId(),
|
|
1631
|
+
type: opts.type,
|
|
1632
|
+
description,
|
|
1633
|
+
url: opts.url ?? null,
|
|
1634
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1635
|
+
sent_upstream: false,
|
|
1636
|
+
upstream_status: null,
|
|
1637
|
+
upstream_error: null
|
|
1638
|
+
};
|
|
1639
|
+
const endpoint = process.env.ATOLL_FEEDBACK_ENDPOINT || (opts.send ? `${resolveConfig().baseUrl || DEFAULT_BASE_URL2}/api/feedback` : null);
|
|
1640
|
+
if (endpoint) {
|
|
1641
|
+
try {
|
|
1642
|
+
const response = await fetch(endpoint, {
|
|
1643
|
+
method: "POST",
|
|
1644
|
+
headers: { "Content-Type": "application/json" },
|
|
1645
|
+
body: JSON.stringify({
|
|
1646
|
+
type: entry.type,
|
|
1647
|
+
description: entry.description,
|
|
1648
|
+
url: entry.url ?? void 0,
|
|
1649
|
+
userName: getReporterName()
|
|
1650
|
+
})
|
|
1651
|
+
});
|
|
1652
|
+
entry.sent_upstream = response.ok;
|
|
1653
|
+
entry.upstream_status = response.status;
|
|
1654
|
+
if (!response.ok) entry.upstream_error = await response.text();
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
entry.upstream_error = err.message ?? String(err);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
appendFeedbackEntry(entry);
|
|
1660
|
+
output(
|
|
1661
|
+
{ feedback: entry },
|
|
1662
|
+
entry.sent_upstream ? success(`Feedback recorded locally and sent upstream (${entry.upstream_status})`) : success("Feedback recorded locally")
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
function readFeedbackEntries() {
|
|
1666
|
+
if (!(0, import_node_fs3.existsSync)(FEEDBACK_PATH)) return [];
|
|
1667
|
+
return (0, import_node_fs3.readFileSync)(FEEDBACK_PATH, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
1668
|
+
}
|
|
1669
|
+
function appendFeedbackEntry(entry) {
|
|
1670
|
+
if (!(0, import_node_fs3.existsSync)(CONFIG_DIR2)) (0, import_node_fs3.mkdirSync)(CONFIG_DIR2, { recursive: true });
|
|
1671
|
+
(0, import_node_fs3.appendFileSync)(FEEDBACK_PATH, JSON.stringify(entry) + "\n", "utf-8");
|
|
1672
|
+
}
|
|
1673
|
+
function getReporterName() {
|
|
1674
|
+
const config = readConfig();
|
|
1675
|
+
return process.env.ATOLL_PROFILE || config.activeProfile || void 0;
|
|
1676
|
+
}
|
|
1677
|
+
function makeId() {
|
|
1678
|
+
return `fb_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1129
1681
|
// src/index.ts
|
|
1130
|
-
|
|
1682
|
+
function readCliVersion() {
|
|
1683
|
+
try {
|
|
1684
|
+
const packageJson = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
1685
|
+
return packageJson.version ?? "0.0.0";
|
|
1686
|
+
} catch {
|
|
1687
|
+
return "0.0.0";
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
var program = new import_commander11.Command("atoll").description("Atoll CLI \u2014 project management from the terminal").version(readCliVersion()).addHelpText("after", `
|
|
1131
1691
|
Examples:
|
|
1132
1692
|
$ atoll issue list
|
|
1133
1693
|
$ atoll issue create --title "Fix login bug" --priority 1
|
|
1134
1694
|
$ atoll issue view ATOLL-42
|
|
1695
|
+
$ atoll heartbeat
|
|
1135
1696
|
$ atoll project list
|
|
1136
1697
|
$ atoll milestone list --project <id>
|
|
1137
|
-
$ atoll config show`).option("--profile <name>", "Use a saved auth profile (env: ATOLL_PROFILE)").option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").hook("preAction", (
|
|
1138
|
-
const opts =
|
|
1698
|
+
$ atoll config show`).option("--profile <name>", "Use a saved auth profile (env: ATOLL_PROFILE)").option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").option("--json", "Emit machine-readable JSON").hook("preAction", (_thisCommand, actionCommand) => {
|
|
1699
|
+
const opts = actionCommand.optsWithGlobals();
|
|
1700
|
+
if (opts.json) {
|
|
1701
|
+
enableJsonMode();
|
|
1702
|
+
}
|
|
1139
1703
|
if (opts.profile) {
|
|
1140
1704
|
process.env.ATOLL_PROFILE = opts.profile;
|
|
1141
1705
|
}
|
|
@@ -1155,7 +1719,11 @@ program.addCommand(projectCommand);
|
|
|
1155
1719
|
program.addCommand(milestoneCommand);
|
|
1156
1720
|
program.addCommand(configCommand);
|
|
1157
1721
|
program.addCommand(webhookCommand);
|
|
1158
|
-
|
|
1722
|
+
program.addCommand(heartbeatCommand);
|
|
1723
|
+
program.addCommand(agentContextCommand);
|
|
1724
|
+
program.addCommand(skillPathCommand);
|
|
1725
|
+
program.addCommand(feedbackCommand);
|
|
1726
|
+
var completionCommand = new import_commander11.Command("completion").description("Output shell completion scripts").addHelpText("after", `
|
|
1159
1727
|
Examples:
|
|
1160
1728
|
$ atoll completion bash >> ~/.bashrc
|
|
1161
1729
|
$ atoll completion zsh >> ~/.zshrc`);
|
|
@@ -1166,6 +1734,7 @@ completionCommand.command("zsh").description("Output zsh completion script").act
|
|
|
1166
1734
|
process.stdout.write(zshCompletion());
|
|
1167
1735
|
});
|
|
1168
1736
|
program.addCommand(completionCommand);
|
|
1737
|
+
addJsonOptionToSubcommands(program);
|
|
1169
1738
|
program.parse();
|
|
1170
1739
|
function bashCompletion() {
|
|
1171
1740
|
return `# Atoll CLI bash completion
|
|
@@ -1175,13 +1744,14 @@ _atoll_completions() {
|
|
|
1175
1744
|
local cur prev words cword
|
|
1176
1745
|
_init_completion || return
|
|
1177
1746
|
|
|
1178
|
-
local commands="auth issue comment project milestone config webhook completion"
|
|
1179
|
-
local issue_cmds="list view create update delete assign unassign"
|
|
1180
|
-
local project_cmds="list create view"
|
|
1747
|
+
local commands="auth issue comment project milestone config webhook heartbeat agent-context skill-path feedback completion"
|
|
1748
|
+
local issue_cmds="list view get create update archive unarchive delete assign unassign"
|
|
1749
|
+
local project_cmds="list create view get"
|
|
1181
1750
|
local milestone_cmds="list create"
|
|
1182
1751
|
local config_cmds="show set-org set-team set-base-url"
|
|
1183
1752
|
local auth_cmds="login logout status profiles use"
|
|
1184
1753
|
local webhook_cmds="list create delete"
|
|
1754
|
+
local feedback_cmds="add list"
|
|
1185
1755
|
local completion_cmds="bash zsh"
|
|
1186
1756
|
|
|
1187
1757
|
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
@@ -1220,6 +1790,11 @@ _atoll_completions() {
|
|
|
1220
1790
|
COMPREPLY=( $(compgen -W "\${webhook_cmds}" -- "\${cur}") )
|
|
1221
1791
|
fi
|
|
1222
1792
|
;;
|
|
1793
|
+
feedback)
|
|
1794
|
+
if [ "\${COMP_CWORD}" -eq 2 ]; then
|
|
1795
|
+
COMPREPLY=( $(compgen -W "\${feedback_cmds}" -- "\${cur}") )
|
|
1796
|
+
fi
|
|
1797
|
+
;;
|
|
1223
1798
|
completion)
|
|
1224
1799
|
if [ "\${COMP_CWORD}" -eq 2 ]; then
|
|
1225
1800
|
COMPREPLY=( $(compgen -W "\${completion_cmds}" -- "\${cur}") )
|
|
@@ -1253,6 +1828,10 @@ _atoll() {
|
|
|
1253
1828
|
'milestone[Manage milestones]' \\
|
|
1254
1829
|
'config[Manage CLI configuration]' \\
|
|
1255
1830
|
'webhook[Manage outbound webhooks]' \\
|
|
1831
|
+
'heartbeat[Get agent heartbeat briefing]' \\
|
|
1832
|
+
'agent-context[Output machine-readable CLI context]' \\
|
|
1833
|
+
'skill-path[Output local Atoll skill manifest paths]' \\
|
|
1834
|
+
'feedback[Record CLI or platform feedback]' \\
|
|
1256
1835
|
'completion[Output shell completion scripts]'
|
|
1257
1836
|
;;
|
|
1258
1837
|
args)
|
|
@@ -1261,7 +1840,10 @@ _atoll() {
|
|
|
1261
1840
|
_values 'subcommand' \\
|
|
1262
1841
|
'list[List issues]' \\
|
|
1263
1842
|
'view[View issue details]' \\
|
|
1843
|
+
'get[Get issue details]' \\
|
|
1264
1844
|
'create[Create a new issue]' \\
|
|
1845
|
+
'archive[Archive an issue]' \\
|
|
1846
|
+
'unarchive[Unarchive an issue]' \\
|
|
1265
1847
|
'update[Update an issue]' \\
|
|
1266
1848
|
'delete[Delete an issue]' \\
|
|
1267
1849
|
'assign[Assign an issue]' \\
|
|
@@ -1271,7 +1853,8 @@ _atoll() {
|
|
|
1271
1853
|
_values 'subcommand' \\
|
|
1272
1854
|
'list[List projects]' \\
|
|
1273
1855
|
'create[Create a project]' \\
|
|
1274
|
-
'view[View project details]'
|
|
1856
|
+
'view[View project details]' \\
|
|
1857
|
+
'get[Get project details]'
|
|
1275
1858
|
;;
|
|
1276
1859
|
milestone)
|
|
1277
1860
|
_values 'subcommand' \\
|
|
@@ -1299,6 +1882,11 @@ _atoll() {
|
|
|
1299
1882
|
'create[Create a webhook]' \\
|
|
1300
1883
|
'delete[Delete a webhook]'
|
|
1301
1884
|
;;
|
|
1885
|
+
feedback)
|
|
1886
|
+
_values 'subcommand' \\
|
|
1887
|
+
'add[Record feedback]' \\
|
|
1888
|
+
'list[List local feedback]'
|
|
1889
|
+
;;
|
|
1302
1890
|
completion)
|
|
1303
1891
|
_values 'shell' 'bash' 'zsh'
|
|
1304
1892
|
;;
|
|
@@ -1310,4 +1898,12 @@ _atoll() {
|
|
|
1310
1898
|
_atoll "$@"
|
|
1311
1899
|
`;
|
|
1312
1900
|
}
|
|
1901
|
+
function addJsonOptionToSubcommands(command) {
|
|
1902
|
+
for (const subcommand of command.commands) {
|
|
1903
|
+
if (!subcommand.options.some((option) => option.long === "--json")) {
|
|
1904
|
+
subcommand.option("--json", "Emit machine-readable JSON");
|
|
1905
|
+
}
|
|
1906
|
+
addJsonOptionToSubcommands(subcommand);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1313
1909
|
//# sourceMappingURL=index.js.map
|