@corners/cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +864 -139
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -1
- package/dist/prompts.d.ts +15 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +59 -0
- package/dist/prompts.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
|
-
import { basename } from "node:path";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { CornersApiClient as DefaultCornersApiClient, } from "./client.js";
|
|
6
|
-
import { ConfigStore } from "./config.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
query CliListWorkstreams {
|
|
10
|
-
workstreams(filter: { status: ACTIVE }, first: 100) {
|
|
11
|
-
edges {
|
|
12
|
-
node {
|
|
13
|
-
id
|
|
14
|
-
cornerId
|
|
15
|
-
name
|
|
16
|
-
summary
|
|
17
|
-
category
|
|
18
|
-
status
|
|
19
|
-
updatedAt
|
|
20
|
-
topic {
|
|
21
|
-
id
|
|
22
|
-
name
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
`;
|
|
6
|
+
import { ConfigStore, LOCAL_ROOT_CONFIG_RELATIVE_PATH, } from "./config.js";
|
|
7
|
+
import { createPromptApi } from "./prompts.js";
|
|
8
|
+
import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
|
|
29
9
|
const WORKSTREAM_LOOKUP_QUERY = `
|
|
30
10
|
query CliWorkstreamLookup($id: ID!) {
|
|
31
11
|
workstream(id: $id) {
|
|
@@ -308,6 +288,47 @@ const REVOKE_MCP_SESSION_MUTATION = `
|
|
|
308
288
|
revokeMCPSession(id: $id)
|
|
309
289
|
}
|
|
310
290
|
`;
|
|
291
|
+
const WHOAMI_QUERY = `
|
|
292
|
+
query CliWhoAmI {
|
|
293
|
+
me {
|
|
294
|
+
id
|
|
295
|
+
handle
|
|
296
|
+
account {
|
|
297
|
+
id
|
|
298
|
+
workspace
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
const LIST_MEMBER_CORNERS_QUERY = `
|
|
304
|
+
query CliMemberCorners {
|
|
305
|
+
memberCorners(first: 50) {
|
|
306
|
+
edges {
|
|
307
|
+
node {
|
|
308
|
+
id
|
|
309
|
+
name
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
`;
|
|
315
|
+
const CREATE_WORKSTREAM_MUTATION = `
|
|
316
|
+
mutation CliCreateWorkstream($input: CreateWorkstreamInput!) {
|
|
317
|
+
createWorkstream(input: $input) {
|
|
318
|
+
id
|
|
319
|
+
cornerId
|
|
320
|
+
name
|
|
321
|
+
summary
|
|
322
|
+
category
|
|
323
|
+
status
|
|
324
|
+
updatedAt
|
|
325
|
+
topic {
|
|
326
|
+
id
|
|
327
|
+
name
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
311
332
|
function createRuntime() {
|
|
312
333
|
return {
|
|
313
334
|
config: new ConfigStore(),
|
|
@@ -315,6 +336,7 @@ function createRuntime() {
|
|
|
315
336
|
stderr: process.stderr,
|
|
316
337
|
stdin: process.stdin,
|
|
317
338
|
cwd: process.cwd(),
|
|
339
|
+
prompts: undefined,
|
|
318
340
|
openUrl: openUrlInBrowser,
|
|
319
341
|
createClient: (input) => new DefaultCornersApiClient(input),
|
|
320
342
|
getVersion: getPackageVersion,
|
|
@@ -329,7 +351,10 @@ function printMainHelp(runtime) {
|
|
|
329
351
|
"",
|
|
330
352
|
"Commands:",
|
|
331
353
|
" auth login|logout|status",
|
|
332
|
-
"
|
|
354
|
+
" init",
|
|
355
|
+
" corner use",
|
|
356
|
+
" whoami",
|
|
357
|
+
" workstream (ws) list|use|current|create|pull|push|question|attach|reply-thread",
|
|
333
358
|
" help",
|
|
334
359
|
" version",
|
|
335
360
|
"",
|
|
@@ -342,6 +367,14 @@ function printMainHelp(runtime) {
|
|
|
342
367
|
" --no-browser",
|
|
343
368
|
].join("\n"));
|
|
344
369
|
}
|
|
370
|
+
function printInitHelp(runtime) {
|
|
371
|
+
printLine(runtime.stdout, [
|
|
372
|
+
"corners init",
|
|
373
|
+
"",
|
|
374
|
+
"Usage:",
|
|
375
|
+
" corners init [--profile <name>] [--api-url <url>] [--json]",
|
|
376
|
+
].join("\n"));
|
|
377
|
+
}
|
|
345
378
|
function printAuthHelp(runtime) {
|
|
346
379
|
printLine(runtime.stdout, [
|
|
347
380
|
"corners auth",
|
|
@@ -352,6 +385,22 @@ function printAuthHelp(runtime) {
|
|
|
352
385
|
" corners auth status [--profile <name>] [--json]",
|
|
353
386
|
].join("\n"));
|
|
354
387
|
}
|
|
388
|
+
function printCornerHelp(runtime) {
|
|
389
|
+
printLine(runtime.stdout, [
|
|
390
|
+
"corners corner",
|
|
391
|
+
"",
|
|
392
|
+
"Usage:",
|
|
393
|
+
" corners corner use <cornerNameOrId> [--path <path>] [--json]",
|
|
394
|
+
].join("\n"));
|
|
395
|
+
}
|
|
396
|
+
function printWhoamiHelp(runtime) {
|
|
397
|
+
printLine(runtime.stdout, [
|
|
398
|
+
"corners whoami",
|
|
399
|
+
"",
|
|
400
|
+
"Usage:",
|
|
401
|
+
" corners whoami [--profile <name>] [--api-url <url>] [--json]",
|
|
402
|
+
].join("\n"));
|
|
403
|
+
}
|
|
355
404
|
function printWorkstreamHelp(runtime) {
|
|
356
405
|
printLine(runtime.stdout, [
|
|
357
406
|
"corners workstream",
|
|
@@ -360,12 +409,13 @@ function printWorkstreamHelp(runtime) {
|
|
|
360
409
|
" corners workstream list [--json]",
|
|
361
410
|
" corners workstream use <workstreamId> [--json]",
|
|
362
411
|
" corners workstream current [--json]",
|
|
363
|
-
" corners workstream
|
|
364
|
-
" corners workstream
|
|
365
|
-
" corners workstream
|
|
366
|
-
" corners workstream question
|
|
412
|
+
" corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>] [--json]",
|
|
413
|
+
" corners workstream pull <workstreamId> [--json]",
|
|
414
|
+
" corners workstream push <workstreamId> [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
|
|
415
|
+
" corners workstream question list <workstreamId> [--json]",
|
|
416
|
+
" corners workstream question ask <workstreamId> [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
|
|
367
417
|
" corners workstream question answer <questionId> [--text <text>] [--json]",
|
|
368
|
-
" corners workstream attach
|
|
418
|
+
" corners workstream attach <workstreamId> --kind <kind> --entity-id <id> [--json]",
|
|
369
419
|
" corners workstream reply-thread <threadId> [--text <text>] [--json]",
|
|
370
420
|
"",
|
|
371
421
|
"Update types:",
|
|
@@ -462,6 +512,83 @@ async function requireStoredProfile(runtime, common) {
|
|
|
462
512
|
}),
|
|
463
513
|
};
|
|
464
514
|
}
|
|
515
|
+
async function fetchCurrentIdentity(client) {
|
|
516
|
+
const data = await client.graphql(WHOAMI_QUERY);
|
|
517
|
+
return {
|
|
518
|
+
userId: data.me.id,
|
|
519
|
+
handle: data.me.handle ?? null,
|
|
520
|
+
accountId: data.me.account?.id ?? null,
|
|
521
|
+
workspace: data.me.account?.workspace ?? null,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async function maybeRefreshStoredProfileIdentity(input) {
|
|
525
|
+
try {
|
|
526
|
+
const identity = await fetchCurrentIdentity(input.client);
|
|
527
|
+
const nextProfile = {
|
|
528
|
+
...input.profile,
|
|
529
|
+
userId: identity.userId,
|
|
530
|
+
handle: identity.handle,
|
|
531
|
+
accountId: identity.accountId,
|
|
532
|
+
workspace: identity.workspace,
|
|
533
|
+
};
|
|
534
|
+
if (nextProfile.userId !== input.profile.userId ||
|
|
535
|
+
nextProfile.handle !== (input.profile.handle ?? null) ||
|
|
536
|
+
nextProfile.accountId !== input.profile.accountId ||
|
|
537
|
+
nextProfile.workspace !== input.profile.workspace) {
|
|
538
|
+
await input.runtime.config.saveProfile(input.profileName, nextProfile, {
|
|
539
|
+
setActive: false,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return nextProfile;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return input.profile;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function resolveAuthStatus(runtime, common) {
|
|
549
|
+
const selected = await runtime.config.getProfile(common.profile ?? null);
|
|
550
|
+
if (!selected) {
|
|
551
|
+
return {
|
|
552
|
+
loggedIn: false,
|
|
553
|
+
profile: common.profile ?? null,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const apiUrl = common.apiUrl ?? selected.profile.apiUrl;
|
|
557
|
+
const client = runtime.createClient({
|
|
558
|
+
apiUrl,
|
|
559
|
+
accessToken: selected.profile.accessToken,
|
|
560
|
+
});
|
|
561
|
+
let remote;
|
|
562
|
+
try {
|
|
563
|
+
remote = await client.validateSession();
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
remote = {
|
|
567
|
+
valid: false,
|
|
568
|
+
error: error.message,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const profile = remote.valid
|
|
572
|
+
? await maybeRefreshStoredProfileIdentity({
|
|
573
|
+
runtime,
|
|
574
|
+
profileName: selected.name,
|
|
575
|
+
profile: selected.profile,
|
|
576
|
+
client,
|
|
577
|
+
})
|
|
578
|
+
: selected.profile;
|
|
579
|
+
return {
|
|
580
|
+
loggedIn: true,
|
|
581
|
+
profile: selected.name,
|
|
582
|
+
apiUrl,
|
|
583
|
+
sessionId: profile.sessionId,
|
|
584
|
+
workspace: profile.workspace,
|
|
585
|
+
handle: profile.handle ?? null,
|
|
586
|
+
accountId: profile.accountId,
|
|
587
|
+
userId: profile.userId,
|
|
588
|
+
remoteValid: remote.valid,
|
|
589
|
+
remoteError: remote.valid ? null : (remote.error ?? null),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
465
592
|
async function pollForDeviceToken(input) {
|
|
466
593
|
const deadline = Date.now() + input.device.expires_in * 1000;
|
|
467
594
|
let intervalMs = input.device.interval * 1000;
|
|
@@ -489,36 +616,281 @@ async function pollForDeviceToken(input) {
|
|
|
489
616
|
printLine(input.stderr, "Device code expired before authorization completed.");
|
|
490
617
|
throw new CLIError("Device code expired");
|
|
491
618
|
}
|
|
492
|
-
async function
|
|
493
|
-
|
|
494
|
-
|
|
619
|
+
async function withPrompts(runtime, run) {
|
|
620
|
+
if (runtime.prompts) {
|
|
621
|
+
return run(runtime.prompts);
|
|
622
|
+
}
|
|
623
|
+
if (runtime.stdin.isTTY === false) {
|
|
624
|
+
throw new CLIError("`corners init` requires an interactive terminal.");
|
|
625
|
+
}
|
|
626
|
+
const prompts = createPromptApi(runtime.stdin, runtime.stderr);
|
|
627
|
+
try {
|
|
628
|
+
return await run(prompts);
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
await prompts.close();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function readOptionalUtf8(path) {
|
|
635
|
+
try {
|
|
636
|
+
return await readFile(path, "utf8");
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error.code === "ENOENT") {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function ensureTrailingNewline(value) {
|
|
646
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
647
|
+
}
|
|
648
|
+
function normalizeCornerLookupValue(value) {
|
|
649
|
+
const normalized = value.trim();
|
|
650
|
+
return normalized.startsWith("#") ? normalized.slice(1) : normalized;
|
|
651
|
+
}
|
|
652
|
+
function normalizeStoredRelativePath(rootDir, targetPath) {
|
|
653
|
+
const relativePath = relative(rootDir, targetPath);
|
|
654
|
+
if (!relativePath) {
|
|
655
|
+
return ".";
|
|
656
|
+
}
|
|
657
|
+
if (relativePath === ".." ||
|
|
658
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
659
|
+
relativePath.includes(`${sep}..${sep}`)) {
|
|
660
|
+
throw new CLIError("The target path must stay within the initialized root.");
|
|
661
|
+
}
|
|
662
|
+
return relativePath.split(sep).join("/");
|
|
663
|
+
}
|
|
664
|
+
function resolvePathWithinRoot(rootDir, cwd, inputPath) {
|
|
665
|
+
const targetPath = inputPath ? resolve(cwd, inputPath) : resolve(cwd);
|
|
666
|
+
normalizeStoredRelativePath(rootDir, targetPath);
|
|
667
|
+
return targetPath;
|
|
668
|
+
}
|
|
669
|
+
function resolveCornerForRelativePath(config, relativePath) {
|
|
670
|
+
let selected = config.defaultCorner;
|
|
671
|
+
let selectedLength = 0;
|
|
672
|
+
for (const override of config.cornerOverrides) {
|
|
673
|
+
const matches = relativePath === override.path ||
|
|
674
|
+
relativePath.startsWith(`${override.path}/`);
|
|
675
|
+
if (!matches) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (override.path.length > selectedLength) {
|
|
679
|
+
selected = {
|
|
680
|
+
id: override.cornerId,
|
|
681
|
+
name: override.cornerName,
|
|
682
|
+
};
|
|
683
|
+
selectedLength = override.path.length;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return selected;
|
|
687
|
+
}
|
|
688
|
+
function resolveCornerForCwd(root, cwd) {
|
|
689
|
+
const absolutePath = resolvePathWithinRoot(root.rootDir, cwd);
|
|
690
|
+
const relativePath = normalizeStoredRelativePath(root.rootDir, absolutePath);
|
|
691
|
+
return {
|
|
692
|
+
relativePath,
|
|
693
|
+
corner: resolveCornerForRelativePath(root.config, relativePath),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function requireLocalRoot(runtime, common) {
|
|
697
|
+
const root = await runtime.config.findLocalRoot(runtime.cwd);
|
|
698
|
+
if (!root) {
|
|
699
|
+
throw new CLIError("No local Corners CLI root found. Run `corners init` from your project root first.", { json: common.json });
|
|
700
|
+
}
|
|
701
|
+
return root;
|
|
702
|
+
}
|
|
703
|
+
async function requirePinnedRootProfile(runtime, common, root) {
|
|
704
|
+
if (common.profile && common.profile !== root.config.profile) {
|
|
705
|
+
throw new CLIError(`This initialized root is pinned to profile ${root.config.profile}.`, { json: common.json });
|
|
706
|
+
}
|
|
707
|
+
const selected = await runtime.config.getProfile(root.config.profile);
|
|
708
|
+
if (!selected) {
|
|
709
|
+
throw new CLIError(`The pinned profile ${root.config.profile} is not available. Run \`corners auth login --profile ${root.config.profile}\` first.`, { json: common.json });
|
|
710
|
+
}
|
|
711
|
+
const pinnedApiUrl = normalizeApiUrl(root.config.apiUrl);
|
|
712
|
+
const requestedApiUrl = common.apiUrl ? normalizeApiUrl(common.apiUrl) : null;
|
|
713
|
+
if (requestedApiUrl && requestedApiUrl !== pinnedApiUrl) {
|
|
714
|
+
throw new CLIError(`This initialized root is pinned to API ${pinnedApiUrl}.`, { json: common.json });
|
|
715
|
+
}
|
|
716
|
+
if (root.config.workspace &&
|
|
717
|
+
selected.profile.workspace &&
|
|
718
|
+
root.config.workspace !== selected.profile.workspace) {
|
|
719
|
+
throw new CLIError(`The pinned profile ${root.config.profile} is now on workspace ${selected.profile.workspace}. Re-run \`corners init\` to refresh this root.`, { json: common.json });
|
|
720
|
+
}
|
|
721
|
+
const profile = {
|
|
722
|
+
...selected.profile,
|
|
723
|
+
apiUrl: pinnedApiUrl,
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
root,
|
|
727
|
+
profileName: selected.name,
|
|
728
|
+
profile,
|
|
729
|
+
client: runtime.createClient({
|
|
730
|
+
apiUrl: profile.apiUrl,
|
|
731
|
+
accessToken: profile.accessToken,
|
|
732
|
+
}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
async function requireCommandProfile(runtime, common) {
|
|
736
|
+
const root = await runtime.config.findLocalRoot(runtime.cwd);
|
|
737
|
+
if (root) {
|
|
738
|
+
return requirePinnedRootProfile(runtime, common, root);
|
|
739
|
+
}
|
|
740
|
+
const selected = await requireStoredProfile(runtime, common);
|
|
741
|
+
return {
|
|
742
|
+
root: null,
|
|
743
|
+
...selected,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function listMemberCorners(client) {
|
|
747
|
+
const data = await client.graphql(LIST_MEMBER_CORNERS_QUERY);
|
|
748
|
+
return data.memberCorners.edges.map((edge) => edge.node);
|
|
749
|
+
}
|
|
750
|
+
function findMemberCorner(corners, value) {
|
|
751
|
+
const normalized = normalizeCornerLookupValue(value);
|
|
752
|
+
return (corners.find((corner) => corner.id === normalized) ??
|
|
753
|
+
corners.find((corner) => corner.name.toLowerCase() === normalized.toLowerCase()) ??
|
|
754
|
+
null);
|
|
755
|
+
}
|
|
756
|
+
async function resolveMemberCorner(client, common, value) {
|
|
757
|
+
const corners = await listMemberCorners(client);
|
|
758
|
+
const corner = findMemberCorner(corners, value);
|
|
759
|
+
if (!corner) {
|
|
760
|
+
throw new CLIError("Corner not found in your joined corners.", {
|
|
761
|
+
json: common.json,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return corner;
|
|
765
|
+
}
|
|
766
|
+
async function resolveExplicitWorkstream(client, common, workstreamId) {
|
|
495
767
|
const data = await client.graphql(WORKSTREAM_LOOKUP_QUERY, { id: workstreamId });
|
|
496
768
|
if (!data.workstream) {
|
|
497
769
|
throw new CLIError("Workstream not found", { json: common.json });
|
|
498
770
|
}
|
|
499
771
|
return data.workstream;
|
|
500
772
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
throw new CLIError("This directory is not bound to a workstream. Run `corners workstream use <workstreamId>` first.");
|
|
773
|
+
function connectWorkstream(config, workstreamId) {
|
|
774
|
+
if (config.connectedWorkstreams.some((entry) => entry.id === workstreamId)) {
|
|
775
|
+
return config;
|
|
505
776
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
777
|
+
return {
|
|
778
|
+
...config,
|
|
779
|
+
connectedWorkstreams: [
|
|
780
|
+
...config.connectedWorkstreams,
|
|
781
|
+
{
|
|
782
|
+
id: workstreamId,
|
|
783
|
+
connectedAt: toIsoString(),
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
};
|
|
510
787
|
}
|
|
511
|
-
function
|
|
512
|
-
if (
|
|
788
|
+
function upsertCornerOverride(config, relativePath, corner) {
|
|
789
|
+
if (relativePath === ".") {
|
|
513
790
|
return {
|
|
514
|
-
|
|
515
|
-
|
|
791
|
+
...config,
|
|
792
|
+
defaultCorner: corner,
|
|
516
793
|
};
|
|
517
794
|
}
|
|
795
|
+
const overrides = config.cornerOverrides.filter((entry) => entry.path !== relativePath);
|
|
796
|
+
overrides.push({
|
|
797
|
+
path: relativePath,
|
|
798
|
+
cornerId: corner.id,
|
|
799
|
+
cornerName: corner.name,
|
|
800
|
+
updatedAt: toIsoString(),
|
|
801
|
+
});
|
|
802
|
+
overrides.sort((left, right) => left.path.localeCompare(right.path));
|
|
518
803
|
return {
|
|
519
|
-
|
|
804
|
+
...config,
|
|
805
|
+
cornerOverrides: overrides,
|
|
520
806
|
};
|
|
521
807
|
}
|
|
808
|
+
const GUIDANCE_SECTION_START = "<!-- corners-cli:start -->";
|
|
809
|
+
const GUIDANCE_SECTION_END = "<!-- corners-cli:end -->";
|
|
810
|
+
function buildGuidanceSection() {
|
|
811
|
+
return [
|
|
812
|
+
"## Corners CLI",
|
|
813
|
+
"",
|
|
814
|
+
"Corners CLI usage is local to each user. These shared instructions describe how AI should use the CLI, not which corners or workstreams a user has selected locally.",
|
|
815
|
+
"",
|
|
816
|
+
"- Only assume Corners CLI is initialized when `.corners/config.json` exists locally in the current repo root or an ancestor folder.",
|
|
817
|
+
"- The current folder selects the default corner through local path rules managed by `corners corner use`.",
|
|
818
|
+
"- Create new workstreams with `corners workstream create`.",
|
|
819
|
+
"- Connect existing workstreams to the local environment with `corners workstream use <workstreamId>`.",
|
|
820
|
+
"- Pass explicit workstream IDs to `corners workstream pull`, `push`, `question`, and `attach` commands.",
|
|
821
|
+
"- `corners workstream list` shows the workstreams connected for the current local environment.",
|
|
822
|
+
"",
|
|
823
|
+
"If `.corners/config.json` is absent, do not assume the repo is initialized for the current user.",
|
|
824
|
+
].join("\n");
|
|
825
|
+
}
|
|
826
|
+
function upsertManagedSection(existing, section) {
|
|
827
|
+
const block = `${GUIDANCE_SECTION_START}\n${section}\n${GUIDANCE_SECTION_END}`;
|
|
828
|
+
const startIndex = existing.indexOf(GUIDANCE_SECTION_START);
|
|
829
|
+
const endIndex = existing.indexOf(GUIDANCE_SECTION_END);
|
|
830
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
831
|
+
return ensureTrailingNewline(`${existing.slice(0, startIndex).trimEnd()}\n\n${block}\n${existing
|
|
832
|
+
.slice(endIndex + GUIDANCE_SECTION_END.length)
|
|
833
|
+
.trimStart()}`.trim());
|
|
834
|
+
}
|
|
835
|
+
const trimmed = existing.trim();
|
|
836
|
+
if (!trimmed) {
|
|
837
|
+
return ensureTrailingNewline(block);
|
|
838
|
+
}
|
|
839
|
+
return ensureTrailingNewline(`${trimmed}\n\n${block}`);
|
|
840
|
+
}
|
|
841
|
+
function buildGuidanceFileContent(target, existing) {
|
|
842
|
+
const section = buildGuidanceSection();
|
|
843
|
+
if (target === "cursor") {
|
|
844
|
+
const base = existing?.trim()
|
|
845
|
+
? existing
|
|
846
|
+
: [
|
|
847
|
+
"---",
|
|
848
|
+
"description: Corners CLI guidance",
|
|
849
|
+
"alwaysApply: true",
|
|
850
|
+
"---",
|
|
851
|
+
"",
|
|
852
|
+
].join("\n");
|
|
853
|
+
return upsertManagedSection(base, section);
|
|
854
|
+
}
|
|
855
|
+
return upsertManagedSection(existing ?? "", section);
|
|
856
|
+
}
|
|
857
|
+
async function buildPlannedEdit(path, nextContent) {
|
|
858
|
+
const existing = await readOptionalUtf8(path);
|
|
859
|
+
if (existing === nextContent) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
path,
|
|
864
|
+
action: existing === null ? "create" : "update",
|
|
865
|
+
content: nextContent,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function ensureGitignoreEntry(existing) {
|
|
869
|
+
const content = existing ?? "";
|
|
870
|
+
const lines = content
|
|
871
|
+
.split(/\r?\n/)
|
|
872
|
+
.map((line) => line.trim())
|
|
873
|
+
.filter(Boolean);
|
|
874
|
+
if (lines.includes(".corners") ||
|
|
875
|
+
lines.includes(".corners/") ||
|
|
876
|
+
lines.includes(LOCAL_ROOT_CONFIG_RELATIVE_PATH)) {
|
|
877
|
+
return ensureTrailingNewline(content);
|
|
878
|
+
}
|
|
879
|
+
const trimmed = content.trimEnd();
|
|
880
|
+
if (!trimmed) {
|
|
881
|
+
return `${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
|
|
882
|
+
}
|
|
883
|
+
return `${trimmed}\n${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
|
|
884
|
+
}
|
|
885
|
+
async function writePlannedEdits(edits) {
|
|
886
|
+
for (const edit of edits) {
|
|
887
|
+
await mkdir(dirname(edit.path), { recursive: true });
|
|
888
|
+
await writeFile(edit.path, edit.content, "utf8");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function formatWorkstreamLine(workstream) {
|
|
892
|
+
return `${workstream.id} ${workstream.name} ${workstream.status.toLowerCase()}${workstream.summary ? ` ${workstream.summary}` : ""}`;
|
|
893
|
+
}
|
|
522
894
|
async function handleAuth(args, runtime, inherited) {
|
|
523
895
|
const subcommand = args[0];
|
|
524
896
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
@@ -579,15 +951,32 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
579
951
|
if (!token.access_token) {
|
|
580
952
|
throw new CLIError("Login completed without an access token");
|
|
581
953
|
}
|
|
582
|
-
|
|
954
|
+
let profile = {
|
|
583
955
|
apiUrl: client.apiUrl,
|
|
584
956
|
accessToken: token.access_token,
|
|
585
957
|
sessionId: token.session_id ?? null,
|
|
586
958
|
workspace: token.workspace ?? null,
|
|
587
959
|
accountId: token.account_id ?? null,
|
|
588
960
|
userId: token.user_id ?? null,
|
|
961
|
+
handle: null,
|
|
589
962
|
createdAt: toIsoString(),
|
|
590
963
|
};
|
|
964
|
+
try {
|
|
965
|
+
const identity = await fetchCurrentIdentity(runtime.createClient({
|
|
966
|
+
apiUrl: profile.apiUrl,
|
|
967
|
+
accessToken: profile.accessToken,
|
|
968
|
+
}));
|
|
969
|
+
profile = {
|
|
970
|
+
...profile,
|
|
971
|
+
workspace: identity.workspace,
|
|
972
|
+
accountId: identity.accountId,
|
|
973
|
+
userId: identity.userId,
|
|
974
|
+
handle: identity.handle,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
// Best-effort profile enrichment. Authentication already succeeded.
|
|
979
|
+
}
|
|
591
980
|
await runtime.config.saveProfile(profileName, profile, {
|
|
592
981
|
setActive: true,
|
|
593
982
|
});
|
|
@@ -598,6 +987,7 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
598
987
|
apiUrl: profile.apiUrl,
|
|
599
988
|
sessionId: profile.sessionId,
|
|
600
989
|
workspace: profile.workspace,
|
|
990
|
+
handle: profile.handle,
|
|
601
991
|
accountId: profile.accountId,
|
|
602
992
|
userId: profile.userId,
|
|
603
993
|
});
|
|
@@ -693,12 +1083,8 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
693
1083
|
printAuthHelp(runtime);
|
|
694
1084
|
return 0;
|
|
695
1085
|
}
|
|
696
|
-
const
|
|
697
|
-
if (!
|
|
698
|
-
const status = {
|
|
699
|
-
loggedIn: false,
|
|
700
|
-
profile: common.profile ?? null,
|
|
701
|
-
};
|
|
1086
|
+
const status = await resolveAuthStatus(runtime, common);
|
|
1087
|
+
if (!status.loggedIn) {
|
|
702
1088
|
if (common.json) {
|
|
703
1089
|
printJson(runtime.stdout, status);
|
|
704
1090
|
}
|
|
@@ -707,31 +1093,6 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
707
1093
|
}
|
|
708
1094
|
return 0;
|
|
709
1095
|
}
|
|
710
|
-
const client = runtime.createClient({
|
|
711
|
-
apiUrl: common.apiUrl ?? selected.profile.apiUrl,
|
|
712
|
-
accessToken: selected.profile.accessToken,
|
|
713
|
-
});
|
|
714
|
-
let remote;
|
|
715
|
-
try {
|
|
716
|
-
remote = await client.validateSession();
|
|
717
|
-
}
|
|
718
|
-
catch (error) {
|
|
719
|
-
remote = {
|
|
720
|
-
valid: false,
|
|
721
|
-
error: error.message,
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
const status = {
|
|
725
|
-
loggedIn: true,
|
|
726
|
-
profile: selected.name,
|
|
727
|
-
apiUrl: common.apiUrl ?? selected.profile.apiUrl,
|
|
728
|
-
sessionId: selected.profile.sessionId,
|
|
729
|
-
workspace: selected.profile.workspace,
|
|
730
|
-
accountId: selected.profile.accountId,
|
|
731
|
-
userId: selected.profile.userId,
|
|
732
|
-
remoteValid: remote.valid,
|
|
733
|
-
remoteError: remote.valid ? null : (remote.error ?? null),
|
|
734
|
-
};
|
|
735
1096
|
if (common.json) {
|
|
736
1097
|
printJson(runtime.stdout, status);
|
|
737
1098
|
}
|
|
@@ -740,6 +1101,7 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
740
1101
|
`Profile: ${status.profile}`,
|
|
741
1102
|
`API URL: ${status.apiUrl}`,
|
|
742
1103
|
`Workspace: ${status.workspace ?? "-"}`,
|
|
1104
|
+
`Handle: ${status.handle ? `@${status.handle}` : "-"}`,
|
|
743
1105
|
`Account: ${status.accountId ?? "-"}`,
|
|
744
1106
|
`User: ${status.userId ?? "-"}`,
|
|
745
1107
|
`Session: ${status.sessionId ?? "-"}`,
|
|
@@ -752,6 +1114,259 @@ async function handleAuth(args, runtime, inherited) {
|
|
|
752
1114
|
throw new CLIError(`Unknown auth command: ${subcommand}`);
|
|
753
1115
|
}
|
|
754
1116
|
}
|
|
1117
|
+
async function handleWhoAmI(args, runtime, inherited) {
|
|
1118
|
+
const parsed = parseArgs({
|
|
1119
|
+
args,
|
|
1120
|
+
allowPositionals: false,
|
|
1121
|
+
options: {
|
|
1122
|
+
json: { type: "boolean" },
|
|
1123
|
+
help: { type: "boolean", short: "h" },
|
|
1124
|
+
profile: { type: "string" },
|
|
1125
|
+
"api-url": { type: "string" },
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
const common = mergeCommonOptions(inherited, {
|
|
1129
|
+
json: parsed.values.json,
|
|
1130
|
+
profile: parsed.values.profile,
|
|
1131
|
+
apiUrl: parsed.values["api-url"],
|
|
1132
|
+
});
|
|
1133
|
+
if (parsed.values.help) {
|
|
1134
|
+
printWhoamiHelp(runtime);
|
|
1135
|
+
return 0;
|
|
1136
|
+
}
|
|
1137
|
+
const status = await resolveAuthStatus(runtime, common);
|
|
1138
|
+
if (!status.loggedIn) {
|
|
1139
|
+
if (common.json) {
|
|
1140
|
+
printJson(runtime.stdout, status);
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
printLine(runtime.stdout, "Not logged in.");
|
|
1144
|
+
}
|
|
1145
|
+
return 0;
|
|
1146
|
+
}
|
|
1147
|
+
if (common.json) {
|
|
1148
|
+
printJson(runtime.stdout, status);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
const identity = status.handle
|
|
1152
|
+
? `@${status.handle}`
|
|
1153
|
+
: (status.userId ?? status.profile);
|
|
1154
|
+
const workspaceSuffix = status.workspace ? ` (${status.workspace})` : "";
|
|
1155
|
+
printLine(runtime.stdout, `${identity}${workspaceSuffix}`);
|
|
1156
|
+
}
|
|
1157
|
+
return 0;
|
|
1158
|
+
}
|
|
1159
|
+
async function handleInit(args, runtime, inherited) {
|
|
1160
|
+
const parsed = parseArgs({
|
|
1161
|
+
args,
|
|
1162
|
+
allowPositionals: false,
|
|
1163
|
+
options: {
|
|
1164
|
+
json: { type: "boolean" },
|
|
1165
|
+
help: { type: "boolean", short: "h" },
|
|
1166
|
+
profile: { type: "string" },
|
|
1167
|
+
"api-url": { type: "string" },
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
const common = mergeCommonOptions(inherited, {
|
|
1171
|
+
json: parsed.values.json,
|
|
1172
|
+
profile: parsed.values.profile,
|
|
1173
|
+
apiUrl: parsed.values["api-url"],
|
|
1174
|
+
});
|
|
1175
|
+
if (parsed.values.help) {
|
|
1176
|
+
printInitHelp(runtime);
|
|
1177
|
+
return 0;
|
|
1178
|
+
}
|
|
1179
|
+
const currentRootPath = await realpath(runtime.cwd);
|
|
1180
|
+
const existingRoot = await runtime.config.findLocalRoot(runtime.cwd);
|
|
1181
|
+
if (existingRoot && existingRoot.rootDir !== currentRootPath) {
|
|
1182
|
+
throw new CLIError(`This directory is already inside initialized root ${existingRoot.rootDir}. Run \`corners init\` there.`, { json: common.json });
|
|
1183
|
+
}
|
|
1184
|
+
const { profileName, profile, client } = await requireStoredProfile(runtime, common);
|
|
1185
|
+
const joinedCorners = await listMemberCorners(client);
|
|
1186
|
+
if (joinedCorners.length === 0) {
|
|
1187
|
+
throw new CLIError("No joined corners are available for this profile.", {
|
|
1188
|
+
json: common.json,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
const rootDir = existingRoot?.rootDir ?? currentRootPath;
|
|
1192
|
+
const existingConfig = existingRoot?.config ?? null;
|
|
1193
|
+
return withPrompts(runtime, async (prompts) => {
|
|
1194
|
+
const guidanceTargets = [
|
|
1195
|
+
{
|
|
1196
|
+
key: "agents",
|
|
1197
|
+
label: "Manage AGENTS.md guidance",
|
|
1198
|
+
relativePath: "AGENTS.md",
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
key: "claude",
|
|
1202
|
+
label: "Manage CLAUDE.md guidance",
|
|
1203
|
+
relativePath: "CLAUDE.md",
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
key: "cursor",
|
|
1207
|
+
label: "Manage Cursor rule guidance",
|
|
1208
|
+
relativePath: ".cursor/rules/corners-cli.mdc",
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
key: "copilot",
|
|
1212
|
+
label: "Manage Copilot instructions",
|
|
1213
|
+
relativePath: ".github/copilot-instructions.md",
|
|
1214
|
+
},
|
|
1215
|
+
];
|
|
1216
|
+
const selectedTargets = [];
|
|
1217
|
+
for (const target of guidanceTargets) {
|
|
1218
|
+
const include = await prompts.confirm(target.label, {
|
|
1219
|
+
defaultValue: true,
|
|
1220
|
+
});
|
|
1221
|
+
if (include) {
|
|
1222
|
+
selectedTargets.push(target);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const selectedCornerId = await prompts.select("Choose the default corner for this local root.", joinedCorners.map((corner) => ({
|
|
1226
|
+
label: `${corner.name} (${corner.id})`,
|
|
1227
|
+
value: corner.id,
|
|
1228
|
+
})), {
|
|
1229
|
+
defaultValue: existingConfig?.defaultCorner.id ?? joinedCorners[0]?.id,
|
|
1230
|
+
});
|
|
1231
|
+
const defaultCorner = joinedCorners.find((corner) => corner.id === selectedCornerId) ??
|
|
1232
|
+
joinedCorners[0];
|
|
1233
|
+
const nextLocalConfig = {
|
|
1234
|
+
version: 1,
|
|
1235
|
+
profile: profileName,
|
|
1236
|
+
workspace: profile.workspace,
|
|
1237
|
+
apiUrl: profile.apiUrl,
|
|
1238
|
+
defaultCorner: {
|
|
1239
|
+
id: defaultCorner.id,
|
|
1240
|
+
name: defaultCorner.name,
|
|
1241
|
+
},
|
|
1242
|
+
cornerOverrides: existingConfig?.cornerOverrides ?? [],
|
|
1243
|
+
connectedWorkstreams: existingConfig?.connectedWorkstreams ?? [],
|
|
1244
|
+
};
|
|
1245
|
+
const plannedEdits = [];
|
|
1246
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
1247
|
+
const gitignoreContent = ensureGitignoreEntry(await readOptionalUtf8(gitignorePath));
|
|
1248
|
+
const gitignoreEdit = await buildPlannedEdit(gitignorePath, gitignoreContent);
|
|
1249
|
+
if (gitignoreEdit) {
|
|
1250
|
+
plannedEdits.push(gitignoreEdit);
|
|
1251
|
+
}
|
|
1252
|
+
const localConfigPath = runtime.config.getLocalConfigPath(rootDir);
|
|
1253
|
+
const localConfigEdit = await buildPlannedEdit(localConfigPath, `${JSON.stringify(nextLocalConfig, null, 2)}\n`);
|
|
1254
|
+
if (localConfigEdit) {
|
|
1255
|
+
plannedEdits.push(localConfigEdit);
|
|
1256
|
+
}
|
|
1257
|
+
for (const target of selectedTargets) {
|
|
1258
|
+
const targetPath = join(rootDir, target.relativePath);
|
|
1259
|
+
const nextContent = buildGuidanceFileContent(target.key, await readOptionalUtf8(targetPath));
|
|
1260
|
+
const edit = await buildPlannedEdit(targetPath, nextContent);
|
|
1261
|
+
if (edit) {
|
|
1262
|
+
plannedEdits.push(edit);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (plannedEdits.length > 0 && !common.json) {
|
|
1266
|
+
printLine(runtime.stderr, [
|
|
1267
|
+
"Planned changes:",
|
|
1268
|
+
...plannedEdits.map((edit) => ` ${edit.action} ${relative(rootDir, edit.path) || "."}`),
|
|
1269
|
+
].join("\n"));
|
|
1270
|
+
}
|
|
1271
|
+
if (plannedEdits.length > 0) {
|
|
1272
|
+
const confirmed = await prompts.confirm("Proceed with these changes?", {
|
|
1273
|
+
defaultValue: true,
|
|
1274
|
+
});
|
|
1275
|
+
if (!confirmed) {
|
|
1276
|
+
if (common.json) {
|
|
1277
|
+
printJson(runtime.stdout, {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
root: rootDir,
|
|
1280
|
+
aborted: true,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
printLine(runtime.stdout, "Aborted. No files were changed.");
|
|
1285
|
+
}
|
|
1286
|
+
return 0;
|
|
1287
|
+
}
|
|
1288
|
+
await writePlannedEdits(plannedEdits);
|
|
1289
|
+
}
|
|
1290
|
+
const payload = {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
root: rootDir,
|
|
1293
|
+
profile: profileName,
|
|
1294
|
+
workspace: profile.workspace,
|
|
1295
|
+
defaultCorner,
|
|
1296
|
+
files: plannedEdits.map((edit) => ({
|
|
1297
|
+
action: edit.action,
|
|
1298
|
+
path: relative(rootDir, edit.path) || ".",
|
|
1299
|
+
})),
|
|
1300
|
+
};
|
|
1301
|
+
if (common.json) {
|
|
1302
|
+
printJson(runtime.stdout, payload);
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}.`);
|
|
1306
|
+
}
|
|
1307
|
+
return 0;
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
async function handleCorner(args, runtime, inherited) {
|
|
1311
|
+
const subcommand = args[0];
|
|
1312
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
1313
|
+
printCornerHelp(runtime);
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
switch (subcommand) {
|
|
1317
|
+
case "use": {
|
|
1318
|
+
const parsed = parseArgs({
|
|
1319
|
+
args: args.slice(1),
|
|
1320
|
+
allowPositionals: true,
|
|
1321
|
+
options: {
|
|
1322
|
+
json: { type: "boolean" },
|
|
1323
|
+
help: { type: "boolean", short: "h" },
|
|
1324
|
+
profile: { type: "string" },
|
|
1325
|
+
"api-url": { type: "string" },
|
|
1326
|
+
path: { type: "string" },
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
const common = mergeCommonOptions(inherited, {
|
|
1330
|
+
json: parsed.values.json,
|
|
1331
|
+
profile: parsed.values.profile,
|
|
1332
|
+
apiUrl: parsed.values["api-url"],
|
|
1333
|
+
});
|
|
1334
|
+
if (parsed.values.help) {
|
|
1335
|
+
printCornerHelp(runtime);
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
const cornerNameOrId = parsed.positionals[0];
|
|
1339
|
+
if (!cornerNameOrId) {
|
|
1340
|
+
throw new CLIError("Usage: corners corner use <cornerNameOrId> [--path <path>]", { json: common.json });
|
|
1341
|
+
}
|
|
1342
|
+
const root = await requireLocalRoot(runtime, common);
|
|
1343
|
+
const { client } = await requirePinnedRootProfile(runtime, common, root);
|
|
1344
|
+
const corner = await resolveMemberCorner(client, common, cornerNameOrId);
|
|
1345
|
+
const cwdPath = await realpath(runtime.cwd);
|
|
1346
|
+
const targetAbsolutePath = resolvePathWithinRoot(root.rootDir, cwdPath, parsed.values.path);
|
|
1347
|
+
const relativePath = normalizeStoredRelativePath(root.rootDir, targetAbsolutePath);
|
|
1348
|
+
const nextConfig = upsertCornerOverride(root.config, relativePath, {
|
|
1349
|
+
id: corner.id,
|
|
1350
|
+
name: corner.name,
|
|
1351
|
+
});
|
|
1352
|
+
await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
|
|
1353
|
+
if (common.json) {
|
|
1354
|
+
printJson(runtime.stdout, {
|
|
1355
|
+
ok: true,
|
|
1356
|
+
root: root.rootDir,
|
|
1357
|
+
path: relativePath,
|
|
1358
|
+
corner,
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
printLine(runtime.stdout, `Set default corner for ${relativePath} to ${corner.name}.`);
|
|
1363
|
+
}
|
|
1364
|
+
return 0;
|
|
1365
|
+
}
|
|
1366
|
+
default:
|
|
1367
|
+
throw new CLIError(`Unknown corner command: ${subcommand}`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
755
1370
|
async function handleWorkstream(args, runtime, inherited) {
|
|
756
1371
|
const subcommand = args[0];
|
|
757
1372
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
@@ -779,16 +1394,44 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
779
1394
|
printWorkstreamHelp(runtime);
|
|
780
1395
|
return 0;
|
|
781
1396
|
}
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
-
const
|
|
1397
|
+
const root = await requireLocalRoot(runtime, common);
|
|
1398
|
+
const { client } = await requirePinnedRootProfile(runtime, common, root);
|
|
1399
|
+
const hydrated = await Promise.all(root.config.connectedWorkstreams.map(async (entry) => {
|
|
1400
|
+
try {
|
|
1401
|
+
return {
|
|
1402
|
+
entry,
|
|
1403
|
+
workstream: await resolveExplicitWorkstream(client, common, entry.id),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
return {
|
|
1408
|
+
entry,
|
|
1409
|
+
workstream: null,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
}));
|
|
1413
|
+
const workstreams = hydrated
|
|
1414
|
+
.filter((entry) => entry.workstream !== null)
|
|
1415
|
+
.map((entry) => entry.workstream);
|
|
1416
|
+
const missingWorkstreamIds = hydrated
|
|
1417
|
+
.filter((entry) => entry.workstream === null)
|
|
1418
|
+
.map((entry) => entry.entry.id);
|
|
785
1419
|
if (common.json) {
|
|
786
|
-
printJson(runtime.stdout,
|
|
1420
|
+
printJson(runtime.stdout, {
|
|
1421
|
+
root: root.rootDir,
|
|
1422
|
+
workstreams,
|
|
1423
|
+
missingWorkstreamIds,
|
|
1424
|
+
});
|
|
787
1425
|
}
|
|
788
1426
|
else {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1427
|
+
if (missingWorkstreamIds.length > 0) {
|
|
1428
|
+
printLine(runtime.stderr, missingWorkstreamIds
|
|
1429
|
+
.map((workstreamId) => `Warning: connected workstream ${workstreamId} is missing or inaccessible.`)
|
|
1430
|
+
.join("\n"));
|
|
1431
|
+
}
|
|
1432
|
+
printLine(runtime.stdout, workstreams.length > 0
|
|
1433
|
+
? workstreams.map(formatWorkstreamLine).join("\n")
|
|
1434
|
+
: "No workstreams are connected to this local environment.");
|
|
792
1435
|
}
|
|
793
1436
|
return 0;
|
|
794
1437
|
}
|
|
@@ -818,26 +1461,22 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
818
1461
|
json: common.json,
|
|
819
1462
|
});
|
|
820
1463
|
}
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
await
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
boundAt: toIsoString(),
|
|
829
|
-
});
|
|
1464
|
+
const root = await requireLocalRoot(runtime, common);
|
|
1465
|
+
const { client } = await requirePinnedRootProfile(runtime, common, root);
|
|
1466
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
1467
|
+
const nextConfig = connectWorkstream(root.config, workstream.id);
|
|
1468
|
+
if (nextConfig !== root.config) {
|
|
1469
|
+
await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
|
|
1470
|
+
}
|
|
830
1471
|
if (common.json) {
|
|
831
1472
|
printJson(runtime.stdout, {
|
|
832
1473
|
ok: true,
|
|
833
|
-
|
|
834
|
-
profile: profileName,
|
|
835
|
-
workspace: profile.workspace,
|
|
1474
|
+
root: root.rootDir,
|
|
836
1475
|
workstream,
|
|
837
1476
|
});
|
|
838
1477
|
}
|
|
839
1478
|
else {
|
|
840
|
-
printLine(runtime.stdout, `
|
|
1479
|
+
printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir}.`);
|
|
841
1480
|
}
|
|
842
1481
|
return 0;
|
|
843
1482
|
}
|
|
@@ -857,30 +1496,85 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
857
1496
|
printWorkstreamHelp(runtime);
|
|
858
1497
|
return 0;
|
|
859
1498
|
}
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1499
|
+
const root = await requireLocalRoot(runtime, common);
|
|
1500
|
+
const cwdPath = await realpath(runtime.cwd);
|
|
1501
|
+
const resolvedCorner = resolveCornerForCwd(root, cwdPath);
|
|
1502
|
+
const payload = {
|
|
1503
|
+
cwd: runtime.cwd,
|
|
1504
|
+
root: root.rootDir,
|
|
1505
|
+
profile: root.config.profile,
|
|
1506
|
+
workspace: root.config.workspace,
|
|
1507
|
+
resolvedCorner,
|
|
1508
|
+
connectedWorkstreamIds: root.config.connectedWorkstreams.map((entry) => entry.id),
|
|
1509
|
+
};
|
|
1510
|
+
if (common.json) {
|
|
1511
|
+
printJson(runtime.stdout, payload);
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
printLine(runtime.stdout, [
|
|
1515
|
+
`Root: ${payload.root}`,
|
|
1516
|
+
`Directory: ${payload.cwd}`,
|
|
1517
|
+
`Profile: ${payload.profile}`,
|
|
1518
|
+
`Workspace: ${payload.workspace ?? "-"}`,
|
|
1519
|
+
`Resolved corner: ${payload.resolvedCorner.corner.name} (${payload.resolvedCorner.corner.id})`,
|
|
1520
|
+
`Connected workstreams: ${payload.connectedWorkstreamIds.length}`,
|
|
1521
|
+
].join("\n"));
|
|
1522
|
+
}
|
|
1523
|
+
return 0;
|
|
1524
|
+
}
|
|
1525
|
+
case "create": {
|
|
1526
|
+
const parsed = parseArgs({
|
|
1527
|
+
args: args.slice(1),
|
|
1528
|
+
allowPositionals: true,
|
|
1529
|
+
options: {
|
|
1530
|
+
json: { type: "boolean" },
|
|
1531
|
+
help: { type: "boolean", short: "h" },
|
|
1532
|
+
profile: { type: "string" },
|
|
1533
|
+
"api-url": { type: "string" },
|
|
1534
|
+
summary: { type: "string" },
|
|
1535
|
+
corner: { type: "string" },
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
const common = mergeCommonOptions(inherited, {
|
|
1539
|
+
json: parsed.values.json,
|
|
1540
|
+
profile: parsed.values.profile,
|
|
1541
|
+
apiUrl: parsed.values["api-url"],
|
|
1542
|
+
});
|
|
1543
|
+
if (parsed.values.help) {
|
|
1544
|
+
printWorkstreamHelp(runtime);
|
|
868
1545
|
return 0;
|
|
869
1546
|
}
|
|
1547
|
+
const name = parsed.positionals.join(" ").trim();
|
|
1548
|
+
if (!name) {
|
|
1549
|
+
throw new CLIError("Usage: corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>]", { json: common.json });
|
|
1550
|
+
}
|
|
1551
|
+
const root = await requireLocalRoot(runtime, common);
|
|
1552
|
+
const { client } = await requirePinnedRootProfile(runtime, common, root);
|
|
1553
|
+
const cwdPath = await realpath(runtime.cwd);
|
|
1554
|
+
const corner = parsed.values.corner
|
|
1555
|
+
? await resolveMemberCorner(client, common, parsed.values.corner)
|
|
1556
|
+
: resolveCornerForCwd(root, cwdPath).corner;
|
|
1557
|
+
const result = await client.graphql(CREATE_WORKSTREAM_MUTATION, {
|
|
1558
|
+
input: {
|
|
1559
|
+
cornerId: corner.id,
|
|
1560
|
+
name,
|
|
1561
|
+
summary: parsed.values.summary,
|
|
1562
|
+
},
|
|
1563
|
+
});
|
|
1564
|
+
const nextConfig = connectWorkstream(root.config, result.createWorkstream.id);
|
|
1565
|
+
if (nextConfig !== root.config) {
|
|
1566
|
+
await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
|
|
1567
|
+
}
|
|
870
1568
|
if (common.json) {
|
|
871
1569
|
printJson(runtime.stdout, {
|
|
872
|
-
|
|
873
|
-
|
|
1570
|
+
ok: true,
|
|
1571
|
+
root: root.rootDir,
|
|
1572
|
+
corner,
|
|
1573
|
+
workstream: result.createWorkstream,
|
|
874
1574
|
});
|
|
875
1575
|
}
|
|
876
1576
|
else {
|
|
877
|
-
printLine(runtime.stdout,
|
|
878
|
-
`Directory: ${runtime.cwd}`,
|
|
879
|
-
`Profile: ${binding.profile ?? "-"}`,
|
|
880
|
-
`Workspace: ${binding.workspace || "-"}`,
|
|
881
|
-
`Workstream: ${binding.workstreamId}`,
|
|
882
|
-
`Corner: ${binding.cornerId}`,
|
|
883
|
-
].join("\n"));
|
|
1577
|
+
printLine(runtime.stdout, `Created ${result.createWorkstream.id} (${result.createWorkstream.name}) in ${corner.name}.`);
|
|
884
1578
|
}
|
|
885
1579
|
return 0;
|
|
886
1580
|
}
|
|
@@ -904,9 +1598,14 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
904
1598
|
printWorkstreamHelp(runtime);
|
|
905
1599
|
return 0;
|
|
906
1600
|
}
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
1601
|
+
const workstreamId = parsed.positionals[0];
|
|
1602
|
+
if (!workstreamId) {
|
|
1603
|
+
throw new CLIError("Usage: corners workstream pull <workstreamId>", {
|
|
1604
|
+
json: common.json,
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1608
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
910
1609
|
const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
|
|
911
1610
|
id: workstream.id,
|
|
912
1611
|
attachmentsFirst: 50,
|
|
@@ -953,16 +1652,21 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
953
1652
|
printWorkstreamHelp(runtime);
|
|
954
1653
|
return 0;
|
|
955
1654
|
}
|
|
956
|
-
const
|
|
1655
|
+
const workstreamId = parsed.positionals[0];
|
|
1656
|
+
if (!workstreamId) {
|
|
1657
|
+
throw new CLIError("Usage: corners workstream push <workstreamId>", {
|
|
1658
|
+
json: common.json,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
957
1661
|
const message = parsed.values.message ??
|
|
958
|
-
(
|
|
959
|
-
?
|
|
1662
|
+
(parsed.positionals.length > 1
|
|
1663
|
+
? parsed.positionals.slice(1).join(" ")
|
|
960
1664
|
: await readTextFromStdin(runtime.stdin));
|
|
961
1665
|
if (!message) {
|
|
962
1666
|
throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
|
|
963
1667
|
}
|
|
964
|
-
const {
|
|
965
|
-
const workstream = await
|
|
1668
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1669
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
966
1670
|
let createdDocument = null;
|
|
967
1671
|
if (parsed.values.file) {
|
|
968
1672
|
const documentContent = await readFile(parsed.values.file, "utf8");
|
|
@@ -1026,9 +1730,12 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
1026
1730
|
printWorkstreamHelp(runtime);
|
|
1027
1731
|
return 0;
|
|
1028
1732
|
}
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1733
|
+
const workstreamId = parsed.positionals[0];
|
|
1734
|
+
if (!workstreamId) {
|
|
1735
|
+
throw new CLIError("Usage: corners workstream question list <workstreamId>", { json: common.json });
|
|
1736
|
+
}
|
|
1737
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1738
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
1032
1739
|
const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
|
|
1033
1740
|
id: workstream.id,
|
|
1034
1741
|
});
|
|
@@ -1071,16 +1778,19 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
1071
1778
|
printWorkstreamHelp(runtime);
|
|
1072
1779
|
return 0;
|
|
1073
1780
|
}
|
|
1074
|
-
const
|
|
1781
|
+
const workstreamId = parsed.positionals[0];
|
|
1782
|
+
if (!workstreamId) {
|
|
1783
|
+
throw new CLIError("Usage: corners workstream question ask <workstreamId> [--question <text>]", { json: common.json });
|
|
1784
|
+
}
|
|
1075
1785
|
const question = parsed.values.question ??
|
|
1076
|
-
(
|
|
1077
|
-
?
|
|
1786
|
+
(parsed.positionals.length > 1
|
|
1787
|
+
? parsed.positionals.slice(1).join(" ")
|
|
1078
1788
|
: await readTextFromStdin(runtime.stdin));
|
|
1079
1789
|
if (!question) {
|
|
1080
1790
|
throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
|
|
1081
1791
|
}
|
|
1082
|
-
const {
|
|
1083
|
-
const workstream = await
|
|
1792
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1793
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
1084
1794
|
const result = await client.graphql(CREATE_WORKSTREAM_QUESTION_MUTATION, {
|
|
1085
1795
|
input: {
|
|
1086
1796
|
workstreamId: workstream.id,
|
|
@@ -1131,7 +1841,7 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
1131
1841
|
json: common.json,
|
|
1132
1842
|
});
|
|
1133
1843
|
}
|
|
1134
|
-
const { client } = await
|
|
1844
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1135
1845
|
const result = await client.graphql(ANSWER_WORKSTREAM_QUESTION_MUTATION, {
|
|
1136
1846
|
questionId,
|
|
1137
1847
|
answerText: answer,
|
|
@@ -1170,14 +1880,14 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
1170
1880
|
printWorkstreamHelp(runtime);
|
|
1171
1881
|
return 0;
|
|
1172
1882
|
}
|
|
1173
|
-
const
|
|
1883
|
+
const workstreamId = parsed.positionals[0];
|
|
1174
1884
|
const kind = parsed.values.kind;
|
|
1175
1885
|
const entityId = parsed.values["entity-id"];
|
|
1176
|
-
if (!kind || !entityId) {
|
|
1177
|
-
throw new CLIError("Usage: corners workstream attach
|
|
1886
|
+
if (!workstreamId || !kind || !entityId) {
|
|
1887
|
+
throw new CLIError("Usage: corners workstream attach <workstreamId> --kind <kind> --entity-id <id>", { json: common.json });
|
|
1178
1888
|
}
|
|
1179
|
-
const {
|
|
1180
|
-
const workstream = await
|
|
1889
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1890
|
+
const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
|
|
1181
1891
|
const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
|
|
1182
1892
|
workstreamId: workstream.id,
|
|
1183
1893
|
kind: toGraphQLAttachmentKind(kind),
|
|
@@ -1220,7 +1930,7 @@ async function handleWorkstream(args, runtime, inherited) {
|
|
|
1220
1930
|
if (!threadId || !text) {
|
|
1221
1931
|
throw new CLIError("Usage: corners workstream reply-thread <threadId> [--text <text>]", { json: common.json });
|
|
1222
1932
|
}
|
|
1223
|
-
const { client } = await
|
|
1933
|
+
const { client } = await requireCommandProfile(runtime, common);
|
|
1224
1934
|
const result = await client.graphql(REPLY_ARTIFACT_THREAD_MUTATION, {
|
|
1225
1935
|
id: threadId,
|
|
1226
1936
|
input: {
|
|
@@ -1260,6 +1970,15 @@ export async function runCli(argv, runtime = createRuntime()) {
|
|
|
1260
1970
|
if (parsed.rest[1] === "auth") {
|
|
1261
1971
|
printAuthHelp(runtime);
|
|
1262
1972
|
}
|
|
1973
|
+
else if (parsed.rest[1] === "init") {
|
|
1974
|
+
printInitHelp(runtime);
|
|
1975
|
+
}
|
|
1976
|
+
else if (parsed.rest[1] === "corner") {
|
|
1977
|
+
printCornerHelp(runtime);
|
|
1978
|
+
}
|
|
1979
|
+
else if (parsed.rest[1] === "whoami") {
|
|
1980
|
+
printWhoamiHelp(runtime);
|
|
1981
|
+
}
|
|
1263
1982
|
else if (parsed.rest[1] === "workstream" || parsed.rest[1] === "ws") {
|
|
1264
1983
|
printWorkstreamHelp(runtime);
|
|
1265
1984
|
}
|
|
@@ -1270,8 +1989,14 @@ export async function runCli(argv, runtime = createRuntime()) {
|
|
|
1270
1989
|
case "version":
|
|
1271
1990
|
printLine(runtime.stdout, runtime.getVersion());
|
|
1272
1991
|
return 0;
|
|
1992
|
+
case "init":
|
|
1993
|
+
return handleInit(parsed.rest.slice(1), runtime, parsed.common);
|
|
1273
1994
|
case "auth":
|
|
1274
1995
|
return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
|
|
1996
|
+
case "corner":
|
|
1997
|
+
return handleCorner(parsed.rest.slice(1), runtime, parsed.common);
|
|
1998
|
+
case "whoami":
|
|
1999
|
+
return handleWhoAmI(parsed.rest.slice(1), runtime, parsed.common);
|
|
1275
2000
|
case "workstream":
|
|
1276
2001
|
case "ws":
|
|
1277
2002
|
return handleWorkstream(parsed.rest.slice(1), runtime, parsed.common);
|