@atollhq/cli 0.1.3 → 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 +645 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,9 +81,9 @@ var init_config = __esm({
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
// src/index.ts
|
|
84
|
-
var
|
|
85
|
-
var
|
|
86
|
-
var
|
|
84
|
+
var import_commander11 = require("commander");
|
|
85
|
+
var import_node_fs4 = require("fs");
|
|
86
|
+
var import_node_path4 = require("path");
|
|
87
87
|
|
|
88
88
|
// src/commands/auth.ts
|
|
89
89
|
var import_commander = require("commander");
|
|
@@ -97,6 +97,9 @@ function isJsonMode() {
|
|
|
97
97
|
if (process.env.OUTPUT_FORMAT === "json") return true;
|
|
98
98
|
return !process.stdout.isTTY;
|
|
99
99
|
}
|
|
100
|
+
function enableJsonMode() {
|
|
101
|
+
process.env.OUTPUT_FORMAT = "json";
|
|
102
|
+
}
|
|
100
103
|
function output(data, humanReadable) {
|
|
101
104
|
if (isJsonMode()) {
|
|
102
105
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
@@ -104,13 +107,34 @@ function output(data, humanReadable) {
|
|
|
104
107
|
console.log(humanReadable);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
110
|
+
function outputJson(data) {
|
|
111
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
112
|
+
}
|
|
107
113
|
function outputError(message) {
|
|
108
114
|
if (isJsonMode()) {
|
|
109
|
-
process.
|
|
115
|
+
process.stderr.write(JSON.stringify({ error: message }) + "\n");
|
|
110
116
|
} else {
|
|
111
117
|
console.error(`Error: ${message}`);
|
|
112
118
|
}
|
|
113
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
|
+
}
|
|
114
138
|
|
|
115
139
|
// src/lib/client.ts
|
|
116
140
|
var DEFAULT_BASE_URL = "https://atollhq.com";
|
|
@@ -140,7 +164,10 @@ var AtollClient = class {
|
|
|
140
164
|
const body = await res.text();
|
|
141
165
|
throw new Error(`API ${res.status}: ${body}`);
|
|
142
166
|
}
|
|
143
|
-
|
|
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);
|
|
144
171
|
}
|
|
145
172
|
async get(path) {
|
|
146
173
|
return this.request(path, { method: "GET" });
|
|
@@ -464,15 +491,26 @@ Examples:
|
|
|
464
491
|
$ atoll issue create --title "Fix login" --priority 1 --status todo
|
|
465
492
|
$ atoll issue update ATOLL-42 --status done
|
|
466
493
|
$ atoll issue assign ATOLL-42 --to <user-id>`);
|
|
467
|
-
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) => {
|
|
468
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
|
+
}
|
|
469
504
|
const client = new AtollClient();
|
|
470
505
|
const org = await resolveOrg(client);
|
|
506
|
+
const limit = normalizeLimit(opts.limit);
|
|
507
|
+
const offset = normalizeOffset(opts.offset);
|
|
471
508
|
const params = new URLSearchParams();
|
|
472
509
|
if (opts.status) params.set("status", opts.status);
|
|
473
510
|
if (opts.assignee) params.set("assigneeId", opts.assignee);
|
|
474
511
|
if (opts.priority !== void 0) params.set("priority", String(opts.priority));
|
|
475
|
-
|
|
512
|
+
params.set("limit", String(limit));
|
|
513
|
+
if (offset > 0) params.set("offset", String(offset));
|
|
476
514
|
const qs = params.toString();
|
|
477
515
|
const [data, projects] = await Promise.all([
|
|
478
516
|
client.get(`/api/orgs/${org.id}/issues${qs ? `?${qs}` : ""}`),
|
|
@@ -481,10 +519,17 @@ issueCommand.command("list").description("List issues").option("--status <status
|
|
|
481
519
|
const enriched = data.issues.map(
|
|
482
520
|
(issue) => attachIssueUrl(issue, client.baseUrl, org.slug, projects)
|
|
483
521
|
);
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
});
|
|
488
533
|
return;
|
|
489
534
|
}
|
|
490
535
|
if (enriched.length === 0) {
|
|
@@ -502,11 +547,14 @@ issueCommand.command("list").description("List issues").option("--status <status
|
|
|
502
547
|
console.log(`${padEnd(id, 10)} ${statusColor(issue.status, padEnd(issue.status, 14))} ${icon} ${padEnd(pri, 8)} ${title}`);
|
|
503
548
|
}
|
|
504
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
|
+
}
|
|
505
553
|
} catch (err) {
|
|
506
554
|
handleApiError(err);
|
|
507
555
|
}
|
|
508
556
|
});
|
|
509
|
-
|
|
557
|
+
async function viewIssue(identifier) {
|
|
510
558
|
try {
|
|
511
559
|
const client = new AtollClient();
|
|
512
560
|
const org = await resolveOrg(client);
|
|
@@ -516,8 +564,8 @@ issueCommand.command("view <identifier>").description("View issue details (UUID
|
|
|
516
564
|
fetchProjectMap(client, org.id)
|
|
517
565
|
]);
|
|
518
566
|
const enriched = attachIssueUrl(issue, client.baseUrl, org.slug, projects);
|
|
519
|
-
if (
|
|
520
|
-
|
|
567
|
+
if (isJsonMode()) {
|
|
568
|
+
outputJson(enriched);
|
|
521
569
|
return;
|
|
522
570
|
}
|
|
523
571
|
const id = formatIdentifier(enriched, projects);
|
|
@@ -544,7 +592,9 @@ Subtasks: ${enriched.sub_tasks.length}`);
|
|
|
544
592
|
} catch (err) {
|
|
545
593
|
handleApiError(err);
|
|
546
594
|
}
|
|
547
|
-
}
|
|
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);
|
|
548
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) => {
|
|
549
599
|
try {
|
|
550
600
|
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
@@ -605,11 +655,22 @@ issueCommand.command("update <identifier>").description("Update an issue").optio
|
|
|
605
655
|
handleApiError(err);
|
|
606
656
|
}
|
|
607
657
|
});
|
|
608
|
-
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) => {
|
|
609
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
|
+
}
|
|
610
664
|
const client = new AtollClient();
|
|
611
665
|
const orgId = await resolveOrgId(client);
|
|
612
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
|
+
}
|
|
613
674
|
await client.delete(`/api/orgs/${orgId}/issues/${issueId}`);
|
|
614
675
|
output(
|
|
615
676
|
{ success: true, id: issueId },
|
|
@@ -619,6 +680,48 @@ issueCommand.command("delete <identifier>").description("Delete an issue (admin/
|
|
|
619
680
|
handleApiError(err);
|
|
620
681
|
}
|
|
621
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
|
+
});
|
|
622
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) => {
|
|
623
726
|
try {
|
|
624
727
|
const client = new AtollClient();
|
|
@@ -693,32 +796,44 @@ function formatTimestamp(ts) {
|
|
|
693
796
|
return d.toLocaleString();
|
|
694
797
|
}
|
|
695
798
|
var commentCommand = new import_commander3.Command("comment").description("Manage issue comments");
|
|
696
|
-
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) => {
|
|
697
800
|
try {
|
|
801
|
+
const limit = normalizeLimit(opts.limit);
|
|
698
802
|
const client = new AtollClient();
|
|
699
803
|
const orgId = await resolveOrgId(client);
|
|
700
804
|
const issueId = await resolveIssueId(client, orgId, identifier);
|
|
701
805
|
const data = await client.get(
|
|
702
806
|
`/api/orgs/${orgId}/issues/${issueId}/comments`
|
|
703
807
|
);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
});
|
|
708
822
|
return;
|
|
709
823
|
}
|
|
710
|
-
if (
|
|
824
|
+
if (comments.length === 0) {
|
|
711
825
|
console.log("No comments found.");
|
|
712
826
|
return;
|
|
713
827
|
}
|
|
714
|
-
for (const comment of
|
|
828
|
+
for (const comment of comments) {
|
|
715
829
|
const author = formatAuthor(comment);
|
|
716
830
|
const time = formatTimestamp(comment.created_at);
|
|
717
831
|
console.log(`${BOLD2}${CYAN}${author}${RESET2} ${DIM2}${time}${RESET2}`);
|
|
718
832
|
console.log(comment.body);
|
|
719
833
|
console.log();
|
|
720
834
|
}
|
|
721
|
-
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}`);
|
|
722
837
|
} catch (err) {
|
|
723
838
|
handleApiError(err);
|
|
724
839
|
}
|
|
@@ -757,11 +872,22 @@ commentCommand.command("update <comment-id>").description("Update a comment").re
|
|
|
757
872
|
handleApiError(err);
|
|
758
873
|
}
|
|
759
874
|
});
|
|
760
|
-
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) => {
|
|
761
876
|
try {
|
|
877
|
+
if (!opts.dryRun && !opts.force) {
|
|
878
|
+
outputError("Comment deletion requires --force.");
|
|
879
|
+
process.exit(2);
|
|
880
|
+
}
|
|
762
881
|
const client = new AtollClient();
|
|
763
882
|
const orgId = await resolveOrgId(client);
|
|
764
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
|
+
}
|
|
765
891
|
await client.delete(`/api/orgs/${orgId}/issues/${issueId}/comments/${commentId}`);
|
|
766
892
|
output(
|
|
767
893
|
{ success: true, id: commentId },
|
|
@@ -787,16 +913,26 @@ Examples:
|
|
|
787
913
|
$ atoll project list
|
|
788
914
|
$ atoll project create --name "My Project" --icon \u{1F680}
|
|
789
915
|
$ atoll project view <project-id>`);
|
|
790
|
-
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) => {
|
|
791
917
|
const client = new AtollClient();
|
|
792
918
|
try {
|
|
919
|
+
const limit = normalizeLimit(opts.limit);
|
|
793
920
|
const org = await resolveOrg(client);
|
|
794
921
|
const data = await client.get(`/api/orgs/${org.id}/projects`);
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
});
|
|
800
936
|
return;
|
|
801
937
|
}
|
|
802
938
|
if (projects.length === 0) {
|
|
@@ -813,6 +949,7 @@ projectCommand.command("list").description("List all projects with progress").ac
|
|
|
813
949
|
if (p.status === "archived") console.log(` ${dim("[archived]")}`);
|
|
814
950
|
console.log("");
|
|
815
951
|
}
|
|
952
|
+
if (truncated) console.log(dim(`${allProjects.length - projects.length} more project(s) hidden; increase --limit to show more.`));
|
|
816
953
|
} catch (err) {
|
|
817
954
|
handleApiError(err);
|
|
818
955
|
}
|
|
@@ -837,7 +974,7 @@ ${project.url}` : ""}`)
|
|
|
837
974
|
handleApiError(err);
|
|
838
975
|
}
|
|
839
976
|
});
|
|
840
|
-
|
|
977
|
+
async function viewProject(projectId) {
|
|
841
978
|
const client = new AtollClient();
|
|
842
979
|
try {
|
|
843
980
|
const org = await resolveOrg(client);
|
|
@@ -845,8 +982,8 @@ projectCommand.command("view <projectId>").description("View project details and
|
|
|
845
982
|
`/api/orgs/${org.id}/projects/${projectId}`
|
|
846
983
|
);
|
|
847
984
|
const p = attachProjectUrl(data.project, client.baseUrl, org.slug);
|
|
848
|
-
if (
|
|
849
|
-
|
|
985
|
+
if (isJsonMode()) {
|
|
986
|
+
outputJson(p);
|
|
850
987
|
return;
|
|
851
988
|
}
|
|
852
989
|
const progress = p.progress ?? 0;
|
|
@@ -869,7 +1006,9 @@ projectCommand.command("view <projectId>").description("View project details and
|
|
|
869
1006
|
} catch (err) {
|
|
870
1007
|
handleApiError(err);
|
|
871
1008
|
}
|
|
872
|
-
}
|
|
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);
|
|
873
1012
|
|
|
874
1013
|
// src/commands/milestone.ts
|
|
875
1014
|
var import_commander5 = require("commander");
|
|
@@ -882,18 +1021,28 @@ var milestoneCommand = new import_commander5.Command("milestone").description("M
|
|
|
882
1021
|
Examples:
|
|
883
1022
|
$ atoll milestone list --project <project-id>
|
|
884
1023
|
$ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
|
|
885
|
-
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) => {
|
|
886
1025
|
const client = new AtollClient();
|
|
887
1026
|
try {
|
|
1027
|
+
const limit = normalizeLimit(opts.limit);
|
|
888
1028
|
const orgId = await resolveOrgId(client);
|
|
889
1029
|
const data = await client.get(
|
|
890
1030
|
`/api/orgs/${orgId}/projects/${opts.project}/milestones`
|
|
891
1031
|
);
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
+
});
|
|
897
1046
|
return;
|
|
898
1047
|
}
|
|
899
1048
|
if (milestones.length === 0) {
|
|
@@ -911,6 +1060,7 @@ milestoneCommand.command("list").description("List milestones for a project").re
|
|
|
911
1060
|
if (m.status === "closed") console.log(` ${dim("[closed]")}`);
|
|
912
1061
|
console.log("");
|
|
913
1062
|
}
|
|
1063
|
+
if (truncated) console.log(dim(`${allMilestones.length - milestones.length} more milestone(s) hidden; increase --limit to show more.`));
|
|
914
1064
|
} catch (err) {
|
|
915
1065
|
handleApiError(err);
|
|
916
1066
|
}
|
|
@@ -1059,32 +1209,44 @@ Examples:
|
|
|
1059
1209
|
$ atoll webhook list
|
|
1060
1210
|
$ atoll webhook create --url https://example.com/hook --events issue.created,issue.updated
|
|
1061
1211
|
$ atoll webhook delete <id>`);
|
|
1062
|
-
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) => {
|
|
1063
1213
|
try {
|
|
1214
|
+
const limit = normalizeLimit(opts.limit);
|
|
1064
1215
|
const client = new AtollClient();
|
|
1065
1216
|
const orgId = await resolveOrgId2(client);
|
|
1066
1217
|
const data = await client.get(`/api/webhooks?orgId=${orgId}`);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
+
});
|
|
1071
1232
|
return;
|
|
1072
1233
|
}
|
|
1073
|
-
if (
|
|
1234
|
+
if (webhooks.length === 0) {
|
|
1074
1235
|
console.log("No webhooks configured.");
|
|
1075
1236
|
return;
|
|
1076
1237
|
}
|
|
1077
1238
|
const header = bold(`${padEnd2("ID", 38)} ${padEnd2("URL", 40)} ${padEnd2("EVENTS", 20)} ACTIVE`);
|
|
1078
1239
|
console.log(header);
|
|
1079
1240
|
console.log("\u2500".repeat(100));
|
|
1080
|
-
for (const wh of
|
|
1241
|
+
for (const wh of webhooks) {
|
|
1081
1242
|
const evts = wh.events.length === 0 ? "all" : wh.events.join(",");
|
|
1082
1243
|
const active = wh.enabled ? green("yes") : gray("no");
|
|
1083
1244
|
const url = wh.url.length > 38 ? wh.url.slice(0, 35) + "\u2026" : wh.url;
|
|
1084
1245
|
const evtStr = evts.length > 18 ? evts.slice(0, 15) + "\u2026" : evts;
|
|
1085
1246
|
console.log(`${padEnd2(wh.id.slice(0, 36), 38)} ${padEnd2(url, 40)} ${padEnd2(evtStr, 20)} ${active}`);
|
|
1086
1247
|
}
|
|
1087
|
-
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.`));
|
|
1088
1250
|
} catch (err) {
|
|
1089
1251
|
handleApiError2(err);
|
|
1090
1252
|
}
|
|
@@ -1115,9 +1277,20 @@ webhookCommand.command("create").description("Create a new webhook").requiredOpt
|
|
|
1115
1277
|
handleApiError2(err);
|
|
1116
1278
|
}
|
|
1117
1279
|
});
|
|
1118
|
-
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) => {
|
|
1119
1281
|
try {
|
|
1282
|
+
if (!opts.dryRun && !opts.force) {
|
|
1283
|
+
outputError("Webhook deletion requires --force.");
|
|
1284
|
+
process.exit(2);
|
|
1285
|
+
}
|
|
1120
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
|
+
}
|
|
1121
1294
|
await client.delete(`/api/webhooks/${id}`);
|
|
1122
1295
|
output(
|
|
1123
1296
|
{ success: true, id },
|
|
@@ -1128,24 +1301,405 @@ webhookCommand.command("delete <id>").description("Delete a webhook").action(asy
|
|
|
1128
1301
|
}
|
|
1129
1302
|
});
|
|
1130
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
|
+
|
|
1131
1681
|
// src/index.ts
|
|
1132
1682
|
function readCliVersion() {
|
|
1133
1683
|
try {
|
|
1134
|
-
const packageJson = JSON.parse((0,
|
|
1684
|
+
const packageJson = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
1135
1685
|
return packageJson.version ?? "0.0.0";
|
|
1136
1686
|
} catch {
|
|
1137
1687
|
return "0.0.0";
|
|
1138
1688
|
}
|
|
1139
1689
|
}
|
|
1140
|
-
var program = new
|
|
1690
|
+
var program = new import_commander11.Command("atoll").description("Atoll CLI \u2014 project management from the terminal").version(readCliVersion()).addHelpText("after", `
|
|
1141
1691
|
Examples:
|
|
1142
1692
|
$ atoll issue list
|
|
1143
1693
|
$ atoll issue create --title "Fix login bug" --priority 1
|
|
1144
1694
|
$ atoll issue view ATOLL-42
|
|
1695
|
+
$ atoll heartbeat
|
|
1145
1696
|
$ atoll project list
|
|
1146
1697
|
$ atoll milestone list --project <id>
|
|
1147
|
-
$ 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", (
|
|
1148
|
-
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
|
+
}
|
|
1149
1703
|
if (opts.profile) {
|
|
1150
1704
|
process.env.ATOLL_PROFILE = opts.profile;
|
|
1151
1705
|
}
|
|
@@ -1165,7 +1719,11 @@ program.addCommand(projectCommand);
|
|
|
1165
1719
|
program.addCommand(milestoneCommand);
|
|
1166
1720
|
program.addCommand(configCommand);
|
|
1167
1721
|
program.addCommand(webhookCommand);
|
|
1168
|
-
|
|
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", `
|
|
1169
1727
|
Examples:
|
|
1170
1728
|
$ atoll completion bash >> ~/.bashrc
|
|
1171
1729
|
$ atoll completion zsh >> ~/.zshrc`);
|
|
@@ -1176,6 +1734,7 @@ completionCommand.command("zsh").description("Output zsh completion script").act
|
|
|
1176
1734
|
process.stdout.write(zshCompletion());
|
|
1177
1735
|
});
|
|
1178
1736
|
program.addCommand(completionCommand);
|
|
1737
|
+
addJsonOptionToSubcommands(program);
|
|
1179
1738
|
program.parse();
|
|
1180
1739
|
function bashCompletion() {
|
|
1181
1740
|
return `# Atoll CLI bash completion
|
|
@@ -1185,13 +1744,14 @@ _atoll_completions() {
|
|
|
1185
1744
|
local cur prev words cword
|
|
1186
1745
|
_init_completion || return
|
|
1187
1746
|
|
|
1188
|
-
local commands="auth issue comment project milestone config webhook completion"
|
|
1189
|
-
local issue_cmds="list view create update delete assign unassign"
|
|
1190
|
-
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"
|
|
1191
1750
|
local milestone_cmds="list create"
|
|
1192
1751
|
local config_cmds="show set-org set-team set-base-url"
|
|
1193
1752
|
local auth_cmds="login logout status profiles use"
|
|
1194
1753
|
local webhook_cmds="list create delete"
|
|
1754
|
+
local feedback_cmds="add list"
|
|
1195
1755
|
local completion_cmds="bash zsh"
|
|
1196
1756
|
|
|
1197
1757
|
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
@@ -1230,6 +1790,11 @@ _atoll_completions() {
|
|
|
1230
1790
|
COMPREPLY=( $(compgen -W "\${webhook_cmds}" -- "\${cur}") )
|
|
1231
1791
|
fi
|
|
1232
1792
|
;;
|
|
1793
|
+
feedback)
|
|
1794
|
+
if [ "\${COMP_CWORD}" -eq 2 ]; then
|
|
1795
|
+
COMPREPLY=( $(compgen -W "\${feedback_cmds}" -- "\${cur}") )
|
|
1796
|
+
fi
|
|
1797
|
+
;;
|
|
1233
1798
|
completion)
|
|
1234
1799
|
if [ "\${COMP_CWORD}" -eq 2 ]; then
|
|
1235
1800
|
COMPREPLY=( $(compgen -W "\${completion_cmds}" -- "\${cur}") )
|
|
@@ -1263,6 +1828,10 @@ _atoll() {
|
|
|
1263
1828
|
'milestone[Manage milestones]' \\
|
|
1264
1829
|
'config[Manage CLI configuration]' \\
|
|
1265
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]' \\
|
|
1266
1835
|
'completion[Output shell completion scripts]'
|
|
1267
1836
|
;;
|
|
1268
1837
|
args)
|
|
@@ -1271,7 +1840,10 @@ _atoll() {
|
|
|
1271
1840
|
_values 'subcommand' \\
|
|
1272
1841
|
'list[List issues]' \\
|
|
1273
1842
|
'view[View issue details]' \\
|
|
1843
|
+
'get[Get issue details]' \\
|
|
1274
1844
|
'create[Create a new issue]' \\
|
|
1845
|
+
'archive[Archive an issue]' \\
|
|
1846
|
+
'unarchive[Unarchive an issue]' \\
|
|
1275
1847
|
'update[Update an issue]' \\
|
|
1276
1848
|
'delete[Delete an issue]' \\
|
|
1277
1849
|
'assign[Assign an issue]' \\
|
|
@@ -1281,7 +1853,8 @@ _atoll() {
|
|
|
1281
1853
|
_values 'subcommand' \\
|
|
1282
1854
|
'list[List projects]' \\
|
|
1283
1855
|
'create[Create a project]' \\
|
|
1284
|
-
'view[View project details]'
|
|
1856
|
+
'view[View project details]' \\
|
|
1857
|
+
'get[Get project details]'
|
|
1285
1858
|
;;
|
|
1286
1859
|
milestone)
|
|
1287
1860
|
_values 'subcommand' \\
|
|
@@ -1309,6 +1882,11 @@ _atoll() {
|
|
|
1309
1882
|
'create[Create a webhook]' \\
|
|
1310
1883
|
'delete[Delete a webhook]'
|
|
1311
1884
|
;;
|
|
1885
|
+
feedback)
|
|
1886
|
+
_values 'subcommand' \\
|
|
1887
|
+
'add[Record feedback]' \\
|
|
1888
|
+
'list[List local feedback]'
|
|
1889
|
+
;;
|
|
1312
1890
|
completion)
|
|
1313
1891
|
_values 'shell' 'bash' 'zsh'
|
|
1314
1892
|
;;
|
|
@@ -1320,4 +1898,12 @@ _atoll() {
|
|
|
1320
1898
|
_atoll "$@"
|
|
1321
1899
|
`;
|
|
1322
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
|
+
}
|
|
1323
1909
|
//# sourceMappingURL=index.js.map
|