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