@corners/cli 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,31 +1,12 @@
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 { CLIError, getPackageVersion, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
8
- const LIST_WORKSTREAMS_QUERY = `
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 { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_SYNC_COMMAND, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
8
+ import { createPromptApi } from "./prompts.js";
9
+ import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
29
10
  const WORKSTREAM_LOOKUP_QUERY = `
30
11
  query CliWorkstreamLookup($id: ID!) {
31
12
  workstream(id: $id) {
@@ -308,6 +289,47 @@ const REVOKE_MCP_SESSION_MUTATION = `
308
289
  revokeMCPSession(id: $id)
309
290
  }
310
291
  `;
292
+ const WHOAMI_QUERY = `
293
+ query CliWhoAmI {
294
+ me {
295
+ id
296
+ handle
297
+ account {
298
+ id
299
+ workspace
300
+ }
301
+ }
302
+ }
303
+ `;
304
+ const LIST_MEMBER_CORNERS_QUERY = `
305
+ query CliMemberCorners {
306
+ memberCorners(first: 50) {
307
+ edges {
308
+ node {
309
+ id
310
+ name
311
+ }
312
+ }
313
+ }
314
+ }
315
+ `;
316
+ const CREATE_WORKSTREAM_MUTATION = `
317
+ mutation CliCreateWorkstream($input: CreateWorkstreamInput!) {
318
+ createWorkstream(input: $input) {
319
+ id
320
+ cornerId
321
+ name
322
+ summary
323
+ category
324
+ status
325
+ updatedAt
326
+ topic {
327
+ id
328
+ name
329
+ }
330
+ }
331
+ }
332
+ `;
311
333
  function createRuntime() {
312
334
  return {
313
335
  config: new ConfigStore(),
@@ -315,6 +337,7 @@ function createRuntime() {
315
337
  stderr: process.stderr,
316
338
  stdin: process.stdin,
317
339
  cwd: process.cwd(),
340
+ prompts: undefined,
318
341
  openUrl: openUrlInBrowser,
319
342
  createClient: (input) => new DefaultCornersApiClient(input),
320
343
  getVersion: getPackageVersion,
@@ -329,7 +352,11 @@ function printMainHelp(runtime) {
329
352
  "",
330
353
  "Commands:",
331
354
  " auth login|logout|status",
332
- " workstream (ws) list|use|current|pull|push|question|attach|reply-thread",
355
+ " guidance status|sync",
356
+ " init",
357
+ " corner use",
358
+ " whoami",
359
+ " workstream (ws) list|use|current|create|pull|push|question|attach|reply-thread",
333
360
  " help",
334
361
  " version",
335
362
  "",
@@ -342,6 +369,23 @@ function printMainHelp(runtime) {
342
369
  " --no-browser",
343
370
  ].join("\n"));
344
371
  }
372
+ function printInitHelp(runtime) {
373
+ printLine(runtime.stdout, [
374
+ "corners init",
375
+ "",
376
+ "Usage:",
377
+ " corners init [--profile <name>] [--api-url <url>] [--json]",
378
+ ].join("\n"));
379
+ }
380
+ function printGuidanceHelp(runtime) {
381
+ printLine(runtime.stdout, [
382
+ "corners guidance",
383
+ "",
384
+ "Usage:",
385
+ " corners guidance status [--json]",
386
+ " corners guidance sync [--json]",
387
+ ].join("\n"));
388
+ }
345
389
  function printAuthHelp(runtime) {
346
390
  printLine(runtime.stdout, [
347
391
  "corners auth",
@@ -352,6 +396,22 @@ function printAuthHelp(runtime) {
352
396
  " corners auth status [--profile <name>] [--json]",
353
397
  ].join("\n"));
354
398
  }
399
+ function printCornerHelp(runtime) {
400
+ printLine(runtime.stdout, [
401
+ "corners corner",
402
+ "",
403
+ "Usage:",
404
+ " corners corner use <cornerNameOrId> [--path <path>] [--json]",
405
+ ].join("\n"));
406
+ }
407
+ function printWhoamiHelp(runtime) {
408
+ printLine(runtime.stdout, [
409
+ "corners whoami",
410
+ "",
411
+ "Usage:",
412
+ " corners whoami [--profile <name>] [--api-url <url>] [--json]",
413
+ ].join("\n"));
414
+ }
355
415
  function printWorkstreamHelp(runtime) {
356
416
  printLine(runtime.stdout, [
357
417
  "corners workstream",
@@ -360,12 +420,13 @@ function printWorkstreamHelp(runtime) {
360
420
  " corners workstream list [--json]",
361
421
  " corners workstream use <workstreamId> [--json]",
362
422
  " corners workstream current [--json]",
363
- " corners workstream pull [workstreamId] [--json]",
364
- " corners workstream push [workstreamId] [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
365
- " corners workstream question list [workstreamId] [--json]",
366
- " corners workstream question ask [workstreamId] [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
423
+ " corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>] [--json]",
424
+ " corners workstream pull <workstreamId> [--json]",
425
+ " corners workstream push <workstreamId> [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
426
+ " corners workstream question list <workstreamId> [--json]",
427
+ " corners workstream question ask <workstreamId> [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
367
428
  " corners workstream question answer <questionId> [--text <text>] [--json]",
368
- " corners workstream attach [workstreamId] --kind <kind> --entity-id <id> [--json]",
429
+ " corners workstream attach <workstreamId> --kind <kind> --entity-id <id> [--json]",
369
430
  " corners workstream reply-thread <threadId> [--text <text>] [--json]",
370
431
  "",
371
432
  "Update types:",
@@ -462,6 +523,83 @@ async function requireStoredProfile(runtime, common) {
462
523
  }),
463
524
  };
464
525
  }
526
+ async function fetchCurrentIdentity(client) {
527
+ const data = await client.graphql(WHOAMI_QUERY);
528
+ return {
529
+ userId: data.me.id,
530
+ handle: data.me.handle ?? null,
531
+ accountId: data.me.account?.id ?? null,
532
+ workspace: data.me.account?.workspace ?? null,
533
+ };
534
+ }
535
+ async function maybeRefreshStoredProfileIdentity(input) {
536
+ try {
537
+ const identity = await fetchCurrentIdentity(input.client);
538
+ const nextProfile = {
539
+ ...input.profile,
540
+ userId: identity.userId,
541
+ handle: identity.handle,
542
+ accountId: identity.accountId,
543
+ workspace: identity.workspace,
544
+ };
545
+ if (nextProfile.userId !== input.profile.userId ||
546
+ nextProfile.handle !== (input.profile.handle ?? null) ||
547
+ nextProfile.accountId !== input.profile.accountId ||
548
+ nextProfile.workspace !== input.profile.workspace) {
549
+ await input.runtime.config.saveProfile(input.profileName, nextProfile, {
550
+ setActive: false,
551
+ });
552
+ }
553
+ return nextProfile;
554
+ }
555
+ catch {
556
+ return input.profile;
557
+ }
558
+ }
559
+ async function resolveAuthStatus(runtime, common) {
560
+ const selected = await runtime.config.getProfile(common.profile ?? null);
561
+ if (!selected) {
562
+ return {
563
+ loggedIn: false,
564
+ profile: common.profile ?? null,
565
+ };
566
+ }
567
+ const apiUrl = common.apiUrl ?? selected.profile.apiUrl;
568
+ const client = runtime.createClient({
569
+ apiUrl,
570
+ accessToken: selected.profile.accessToken,
571
+ });
572
+ let remote;
573
+ try {
574
+ remote = await client.validateSession();
575
+ }
576
+ catch (error) {
577
+ remote = {
578
+ valid: false,
579
+ error: error.message,
580
+ };
581
+ }
582
+ const profile = remote.valid
583
+ ? await maybeRefreshStoredProfileIdentity({
584
+ runtime,
585
+ profileName: selected.name,
586
+ profile: selected.profile,
587
+ client,
588
+ })
589
+ : selected.profile;
590
+ return {
591
+ loggedIn: true,
592
+ profile: selected.name,
593
+ apiUrl,
594
+ sessionId: profile.sessionId,
595
+ workspace: profile.workspace,
596
+ handle: profile.handle ?? null,
597
+ accountId: profile.accountId,
598
+ userId: profile.userId,
599
+ remoteValid: remote.valid,
600
+ remoteError: remote.valid ? null : (remote.error ?? null),
601
+ };
602
+ }
465
603
  async function pollForDeviceToken(input) {
466
604
  const deadline = Date.now() + input.device.expires_in * 1000;
467
605
  let intervalMs = input.device.interval * 1000;
@@ -489,36 +627,336 @@ async function pollForDeviceToken(input) {
489
627
  printLine(input.stderr, "Device code expired before authorization completed.");
490
628
  throw new CLIError("Device code expired");
491
629
  }
492
- async function resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, explicitWorkstreamId) {
493
- const workstreamId = explicitWorkstreamId ||
494
- (await resolveBoundWorkstreamId(runtime, profileName));
630
+ async function withPrompts(runtime, run) {
631
+ if (runtime.prompts) {
632
+ return run(runtime.prompts);
633
+ }
634
+ if (runtime.stdin.isTTY === false) {
635
+ throw new CLIError("`corners init` requires an interactive terminal.");
636
+ }
637
+ const prompts = createPromptApi(runtime.stdin, runtime.stderr);
638
+ try {
639
+ return await run(prompts);
640
+ }
641
+ finally {
642
+ await prompts.close();
643
+ }
644
+ }
645
+ async function readOptionalUtf8(path) {
646
+ try {
647
+ return await readFile(path, "utf8");
648
+ }
649
+ catch (error) {
650
+ if (error.code === "ENOENT") {
651
+ return null;
652
+ }
653
+ throw error;
654
+ }
655
+ }
656
+ function ensureTrailingNewline(value) {
657
+ return value.endsWith("\n") ? value : `${value}\n`;
658
+ }
659
+ function normalizeCornerLookupValue(value) {
660
+ const normalized = value.trim();
661
+ return normalized.startsWith("#") ? normalized.slice(1) : normalized;
662
+ }
663
+ function normalizeStoredRelativePath(rootDir, targetPath) {
664
+ const relativePath = relative(rootDir, targetPath);
665
+ if (!relativePath) {
666
+ return ".";
667
+ }
668
+ if (relativePath === ".." ||
669
+ relativePath.startsWith(`..${sep}`) ||
670
+ relativePath.includes(`${sep}..${sep}`)) {
671
+ throw new CLIError("The target path must stay within the initialized root.");
672
+ }
673
+ return relativePath.split(sep).join("/");
674
+ }
675
+ function resolvePathWithinRoot(rootDir, cwd, inputPath) {
676
+ const targetPath = inputPath ? resolve(cwd, inputPath) : resolve(cwd);
677
+ normalizeStoredRelativePath(rootDir, targetPath);
678
+ return targetPath;
679
+ }
680
+ function resolveCornerForRelativePath(config, relativePath) {
681
+ let selected = config.defaultCorner;
682
+ let selectedLength = 0;
683
+ for (const override of config.cornerOverrides) {
684
+ const matches = relativePath === override.path ||
685
+ relativePath.startsWith(`${override.path}/`);
686
+ if (!matches) {
687
+ continue;
688
+ }
689
+ if (override.path.length > selectedLength) {
690
+ selected = {
691
+ id: override.cornerId,
692
+ name: override.cornerName,
693
+ };
694
+ selectedLength = override.path.length;
695
+ }
696
+ }
697
+ return selected;
698
+ }
699
+ function resolveCornerForCwd(root, cwd) {
700
+ const absolutePath = resolvePathWithinRoot(root.rootDir, cwd);
701
+ const relativePath = normalizeStoredRelativePath(root.rootDir, absolutePath);
702
+ return {
703
+ relativePath,
704
+ corner: resolveCornerForRelativePath(root.config, relativePath),
705
+ };
706
+ }
707
+ async function resolveGuidanceState(root) {
708
+ const expectedBlock = buildExpectedManagedGuidanceBlock();
709
+ const fileEntries = await Promise.all(GUIDANCE_TARGETS.map(async (target) => ({
710
+ target,
711
+ content: await readOptionalUtf8(join(root.rootDir, target.relativePath)),
712
+ })));
713
+ const discoveredManagedTargets = GUIDANCE_TARGETS.map((target) => target.key)
714
+ .filter((key) => {
715
+ const entry = fileEntries.find((candidate) => candidate.target.key === key);
716
+ return extractManagedGuidanceBlock(entry?.content ?? null) !== null;
717
+ });
718
+ const configuredManagedTargets = root.config.guidance?.managedTargets;
719
+ const managedTargets = configuredManagedTargets !== undefined
720
+ ? normalizeGuidanceTargets(configuredManagedTargets)
721
+ : discoveredManagedTargets;
722
+ const files = fileEntries.map(({ target, content }) => {
723
+ if (content === null) {
724
+ return {
725
+ target: target.key,
726
+ path: target.relativePath,
727
+ state: "missing",
728
+ };
729
+ }
730
+ if (!managedTargets.includes(target.key)) {
731
+ return {
732
+ target: target.key,
733
+ path: target.relativePath,
734
+ state: "unmanaged",
735
+ };
736
+ }
737
+ return {
738
+ target: target.key,
739
+ path: target.relativePath,
740
+ state: extractManagedGuidanceBlock(content) === expectedBlock
741
+ ? "current"
742
+ : "stale",
743
+ };
744
+ });
745
+ const staleFiles = files
746
+ .filter((file) => file.state === "stale")
747
+ .map((file) => file.path);
748
+ return {
749
+ stale: staleFiles.length > 0,
750
+ lastSyncedAt: root.config.guidance?.syncedAt ?? null,
751
+ managedTargets,
752
+ staleFiles,
753
+ files,
754
+ };
755
+ }
756
+ function buildGuidanceWarning(guidance) {
757
+ if (!guidance.stale) {
758
+ return null;
759
+ }
760
+ const syncedSuffix = guidance.lastSyncedAt
761
+ ? ` Last synced: ${guidance.lastSyncedAt}.`
762
+ : "";
763
+ return `Warning: managed Corners guidance is out of date in ${guidance.staleFiles.length} file(s).${syncedSuffix} Run \`${GUIDANCE_SYNC_COMMAND}\`.`;
764
+ }
765
+ function buildGuidanceJsonMetadata(guidance) {
766
+ return {
767
+ stale: guidance.stale,
768
+ lastSyncedAt: guidance.lastSyncedAt,
769
+ staleFiles: guidance.staleFiles,
770
+ syncCommand: GUIDANCE_SYNC_COMMAND,
771
+ };
772
+ }
773
+ function withGuidanceJsonMetadata(payload, guidance) {
774
+ if (!guidance) {
775
+ return payload;
776
+ }
777
+ return {
778
+ ...payload,
779
+ _corners: {
780
+ guidance: buildGuidanceJsonMetadata(guidance),
781
+ },
782
+ };
783
+ }
784
+ async function resolveRootContext(runtime, common, root, options) {
785
+ const guidance = await resolveGuidanceState(root);
786
+ if ((options?.emitWarning ?? true) && !common.json) {
787
+ const warning = buildGuidanceWarning(guidance);
788
+ if (warning) {
789
+ printLine(runtime.stderr, warning);
790
+ }
791
+ }
792
+ return {
793
+ root,
794
+ guidance,
795
+ };
796
+ }
797
+ async function requireLocalRoot(runtime, common, options) {
798
+ const root = await runtime.config.findLocalRoot(runtime.cwd);
799
+ if (!root) {
800
+ throw new CLIError("No local Corners CLI root found. Run `corners init` from your project root first.", { json: common.json });
801
+ }
802
+ return resolveRootContext(runtime, common, root, options);
803
+ }
804
+ async function requirePinnedRootProfile(runtime, common, rootContext) {
805
+ const { root, guidance } = rootContext;
806
+ if (common.profile && common.profile !== root.config.profile) {
807
+ throw new CLIError(`This initialized root is pinned to profile ${root.config.profile}.`, { json: common.json });
808
+ }
809
+ const selected = await runtime.config.getProfile(root.config.profile);
810
+ if (!selected) {
811
+ throw new CLIError(`The pinned profile ${root.config.profile} is not available. Run \`corners auth login --profile ${root.config.profile}\` first.`, { json: common.json });
812
+ }
813
+ const pinnedApiUrl = normalizeApiUrl(root.config.apiUrl);
814
+ const requestedApiUrl = common.apiUrl ? normalizeApiUrl(common.apiUrl) : null;
815
+ if (requestedApiUrl && requestedApiUrl !== pinnedApiUrl) {
816
+ throw new CLIError(`This initialized root is pinned to API ${pinnedApiUrl}.`, { json: common.json });
817
+ }
818
+ if (root.config.workspace &&
819
+ selected.profile.workspace &&
820
+ root.config.workspace !== selected.profile.workspace) {
821
+ 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 });
822
+ }
823
+ const profile = {
824
+ ...selected.profile,
825
+ apiUrl: pinnedApiUrl,
826
+ };
827
+ return {
828
+ root,
829
+ guidance,
830
+ profileName: selected.name,
831
+ profile,
832
+ client: runtime.createClient({
833
+ apiUrl: profile.apiUrl,
834
+ accessToken: profile.accessToken,
835
+ }),
836
+ };
837
+ }
838
+ async function requireCommandProfile(runtime, common) {
839
+ const root = await runtime.config.findLocalRoot(runtime.cwd);
840
+ if (root) {
841
+ const rootContext = await resolveRootContext(runtime, common, root);
842
+ return requirePinnedRootProfile(runtime, common, rootContext);
843
+ }
844
+ const selected = await requireStoredProfile(runtime, common);
845
+ return {
846
+ root: null,
847
+ guidance: null,
848
+ ...selected,
849
+ };
850
+ }
851
+ async function listMemberCorners(client) {
852
+ const data = await client.graphql(LIST_MEMBER_CORNERS_QUERY);
853
+ return data.memberCorners.edges.map((edge) => edge.node);
854
+ }
855
+ function findMemberCorner(corners, value) {
856
+ const normalized = normalizeCornerLookupValue(value);
857
+ return (corners.find((corner) => corner.id === normalized) ??
858
+ corners.find((corner) => corner.name.toLowerCase() === normalized.toLowerCase()) ??
859
+ null);
860
+ }
861
+ async function resolveMemberCorner(client, common, value) {
862
+ const corners = await listMemberCorners(client);
863
+ const corner = findMemberCorner(corners, value);
864
+ if (!corner) {
865
+ throw new CLIError("Corner not found in your joined corners.", {
866
+ json: common.json,
867
+ });
868
+ }
869
+ return corner;
870
+ }
871
+ async function resolveExplicitWorkstream(client, common, workstreamId) {
495
872
  const data = await client.graphql(WORKSTREAM_LOOKUP_QUERY, { id: workstreamId });
496
873
  if (!data.workstream) {
497
874
  throw new CLIError("Workstream not found", { json: common.json });
498
875
  }
499
876
  return data.workstream;
500
877
  }
501
- async function resolveBoundWorkstreamId(runtime, profileName) {
502
- const binding = await runtime.config.getBinding(runtime.cwd);
503
- if (!binding) {
504
- throw new CLIError("This directory is not bound to a workstream. Run `corners workstream use <workstreamId>` first.");
878
+ function connectWorkstream(config, workstreamId) {
879
+ if (config.connectedWorkstreams.some((entry) => entry.id === workstreamId)) {
880
+ return config;
505
881
  }
506
- if (binding.profile && binding.profile !== profileName) {
507
- throw new CLIError(`This directory is bound to profile ${binding.profile}. Switch profiles or rebind the directory.`);
508
- }
509
- return binding.workstreamId;
882
+ return {
883
+ ...config,
884
+ connectedWorkstreams: [
885
+ ...config.connectedWorkstreams,
886
+ {
887
+ id: workstreamId,
888
+ connectedAt: toIsoString(),
889
+ },
890
+ ],
891
+ };
510
892
  }
511
- function maybeTakeWorkstreamId(positionals) {
512
- if (positionals[0]?.startsWith("ws_")) {
893
+ function upsertCornerOverride(config, relativePath, corner) {
894
+ if (relativePath === ".") {
513
895
  return {
514
- workstreamId: positionals[0],
515
- rest: positionals.slice(1),
896
+ ...config,
897
+ defaultCorner: corner,
516
898
  };
517
899
  }
900
+ const overrides = config.cornerOverrides.filter((entry) => entry.path !== relativePath);
901
+ overrides.push({
902
+ path: relativePath,
903
+ cornerId: corner.id,
904
+ cornerName: corner.name,
905
+ updatedAt: toIsoString(),
906
+ });
907
+ overrides.sort((left, right) => left.path.localeCompare(right.path));
518
908
  return {
519
- rest: positionals,
909
+ ...config,
910
+ cornerOverrides: overrides,
520
911
  };
521
912
  }
913
+ async function buildPlannedEdit(path, nextContent) {
914
+ const existing = await readOptionalUtf8(path);
915
+ if (existing === nextContent) {
916
+ return null;
917
+ }
918
+ return {
919
+ path,
920
+ action: existing === null ? "create" : "update",
921
+ content: nextContent,
922
+ };
923
+ }
924
+ function withUpdatedGuidanceConfig(config, managedTargets, syncedAt) {
925
+ return {
926
+ ...config,
927
+ guidance: {
928
+ managedTargets: normalizeGuidanceTargets(managedTargets),
929
+ syncedAt,
930
+ revision: buildGuidanceRevision(),
931
+ },
932
+ };
933
+ }
934
+ function ensureGitignoreEntry(existing) {
935
+ const content = existing ?? "";
936
+ const lines = content
937
+ .split(/\r?\n/)
938
+ .map((line) => line.trim())
939
+ .filter(Boolean);
940
+ if (lines.includes(".corners") ||
941
+ lines.includes(".corners/") ||
942
+ lines.includes(LOCAL_ROOT_CONFIG_RELATIVE_PATH)) {
943
+ return ensureTrailingNewline(content);
944
+ }
945
+ const trimmed = content.trimEnd();
946
+ if (!trimmed) {
947
+ return `${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
948
+ }
949
+ return `${trimmed}\n${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
950
+ }
951
+ async function writePlannedEdits(edits) {
952
+ for (const edit of edits) {
953
+ await mkdir(dirname(edit.path), { recursive: true });
954
+ await writeFile(edit.path, edit.content, "utf8");
955
+ }
956
+ }
957
+ function formatWorkstreamLine(workstream) {
958
+ return `${workstream.id} ${workstream.name} ${workstream.status.toLowerCase()}${workstream.summary ? ` ${workstream.summary}` : ""}`;
959
+ }
522
960
  async function handleAuth(args, runtime, inherited) {
523
961
  const subcommand = args[0];
524
962
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -579,15 +1017,32 @@ async function handleAuth(args, runtime, inherited) {
579
1017
  if (!token.access_token) {
580
1018
  throw new CLIError("Login completed without an access token");
581
1019
  }
582
- const profile = {
1020
+ let profile = {
583
1021
  apiUrl: client.apiUrl,
584
1022
  accessToken: token.access_token,
585
1023
  sessionId: token.session_id ?? null,
586
1024
  workspace: token.workspace ?? null,
587
1025
  accountId: token.account_id ?? null,
588
1026
  userId: token.user_id ?? null,
1027
+ handle: null,
589
1028
  createdAt: toIsoString(),
590
1029
  };
1030
+ try {
1031
+ const identity = await fetchCurrentIdentity(runtime.createClient({
1032
+ apiUrl: profile.apiUrl,
1033
+ accessToken: profile.accessToken,
1034
+ }));
1035
+ profile = {
1036
+ ...profile,
1037
+ workspace: identity.workspace,
1038
+ accountId: identity.accountId,
1039
+ userId: identity.userId,
1040
+ handle: identity.handle,
1041
+ };
1042
+ }
1043
+ catch {
1044
+ // Best-effort profile enrichment. Authentication already succeeded.
1045
+ }
591
1046
  await runtime.config.saveProfile(profileName, profile, {
592
1047
  setActive: true,
593
1048
  });
@@ -598,6 +1053,7 @@ async function handleAuth(args, runtime, inherited) {
598
1053
  apiUrl: profile.apiUrl,
599
1054
  sessionId: profile.sessionId,
600
1055
  workspace: profile.workspace,
1056
+ handle: profile.handle,
601
1057
  accountId: profile.accountId,
602
1058
  userId: profile.userId,
603
1059
  });
@@ -693,12 +1149,8 @@ async function handleAuth(args, runtime, inherited) {
693
1149
  printAuthHelp(runtime);
694
1150
  return 0;
695
1151
  }
696
- const selected = await runtime.config.getProfile(common.profile ?? null);
697
- if (!selected) {
698
- const status = {
699
- loggedIn: false,
700
- profile: common.profile ?? null,
701
- };
1152
+ const status = await resolveAuthStatus(runtime, common);
1153
+ if (!status.loggedIn) {
702
1154
  if (common.json) {
703
1155
  printJson(runtime.stdout, status);
704
1156
  }
@@ -707,31 +1159,6 @@ async function handleAuth(args, runtime, inherited) {
707
1159
  }
708
1160
  return 0;
709
1161
  }
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
1162
  if (common.json) {
736
1163
  printJson(runtime.stdout, status);
737
1164
  }
@@ -740,6 +1167,7 @@ async function handleAuth(args, runtime, inherited) {
740
1167
  `Profile: ${status.profile}`,
741
1168
  `API URL: ${status.apiUrl}`,
742
1169
  `Workspace: ${status.workspace ?? "-"}`,
1170
+ `Handle: ${status.handle ? `@${status.handle}` : "-"}`,
743
1171
  `Account: ${status.accountId ?? "-"}`,
744
1172
  `User: ${status.userId ?? "-"}`,
745
1173
  `Session: ${status.sessionId ?? "-"}`,
@@ -752,6 +1180,359 @@ async function handleAuth(args, runtime, inherited) {
752
1180
  throw new CLIError(`Unknown auth command: ${subcommand}`);
753
1181
  }
754
1182
  }
1183
+ async function handleWhoAmI(args, runtime, inherited) {
1184
+ const parsed = parseArgs({
1185
+ args,
1186
+ allowPositionals: false,
1187
+ options: {
1188
+ json: { type: "boolean" },
1189
+ help: { type: "boolean", short: "h" },
1190
+ profile: { type: "string" },
1191
+ "api-url": { type: "string" },
1192
+ },
1193
+ });
1194
+ const common = mergeCommonOptions(inherited, {
1195
+ json: parsed.values.json,
1196
+ profile: parsed.values.profile,
1197
+ apiUrl: parsed.values["api-url"],
1198
+ });
1199
+ if (parsed.values.help) {
1200
+ printWhoamiHelp(runtime);
1201
+ return 0;
1202
+ }
1203
+ const status = await resolveAuthStatus(runtime, common);
1204
+ if (!status.loggedIn) {
1205
+ if (common.json) {
1206
+ printJson(runtime.stdout, status);
1207
+ }
1208
+ else {
1209
+ printLine(runtime.stdout, "Not logged in.");
1210
+ }
1211
+ return 0;
1212
+ }
1213
+ if (common.json) {
1214
+ printJson(runtime.stdout, status);
1215
+ }
1216
+ else {
1217
+ const identity = status.handle
1218
+ ? `@${status.handle}`
1219
+ : (status.userId ?? status.profile);
1220
+ const workspaceSuffix = status.workspace ? ` (${status.workspace})` : "";
1221
+ printLine(runtime.stdout, `${identity}${workspaceSuffix}`);
1222
+ }
1223
+ return 0;
1224
+ }
1225
+ async function handleInit(args, runtime, inherited) {
1226
+ const parsed = parseArgs({
1227
+ args,
1228
+ allowPositionals: false,
1229
+ options: {
1230
+ json: { type: "boolean" },
1231
+ help: { type: "boolean", short: "h" },
1232
+ profile: { type: "string" },
1233
+ "api-url": { type: "string" },
1234
+ },
1235
+ });
1236
+ const common = mergeCommonOptions(inherited, {
1237
+ json: parsed.values.json,
1238
+ profile: parsed.values.profile,
1239
+ apiUrl: parsed.values["api-url"],
1240
+ });
1241
+ if (parsed.values.help) {
1242
+ printInitHelp(runtime);
1243
+ return 0;
1244
+ }
1245
+ const currentRootPath = await realpath(runtime.cwd);
1246
+ const existingRoot = await runtime.config.findLocalRoot(runtime.cwd);
1247
+ if (existingRoot && existingRoot.rootDir !== currentRootPath) {
1248
+ throw new CLIError(`This directory is already inside initialized root ${existingRoot.rootDir}. Run \`corners init\` there.`, { json: common.json });
1249
+ }
1250
+ const { profileName, profile, client } = await requireStoredProfile(runtime, common);
1251
+ const joinedCorners = await listMemberCorners(client);
1252
+ if (joinedCorners.length === 0) {
1253
+ throw new CLIError("No joined corners are available for this profile.", {
1254
+ json: common.json,
1255
+ });
1256
+ }
1257
+ const rootDir = existingRoot?.rootDir ?? currentRootPath;
1258
+ const existingConfig = existingRoot?.config ?? null;
1259
+ return withPrompts(runtime, async (prompts) => {
1260
+ const selectedTargets = [];
1261
+ for (const target of GUIDANCE_TARGETS) {
1262
+ const include = await prompts.confirm(target.label, {
1263
+ defaultValue: true,
1264
+ });
1265
+ if (include) {
1266
+ selectedTargets.push(target);
1267
+ }
1268
+ }
1269
+ const selectedCornerId = await prompts.select("Choose the default corner for this local root.", joinedCorners.map((corner) => ({
1270
+ label: `${corner.name} (${corner.id})`,
1271
+ value: corner.id,
1272
+ })), {
1273
+ defaultValue: existingConfig?.defaultCorner.id ?? joinedCorners[0]?.id,
1274
+ });
1275
+ const defaultCorner = joinedCorners.find((corner) => corner.id === selectedCornerId) ??
1276
+ joinedCorners[0];
1277
+ const syncedAt = toIsoString();
1278
+ const nextLocalConfig = withUpdatedGuidanceConfig({
1279
+ version: 1,
1280
+ profile: profileName,
1281
+ workspace: profile.workspace,
1282
+ apiUrl: profile.apiUrl,
1283
+ defaultCorner: {
1284
+ id: defaultCorner.id,
1285
+ name: defaultCorner.name,
1286
+ },
1287
+ cornerOverrides: existingConfig?.cornerOverrides ?? [],
1288
+ connectedWorkstreams: existingConfig?.connectedWorkstreams ?? [],
1289
+ }, selectedTargets.map((target) => target.key), syncedAt);
1290
+ const plannedEdits = [];
1291
+ const gitignorePath = join(rootDir, ".gitignore");
1292
+ const gitignoreContent = ensureGitignoreEntry(await readOptionalUtf8(gitignorePath));
1293
+ const gitignoreEdit = await buildPlannedEdit(gitignorePath, gitignoreContent);
1294
+ if (gitignoreEdit) {
1295
+ plannedEdits.push(gitignoreEdit);
1296
+ }
1297
+ const localConfigPath = runtime.config.getLocalConfigPath(rootDir);
1298
+ const localConfigEdit = await buildPlannedEdit(localConfigPath, `${JSON.stringify(nextLocalConfig, null, 2)}\n`);
1299
+ if (localConfigEdit) {
1300
+ plannedEdits.push(localConfigEdit);
1301
+ }
1302
+ for (const target of selectedTargets) {
1303
+ const targetPath = join(rootDir, target.relativePath);
1304
+ const nextContent = buildGuidanceFileContent(target.key, await readOptionalUtf8(targetPath));
1305
+ const edit = await buildPlannedEdit(targetPath, nextContent);
1306
+ if (edit) {
1307
+ plannedEdits.push(edit);
1308
+ }
1309
+ }
1310
+ if (plannedEdits.length > 0 && !common.json) {
1311
+ printLine(runtime.stderr, [
1312
+ "Planned changes:",
1313
+ ...plannedEdits.map((edit) => ` ${edit.action} ${relative(rootDir, edit.path) || "."}`),
1314
+ ].join("\n"));
1315
+ }
1316
+ if (plannedEdits.length > 0) {
1317
+ const confirmed = await prompts.confirm("Proceed with these changes?", {
1318
+ defaultValue: true,
1319
+ });
1320
+ if (!confirmed) {
1321
+ if (common.json) {
1322
+ printJson(runtime.stdout, {
1323
+ ok: false,
1324
+ root: rootDir,
1325
+ aborted: true,
1326
+ });
1327
+ }
1328
+ else {
1329
+ printLine(runtime.stdout, "Aborted. No files were changed.");
1330
+ }
1331
+ return 0;
1332
+ }
1333
+ await writePlannedEdits(plannedEdits);
1334
+ }
1335
+ const payload = {
1336
+ ok: true,
1337
+ root: rootDir,
1338
+ profile: profileName,
1339
+ workspace: profile.workspace,
1340
+ defaultCorner,
1341
+ files: plannedEdits.map((edit) => ({
1342
+ action: edit.action,
1343
+ path: relative(rootDir, edit.path) || ".",
1344
+ })),
1345
+ };
1346
+ if (common.json) {
1347
+ printJson(runtime.stdout, payload);
1348
+ }
1349
+ else {
1350
+ printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}. Run \`${GUIDANCE_SYNC_COMMAND}\` later to refresh managed guidance.`);
1351
+ }
1352
+ return 0;
1353
+ });
1354
+ }
1355
+ async function handleGuidance(args, runtime, inherited) {
1356
+ const subcommand = args[0];
1357
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1358
+ printGuidanceHelp(runtime);
1359
+ return 0;
1360
+ }
1361
+ switch (subcommand) {
1362
+ case "status": {
1363
+ const parsed = parseArgs({
1364
+ args: args.slice(1),
1365
+ allowPositionals: false,
1366
+ options: {
1367
+ json: { type: "boolean" },
1368
+ help: { type: "boolean", short: "h" },
1369
+ },
1370
+ });
1371
+ const common = mergeCommonOptions(inherited, {
1372
+ json: parsed.values.json,
1373
+ });
1374
+ if (parsed.values.help) {
1375
+ printGuidanceHelp(runtime);
1376
+ return 0;
1377
+ }
1378
+ const rootContext = await requireLocalRoot(runtime, common, {
1379
+ emitWarning: false,
1380
+ });
1381
+ const payload = {
1382
+ ok: true,
1383
+ root: rootContext.root.rootDir,
1384
+ guidance: rootContext.guidance,
1385
+ };
1386
+ if (common.json) {
1387
+ printJson(runtime.stdout, payload);
1388
+ }
1389
+ else {
1390
+ printLine(runtime.stdout, [
1391
+ `Root: ${payload.root}`,
1392
+ `Stale: ${payload.guidance.stale ? "yes" : "no"}`,
1393
+ `Last synced: ${payload.guidance.lastSyncedAt ?? "-"}`,
1394
+ `Managed targets: ${payload.guidance.managedTargets.join(", ") || "-"}`,
1395
+ ...payload.guidance.files.map((file) => `${file.path}: ${file.state}`),
1396
+ ].join("\n"));
1397
+ }
1398
+ return 0;
1399
+ }
1400
+ case "sync": {
1401
+ const parsed = parseArgs({
1402
+ args: args.slice(1),
1403
+ allowPositionals: false,
1404
+ options: {
1405
+ json: { type: "boolean" },
1406
+ help: { type: "boolean", short: "h" },
1407
+ },
1408
+ });
1409
+ const common = mergeCommonOptions(inherited, {
1410
+ json: parsed.values.json,
1411
+ });
1412
+ if (parsed.values.help) {
1413
+ printGuidanceHelp(runtime);
1414
+ return 0;
1415
+ }
1416
+ const rootContext = await requireLocalRoot(runtime, common, {
1417
+ emitWarning: false,
1418
+ });
1419
+ const managedTargets = rootContext.root.config.guidance?.managedTargets !== undefined
1420
+ ? normalizeGuidanceTargets(rootContext.root.config.guidance.managedTargets)
1421
+ : rootContext.guidance.managedTargets;
1422
+ const edits = [];
1423
+ const actions = new Map();
1424
+ for (const target of GUIDANCE_TARGETS) {
1425
+ if (!managedTargets.includes(target.key)) {
1426
+ actions.set(target.key, "skipped");
1427
+ continue;
1428
+ }
1429
+ const targetPath = join(rootContext.root.rootDir, target.relativePath);
1430
+ const nextContent = buildGuidanceFileContent(target.key, await readOptionalUtf8(targetPath));
1431
+ const edit = await buildPlannedEdit(targetPath, nextContent);
1432
+ if (edit) {
1433
+ edits.push(edit);
1434
+ actions.set(target.key, edit.action);
1435
+ }
1436
+ else {
1437
+ actions.set(target.key, "unchanged");
1438
+ }
1439
+ }
1440
+ if (edits.length > 0) {
1441
+ await writePlannedEdits(edits);
1442
+ }
1443
+ const syncedAt = toIsoString();
1444
+ const nextConfig = withUpdatedGuidanceConfig(rootContext.root.config, managedTargets, syncedAt);
1445
+ await runtime.config.writeLocalConfig(rootContext.root.rootDir, nextConfig);
1446
+ const nextRoot = {
1447
+ ...rootContext.root,
1448
+ config: nextConfig,
1449
+ };
1450
+ const nextGuidance = await resolveGuidanceState(nextRoot);
1451
+ const guidance = {
1452
+ ...nextGuidance,
1453
+ files: nextGuidance.files.map((file) => ({
1454
+ ...file,
1455
+ action: actions.get(file.target) ?? "skipped",
1456
+ })),
1457
+ };
1458
+ const payload = {
1459
+ ok: true,
1460
+ root: nextRoot.rootDir,
1461
+ guidance,
1462
+ };
1463
+ if (common.json) {
1464
+ printJson(runtime.stdout, payload);
1465
+ }
1466
+ else {
1467
+ const updatedCount = guidance.files.filter((file) => file.action === "create" || file.action === "update").length;
1468
+ printLine(runtime.stdout, `Synchronized Corners guidance at ${payload.root}. Updated ${updatedCount} file(s).`);
1469
+ }
1470
+ return 0;
1471
+ }
1472
+ default:
1473
+ throw new CLIError(`Unknown guidance command: ${subcommand}`);
1474
+ }
1475
+ }
1476
+ async function handleCorner(args, runtime, inherited) {
1477
+ const subcommand = args[0];
1478
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1479
+ printCornerHelp(runtime);
1480
+ return 0;
1481
+ }
1482
+ switch (subcommand) {
1483
+ case "use": {
1484
+ const parsed = parseArgs({
1485
+ args: args.slice(1),
1486
+ allowPositionals: true,
1487
+ options: {
1488
+ json: { type: "boolean" },
1489
+ help: { type: "boolean", short: "h" },
1490
+ profile: { type: "string" },
1491
+ "api-url": { type: "string" },
1492
+ path: { type: "string" },
1493
+ },
1494
+ });
1495
+ const common = mergeCommonOptions(inherited, {
1496
+ json: parsed.values.json,
1497
+ profile: parsed.values.profile,
1498
+ apiUrl: parsed.values["api-url"],
1499
+ });
1500
+ if (parsed.values.help) {
1501
+ printCornerHelp(runtime);
1502
+ return 0;
1503
+ }
1504
+ const cornerNameOrId = parsed.positionals[0];
1505
+ if (!cornerNameOrId) {
1506
+ throw new CLIError("Usage: corners corner use <cornerNameOrId> [--path <path>]", { json: common.json });
1507
+ }
1508
+ const rootContext = await requireLocalRoot(runtime, common);
1509
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1510
+ const corner = await resolveMemberCorner(client, common, cornerNameOrId);
1511
+ const cwdPath = await realpath(runtime.cwd);
1512
+ const targetAbsolutePath = resolvePathWithinRoot(root.rootDir, cwdPath, parsed.values.path);
1513
+ const relativePath = normalizeStoredRelativePath(root.rootDir, targetAbsolutePath);
1514
+ const nextConfig = upsertCornerOverride(root.config, relativePath, {
1515
+ id: corner.id,
1516
+ name: corner.name,
1517
+ });
1518
+ await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1519
+ if (common.json) {
1520
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1521
+ ok: true,
1522
+ root: root.rootDir,
1523
+ path: relativePath,
1524
+ corner,
1525
+ }, guidance));
1526
+ }
1527
+ else {
1528
+ printLine(runtime.stdout, `Set default corner for ${relativePath} to ${corner.name}.`);
1529
+ }
1530
+ return 0;
1531
+ }
1532
+ default:
1533
+ throw new CLIError(`Unknown corner command: ${subcommand}`);
1534
+ }
1535
+ }
755
1536
  async function handleWorkstream(args, runtime, inherited) {
756
1537
  const subcommand = args[0];
757
1538
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -779,16 +1560,44 @@ async function handleWorkstream(args, runtime, inherited) {
779
1560
  printWorkstreamHelp(runtime);
780
1561
  return 0;
781
1562
  }
782
- const { client } = await requireStoredProfile(runtime, common);
783
- const data = await client.graphql(LIST_WORKSTREAMS_QUERY);
784
- const workstreams = data.workstreams.edges.map((edge) => edge.node);
1563
+ const rootContext = await requireLocalRoot(runtime, common);
1564
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1565
+ const hydrated = await Promise.all(root.config.connectedWorkstreams.map(async (entry) => {
1566
+ try {
1567
+ return {
1568
+ entry,
1569
+ workstream: await resolveExplicitWorkstream(client, common, entry.id),
1570
+ };
1571
+ }
1572
+ catch {
1573
+ return {
1574
+ entry,
1575
+ workstream: null,
1576
+ };
1577
+ }
1578
+ }));
1579
+ const workstreams = hydrated
1580
+ .filter((entry) => entry.workstream !== null)
1581
+ .map((entry) => entry.workstream);
1582
+ const missingWorkstreamIds = hydrated
1583
+ .filter((entry) => entry.workstream === null)
1584
+ .map((entry) => entry.entry.id);
785
1585
  if (common.json) {
786
- printJson(runtime.stdout, workstreams);
1586
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1587
+ root: root.rootDir,
1588
+ workstreams,
1589
+ missingWorkstreamIds,
1590
+ }, guidance));
787
1591
  }
788
1592
  else {
789
- printLine(runtime.stdout, workstreams
790
- .map((workstream) => `${workstream.id} ${workstream.name} ${workstream.status.toLowerCase()}${workstream.summary ? ` ${workstream.summary}` : ""}`)
791
- .join("\n"));
1593
+ if (missingWorkstreamIds.length > 0) {
1594
+ printLine(runtime.stderr, missingWorkstreamIds
1595
+ .map((workstreamId) => `Warning: connected workstream ${workstreamId} is missing or inaccessible.`)
1596
+ .join("\n"));
1597
+ }
1598
+ printLine(runtime.stdout, workstreams.length > 0
1599
+ ? workstreams.map(formatWorkstreamLine).join("\n")
1600
+ : "No workstreams are connected to this local environment.");
792
1601
  }
793
1602
  return 0;
794
1603
  }
@@ -818,26 +1627,22 @@ async function handleWorkstream(args, runtime, inherited) {
818
1627
  json: common.json,
819
1628
  });
820
1629
  }
821
- const { profileName, profile, client } = await requireStoredProfile(runtime, common);
822
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
823
- await runtime.config.setBinding(runtime.cwd, {
824
- profile: profileName,
825
- workspace: profile.workspace ?? "",
826
- workstreamId: workstream.id,
827
- cornerId: workstream.cornerId,
828
- boundAt: toIsoString(),
829
- });
1630
+ const rootContext = await requireLocalRoot(runtime, common);
1631
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1632
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1633
+ const nextConfig = connectWorkstream(root.config, workstream.id);
1634
+ if (nextConfig !== root.config) {
1635
+ await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1636
+ }
830
1637
  if (common.json) {
831
- printJson(runtime.stdout, {
1638
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
832
1639
  ok: true,
833
- cwd: runtime.cwd,
834
- profile: profileName,
835
- workspace: profile.workspace,
1640
+ root: root.rootDir,
836
1641
  workstream,
837
- });
1642
+ }, guidance));
838
1643
  }
839
1644
  else {
840
- printLine(runtime.stdout, `Bound ${runtime.cwd} to ${workstream.id} (${workstream.name}).`);
1645
+ printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir}.`);
841
1646
  }
842
1647
  return 0;
843
1648
  }
@@ -857,33 +1662,88 @@ async function handleWorkstream(args, runtime, inherited) {
857
1662
  printWorkstreamHelp(runtime);
858
1663
  return 0;
859
1664
  }
860
- const binding = await runtime.config.getBinding(runtime.cwd);
861
- if (!binding) {
862
- if (common.json) {
863
- printJson(runtime.stdout, { cwd: runtime.cwd, binding: null });
864
- }
865
- else {
866
- printLine(runtime.stdout, "No workstream is bound to this directory.");
867
- }
868
- return 0;
869
- }
1665
+ const { root, guidance } = await requireLocalRoot(runtime, common);
1666
+ const cwdPath = await realpath(runtime.cwd);
1667
+ const resolvedCorner = resolveCornerForCwd(root, cwdPath);
1668
+ const payload = {
1669
+ cwd: runtime.cwd,
1670
+ root: root.rootDir,
1671
+ profile: root.config.profile,
1672
+ workspace: root.config.workspace,
1673
+ resolvedCorner,
1674
+ connectedWorkstreamIds: root.config.connectedWorkstreams.map((entry) => entry.id),
1675
+ };
870
1676
  if (common.json) {
871
- printJson(runtime.stdout, {
872
- cwd: runtime.cwd,
873
- binding,
874
- });
1677
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
875
1678
  }
876
1679
  else {
877
1680
  printLine(runtime.stdout, [
878
- `Directory: ${runtime.cwd}`,
879
- `Profile: ${binding.profile ?? "-"}`,
880
- `Workspace: ${binding.workspace || "-"}`,
881
- `Workstream: ${binding.workstreamId}`,
882
- `Corner: ${binding.cornerId}`,
1681
+ `Root: ${payload.root}`,
1682
+ `Directory: ${payload.cwd}`,
1683
+ `Profile: ${payload.profile}`,
1684
+ `Workspace: ${payload.workspace ?? "-"}`,
1685
+ `Resolved corner: ${payload.resolvedCorner.corner.name} (${payload.resolvedCorner.corner.id})`,
1686
+ `Connected workstreams: ${payload.connectedWorkstreamIds.length}`,
883
1687
  ].join("\n"));
884
1688
  }
885
1689
  return 0;
886
1690
  }
1691
+ case "create": {
1692
+ const parsed = parseArgs({
1693
+ args: args.slice(1),
1694
+ allowPositionals: true,
1695
+ options: {
1696
+ json: { type: "boolean" },
1697
+ help: { type: "boolean", short: "h" },
1698
+ profile: { type: "string" },
1699
+ "api-url": { type: "string" },
1700
+ summary: { type: "string" },
1701
+ corner: { type: "string" },
1702
+ },
1703
+ });
1704
+ const common = mergeCommonOptions(inherited, {
1705
+ json: parsed.values.json,
1706
+ profile: parsed.values.profile,
1707
+ apiUrl: parsed.values["api-url"],
1708
+ });
1709
+ if (parsed.values.help) {
1710
+ printWorkstreamHelp(runtime);
1711
+ return 0;
1712
+ }
1713
+ const name = parsed.positionals.join(" ").trim();
1714
+ if (!name) {
1715
+ throw new CLIError("Usage: corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>]", { json: common.json });
1716
+ }
1717
+ const rootContext = await requireLocalRoot(runtime, common);
1718
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1719
+ const cwdPath = await realpath(runtime.cwd);
1720
+ const corner = parsed.values.corner
1721
+ ? await resolveMemberCorner(client, common, parsed.values.corner)
1722
+ : resolveCornerForCwd(root, cwdPath).corner;
1723
+ const result = await client.graphql(CREATE_WORKSTREAM_MUTATION, {
1724
+ input: {
1725
+ cornerId: corner.id,
1726
+ name,
1727
+ summary: parsed.values.summary,
1728
+ },
1729
+ });
1730
+ const nextConfig = connectWorkstream(root.config, result.createWorkstream.id);
1731
+ if (nextConfig !== root.config) {
1732
+ await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1733
+ }
1734
+ if (common.json) {
1735
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1736
+ ok: true,
1737
+ root: root.rootDir,
1738
+ corner,
1739
+ workstream: result.createWorkstream,
1740
+ }, guidance));
1741
+ }
1742
+ else {
1743
+ printLine(runtime.stdout, `Created ${result.createWorkstream.id} (${result.createWorkstream.name}) in ${corner.name}.`);
1744
+ }
1745
+ return 0;
1746
+ }
887
1747
  case "pull": {
888
1748
  const parsed = parseArgs({
889
1749
  args: args.slice(1),
@@ -904,16 +1764,21 @@ async function handleWorkstream(args, runtime, inherited) {
904
1764
  printWorkstreamHelp(runtime);
905
1765
  return 0;
906
1766
  }
907
- const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
908
- const { profileName, client } = await requireStoredProfile(runtime, common);
909
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1767
+ const workstreamId = parsed.positionals[0];
1768
+ if (!workstreamId) {
1769
+ throw new CLIError("Usage: corners workstream pull <workstreamId>", {
1770
+ json: common.json,
1771
+ });
1772
+ }
1773
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1774
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
910
1775
  const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
911
1776
  id: workstream.id,
912
1777
  attachmentsFirst: 50,
913
1778
  feedFirst: 20,
914
1779
  });
915
1780
  if (common.json) {
916
- printJson(runtime.stdout, data.workstream);
1781
+ printJson(runtime.stdout, withGuidanceJsonMetadata(data.workstream, guidance));
917
1782
  }
918
1783
  else {
919
1784
  const snapshot = data.workstream;
@@ -953,16 +1818,21 @@ async function handleWorkstream(args, runtime, inherited) {
953
1818
  printWorkstreamHelp(runtime);
954
1819
  return 0;
955
1820
  }
956
- const { workstreamId, rest } = maybeTakeWorkstreamId(parsed.positionals);
1821
+ const workstreamId = parsed.positionals[0];
1822
+ if (!workstreamId) {
1823
+ throw new CLIError("Usage: corners workstream push <workstreamId>", {
1824
+ json: common.json,
1825
+ });
1826
+ }
957
1827
  const message = parsed.values.message ??
958
- (rest.length > 0
959
- ? rest.join(" ")
1828
+ (parsed.positionals.length > 1
1829
+ ? parsed.positionals.slice(1).join(" ")
960
1830
  : await readTextFromStdin(runtime.stdin));
961
1831
  if (!message) {
962
1832
  throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
963
1833
  }
964
- const { profileName, client } = await requireStoredProfile(runtime, common);
965
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1834
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1835
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
966
1836
  let createdDocument = null;
967
1837
  if (parsed.values.file) {
968
1838
  const documentContent = await readFile(parsed.values.file, "utf8");
@@ -992,7 +1862,7 @@ async function handleWorkstream(args, runtime, inherited) {
992
1862
  document: createdDocument,
993
1863
  };
994
1864
  if (common.json) {
995
- printJson(runtime.stdout, payload);
1865
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
996
1866
  }
997
1867
  else {
998
1868
  printLine(runtime.stdout, `Recorded ${String(parsed.values.type ?? "status")} update on ${workstream.id}.`);
@@ -1026,9 +1896,12 @@ async function handleWorkstream(args, runtime, inherited) {
1026
1896
  printWorkstreamHelp(runtime);
1027
1897
  return 0;
1028
1898
  }
1029
- const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
1030
- const { profileName, client } = await requireStoredProfile(runtime, common);
1031
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1899
+ const workstreamId = parsed.positionals[0];
1900
+ if (!workstreamId) {
1901
+ throw new CLIError("Usage: corners workstream question list <workstreamId>", { json: common.json });
1902
+ }
1903
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1904
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1032
1905
  const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
1033
1906
  id: workstream.id,
1034
1907
  });
@@ -1036,7 +1909,7 @@ async function handleWorkstream(args, runtime, inherited) {
1036
1909
  throw new CLIError("Workstream not found", { json: common.json });
1037
1910
  }
1038
1911
  if (common.json) {
1039
- printJson(runtime.stdout, data.workstream);
1912
+ printJson(runtime.stdout, withGuidanceJsonMetadata(data.workstream, guidance));
1040
1913
  }
1041
1914
  else {
1042
1915
  printLine(runtime.stdout, [
@@ -1071,16 +1944,19 @@ async function handleWorkstream(args, runtime, inherited) {
1071
1944
  printWorkstreamHelp(runtime);
1072
1945
  return 0;
1073
1946
  }
1074
- const { workstreamId, rest } = maybeTakeWorkstreamId(parsed.positionals);
1947
+ const workstreamId = parsed.positionals[0];
1948
+ if (!workstreamId) {
1949
+ throw new CLIError("Usage: corners workstream question ask <workstreamId> [--question <text>]", { json: common.json });
1950
+ }
1075
1951
  const question = parsed.values.question ??
1076
- (rest.length > 0
1077
- ? rest.join(" ")
1952
+ (parsed.positionals.length > 1
1953
+ ? parsed.positionals.slice(1).join(" ")
1078
1954
  : await readTextFromStdin(runtime.stdin));
1079
1955
  if (!question) {
1080
1956
  throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
1081
1957
  }
1082
- const { profileName, client } = await requireStoredProfile(runtime, common);
1083
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1958
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1959
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1084
1960
  const result = await client.graphql(CREATE_WORKSTREAM_QUESTION_MUTATION, {
1085
1961
  input: {
1086
1962
  workstreamId: workstream.id,
@@ -1090,7 +1966,7 @@ async function handleWorkstream(args, runtime, inherited) {
1090
1966
  },
1091
1967
  });
1092
1968
  if (common.json) {
1093
- printJson(runtime.stdout, result.createWorkstreamQuestion);
1969
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.createWorkstreamQuestion, guidance));
1094
1970
  }
1095
1971
  else {
1096
1972
  printLine(runtime.stdout, `Created question on ${workstream.id}.`);
@@ -1131,13 +2007,13 @@ async function handleWorkstream(args, runtime, inherited) {
1131
2007
  json: common.json,
1132
2008
  });
1133
2009
  }
1134
- const { client } = await requireStoredProfile(runtime, common);
2010
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1135
2011
  const result = await client.graphql(ANSWER_WORKSTREAM_QUESTION_MUTATION, {
1136
2012
  questionId,
1137
2013
  answerText: answer,
1138
2014
  });
1139
2015
  if (common.json) {
1140
- printJson(runtime.stdout, result.answerWorkstreamQuestion);
2016
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.answerWorkstreamQuestion, guidance));
1141
2017
  }
1142
2018
  else {
1143
2019
  printLine(runtime.stdout, `Answered ${questionId}.`);
@@ -1170,21 +2046,21 @@ async function handleWorkstream(args, runtime, inherited) {
1170
2046
  printWorkstreamHelp(runtime);
1171
2047
  return 0;
1172
2048
  }
1173
- const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
2049
+ const workstreamId = parsed.positionals[0];
1174
2050
  const kind = parsed.values.kind;
1175
2051
  const entityId = parsed.values["entity-id"];
1176
- if (!kind || !entityId) {
1177
- throw new CLIError("Usage: corners workstream attach [workstreamId] --kind <kind> --entity-id <id>", { json: common.json });
2052
+ if (!workstreamId || !kind || !entityId) {
2053
+ throw new CLIError("Usage: corners workstream attach <workstreamId> --kind <kind> --entity-id <id>", { json: common.json });
1178
2054
  }
1179
- const { profileName, client } = await requireStoredProfile(runtime, common);
1180
- const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
2055
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2056
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1181
2057
  const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
1182
2058
  workstreamId: workstream.id,
1183
2059
  kind: toGraphQLAttachmentKind(kind),
1184
2060
  entityId,
1185
2061
  });
1186
2062
  if (common.json) {
1187
- printJson(runtime.stdout, result.addWorkstreamAttachment);
2063
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.addWorkstreamAttachment, guidance));
1188
2064
  }
1189
2065
  else {
1190
2066
  printLine(runtime.stdout, `Attached ${kind}:${entityId} to ${workstream.id}.`);
@@ -1220,7 +2096,7 @@ async function handleWorkstream(args, runtime, inherited) {
1220
2096
  if (!threadId || !text) {
1221
2097
  throw new CLIError("Usage: corners workstream reply-thread <threadId> [--text <text>]", { json: common.json });
1222
2098
  }
1223
- const { client } = await requireStoredProfile(runtime, common);
2099
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1224
2100
  const result = await client.graphql(REPLY_ARTIFACT_THREAD_MUTATION, {
1225
2101
  id: threadId,
1226
2102
  input: {
@@ -1228,7 +2104,7 @@ async function handleWorkstream(args, runtime, inherited) {
1228
2104
  },
1229
2105
  });
1230
2106
  if (common.json) {
1231
- printJson(runtime.stdout, result.replyArtifactThread);
2107
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.replyArtifactThread, guidance));
1232
2108
  }
1233
2109
  else {
1234
2110
  printLine(runtime.stdout, `Replied to ${threadId}.`);
@@ -1260,6 +2136,18 @@ export async function runCli(argv, runtime = createRuntime()) {
1260
2136
  if (parsed.rest[1] === "auth") {
1261
2137
  printAuthHelp(runtime);
1262
2138
  }
2139
+ else if (parsed.rest[1] === "guidance") {
2140
+ printGuidanceHelp(runtime);
2141
+ }
2142
+ else if (parsed.rest[1] === "init") {
2143
+ printInitHelp(runtime);
2144
+ }
2145
+ else if (parsed.rest[1] === "corner") {
2146
+ printCornerHelp(runtime);
2147
+ }
2148
+ else if (parsed.rest[1] === "whoami") {
2149
+ printWhoamiHelp(runtime);
2150
+ }
1263
2151
  else if (parsed.rest[1] === "workstream" || parsed.rest[1] === "ws") {
1264
2152
  printWorkstreamHelp(runtime);
1265
2153
  }
@@ -1270,8 +2158,16 @@ export async function runCli(argv, runtime = createRuntime()) {
1270
2158
  case "version":
1271
2159
  printLine(runtime.stdout, runtime.getVersion());
1272
2160
  return 0;
2161
+ case "init":
2162
+ return handleInit(parsed.rest.slice(1), runtime, parsed.common);
2163
+ case "guidance":
2164
+ return handleGuidance(parsed.rest.slice(1), runtime, parsed.common);
1273
2165
  case "auth":
1274
2166
  return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
2167
+ case "corner":
2168
+ return handleCorner(parsed.rest.slice(1), runtime, parsed.common);
2169
+ case "whoami":
2170
+ return handleWhoAmI(parsed.rest.slice(1), runtime, parsed.common);
1275
2171
  case "workstream":
1276
2172
  case "ws":
1277
2173
  return handleWorkstream(parsed.rest.slice(1), runtime, parsed.common);