flightdesk 0.6.0 → 0.6.2
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 +7 -4
- package/main.js +287 -183
- package/main.js.map +4 -4
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -3053,6 +3053,9 @@ function readFolderCredential() {
|
|
|
3053
3053
|
return null;
|
|
3054
3054
|
}
|
|
3055
3055
|
var folderCredential;
|
|
3056
|
+
function credentialSource() {
|
|
3057
|
+
return folder()?.source ?? `home (${CONFIG_FILE})`;
|
|
3058
|
+
}
|
|
3056
3059
|
function folder() {
|
|
3057
3060
|
if (folderCredential === void 0) folderCredential = readFolderCredential();
|
|
3058
3061
|
return folderCredential;
|
|
@@ -3281,12 +3284,35 @@ var FlightDeskAPI = class _FlightDeskAPI {
|
|
|
3281
3284
|
prNumber
|
|
3282
3285
|
sessionViewUrl
|
|
3283
3286
|
sessionTeleportId
|
|
3287
|
+
sourceSystem
|
|
3288
|
+
sourceRef
|
|
3289
|
+
sourceUrl
|
|
3284
3290
|
}
|
|
3285
3291
|
}
|
|
3286
3292
|
`;
|
|
3287
3293
|
const result = await this.graphql(query, { taskId, input });
|
|
3288
3294
|
return result.userUpdateTask;
|
|
3289
3295
|
}
|
|
3296
|
+
/** "Continue in FlightDesk": attach a locally-built PR and hand the work to the pipeline. */
|
|
3297
|
+
async handoffPullRequest(input) {
|
|
3298
|
+
const query = `
|
|
3299
|
+
mutation UserHandoffPullRequest($input: UserHandoffPullRequestInput!) {
|
|
3300
|
+
userHandoffPullRequest(input: $input) {
|
|
3301
|
+
taskId
|
|
3302
|
+
created
|
|
3303
|
+
orchestrated
|
|
3304
|
+
phase
|
|
3305
|
+
waitingOn
|
|
3306
|
+
agentUserId
|
|
3307
|
+
prNumber
|
|
3308
|
+
prUrl
|
|
3309
|
+
message
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
`;
|
|
3313
|
+
const result = await this.graphql(query, { input });
|
|
3314
|
+
return result.userHandoffPullRequest;
|
|
3315
|
+
}
|
|
3290
3316
|
async registerSession(taskId, input) {
|
|
3291
3317
|
const query = `
|
|
3292
3318
|
mutation RegisterSession($taskId: String!, $input: RegisterSessionInput!) {
|
|
@@ -3603,7 +3629,7 @@ async function execute(query, variables) {
|
|
|
3603
3629
|
}
|
|
3604
3630
|
function registerQuestionCommands(program3) {
|
|
3605
3631
|
const questions = program3.command("questions").description("Ask roles, poll questions, and consume answers");
|
|
3606
|
-
questions.command("ask <taskId>").option("--to-role <role>", "requester, product_owner, tech_owner, client_contact, triager").option("--to-user <id>", "Address a specific human member directly").requiredOption("--key <key>", "Stable retry key").requiredOption("--body <text>", "Question").requiredOption("--context <text>", "Short answer context").option("--default <text>", "Proposed default; required for
|
|
3632
|
+
questions.command("ask <taskId>").option("--to-role <role>", "requester, product_owner, tech_owner, client_contact, triager").option("--to-user <id>", "Address a specific human member directly").requiredOption("--key <key>", "Stable retry key").requiredOption("--body <text>", "Question").requiredOption("--context <text>", "Short answer context").option("--default <text>", "Proposed default; required for ordinary agent questions").option("--requires-explicit-answer", "Permission decision: blocking, with no default").option("--options <values...>", "Suggested answer choices").option("--due-at <iso>", "Optional due timestamp").option("--nonblocking", "Allow work to continue").action(
|
|
3607
3633
|
(taskId, options) => execute("mutation($input:JSON!){userAskQuestion(input:$input)}", {
|
|
3608
3634
|
input: {
|
|
3609
3635
|
taskId,
|
|
@@ -3613,6 +3639,7 @@ function registerQuestionCommands(program3) {
|
|
|
3613
3639
|
body: options.body,
|
|
3614
3640
|
context: options.context,
|
|
3615
3641
|
proposedDefault: options.default,
|
|
3642
|
+
requiresExplicitAnswer: options.requiresExplicitAnswer,
|
|
3616
3643
|
blocking: !options.nonblocking,
|
|
3617
3644
|
options: options.options,
|
|
3618
3645
|
dueAt: options.dueAt
|
|
@@ -3645,18 +3672,29 @@ async function execute2(query, variables, attempts = 3) {
|
|
|
3645
3672
|
}
|
|
3646
3673
|
function registerPlanCommands(program3) {
|
|
3647
3674
|
const plan = program3.command("plan").description("Task plans held in FlightDesk");
|
|
3648
|
-
plan.command("submit <taskId>").description("Submit a plan revision from a markdown file (or --body)").option("-f, --file <path>", "Markdown file holding the plan").option("--body <markdown>", "Plan body inline (prefer --file)").option("--summary <text>", "One-paragraph summary").
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3675
|
+
plan.command("submit <taskId>").description("Submit a plan revision from a markdown file (or --body)").option("-f, --file <path>", "Markdown file holding the plan").option("--body <markdown>", "Plan body inline (prefer --file)").option("--summary <text>", "One-paragraph summary").option("--test-plan <path>", "Markdown file: the steps a human runs on the preview").option("--release-note <text>", "One or two sentences for staff, confirmed at closeout").action(
|
|
3676
|
+
async (taskId, options) => {
|
|
3677
|
+
const body = options.file ? (0, import_node_fs.readFileSync)(options.file, "utf8") : options.body;
|
|
3678
|
+
if (!body?.trim()) {
|
|
3679
|
+
console.error("Provide --file <path> or --body <markdown>");
|
|
3680
|
+
process.exit(1);
|
|
3681
|
+
}
|
|
3682
|
+
const testPlan = options.testPlan ? (0, import_node_fs.readFileSync)(options.testPlan, "utf8") : void 0;
|
|
3683
|
+
await execute2(
|
|
3684
|
+
"mutation($input:JSON!){userSubmitPlan(input:$input)}",
|
|
3685
|
+
{
|
|
3686
|
+
input: {
|
|
3687
|
+
taskId,
|
|
3688
|
+
body,
|
|
3689
|
+
summary: options.summary,
|
|
3690
|
+
testPlan,
|
|
3691
|
+
releaseNote: options.releaseNote
|
|
3692
|
+
}
|
|
3693
|
+
},
|
|
3694
|
+
1
|
|
3695
|
+
);
|
|
3653
3696
|
}
|
|
3654
|
-
|
|
3655
|
-
"mutation($input:JSON!){userSubmitPlan(input:$input)}",
|
|
3656
|
-
{ input: { taskId, body, summary: options.summary } },
|
|
3657
|
-
1
|
|
3658
|
-
);
|
|
3659
|
-
});
|
|
3697
|
+
);
|
|
3660
3698
|
plan.command("show <taskId>").description("Print the plan revisions and approval state").action(
|
|
3661
3699
|
(taskId) => execute2("query($taskId:String!){userTaskPlan(taskId:$taskId)}", { taskId })
|
|
3662
3700
|
);
|
|
@@ -3670,6 +3708,39 @@ function registerPlanCommands(program3) {
|
|
|
3670
3708
|
);
|
|
3671
3709
|
}
|
|
3672
3710
|
|
|
3711
|
+
// apps/cli/src/commands/whoami.ts
|
|
3712
|
+
function registerWhoamiCommand(program3) {
|
|
3713
|
+
program3.command("whoami").description("Show who the resolved API key authenticates as, and where the key came from").option("--json", "Machine-readable output").action(async (options) => {
|
|
3714
|
+
const config = loadConfig();
|
|
3715
|
+
if (!config.apiKey) {
|
|
3716
|
+
console.error("No API key configured. Run: flightdesk init");
|
|
3717
|
+
process.exit(1);
|
|
3718
|
+
}
|
|
3719
|
+
const apiUrl = getApiUrl();
|
|
3720
|
+
const source = credentialSource();
|
|
3721
|
+
try {
|
|
3722
|
+
const me = await fetchUserInfo(config.apiKey, apiUrl);
|
|
3723
|
+
const email = me.emails?.find((e) => e.primary)?.email ?? me.emails?.[0]?.email ?? null;
|
|
3724
|
+
const organizations = (me.organizations ?? []).map((o) => o.organization);
|
|
3725
|
+
if (options.json) {
|
|
3726
|
+
console.log(JSON.stringify({ id: me.id, email, organizations, apiUrl, source }, null, 2));
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
console.log(`${email ?? me.id}${email ? ` (${me.id})` : ""}`);
|
|
3730
|
+
console.log(`API: ${apiUrl}`);
|
|
3731
|
+
console.log(`Key: ${source}`);
|
|
3732
|
+
console.log(
|
|
3733
|
+
`Orgs: ${organizations.length ? organizations.map((o) => `${o.name} (${o.id})`).join(", ") : "none"}`
|
|
3734
|
+
);
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
console.error(
|
|
3737
|
+
`Not authenticated against ${apiUrl} (key from ${source}): ${error.message}`
|
|
3738
|
+
);
|
|
3739
|
+
process.exit(1);
|
|
3740
|
+
}
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3673
3744
|
// apps/cli/src/commands/dispatch.ts
|
|
3674
3745
|
var os2 = __toESM(require("node:os"));
|
|
3675
3746
|
async function execute3(query, variables) {
|
|
@@ -3728,6 +3799,11 @@ function registerDispatchCommands(program3) {
|
|
|
3728
3799
|
});
|
|
3729
3800
|
});
|
|
3730
3801
|
const turn = program3.command("turn").description("Report the outcome of an agent turn");
|
|
3802
|
+
turn.command("progress <line>").description("Say what this turn is doing now \u2014 one line, no outcome, safe to repeat").requiredOption("--dispatch <id>", "Dispatch id ($FLIGHTDESK_DISPATCH_ID)").action(
|
|
3803
|
+
(line, options) => execute3("mutation($input: JSON!) { userReportProgress(input: $input) }", {
|
|
3804
|
+
input: { dispatchId: options.dispatch, message: line }
|
|
3805
|
+
})
|
|
3806
|
+
);
|
|
3731
3807
|
turn.command("end").requiredOption("--dispatch <id>", "Dispatch id ($FLIGHTDESK_DISPATCH_ID)").requiredOption("--outcome <outcome>", "done | waiting | failed | blocked").option("--waiting-on <what>", "session | ci | review | qa | merge | deploy | dependency").option("--ref <url>", "PR URL or other reference").option("--summary <text>", "One-paragraph summary").action(
|
|
3732
3808
|
(options) => execute3("mutation($input: JSON!) { userEndTurn(input: $input) }", {
|
|
3733
3809
|
input: {
|
|
@@ -3909,10 +3985,7 @@ async function registerCommand(taskId, options) {
|
|
|
3909
3985
|
const { config, org: org2 } = requireActiveOrg();
|
|
3910
3986
|
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
3911
3987
|
try {
|
|
3912
|
-
|
|
3913
|
-
if (!projectId) {
|
|
3914
|
-
projectId = await autoDetectProject(api, org2);
|
|
3915
|
-
}
|
|
3988
|
+
const target = taskId ? { taskId } : { projectId: options.project || await autoDetectProject(api, org2) };
|
|
3916
3989
|
let viewUrl = options.viewUrl;
|
|
3917
3990
|
let teleportId = options.teleportId;
|
|
3918
3991
|
if (options.prompt) {
|
|
@@ -3928,15 +4001,17 @@ async function registerCommand(taskId, options) {
|
|
|
3928
4001
|
viewUrl = viewUrl || parsed.viewUrl;
|
|
3929
4002
|
teleportId = teleportId || parsed.teleportId;
|
|
3930
4003
|
}
|
|
3931
|
-
let actualTaskId
|
|
3932
|
-
if (
|
|
4004
|
+
let actualTaskId;
|
|
4005
|
+
if ("taskId" in target) {
|
|
4006
|
+
actualTaskId = target.taskId;
|
|
4007
|
+
} else {
|
|
3933
4008
|
if (!options.title) {
|
|
3934
4009
|
console.error('Task title required when creating a new task. Use --title "..."');
|
|
3935
4010
|
process.exit(1);
|
|
3936
4011
|
}
|
|
3937
4012
|
console.log(`Creating task: ${options.title}`);
|
|
3938
4013
|
const task2 = await api.createTask({
|
|
3939
|
-
projectId,
|
|
4014
|
+
projectId: target.projectId,
|
|
3940
4015
|
title: options.title,
|
|
3941
4016
|
// Launch-first orchestrators do not know the FlightDesk UUID when the
|
|
3942
4017
|
// cloud prompt is sent. Persisting that same prompt on the task lets
|
|
@@ -3957,8 +4032,7 @@ async function registerCommand(taskId, options) {
|
|
|
3957
4032
|
} else {
|
|
3958
4033
|
await api.updateTask(actualTaskId, {
|
|
3959
4034
|
sessionViewUrl: viewUrl,
|
|
3960
|
-
sessionTeleportId: teleportId
|
|
3961
|
-
status: "DISPATCHED"
|
|
4035
|
+
sessionTeleportId: teleportId
|
|
3962
4036
|
});
|
|
3963
4037
|
}
|
|
3964
4038
|
console.log("\u2705 Session registered");
|
|
@@ -4020,30 +4094,25 @@ Output: ${output.slice(0, 500)}`));
|
|
|
4020
4094
|
}
|
|
4021
4095
|
function readStdin() {
|
|
4022
4096
|
return new Promise((resolve) => {
|
|
4097
|
+
const input = process.stdin;
|
|
4023
4098
|
let data = "";
|
|
4024
|
-
|
|
4025
|
-
process.stdin.setEncoding("utf8");
|
|
4026
|
-
process.stdin.on("data", (chunk) => {
|
|
4099
|
+
const append = (chunk) => {
|
|
4027
4100
|
data += chunk;
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
resolved = true;
|
|
4044
|
-
resolve(data);
|
|
4045
|
-
}
|
|
4046
|
-
}, 3e4);
|
|
4101
|
+
};
|
|
4102
|
+
const finish = () => {
|
|
4103
|
+
clearTimeout(timeout);
|
|
4104
|
+
input.off("data", append);
|
|
4105
|
+
input.off("end", finish);
|
|
4106
|
+
input.off("close", finish);
|
|
4107
|
+
input.pause();
|
|
4108
|
+
resolve(data);
|
|
4109
|
+
};
|
|
4110
|
+
const timeout = setTimeout(finish, 3e4);
|
|
4111
|
+
input.setEncoding("utf8");
|
|
4112
|
+
input.on("data", append);
|
|
4113
|
+
input.once("end", finish);
|
|
4114
|
+
input.once("close", finish);
|
|
4115
|
+
if (input.readableEnded || input.destroyed) finish();
|
|
4047
4116
|
});
|
|
4048
4117
|
}
|
|
4049
4118
|
function parseClaudeOutput(output) {
|
|
@@ -4112,6 +4181,32 @@ async function autoDetectProject(api, activeOrg) {
|
|
|
4112
4181
|
return project2.id;
|
|
4113
4182
|
}
|
|
4114
4183
|
|
|
4184
|
+
// apps/cli/src/lib/task-status.ts
|
|
4185
|
+
var TASK_STATUSES = [
|
|
4186
|
+
["PENDING", "\u2B1C"],
|
|
4187
|
+
["DISPATCHED", "\u{1F680}"],
|
|
4188
|
+
["IN_PROGRESS", "\u{1F504}"],
|
|
4189
|
+
["BRANCH_CREATED", "\u{1F33F}"],
|
|
4190
|
+
["PR_OPEN", "\u{1F4DD}"],
|
|
4191
|
+
["PREVIEW_STARTING", "\u23F3"],
|
|
4192
|
+
["PREVIEW_READY", "\u{1F310}"],
|
|
4193
|
+
["REVIEW_RUNNING", "\u{1F50D}"],
|
|
4194
|
+
["REVIEW_DONE", "\u2705"],
|
|
4195
|
+
["QA_READY", "\u{1F9EA}"],
|
|
4196
|
+
["QA_CHANGES_REQUESTED", "\u{1F527}"],
|
|
4197
|
+
["QA_APPROVED", "\u{1F44D}"],
|
|
4198
|
+
["MERGED", "\u{1F389}"],
|
|
4199
|
+
["ARCHIVED", "\u{1F4E6}"]
|
|
4200
|
+
];
|
|
4201
|
+
var TASK_STATUS_ORDER = TASK_STATUSES.map(([status]) => status);
|
|
4202
|
+
var EMOJI_BY_STATUS = new Map(TASK_STATUSES);
|
|
4203
|
+
function taskStatusLabel(status) {
|
|
4204
|
+
return status ?? "No coding run";
|
|
4205
|
+
}
|
|
4206
|
+
function taskStatusEmoji(status) {
|
|
4207
|
+
return status === null ? "\u2B1C" : EMOJI_BY_STATUS.get(status) ?? "\u2753";
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4115
4210
|
// apps/cli/src/commands/status.ts
|
|
4116
4211
|
async function statusCommand(options) {
|
|
4117
4212
|
const { config, org: org2 } = requireActiveOrg();
|
|
@@ -4122,33 +4217,18 @@ async function statusCommand(options) {
|
|
|
4122
4217
|
console.log("No tasks found.");
|
|
4123
4218
|
return;
|
|
4124
4219
|
}
|
|
4125
|
-
const byStatus =
|
|
4220
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
4126
4221
|
for (const task2 of tasks) {
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
byStatus[task2.status].push(task2);
|
|
4222
|
+
const group = byStatus.get(task2.status) ?? [];
|
|
4223
|
+
group.push(task2);
|
|
4224
|
+
byStatus.set(task2.status, group);
|
|
4131
4225
|
}
|
|
4132
|
-
const statusOrder = [
|
|
4133
|
-
"PENDING",
|
|
4134
|
-
"DISPATCHED",
|
|
4135
|
-
"IN_PROGRESS",
|
|
4136
|
-
"BRANCH_CREATED",
|
|
4137
|
-
"PR_OPEN",
|
|
4138
|
-
"PREVIEW_STARTING",
|
|
4139
|
-
"PREVIEW_READY",
|
|
4140
|
-
"REVIEW_RUNNING",
|
|
4141
|
-
"REVIEW_DONE",
|
|
4142
|
-
"QA_READY",
|
|
4143
|
-
"QA_APPROVED",
|
|
4144
|
-
"MERGED",
|
|
4145
|
-
"ARCHIVED"
|
|
4146
|
-
];
|
|
4226
|
+
const statusOrder = /* @__PURE__ */ new Set([...TASK_STATUS_ORDER, null, ...byStatus.keys()]);
|
|
4147
4227
|
console.log("\n\u{1F4CB} FlightDesk Tasks\n");
|
|
4148
4228
|
for (const status of statusOrder) {
|
|
4149
|
-
const statusTasks = byStatus
|
|
4229
|
+
const statusTasks = byStatus.get(status);
|
|
4150
4230
|
if (!statusTasks || statusTasks.length === 0) continue;
|
|
4151
|
-
console.log(`${
|
|
4231
|
+
console.log(`${taskStatusEmoji(status)} ${taskStatusLabel(status)} (${statusTasks.length})`);
|
|
4152
4232
|
for (const task2 of statusTasks) {
|
|
4153
4233
|
console.log(` ${task2.id.slice(0, 8)} - ${task2.title}`);
|
|
4154
4234
|
if (task2.branchName) {
|
|
@@ -4165,40 +4245,21 @@ async function statusCommand(options) {
|
|
|
4165
4245
|
process.exit(1);
|
|
4166
4246
|
}
|
|
4167
4247
|
}
|
|
4168
|
-
function getStatusEmoji(status) {
|
|
4169
|
-
switch (status) {
|
|
4170
|
-
case "PENDING":
|
|
4171
|
-
return "\u2B1C";
|
|
4172
|
-
case "DISPATCHED":
|
|
4173
|
-
return "\u{1F680}";
|
|
4174
|
-
case "IN_PROGRESS":
|
|
4175
|
-
return "\u{1F504}";
|
|
4176
|
-
case "BRANCH_CREATED":
|
|
4177
|
-
return "\u{1F33F}";
|
|
4178
|
-
case "PR_OPEN":
|
|
4179
|
-
return "\u{1F4DD}";
|
|
4180
|
-
case "PREVIEW_STARTING":
|
|
4181
|
-
return "\u23F3";
|
|
4182
|
-
case "PREVIEW_READY":
|
|
4183
|
-
return "\u{1F310}";
|
|
4184
|
-
case "REVIEW_RUNNING":
|
|
4185
|
-
return "\u{1F50D}";
|
|
4186
|
-
case "REVIEW_DONE":
|
|
4187
|
-
return "\u2705";
|
|
4188
|
-
case "QA_READY":
|
|
4189
|
-
return "\u{1F9EA}";
|
|
4190
|
-
case "QA_APPROVED":
|
|
4191
|
-
return "\u{1F44D}";
|
|
4192
|
-
case "MERGED":
|
|
4193
|
-
return "\u{1F389}";
|
|
4194
|
-
case "ARCHIVED":
|
|
4195
|
-
return "\u{1F4E6}";
|
|
4196
|
-
default:
|
|
4197
|
-
return "\u2753";
|
|
4198
|
-
}
|
|
4199
|
-
}
|
|
4200
4248
|
|
|
4201
4249
|
// apps/cli/src/commands/task.ts
|
|
4250
|
+
function sourceIdentityEdit(options) {
|
|
4251
|
+
const set = {
|
|
4252
|
+
...options.sourceSystem === void 0 ? {} : { sourceSystem: options.sourceSystem },
|
|
4253
|
+
...options.sourceRef === void 0 ? {} : { sourceRef: options.sourceRef },
|
|
4254
|
+
...options.sourceUrl === void 0 ? {} : { sourceUrl: options.sourceUrl }
|
|
4255
|
+
};
|
|
4256
|
+
if (!options.clearSource) return set;
|
|
4257
|
+
if (Object.keys(set).length > 0)
|
|
4258
|
+
throw new Error(
|
|
4259
|
+
"--clear-source cannot be combined with --source-system/--source-ref/--source-url"
|
|
4260
|
+
);
|
|
4261
|
+
return { sourceSystem: null, sourceRef: null, sourceUrl: null };
|
|
4262
|
+
}
|
|
4202
4263
|
async function taskCommand(action, options) {
|
|
4203
4264
|
const { config, org: org2 } = requireActiveOrg();
|
|
4204
4265
|
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
@@ -4234,13 +4295,15 @@ async function handleCreate(api, options) {
|
|
|
4234
4295
|
subprojectId: options.subproject,
|
|
4235
4296
|
sourceSystem: options.sourceSystem,
|
|
4236
4297
|
sourceRef: options.sourceRef,
|
|
4237
|
-
sourceUrl: options.sourceUrl
|
|
4298
|
+
sourceUrl: options.sourceUrl,
|
|
4299
|
+
followUpOf: options.followUpOf,
|
|
4300
|
+
followUpDispatchId: options.followUpDispatch
|
|
4238
4301
|
});
|
|
4239
4302
|
console.log(`
|
|
4240
4303
|
\u2705 Task created`);
|
|
4241
4304
|
console.log(` ID: ${task2.id}`);
|
|
4242
4305
|
console.log(` Title: ${task2.title}`);
|
|
4243
|
-
console.log(` Status: ${task2.status}`);
|
|
4306
|
+
console.log(` Status: ${taskStatusLabel(task2.status)}`);
|
|
4244
4307
|
}
|
|
4245
4308
|
async function handleList(api, options) {
|
|
4246
4309
|
const tasks = await api.listTasks({
|
|
@@ -4253,10 +4316,10 @@ async function handleList(api, options) {
|
|
|
4253
4316
|
}
|
|
4254
4317
|
console.log("\nTasks:\n");
|
|
4255
4318
|
for (const task2 of tasks) {
|
|
4256
|
-
const statusEmoji =
|
|
4319
|
+
const statusEmoji = taskStatusEmoji(task2.status);
|
|
4257
4320
|
console.log(`${statusEmoji} ${task2.id.slice(0, 8)} - ${task2.title}`);
|
|
4258
4321
|
if (task2.project?.name) console.log(` Project: ${task2.project.name}`);
|
|
4259
|
-
console.log(` Status: ${task2.status}`);
|
|
4322
|
+
console.log(` Status: ${taskStatusLabel(task2.status)}`);
|
|
4260
4323
|
if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
|
|
4261
4324
|
if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
|
|
4262
4325
|
console.log("");
|
|
@@ -4272,11 +4335,10 @@ async function handleStatus(api, options) {
|
|
|
4272
4335
|
\u{1F4CB} Task: ${task2.title}
|
|
4273
4336
|
`);
|
|
4274
4337
|
console.log(`ID: ${task2.id}`);
|
|
4275
|
-
console.log(`Status: ${
|
|
4338
|
+
console.log(`Status: ${taskStatusEmoji(task2.status)} ${taskStatusLabel(task2.status)}`);
|
|
4276
4339
|
if (task2.project) {
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
);
|
|
4340
|
+
const repo = task2.project.githubRepo ? " (" + task2.project.githubRepo + ")" : "";
|
|
4341
|
+
console.log(`Project: ${task2.project.name}${repo}`);
|
|
4280
4342
|
}
|
|
4281
4343
|
if (task2.description) {
|
|
4282
4344
|
console.log(`
|
|
@@ -4302,31 +4364,56 @@ function parseSessionId(value) {
|
|
|
4302
4364
|
}
|
|
4303
4365
|
return value;
|
|
4304
4366
|
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4367
|
+
var PR_NUMBER_IN_URL = /\/pull\/(\d+)/;
|
|
4368
|
+
var SOURCE_FIELDS = ["sourceSystem", "sourceRef", "sourceUrl"];
|
|
4369
|
+
function updateInput(options) {
|
|
4370
|
+
const input = { ...sourceIdentityEdit(options) };
|
|
4371
|
+
if (options.releaseNote !== void 0) input.releaseNote = options.releaseNote;
|
|
4307
4372
|
if (options.status) input.status = options.status;
|
|
4308
4373
|
if (options.branch) input.branchName = options.branch;
|
|
4309
4374
|
if (options.prUrl) {
|
|
4310
4375
|
input.prUrl = options.prUrl;
|
|
4311
|
-
const prNumberMatch = options.prUrl
|
|
4312
|
-
if (prNumberMatch)
|
|
4313
|
-
input.prNumber = Number.parseInt(prNumberMatch[1], 10);
|
|
4314
|
-
}
|
|
4376
|
+
const prNumberMatch = PR_NUMBER_IN_URL.exec(options.prUrl);
|
|
4377
|
+
if (prNumberMatch) input.prNumber = Number.parseInt(prNumberMatch[1], 10);
|
|
4315
4378
|
}
|
|
4316
|
-
if (options.prNumber)
|
|
4317
|
-
|
|
4379
|
+
if (options.prNumber) input.prNumber = Number.parseInt(options.prNumber, 10);
|
|
4380
|
+
return input;
|
|
4381
|
+
}
|
|
4382
|
+
async function autoTransition(api, options, input) {
|
|
4383
|
+
if (!options.branch || options.status) return;
|
|
4384
|
+
const current = await api.getTask(options.taskId);
|
|
4385
|
+
if (current?.status !== "DISPATCHED") return;
|
|
4386
|
+
input.status = "IN_PROGRESS";
|
|
4387
|
+
console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
|
|
4388
|
+
}
|
|
4389
|
+
function printUpdated(task2, input) {
|
|
4390
|
+
console.log(`
|
|
4391
|
+
\u2705 Task updated`);
|
|
4392
|
+
console.log(` Status: ${taskStatusLabel(task2.status)}`);
|
|
4393
|
+
if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
|
|
4394
|
+
if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
|
|
4395
|
+
if (task2.prNumber) console.log(` PR #: ${task2.prNumber}`);
|
|
4396
|
+
if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
|
|
4397
|
+
if (SOURCE_FIELDS.some((field) => field in input)) console.log(` Source: ${describeSource(task2)}`);
|
|
4398
|
+
}
|
|
4399
|
+
async function syncAfterPrUrl(api, taskId) {
|
|
4400
|
+
console.log(`
|
|
4401
|
+
\u{1F504} Syncing PR review state...`);
|
|
4402
|
+
try {
|
|
4403
|
+
printSyncResult(await api.syncTaskChecks(taskId));
|
|
4404
|
+
} catch (err) {
|
|
4405
|
+
console.log(` \u26A0\uFE0F Sync failed (checks will update when webhooks arrive): ${err}`);
|
|
4318
4406
|
}
|
|
4407
|
+
}
|
|
4408
|
+
async function handleUpdate(api, options) {
|
|
4409
|
+
const input = updateInput(options);
|
|
4319
4410
|
if (Object.keys(input).length === 0 && !options.session) {
|
|
4320
|
-
console.error(
|
|
4411
|
+
console.error(
|
|
4412
|
+
"No updates specified. Use --status, --branch, --pr-url, --session, --release-note, --source-system/--source-ref/--source-url or --clear-source"
|
|
4413
|
+
);
|
|
4321
4414
|
process.exit(1);
|
|
4322
4415
|
}
|
|
4323
|
-
|
|
4324
|
-
const current = await api.getTask(options.taskId);
|
|
4325
|
-
if (current?.status === "DISPATCHED") {
|
|
4326
|
-
input.status = "IN_PROGRESS";
|
|
4327
|
-
console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4416
|
+
await autoTransition(api, options, input);
|
|
4330
4417
|
console.log(`Updating task ${options.taskId}...`);
|
|
4331
4418
|
let task2;
|
|
4332
4419
|
if (options.session) {
|
|
@@ -4336,27 +4423,14 @@ async function handleUpdate(api, options) {
|
|
|
4336
4423
|
teleportId: sessionId
|
|
4337
4424
|
});
|
|
4338
4425
|
}
|
|
4339
|
-
if (Object.keys(input).length > 0)
|
|
4340
|
-
task2 = await api.updateTask(options.taskId, input);
|
|
4341
|
-
}
|
|
4426
|
+
if (Object.keys(input).length > 0) task2 = await api.updateTask(options.taskId, input);
|
|
4342
4427
|
if (!task2) return;
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
|
|
4350
|
-
if (options.prUrl) {
|
|
4351
|
-
console.log(`
|
|
4352
|
-
\u{1F504} Syncing PR review state...`);
|
|
4353
|
-
try {
|
|
4354
|
-
const syncResult = await api.syncTaskChecks(options.taskId);
|
|
4355
|
-
printSyncResult(syncResult);
|
|
4356
|
-
} catch (err) {
|
|
4357
|
-
console.log(` \u26A0\uFE0F Sync failed (checks will update when webhooks arrive): ${err}`);
|
|
4358
|
-
}
|
|
4359
|
-
}
|
|
4428
|
+
printUpdated(task2, input);
|
|
4429
|
+
if (options.prUrl) await syncAfterPrUrl(api, options.taskId);
|
|
4430
|
+
}
|
|
4431
|
+
function describeSource(task2) {
|
|
4432
|
+
const parts = [task2.sourceSystem, task2.sourceRef, task2.sourceUrl].filter(Boolean);
|
|
4433
|
+
return parts.length > 0 ? parts.join(" ") : "(cleared)";
|
|
4360
4434
|
}
|
|
4361
4435
|
async function handleSync(api, options) {
|
|
4362
4436
|
console.log(`Syncing PR review state for task ${options.taskId}...`);
|
|
@@ -4389,36 +4463,54 @@ function getCheckIcon(status) {
|
|
|
4389
4463
|
return "\u2753";
|
|
4390
4464
|
}
|
|
4391
4465
|
}
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4466
|
+
|
|
4467
|
+
// apps/cli/src/commands/handoff.ts
|
|
4468
|
+
var PR_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/\s]+\/[^/\s]+)\/pull\/([1-9]\d*)(?:[/?#]|$)/i;
|
|
4469
|
+
var PR_NUMBER = /^#?([1-9]\d*)$/;
|
|
4470
|
+
function parsePrArgument(value) {
|
|
4471
|
+
const trimmed = value.trim();
|
|
4472
|
+
const url = PR_URL.exec(trimmed);
|
|
4473
|
+
if (url) return { prUrl: trimmed, prNumber: Number(url[2]), repo: url[1] };
|
|
4474
|
+
const number = PR_NUMBER.exec(trimmed);
|
|
4475
|
+
if (number) return { prNumber: Number(number[1]) };
|
|
4476
|
+
throw new Error(`"${value}" is not a pull request: give its GitHub URL or its number`);
|
|
4477
|
+
}
|
|
4478
|
+
async function projectForRepo(api, repo) {
|
|
4479
|
+
const slug = repo ?? detectGitRepo()?.remote;
|
|
4480
|
+
if (!slug)
|
|
4481
|
+
throw new Error(
|
|
4482
|
+
"Name the task (--task) or project (--project), or run this inside the repository"
|
|
4483
|
+
);
|
|
4484
|
+
const matches = (await api.listProjects()).filter(
|
|
4485
|
+
(project2) => project2.githubRepo?.toLowerCase() === slug.toLowerCase()
|
|
4486
|
+
);
|
|
4487
|
+
if (matches.length === 1) return matches[0].id;
|
|
4488
|
+
if (!matches.length) throw new Error(`No FlightDesk project tracks ${slug}; pass --project`);
|
|
4489
|
+
throw new Error(`Several projects track ${slug}; pass --project`);
|
|
4490
|
+
}
|
|
4491
|
+
async function handoffCommand(options) {
|
|
4492
|
+
const { config, org: org2 } = requireActiveOrg();
|
|
4493
|
+
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
4494
|
+
try {
|
|
4495
|
+
const pr = parsePrArgument(options.pr);
|
|
4496
|
+
const projectId = options.task ? void 0 : options.project ?? await projectForRepo(api, pr.repo);
|
|
4497
|
+
const result = await api.handoffPullRequest({
|
|
4498
|
+
taskId: options.task,
|
|
4499
|
+
projectId,
|
|
4500
|
+
prUrl: pr.prUrl,
|
|
4501
|
+
prNumber: pr.prNumber,
|
|
4502
|
+
title: options.title
|
|
4503
|
+
});
|
|
4504
|
+
const created = result.created ? " (new task)" : "";
|
|
4505
|
+
const waiting = result.waitingOn ? ` (waiting on ${result.waitingOn})` : "";
|
|
4506
|
+
console.log(`
|
|
4507
|
+
\u2705 PR #${result.prNumber} handed off${created}`);
|
|
4508
|
+
console.log(` Task: ${result.taskId}`);
|
|
4509
|
+
console.log(` Phase: ${result.phase}${waiting}`);
|
|
4510
|
+
console.log(` ${result.message}`);
|
|
4511
|
+
} catch (error) {
|
|
4512
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4513
|
+
process.exit(1);
|
|
4422
4514
|
}
|
|
4423
4515
|
}
|
|
4424
4516
|
|
|
@@ -4426,7 +4518,7 @@ function getStatusEmoji2(status) {
|
|
|
4426
4518
|
async function promptCommand(taskId, options) {
|
|
4427
4519
|
const { config, org: org2 } = requireActiveOrg();
|
|
4428
4520
|
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
4429
|
-
const validTypes = ["review", "test_plan"
|
|
4521
|
+
const validTypes = ["review", "test_plan"];
|
|
4430
4522
|
if (!validTypes.includes(options.type)) {
|
|
4431
4523
|
console.error(`Invalid prompt type: ${options.type}`);
|
|
4432
4524
|
console.error(`Valid types: ${validTypes.join(", ")}`);
|
|
@@ -4782,11 +4874,12 @@ async function previewCommand(action, options) {
|
|
|
4782
4874
|
process.exit(1);
|
|
4783
4875
|
}
|
|
4784
4876
|
}
|
|
4785
|
-
function
|
|
4877
|
+
function getStatusEmoji(status) {
|
|
4786
4878
|
const statusEmojis = {
|
|
4787
4879
|
STARTING: "\u{1F504}",
|
|
4788
4880
|
READY: "\u2705",
|
|
4789
4881
|
SUSPENDED: "\u{1F4A4}",
|
|
4882
|
+
STOPPED: "\u{1F4A4}",
|
|
4790
4883
|
RESTARTING: "\u{1F504}",
|
|
4791
4884
|
TEARDOWN: "\u{1F5D1}\uFE0F",
|
|
4792
4885
|
ERROR: "\u274C"
|
|
@@ -4801,7 +4894,7 @@ async function handleStatus2(api, options) {
|
|
|
4801
4894
|
console.log(" or the PR may be closed.");
|
|
4802
4895
|
return;
|
|
4803
4896
|
}
|
|
4804
|
-
const emoji =
|
|
4897
|
+
const emoji = getStatusEmoji(instance.status);
|
|
4805
4898
|
console.log("\n\u{1F4E6} Preview Environment");
|
|
4806
4899
|
console.log("\u2500".repeat(50));
|
|
4807
4900
|
console.log(`${emoji} Status: ${instance.status}`);
|
|
@@ -4880,8 +4973,8 @@ async function handleMount(api, options) {
|
|
|
4880
4973
|
console.error("No preview environment found for this task");
|
|
4881
4974
|
process.exit(1);
|
|
4882
4975
|
}
|
|
4883
|
-
if (instance.status === "SUSPENDED") {
|
|
4884
|
-
console.log("\u23F8\uFE0F Instance is
|
|
4976
|
+
if (instance.status === "SUSPENDED" || instance.status === "STOPPED") {
|
|
4977
|
+
console.log("\u23F8\uFE0F Instance is stopped. Starting it...");
|
|
4885
4978
|
await api.resumeInstance(options.taskId);
|
|
4886
4979
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
4887
4980
|
}
|
|
@@ -5012,7 +5105,8 @@ var program2 = new Command();
|
|
|
5012
5105
|
registerDispatchCommands(program2);
|
|
5013
5106
|
registerQuestionCommands(program2);
|
|
5014
5107
|
registerPlanCommands(program2);
|
|
5015
|
-
program2
|
|
5108
|
+
registerWhoamiCommand(program2);
|
|
5109
|
+
program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.6.2").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
|
|
5016
5110
|
program2.hook("preAction", () => {
|
|
5017
5111
|
const opts = program2.opts();
|
|
5018
5112
|
if (opts.api) {
|
|
@@ -5025,19 +5119,29 @@ program2.hook("preAction", () => {
|
|
|
5025
5119
|
}
|
|
5026
5120
|
});
|
|
5027
5121
|
program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
|
|
5028
|
-
program2.command("register [task-id]").description(
|
|
5029
|
-
"Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)"
|
|
5030
|
-
).option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action(registerCommand);
|
|
5122
|
+
program2.command("register [task-id]").description("Register a Claude Code session with an existing task, or create a new task").option("-p, --project <id>", "Project for a new task (auto-detected from git repo if omitted)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action(registerCommand);
|
|
5031
5123
|
var project = program2.command("project").description("Project management commands");
|
|
5032
5124
|
project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
|
|
5033
5125
|
var task = program2.command("task").description("Task management commands");
|
|
5034
|
-
task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").
|
|
5126
|
+
task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").option(
|
|
5127
|
+
"--follow-up-of <taskId>",
|
|
5128
|
+
"File this as a finding from another task: something real and pre-existing that is out of that task's scope. Lands in Backlog, unassigned, and never dispatches itself."
|
|
5129
|
+
).option("--follow-up-dispatch <id>", "Your $FLIGHTDESK_DISPATCH_ID, when filing a follow-up").action((options) => taskCommand("create", options));
|
|
5035
5130
|
task.command("list").description("List tasks").option("-p, --project <id>", "Filter by project ID").option("--status <status>", "Filter by status").action((options) => taskCommand("list", options));
|
|
5036
5131
|
task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
|
|
5037
|
-
task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--pr-number <number>", "Pull request number (use if --pr-url is not enough)").option("--session <session>", "Claude Code session (URL or session ID)").
|
|
5132
|
+
task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--release-note <text>", "Confirm or revise the staff-facing release note").option("--pr-number <number>", "Pull request number (use if --pr-url is not enough)").option("--session <session>", "Claude Code session (URL or session ID)").option("--source-system <system>", "Source system: linear, notion or qalatra (humans only)").option(
|
|
5133
|
+
"--source-ref <ref>",
|
|
5134
|
+
"Source issue key, e.g. PIR-256 (must use a team key the project allows)"
|
|
5135
|
+
).option("--source-url <url>", "Source issue URL").option("--clear-source", "Clear a wrong source identity so PRs naming it stop binding here").action((taskId, options) => taskCommand("update", { taskId, ...options }));
|
|
5136
|
+
task.command("handoff").description(
|
|
5137
|
+
"Continue in FlightDesk: attach a PR you built locally and hand the work to the pipeline (CI, then QA; failures go to the agent as a fix turn)"
|
|
5138
|
+
).requiredOption("--pr <url|number>", "The pull request: its GitHub URL or its number").option("--task <id>", "The task to hand off (omit to use or create the task for the PR)").option(
|
|
5139
|
+
"-p, --project <id>",
|
|
5140
|
+
"Project for the task (detected from the PR URL or repo if omitted)"
|
|
5141
|
+
).option("--title <title>", "Title for a task the hand-off creates (defaults to the PR title)").action((options) => handoffCommand(options));
|
|
5038
5142
|
task.command("sync <task-id>").description("Sync PR review state (Copilot feedback, Claude reviews, human reviews) from GitHub").action((taskId) => taskCommand("sync", { taskId }));
|
|
5039
5143
|
program2.command("status").description("Show status of all active tasks").option("-p, --project <id>", "Filter by project").action(statusCommand);
|
|
5040
|
-
program2.command("prompt <task-id>").description("Get a prompt for a task (ready to paste into Claude)").option("--type <type>", "Prompt type: review, test_plan
|
|
5144
|
+
program2.command("prompt <task-id>").description("Get a prompt for a task (ready to paste into Claude)").option("--type <type>", "Prompt type: review, test_plan", "review").action(promptCommand);
|
|
5041
5145
|
var org = program2.command("org").description("Organization management");
|
|
5042
5146
|
org.command("list").description("List your organizations").action(orgListCommand);
|
|
5043
5147
|
org.command("switch <name>").description("Switch active organization").action(orgSwitchCommand);
|