reffy-cli 1.5.0 → 1.7.1

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
@@ -10,7 +10,7 @@ import { archivePlanningChange } from "./plan-archive.js";
10
10
  import { createPlanScaffold } from "./plan.js";
11
11
  import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPlanningPath } from "./planning-paths.js";
12
12
  import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
13
- import { assertRemoteIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, requireWorkspaceIdentity, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, updateRemoteConfigMetadata, validateImportResult, } from "./remote.js";
13
+ import { assertWorkspaceSummaryIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureManagerInit, ensureWorkspaceTarget, extractWorkspaceSummaryIdentity, PaseoManagerClient, PaseoWorkspaceBackendClient, readRemoteConfig, RemoteHttpError, removeWorkspaceTarget, requireWorkspaceIdentity, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, updateRemoteConfigMetadata, validateImportResult, } from "./remote.js";
14
14
  import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
15
15
  import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
16
16
  import { ReferencesStore } from "./storage.js";
@@ -485,18 +485,29 @@ function planUsage() {
485
485
  function remoteUsage() {
486
486
  return [
487
487
  "Usage:",
488
- " reffy remote init [--repo PATH] [--output text|json] [--workspace-id ID] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--provision] [--env-file PATH]",
489
- " reffy remote status [--repo PATH] [--output text|json] [--workspace-id ID] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
490
- " reffy remote push [--repo PATH] [--output text|json] [--workspace-id ID] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
491
- " reffy remote ls [--repo PATH] [--output text|json] [--workspace-id ID] [--prefix PATH] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
492
- " reffy remote cat <path> [--repo PATH] [--output text|json] [--workspace-id ID] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
488
+ " reffy remote init [--repo PATH] [--output text|json] [--workspace-id ID] [--endpoint URL] [--manager-pod NAME] [--manager-actor ID] [--provision] [--create|--resolve] [--label TEXT] [--env-file PATH]",
489
+ " reffy remote status [--repo PATH] [--output text|json] [--workspace-id ID] [--project-id ID] [--env-file PATH]",
490
+ " reffy remote push [--repo PATH] [--output text|json] [--workspace-id ID] [--env-file PATH]",
491
+ " reffy remote ls [--repo PATH] [--output text|json] [--workspace-id ID] [--project-id ID] [--prefix PATH] [--env-file PATH]",
492
+ " reffy remote cat <path> [--repo PATH] [--output text|json] [--workspace-id ID] [--project-id ID] [--env-file PATH]",
493
+ " reffy remote snapshot [--repo PATH] [--output text|json] [--workspace-id ID] [--project-id ID] [--env-file PATH]",
494
+ " reffy remote workspace create <workspace-id> [--repo PATH] [--output text|json] [--label TEXT] [--env-file PATH]",
495
+ " reffy remote workspace get <workspace-id> [--repo PATH] [--output text|json] [--env-file PATH]",
496
+ " reffy remote workspace delete <workspace-id> --yes [--repo PATH] [--output text|json] [--env-file PATH]",
497
+ " reffy remote project register [--repo PATH] [--output text|json] [--workspace-id ID] [--project-id ID] [--env-file PATH]",
498
+ " reffy remote project list [--repo PATH] [--output text|json] [--workspace-id ID] [--env-file PATH]",
493
499
  "",
494
500
  "Options:",
495
501
  " --workspace-id ID Selected workspace projection. Required when the manifest has multiple workspace_ids; otherwise inferred.",
502
+ " --project-id ID Override the local project_id (defaults to manifest.project_id) when reading another contributor's documents or registration.",
496
503
  " --endpoint URL Paseo base URL. Defaults to saved config or PASEO_ENDPOINT/.env.",
497
- " --pod-name NAME Paseo pod name. Defaults to saved target for the selected workspace id or PASEO_POD_NAME/.env.",
498
- " --actor-id ID Paseo actor id. Defaults to saved target for the selected workspace id or PASEO_ACTOR_ID/.env.",
499
- " --provision Create missing pod/actor during `remote init`.",
504
+ " --manager-pod Paseo pod hosting the workspace manager actor. Defaults to saved config or PASEO_MANAGER_POD/.env.",
505
+ " --manager-actor reffyWorkspaceManager.v1 actor id. Defaults to saved config or PASEO_MANAGER_ACTOR/.env.",
506
+ " --provision During `remote init`, create a fresh pod and manager actor when one is not configured.",
507
+ " --create During `remote init`, force-create the workspace through the manager (recoverable via 409).",
508
+ " --resolve During `remote init`, resolve an existing workspace through the manager and fail if it does not exist.",
509
+ " --label TEXT Optional human-readable workspace label passed to `workspace create`.",
510
+ " --yes Confirm a destructive operation (required for `workspace delete`).",
500
511
  " --prefix PATH Prefix filter for `remote ls`.",
501
512
  " --env-file PATH Dotenv file to load before resolving env-backed options. Defaults to .env.",
502
513
  ].join("\n");
@@ -677,70 +688,69 @@ function parsePlanArgs(argv) {
677
688
  }
678
689
  return args;
679
690
  }
691
+ const REMOTE_VALUE_FLAGS = new Set([
692
+ "--repo",
693
+ "--output",
694
+ "--endpoint",
695
+ "--manager-pod",
696
+ "--manager-actor",
697
+ "--env-file",
698
+ "--prefix",
699
+ "--workspace-id",
700
+ "--project-id",
701
+ "--label",
702
+ ]);
703
+ const REMOTE_BOOLEAN_FLAGS = new Set(["--json", "--provision", "--create", "--resolve", "--yes"]);
680
704
  function parseRemoteArgs(argv) {
681
705
  const repoRoot = parseRepoArg(argv);
682
706
  const args = {
683
707
  repoRoot,
684
708
  provision: false,
709
+ create: false,
710
+ resolve: false,
711
+ yes: false,
712
+ };
713
+ const setStr = (flag, value) => {
714
+ if (!value)
715
+ throw new Error(`${flag} requires a value`);
716
+ return value;
685
717
  };
686
718
  for (let i = 0; i < argv.length; i += 1) {
687
719
  const arg = argv[i];
688
- if (arg === "--repo") {
720
+ if (arg === "--repo" || arg === "--output") {
689
721
  i += 1;
690
722
  continue;
691
723
  }
692
- if (arg.startsWith("--repo="))
724
+ if (arg.startsWith("--repo=") || arg.startsWith("--output="))
693
725
  continue;
694
726
  if (arg === "--json")
695
727
  continue;
696
- if (arg === "--output") {
697
- i += 1;
698
- continue;
699
- }
700
- if (arg.startsWith("--output="))
701
- continue;
702
728
  if (arg === "--endpoint") {
703
- const value = argv[i + 1];
704
- if (!value)
705
- throw new Error("--endpoint requires a value");
706
- args.endpoint = value;
707
- i += 1;
729
+ args.endpoint = setStr(arg, argv[++i]);
708
730
  continue;
709
731
  }
710
732
  if (arg.startsWith("--endpoint=")) {
711
733
  args.endpoint = arg.split("=", 2)[1];
712
734
  continue;
713
735
  }
714
- if (arg === "--pod-name") {
715
- const value = argv[i + 1];
716
- if (!value)
717
- throw new Error("--pod-name requires a value");
718
- args.podName = value;
719
- i += 1;
736
+ if (arg === "--manager-pod") {
737
+ args.managerPod = setStr(arg, argv[++i]);
720
738
  continue;
721
739
  }
722
- if (arg.startsWith("--pod-name=")) {
723
- args.podName = arg.split("=", 2)[1];
740
+ if (arg.startsWith("--manager-pod=")) {
741
+ args.managerPod = arg.split("=", 2)[1];
724
742
  continue;
725
743
  }
726
- if (arg === "--actor-id") {
727
- const value = argv[i + 1];
728
- if (!value)
729
- throw new Error("--actor-id requires a value");
730
- args.actorId = value;
731
- i += 1;
744
+ if (arg === "--manager-actor") {
745
+ args.managerActor = setStr(arg, argv[++i]);
732
746
  continue;
733
747
  }
734
- if (arg.startsWith("--actor-id=")) {
735
- args.actorId = arg.split("=", 2)[1];
748
+ if (arg.startsWith("--manager-actor=")) {
749
+ args.managerActor = arg.split("=", 2)[1];
736
750
  continue;
737
751
  }
738
752
  if (arg === "--env-file") {
739
- const value = argv[i + 1];
740
- if (!value)
741
- throw new Error("--env-file requires a value");
742
- args.envFile = value;
743
- i += 1;
753
+ args.envFile = setStr(arg, argv[++i]);
744
754
  continue;
745
755
  }
746
756
  if (arg.startsWith("--env-file=")) {
@@ -748,11 +758,7 @@ function parseRemoteArgs(argv) {
748
758
  continue;
749
759
  }
750
760
  if (arg === "--prefix") {
751
- const value = argv[i + 1];
752
- if (!value)
753
- throw new Error("--prefix requires a value");
754
- args.prefix = value;
755
- i += 1;
761
+ args.prefix = setStr(arg, argv[++i]);
756
762
  continue;
757
763
  }
758
764
  if (arg.startsWith("--prefix=")) {
@@ -760,54 +766,66 @@ function parseRemoteArgs(argv) {
760
766
  continue;
761
767
  }
762
768
  if (arg === "--workspace-id") {
763
- const value = argv[i + 1];
764
- if (!value)
765
- throw new Error("--workspace-id requires a value");
766
- args.workspaceId = value;
767
- i += 1;
769
+ args.workspaceId = setStr(arg, argv[++i]);
768
770
  continue;
769
771
  }
770
772
  if (arg.startsWith("--workspace-id=")) {
771
773
  args.workspaceId = arg.split("=", 2)[1];
772
774
  continue;
773
775
  }
776
+ if (arg === "--project-id") {
777
+ args.projectId = setStr(arg, argv[++i]);
778
+ continue;
779
+ }
780
+ if (arg.startsWith("--project-id=")) {
781
+ args.projectId = arg.split("=", 2)[1];
782
+ continue;
783
+ }
784
+ if (arg === "--label") {
785
+ args.label = setStr(arg, argv[++i]);
786
+ continue;
787
+ }
788
+ if (arg.startsWith("--label=")) {
789
+ args.label = arg.split("=", 2)[1];
790
+ continue;
791
+ }
774
792
  if (arg === "--provision") {
775
793
  args.provision = true;
776
794
  continue;
777
795
  }
796
+ if (arg === "--create") {
797
+ args.create = true;
798
+ continue;
799
+ }
800
+ if (arg === "--resolve") {
801
+ args.resolve = true;
802
+ continue;
803
+ }
804
+ if (arg === "--yes") {
805
+ args.yes = true;
806
+ continue;
807
+ }
778
808
  if (arg.startsWith("--")) {
779
809
  throw new Error(`Unknown remote option: ${arg}`);
780
810
  }
781
811
  }
812
+ if (args.create && args.resolve) {
813
+ throw new Error("--create and --resolve are mutually exclusive");
814
+ }
782
815
  return args;
783
816
  }
784
817
  function getRemotePositionalArgs(argv) {
785
818
  const positionals = [];
786
819
  for (let i = 0; i < argv.length; i += 1) {
787
820
  const arg = argv[i];
788
- if (arg === "--repo" ||
789
- arg === "--output" ||
790
- arg === "--endpoint" ||
791
- arg === "--pod-name" ||
792
- arg === "--actor-id" ||
793
- arg === "--env-file" ||
794
- arg === "--prefix" ||
795
- arg === "--workspace-id") {
821
+ if (REMOTE_VALUE_FLAGS.has(arg)) {
796
822
  i += 1;
797
823
  continue;
798
824
  }
799
- if (arg.startsWith("--repo=") ||
800
- arg.startsWith("--output=") ||
801
- arg.startsWith("--endpoint=") ||
802
- arg.startsWith("--pod-name=") ||
803
- arg.startsWith("--actor-id=") ||
804
- arg.startsWith("--env-file=") ||
805
- arg.startsWith("--prefix=") ||
806
- arg.startsWith("--workspace-id=") ||
807
- arg === "--json" ||
808
- arg === "--provision") {
825
+ if (Array.from(REMOTE_VALUE_FLAGS).some((flag) => arg.startsWith(`${flag}=`)))
826
+ continue;
827
+ if (REMOTE_BOOLEAN_FLAGS.has(arg))
809
828
  continue;
810
- }
811
829
  if (arg.startsWith("--"))
812
830
  continue;
813
831
  positionals.push(arg);
@@ -830,6 +848,10 @@ async function resolveRemoteContext(args, opts = {}) {
830
848
  const identityInfo = await store.getWorkspaceIdentity();
831
849
  const workspaceId = resolveSelectedWorkspaceId(identityInfo, args.workspaceId);
832
850
  const identity = requireWorkspaceIdentity(identityInfo, workspaceId);
851
+ const projectId = (args.projectId ?? identity.project_id).trim();
852
+ if (!projectId) {
853
+ throw new Error("Could not resolve a project id; pass --project-id or define manifest.project_id");
854
+ }
833
855
  let savedConfig = null;
834
856
  let replacedLegacy = false;
835
857
  try {
@@ -837,37 +859,252 @@ async function resolveRemoteContext(args, opts = {}) {
837
859
  }
838
860
  catch (error) {
839
861
  const message = error instanceof Error ? error.message : String(error);
840
- if (opts.tolerateLegacyV1Config && message.includes("legacy v1 single-target shape")) {
862
+ if (opts.tolerateLegacyConfig &&
863
+ (message.includes("legacy v1 single-target shape") ||
864
+ message.includes("legacy v2 combined-backend shape") ||
865
+ message.includes("legacy v3 shape"))) {
841
866
  replacedLegacy = true;
842
867
  }
843
868
  else {
844
869
  throw error;
845
870
  }
846
871
  }
847
- const overrideValues = {
848
- endpoint: args.endpoint ?? process.env.PASEO_ENDPOINT,
849
- pod_name: args.podName ?? process.env.PASEO_POD_NAME,
850
- actor_id: args.actorId ?? process.env.PASEO_ACTOR_ID,
851
- };
852
- const resolvedTarget = resolveRemoteTarget(savedConfig, workspaceId, overrideValues);
853
- const hasOverrides = Boolean(overrideValues.endpoint || overrideValues.pod_name || overrideValues.actor_id);
854
- const savedHasTarget = Boolean(savedConfig?.targets?.[workspaceId]);
855
- const linkageSource = resolvedTarget
856
- ? savedHasTarget && !hasOverrides
857
- ? "saved_config"
858
- : "cli_or_env_overrides"
859
- : "unconfigured";
872
+ const endpointForResolve = (args.endpoint ?? process.env.PASEO_ENDPOINT ?? "").trim();
873
+ const resolvedTarget = endpointForResolve
874
+ ? resolveRemoteTarget(savedConfig, workspaceId, endpointForResolve)
875
+ : null;
860
876
  return {
861
877
  store,
862
878
  identity,
863
879
  workspaceId,
880
+ projectId,
864
881
  configPath: resolveRemoteConfigPath(args.repoRoot),
865
882
  savedConfig,
866
883
  resolvedTarget,
867
- linkageSource,
868
884
  replacedLegacy,
869
885
  };
870
886
  }
887
+ function resolvePaseoEndpoint(args) {
888
+ const endpoint = (args.endpoint ?? process.env.PASEO_ENDPOINT ?? "").trim();
889
+ if (!endpoint) {
890
+ throw new Error("PASEO_ENDPOINT is required for remote commands. Add it to your .env (or export it in your shell) and retry. The endpoint is no longer persisted in .reffy/state/remote.json.");
891
+ }
892
+ return endpoint;
893
+ }
894
+ function resolvePaseoToken(opts) {
895
+ const token = process.env.PASEO_TOKEN?.trim() || undefined;
896
+ if (opts.required && !token) {
897
+ throw new Error("PASEO_TOKEN is required for remote commands. It is the bearer token returned by `reffy remote init --provision`; copy it from your team secret store into your .env (or export it). The CLI never persists this token to disk.");
898
+ }
899
+ return token;
900
+ }
901
+ function findRemoteHttpError(error) {
902
+ if (error instanceof RemoteHttpError)
903
+ return error;
904
+ if (error instanceof Error && error.cause)
905
+ return findRemoteHttpError(error.cause);
906
+ return null;
907
+ }
908
+ function decorateRemoteError(error, fallback) {
909
+ const httpError = findRemoteHttpError(error);
910
+ if (!httpError)
911
+ return fallback;
912
+ // Inspect the request URL to decide which hint applies. The path shape
913
+ // unambiguously tells us whether the call was a manager workspace lookup,
914
+ // a manager project registration, or a workspace-backend per-project read.
915
+ let pathname = "";
916
+ try {
917
+ pathname = new URL(httpError.url).pathname;
918
+ }
919
+ catch {
920
+ pathname = httpError.url;
921
+ }
922
+ const projectRouteMatch = /\/projects\/[^/]+(?:\/|$)/.test(pathname);
923
+ const workspacesRouteMatch = /\/workspaces\/[^/]+(?:\/|$)/.test(pathname);
924
+ const backendProjectMatch = /\/workspace\/projects\/[^/]+/.test(pathname);
925
+ if (httpError.status === 401) {
926
+ return `${fallback}\nAuthorization rejected by Paseo. Check that PASEO_TOKEN matches the current manager token in your team secret store, and that it is exported into the shell running reffy.`;
927
+ }
928
+ if (httpError.status === 404) {
929
+ if (backendProjectMatch || (projectRouteMatch && workspacesRouteMatch)) {
930
+ return `${fallback}\nThe project is not registered for this workspace. Run \`reffy remote project register\` (or \`reffy remote init\`) to register, then retry.`;
931
+ }
932
+ if (workspacesRouteMatch) {
933
+ return `${fallback}\nThe workspace was not found on the manager. Run \`reffy remote workspace create <workspace-id>\` to provision it, then retry.`;
934
+ }
935
+ }
936
+ return fallback;
937
+ }
938
+ async function handleRemoteWorkspaceSubcommand(parsed, remoteArgs, output, reportError) {
939
+ const positionals = getRemotePositionalArgs(remoteArgs);
940
+ // positionals[0] is the workspace subcommand verb (create|get); positionals[1] is the workspace id
941
+ const verb = positionals[0];
942
+ const workspaceIdArg = positionals[1];
943
+ if (!verb || (verb !== "create" && verb !== "get" && verb !== "delete")) {
944
+ console.error("reffy remote workspace requires a subcommand: create | get | delete");
945
+ console.error(remoteUsage());
946
+ return 1;
947
+ }
948
+ if (!workspaceIdArg) {
949
+ console.error(`reffy remote workspace ${verb} requires a workspace id`);
950
+ return 1;
951
+ }
952
+ try {
953
+ await loadDotEnvIfPresent(parsed.repoRoot, parsed.envFile ?? ".env");
954
+ const endpoint = resolvePaseoEndpoint(parsed);
955
+ const token = resolvePaseoToken({ required: true });
956
+ const savedConfig = await readRemoteConfig(parsed.repoRoot);
957
+ if (!savedConfig) {
958
+ throw new Error("Remote linkage is not initialized. Run `reffy remote init --provision` (or with --manager-pod/--manager-actor) first.");
959
+ }
960
+ if (verb === "delete") {
961
+ if (!parsed.yes) {
962
+ throw new Error(`Refusing to delete workspace "${workspaceIdArg}" without --yes. Workspace deletion is destructive: it removes the shared workspace and all contributions stored under it on the manager. Re-run with --yes to confirm.`);
963
+ }
964
+ const manager = new PaseoManagerClient(endpoint, savedConfig.manager, token);
965
+ const result = await manager.deleteWorkspace(workspaceIdArg);
966
+ const removal = await removeWorkspaceTarget(parsed.repoRoot, savedConfig, workspaceIdArg);
967
+ const payload = {
968
+ status: "ok",
969
+ command: "remote",
970
+ subcommand: "workspace delete",
971
+ workspace_id: workspaceIdArg,
972
+ manager: savedConfig.manager,
973
+ already_absent: result.alreadyAbsent,
974
+ local_target_removed: removal.existed,
975
+ configured_workspace_ids: Object.keys(removal.config.targets).sort(),
976
+ };
977
+ if (output === "json") {
978
+ printResult(output, payload);
979
+ }
980
+ else {
981
+ console.log(`Workspace delete: ${result.alreadyAbsent ? "already_absent" : "deleted"}`);
982
+ console.log(`Workspace id: ${workspaceIdArg}`);
983
+ console.log(`Local target removed: ${removal.existed ? "yes" : "no"}`);
984
+ console.log(`Configured workspace ids: ${payload.configured_workspace_ids.join(", ") || "(none)"}`);
985
+ }
986
+ return 0;
987
+ }
988
+ const result = await ensureWorkspaceTarget(parsed.repoRoot, savedConfig, {
989
+ workspaceId: workspaceIdArg,
990
+ mode: verb === "create" ? "create" : "resolve",
991
+ endpoint,
992
+ token,
993
+ metadata: parsed.label ? { label: parsed.label } : undefined,
994
+ });
995
+ const payload = {
996
+ status: "ok",
997
+ command: "remote",
998
+ subcommand: `workspace ${verb}`,
999
+ workspace_id: workspaceIdArg,
1000
+ manager: result.config.manager,
1001
+ workspace_backend: result.workspace_backend,
1002
+ outcome: result.outcome,
1003
+ };
1004
+ if (output === "json") {
1005
+ printResult(output, payload);
1006
+ }
1007
+ else {
1008
+ console.log(`Workspace ${verb}: ${result.outcome}`);
1009
+ console.log(`Workspace id: ${workspaceIdArg}`);
1010
+ console.log(`Workspace backend: ${result.workspace_backend.pod_name}/${result.workspace_backend.actor_id}`);
1011
+ }
1012
+ return 0;
1013
+ }
1014
+ catch (error) {
1015
+ return reportError(`workspace ${verb}`, error);
1016
+ }
1017
+ }
1018
+ async function handleRemoteProjectSubcommand(parsed, remoteArgs, output, reportError) {
1019
+ const positionals = getRemotePositionalArgs(remoteArgs);
1020
+ const verb = positionals[0];
1021
+ if (!verb || (verb !== "register" && verb !== "list")) {
1022
+ console.error("reffy remote project requires a subcommand: register | list");
1023
+ console.error(remoteUsage());
1024
+ return 1;
1025
+ }
1026
+ try {
1027
+ const context = await resolveRemoteContext(parsed);
1028
+ if (!context.savedConfig) {
1029
+ throw new Error("Remote linkage is not initialized. Run `reffy remote init --provision` (or with --manager-pod/--manager-actor) first.");
1030
+ }
1031
+ const endpoint = resolvePaseoEndpoint(parsed);
1032
+ const token = resolvePaseoToken({ required: true });
1033
+ const manager = new PaseoManagerClient(endpoint, context.savedConfig.manager, token);
1034
+ if (verb === "register") {
1035
+ const registration = await manager.registerProject(context.workspaceId, context.projectId);
1036
+ const payload = {
1037
+ status: "ok",
1038
+ command: "remote",
1039
+ subcommand: "project register",
1040
+ workspace_id: context.workspaceId,
1041
+ project_id: context.projectId,
1042
+ registration: registration.alreadyRegistered ? "already_registered" : "registered",
1043
+ };
1044
+ if (output === "json") {
1045
+ printResult(output, payload);
1046
+ }
1047
+ else {
1048
+ console.log(`Project registration: ${payload.registration}`);
1049
+ console.log(`Workspace id: ${payload.workspace_id}`);
1050
+ console.log(`Project id: ${payload.project_id}`);
1051
+ }
1052
+ return 0;
1053
+ }
1054
+ // list
1055
+ const list = await manager.listProjects(context.workspaceId);
1056
+ const projects = (list.projects ?? [])
1057
+ .map((p) => (typeof p.project_id === "string" ? p.project_id : ""))
1058
+ .filter(Boolean)
1059
+ .sort();
1060
+ const payload = {
1061
+ status: "ok",
1062
+ command: "remote",
1063
+ subcommand: "project list",
1064
+ workspace_id: context.workspaceId,
1065
+ projects,
1066
+ };
1067
+ if (output === "json") {
1068
+ printResult(output, payload);
1069
+ }
1070
+ else if (projects.length === 0) {
1071
+ console.log("(no projects registered)");
1072
+ }
1073
+ else {
1074
+ for (const id of projects)
1075
+ console.log(id);
1076
+ }
1077
+ return 0;
1078
+ }
1079
+ catch (error) {
1080
+ return reportError(`project ${verb}`, error);
1081
+ }
1082
+ }
1083
+ async function ensureResolvedTarget(args, context, runtime) {
1084
+ if (context.resolvedTarget) {
1085
+ return { target: context.resolvedTarget, recovered: false };
1086
+ }
1087
+ if (!context.savedConfig) {
1088
+ throw new Error(`Remote linkage is not configured. Run \`reffy remote init --workspace-id ${context.workspaceId}\` to configure manager identity and resolve the workspace backend. Expected config path: ${context.configPath}`);
1089
+ }
1090
+ // Manager identity is configured but workspace_backend identity is missing for this workspace.
1091
+ // Recover by resolving through the manager.
1092
+ const result = await ensureWorkspaceTarget(args.repoRoot, context.savedConfig, {
1093
+ workspaceId: context.workspaceId,
1094
+ mode: "resolve",
1095
+ endpoint: runtime.endpoint,
1096
+ token: runtime.token,
1097
+ });
1098
+ return {
1099
+ target: {
1100
+ endpoint: runtime.endpoint,
1101
+ manager: result.config.manager,
1102
+ workspace_backend: result.workspace_backend,
1103
+ last_imported_at: result.config.targets[context.workspaceId]?.last_imported_at,
1104
+ },
1105
+ recovered: true,
1106
+ };
1107
+ }
871
1108
  async function main() {
872
1109
  const [, , command, ...rest] = process.argv;
873
1110
  if (!command || command === "--help" || command === "-h") {
@@ -925,107 +1162,165 @@ async function main() {
925
1162
  }
926
1163
  const output = parseOutputMode(remoteArgs);
927
1164
  const parsed = parseRemoteArgs(remoteArgs);
1165
+ const reportError = (subcmd, error) => {
1166
+ const message = error instanceof Error ? error.message : String(error);
1167
+ const decorated = decorateRemoteError(error, message);
1168
+ if (output === "json") {
1169
+ printResult(output, { status: "error", command: "remote", subcommand: subcmd, error: decorated });
1170
+ }
1171
+ else {
1172
+ console.error(decorated);
1173
+ }
1174
+ return 1;
1175
+ };
928
1176
  try {
1177
+ if (subcommand === "workspace") {
1178
+ return await handleRemoteWorkspaceSubcommand(parsed, remoteArgs, output, reportError);
1179
+ }
1180
+ if (subcommand === "project") {
1181
+ return await handleRemoteProjectSubcommand(parsed, remoteArgs, output, reportError);
1182
+ }
929
1183
  if (subcommand === "init") {
930
- const { identity, workspaceId, configPath, savedConfig, replacedLegacy } = await resolveRemoteContext(parsed, { tolerateLegacyV1Config: true });
931
- const endpoint = parsed.endpoint ?? process.env.PASEO_ENDPOINT ?? savedConfig?.endpoint;
932
- if (!endpoint) {
933
- throw new Error("Remote init requires --endpoint or PASEO_ENDPOINT (optionally loaded from .env)");
934
- }
935
- const result = await ensureRemoteInit(parsed.repoRoot, {
1184
+ const { identity, workspaceId, projectId, configPath, savedConfig, replacedLegacy } = await resolveRemoteContext(parsed, { tolerateLegacyConfig: true });
1185
+ const endpoint = resolvePaseoEndpoint(parsed);
1186
+ // Token is required up front unless we're going to create a fresh manager actor
1187
+ // during this run (which is what mints it). That happens when no manager actor id
1188
+ // is configured anywhere and --provision is set.
1189
+ const willMintToken = parsed.provision &&
1190
+ !(parsed.managerActor ?? process.env.PASEO_MANAGER_ACTOR ?? savedConfig?.manager.actor_id);
1191
+ const tokenFromEnv = resolvePaseoToken({ required: !willMintToken });
1192
+ const managerInit = await ensureManagerInit(parsed.repoRoot, {
936
1193
  endpoint,
937
- podName: parsed.podName ?? process.env.PASEO_POD_NAME,
938
- actorId: parsed.actorId ?? process.env.PASEO_ACTOR_ID,
1194
+ managerPodName: parsed.managerPod ?? process.env.PASEO_MANAGER_POD,
1195
+ managerActorId: parsed.managerActor ?? process.env.PASEO_MANAGER_ACTOR,
939
1196
  provision: parsed.provision,
940
- identity,
941
1197
  existingConfig: savedConfig,
942
1198
  });
1199
+ const effectiveToken = managerInit.manager_auth_token ?? tokenFromEnv;
1200
+ if (!effectiveToken) {
1201
+ throw new Error("Internal error: no bearer token available after manager init. Either --provision should have minted one or PASEO_TOKEN should have been required up front.");
1202
+ }
1203
+ const targetMode = parsed.create
1204
+ ? "create"
1205
+ : parsed.resolve
1206
+ ? "resolve"
1207
+ : "auto";
1208
+ const targetResult = await ensureWorkspaceTarget(parsed.repoRoot, managerInit.config, {
1209
+ workspaceId,
1210
+ mode: targetMode,
1211
+ endpoint,
1212
+ token: effectiveToken,
1213
+ metadata: parsed.label ? { label: parsed.label } : undefined,
1214
+ });
1215
+ const manager = new PaseoManagerClient(endpoint, targetResult.config.manager, effectiveToken);
1216
+ const registration = await manager.registerProject(workspaceId, projectId);
943
1217
  const gitignore = await ensureGitignoreEntries(parsed.repoRoot, [".reffy/state/"]);
1218
+ const tokenJustIssued = managerInit.manager_auth_token != null;
944
1219
  const payload = {
945
1220
  status: "ok",
946
1221
  command: "remote",
947
1222
  subcommand: "init",
948
- provider: result.config.provider,
1223
+ provider: targetResult.config.provider,
949
1224
  config_path: configPath,
950
- endpoint: result.config.endpoint,
1225
+ endpoint,
951
1226
  workspace_id: workspaceId,
952
- pod_name: result.target.pod_name,
953
- actor_id: result.target.actor_id,
954
- created_pod: result.created_pod,
955
- created_actor: result.created_actor,
956
- project_id: identity.project_id,
957
- linkage_mode: result.created_pod || result.created_actor ? "provisioned" : "existing",
958
- configured_workspace_ids: Object.keys(result.config.targets).sort(),
959
- replaced_legacy_v1_config: replacedLegacy,
1227
+ project_id: projectId,
1228
+ manager: targetResult.config.manager,
1229
+ workspace_backend: targetResult.workspace_backend,
1230
+ created_pod: managerInit.created_pod,
1231
+ created_manager_actor: managerInit.created_actor,
1232
+ workspace_outcome: targetResult.outcome,
1233
+ project_registration: registration.alreadyRegistered ? "already_registered" : "registered",
1234
+ configured_workspace_ids: Object.keys(targetResult.config.targets).sort(),
1235
+ replaced_legacy_config: replacedLegacy,
960
1236
  gitignore_path: gitignore.path,
961
1237
  gitignore_added: gitignore.added,
1238
+ // One-time issuance: present in this response only; never persisted to disk.
1239
+ manager_token: managerInit.manager_auth_token ?? null,
1240
+ manager_token_persisted: false,
962
1241
  };
963
1242
  if (output === "json") {
964
1243
  printResult(output, payload);
965
1244
  }
966
1245
  else {
967
1246
  if (replacedLegacy) {
968
- console.log("Replacing legacy v1 remote linkage with v2 target map");
1247
+ console.log("Replacing legacy remote linkage with v4 (no endpoint, bearer-token auth)");
969
1248
  }
970
1249
  console.log("Remote linkage ready");
971
1250
  console.log(`Config: ${payload.config_path}`);
972
- console.log(`Endpoint: ${payload.endpoint}`);
1251
+ console.log(`Endpoint: ${payload.endpoint} (sourced from env, not persisted)`);
1252
+ console.log(`Manager: ${payload.manager.pod_name}/${payload.manager.actor_id}`);
973
1253
  console.log(`Workspace id: ${payload.workspace_id}`);
974
- console.log(`Pod: ${payload.pod_name}`);
975
- console.log(`Actor: ${payload.actor_id}`);
1254
+ console.log(`Workspace backend: ${payload.workspace_backend.pod_name}/${payload.workspace_backend.actor_id}`);
1255
+ console.log(`Workspace outcome: ${payload.workspace_outcome}`);
976
1256
  console.log(`Project identity: ${payload.project_id}`);
1257
+ console.log(`Project registration: ${payload.project_registration}`);
977
1258
  console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
978
- console.log(`Created actor: ${payload.created_actor ? "yes" : "no"}`);
979
- console.log(`Linkage mode: ${payload.linkage_mode}`);
1259
+ console.log(`Created manager actor: ${payload.created_manager_actor ? "yes" : "no"}`);
980
1260
  console.log(`Configured workspace ids: ${payload.configured_workspace_ids.join(", ")}`);
981
1261
  if (payload.gitignore_added.length > 0) {
982
1262
  console.log(`Updated .gitignore: ${payload.gitignore_added.join(", ")}`);
983
1263
  }
1264
+ if (tokenJustIssued && payload.manager_token) {
1265
+ console.log("");
1266
+ console.log("=================== MANAGER BEARER TOKEN ===================");
1267
+ console.log("Save this now. It will not be shown again and is not stored on disk.");
1268
+ console.log("Add it to your team secret store and export PASEO_TOKEN before");
1269
+ console.log("running any further reffy remote commands in this or any other repo.");
1270
+ console.log("");
1271
+ console.log(` ${payload.manager_token}`);
1272
+ console.log("============================================================");
1273
+ }
984
1274
  }
985
1275
  return 0;
986
1276
  }
987
1277
  const context = await resolveRemoteContext(parsed);
988
- if (!context.resolvedTarget) {
989
- throw new Error(`Remote linkage is not configured for workspace_id "${context.workspaceId}". Run \`reffy remote init --workspace-id ${context.workspaceId}\` or provide endpoint, pod name, and actor id. Expected config path: ${context.configPath}`);
990
- }
991
- const client = new PaseoRemoteClient(context.resolvedTarget);
1278
+ const endpoint = resolvePaseoEndpoint(parsed);
1279
+ const token = resolvePaseoToken({ required: true });
1280
+ const { target: resolvedTarget, recovered } = await ensureResolvedTarget(parsed, context, {
1281
+ endpoint,
1282
+ token,
1283
+ });
1284
+ const backendClient = new PaseoWorkspaceBackendClient(resolvedTarget.endpoint, resolvedTarget.workspace_backend, token);
1285
+ const managerClient = new PaseoManagerClient(resolvedTarget.endpoint, resolvedTarget.manager, token);
992
1286
  const linkageLabel = describeRemoteLinkage({
993
- endpoint: context.resolvedTarget.endpoint,
994
- pod_name: context.resolvedTarget.pod_name,
995
- actor_id: context.resolvedTarget.actor_id,
1287
+ endpoint: resolvedTarget.endpoint,
1288
+ manager: resolvedTarget.manager,
1289
+ workspace_backend: resolvedTarget.workspace_backend,
996
1290
  workspace_id: context.workspaceId,
997
1291
  });
998
1292
  if (subcommand === "status") {
999
1293
  let workspace;
1000
1294
  try {
1001
- workspace = await client.getWorkspace(context.workspaceId);
1295
+ workspace = await backendClient.getWorkspace();
1002
1296
  }
1003
1297
  catch (error) {
1004
1298
  const message = error instanceof Error ? error.message : String(error);
1005
- throw new Error(`Remote status failed using ${linkageLabel}: ${message}`);
1299
+ throw new Error(`Remote status failed using ${linkageLabel}: ${message}`, { cause: error });
1006
1300
  }
1007
- assertRemoteIdentity(workspace, context.identity);
1008
- const remoteIdentity = extractWorkspaceIdentity(workspace);
1301
+ assertWorkspaceSummaryIdentity(workspace, context.workspaceId);
1302
+ const summaryIdentity = extractWorkspaceSummaryIdentity(workspace);
1009
1303
  const payload = {
1010
1304
  status: "ok",
1011
1305
  command: "remote",
1012
1306
  subcommand: "status",
1013
1307
  config_path: context.configPath,
1014
1308
  provider: "paseo",
1015
- endpoint: context.resolvedTarget.endpoint,
1309
+ endpoint: resolvedTarget.endpoint,
1016
1310
  workspace_id: context.workspaceId,
1017
- pod_name: context.resolvedTarget.pod_name,
1018
- actor_id: context.resolvedTarget.actor_id,
1019
- linkage_source: context.linkageSource,
1311
+ project_id: context.projectId,
1312
+ manager: resolvedTarget.manager,
1313
+ workspace_backend: resolvedTarget.workspace_backend,
1314
+ recovered_workspace_backend: recovered,
1020
1315
  local_identity: context.identity,
1021
1316
  remote_identity: {
1022
- project_id: remoteIdentity.project_id,
1023
- workspace_id: remoteIdentity.workspace_id,
1024
- actor_type: remoteIdentity.actor_type,
1025
- backend_version: remoteIdentity.backend_version,
1317
+ workspace_id: summaryIdentity.workspace_id,
1318
+ actor_type: summaryIdentity.actor_type,
1319
+ backend_version: summaryIdentity.backend_version,
1026
1320
  },
1027
- document_count: remoteIdentity.document_count,
1028
- last_imported_at: context.resolvedTarget.last_imported_at ?? null,
1321
+ document_count: summaryIdentity.document_count,
1322
+ registered_project_count: summaryIdentity.registered_project_count,
1323
+ last_imported_at: resolvedTarget.last_imported_at ?? null,
1029
1324
  workspace,
1030
1325
  };
1031
1326
  if (output === "json") {
@@ -1035,50 +1330,55 @@ async function main() {
1035
1330
  console.log("Remote workspace reachable");
1036
1331
  console.log(`Config: ${payload.config_path}`);
1037
1332
  console.log(`Endpoint: ${payload.endpoint}`);
1333
+ console.log(`Manager: ${payload.manager.pod_name}/${payload.manager.actor_id}`);
1038
1334
  console.log(`Workspace id: ${payload.workspace_id}`);
1039
- console.log(`Pod: ${payload.pod_name}`);
1040
- console.log(`Actor: ${payload.actor_id}`);
1041
- console.log(`Linkage source: ${payload.linkage_source}`);
1335
+ console.log(`Workspace backend: ${payload.workspace_backend.pod_name}/${payload.workspace_backend.actor_id}`);
1336
+ if (recovered)
1337
+ console.log("Recovered workspace backend identity through the manager");
1042
1338
  console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.workspace_id}`);
1043
- console.log(`Remote identity: ${String(payload.remote_identity.project_id)}/${String(payload.remote_identity.workspace_id)} (${String(payload.remote_identity.actor_type)} ${String(payload.remote_identity.backend_version)})`);
1339
+ console.log(`Remote identity: workspace=${String(payload.remote_identity.workspace_id)} (${String(payload.remote_identity.actor_type)} ${String(payload.remote_identity.backend_version)})`);
1044
1340
  console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
1341
+ console.log(`Registered projects: ${String(payload.registered_project_count ?? "unknown")}`);
1045
1342
  console.log(`Last imported at: ${String(payload.last_imported_at ?? "never")}`);
1046
1343
  }
1047
1344
  return 0;
1048
1345
  }
1049
1346
  if (subcommand === "push") {
1050
1347
  const documents = await collectWorkspaceDocuments(parsed.repoRoot);
1348
+ const registration = await managerClient.registerProject(context.workspaceId, context.projectId);
1051
1349
  let importResult;
1052
1350
  try {
1053
- importResult = validateImportResult(await client.importWorkspace(context.workspaceId, documents, true));
1351
+ importResult = validateImportResult(await backendClient.importProject(context.projectId, documents, true));
1054
1352
  }
1055
1353
  catch (error) {
1056
1354
  const message = error instanceof Error ? error.message : String(error);
1057
- throw new Error(`Remote push failed using ${linkageLabel}: ${message}`);
1355
+ throw new Error(`Remote push failed using ${linkageLabel}: ${message}`, { cause: error });
1058
1356
  }
1059
1357
  let workspace;
1060
1358
  try {
1061
- workspace = await client.getWorkspace(context.workspaceId);
1359
+ workspace = await backendClient.getWorkspace();
1062
1360
  }
1063
1361
  catch (error) {
1064
1362
  const message = error instanceof Error ? error.message : String(error);
1065
- throw new Error(`Remote push could not verify remote state using ${linkageLabel}: ${message}`);
1363
+ throw new Error(`Remote push could not verify remote state using ${linkageLabel}: ${message}`, { cause: error });
1066
1364
  }
1067
- assertRemoteIdentity(workspace, context.identity);
1365
+ assertWorkspaceSummaryIdentity(workspace, context.workspaceId);
1068
1366
  const importedAt = new Date().toISOString();
1069
1367
  await updateRemoteConfigMetadata(parsed.repoRoot, context.workspaceId, {
1070
1368
  last_imported_at: importedAt,
1071
1369
  });
1370
+ const summary = extractWorkspaceSummaryIdentity(workspace);
1072
1371
  const payload = {
1073
1372
  status: "ok",
1074
1373
  command: "remote",
1075
1374
  subcommand: "push",
1076
- endpoint: context.resolvedTarget.endpoint,
1375
+ endpoint: resolvedTarget.endpoint,
1077
1376
  workspace_id: context.workspaceId,
1078
- pod_name: context.resolvedTarget.pod_name,
1079
- actor_id: context.resolvedTarget.actor_id,
1080
- linkage_source: context.linkageSource,
1081
- project_id: context.identity.project_id,
1377
+ project_id: context.projectId,
1378
+ manager: resolvedTarget.manager,
1379
+ workspace_backend: resolvedTarget.workspace_backend,
1380
+ recovered_workspace_backend: recovered,
1381
+ project_registration: registration.alreadyRegistered ? "already_registered" : "registered",
1082
1382
  local_document_count: documents.length,
1083
1383
  imported: importResult.imported,
1084
1384
  created: importResult.created,
@@ -1086,7 +1386,7 @@ async function main() {
1086
1386
  deleted: importResult.deleted,
1087
1387
  replace_missing: true,
1088
1388
  last_imported_at: importedAt,
1089
- remote_document_count: extractWorkspaceIdentity(workspace).document_count,
1389
+ remote_document_count: summary.document_count,
1090
1390
  };
1091
1391
  if (output === "json") {
1092
1392
  printResult(output, payload);
@@ -1095,9 +1395,11 @@ async function main() {
1095
1395
  console.log("Remote push complete");
1096
1396
  console.log(`Endpoint: ${payload.endpoint}`);
1097
1397
  console.log(`Workspace id: ${payload.workspace_id}`);
1098
- console.log(`Pod: ${payload.pod_name}`);
1099
- console.log(`Actor: ${payload.actor_id}`);
1100
- console.log(`Linkage source: ${payload.linkage_source}`);
1398
+ console.log(`Project id: ${payload.project_id}`);
1399
+ console.log(`Project registration: ${payload.project_registration}`);
1400
+ console.log(`Workspace backend: ${payload.workspace_backend.pod_name}/${payload.workspace_backend.actor_id}`);
1401
+ if (recovered)
1402
+ console.log("Recovered workspace backend identity through the manager");
1101
1403
  console.log("Mode: remote reflects local (replace_missing=true)");
1102
1404
  console.log(`Local document count: ${String(payload.local_document_count)}`);
1103
1405
  console.log(`Imported: ${String(payload.imported)}`);
@@ -1110,15 +1412,15 @@ async function main() {
1110
1412
  return 0;
1111
1413
  }
1112
1414
  if (subcommand === "ls") {
1113
- let snapshot;
1415
+ let listing;
1114
1416
  try {
1115
- snapshot = await client.getSnapshot(context.workspaceId, parsed.prefix);
1417
+ listing = await backendClient.listProjectDocuments(context.projectId, { prefix: parsed.prefix });
1116
1418
  }
1117
1419
  catch (error) {
1118
1420
  const message = error instanceof Error ? error.message : String(error);
1119
- throw new Error(`Remote ls failed using ${linkageLabel}: ${message}`);
1421
+ throw new Error(`Remote ls failed using ${linkageLabel}: ${message}`, { cause: error });
1120
1422
  }
1121
- const paths = (snapshot.documents ?? [])
1423
+ const paths = (listing.documents ?? [])
1122
1424
  .map((document) => (typeof document.path === "string" ? document.path : ""))
1123
1425
  .filter(Boolean)
1124
1426
  .sort();
@@ -1126,11 +1428,10 @@ async function main() {
1126
1428
  status: "ok",
1127
1429
  command: "remote",
1128
1430
  subcommand: "ls",
1129
- endpoint: context.resolvedTarget.endpoint,
1431
+ endpoint: resolvedTarget.endpoint,
1130
1432
  workspace_id: context.workspaceId,
1131
- pod_name: context.resolvedTarget.pod_name,
1132
- actor_id: context.resolvedTarget.actor_id,
1133
- linkage_source: context.linkageSource,
1433
+ project_id: context.projectId,
1434
+ workspace_backend: resolvedTarget.workspace_backend,
1134
1435
  prefix: parsed.prefix ?? null,
1135
1436
  paths,
1136
1437
  };
@@ -1157,11 +1458,11 @@ async function main() {
1157
1458
  }
1158
1459
  let document;
1159
1460
  try {
1160
- document = await client.getDocument(context.workspaceId, documentPath);
1461
+ document = await backendClient.getProjectDocument(context.projectId, documentPath);
1161
1462
  }
1162
1463
  catch (error) {
1163
1464
  const message = error instanceof Error ? error.message : String(error);
1164
- throw new Error(`Remote cat failed using ${linkageLabel}: ${message}`);
1465
+ throw new Error(`Remote cat failed using ${linkageLabel}: ${message}`, { cause: error });
1165
1466
  }
1166
1467
  if (!document.document) {
1167
1468
  throw new Error(`Remote document not found using ${linkageLabel}: ${documentPath}`);
@@ -1170,11 +1471,10 @@ async function main() {
1170
1471
  status: "ok",
1171
1472
  command: "remote",
1172
1473
  subcommand: "cat",
1173
- endpoint: context.resolvedTarget.endpoint,
1474
+ endpoint: resolvedTarget.endpoint,
1174
1475
  workspace_id: context.workspaceId,
1175
- pod_name: context.resolvedTarget.pod_name,
1176
- actor_id: context.resolvedTarget.actor_id,
1177
- linkage_source: context.linkageSource,
1476
+ project_id: context.projectId,
1477
+ workspace_backend: resolvedTarget.workspace_backend,
1178
1478
  document: document.document,
1179
1479
  };
1180
1480
  if (output === "json") {
@@ -1189,24 +1489,44 @@ async function main() {
1189
1489
  }
1190
1490
  return 0;
1191
1491
  }
1492
+ if (subcommand === "snapshot") {
1493
+ let snapshot;
1494
+ try {
1495
+ snapshot = await backendClient.getProjectSnapshot(context.projectId);
1496
+ }
1497
+ catch (error) {
1498
+ const message = error instanceof Error ? error.message : String(error);
1499
+ throw new Error(`Remote snapshot failed using ${linkageLabel}: ${message}`, { cause: error });
1500
+ }
1501
+ const payload = {
1502
+ status: "ok",
1503
+ command: "remote",
1504
+ subcommand: "snapshot",
1505
+ endpoint: resolvedTarget.endpoint,
1506
+ workspace_id: context.workspaceId,
1507
+ project_id: context.projectId,
1508
+ workspace_backend: resolvedTarget.workspace_backend,
1509
+ snapshot,
1510
+ };
1511
+ if (output === "json") {
1512
+ printResult(output, payload);
1513
+ }
1514
+ else {
1515
+ const docs = (snapshot.documents ?? []).map((d) => (typeof d.path === "string" ? d.path : "")).filter(Boolean);
1516
+ console.log(`Project snapshot for ${context.projectId} on workspace ${context.workspaceId}`);
1517
+ console.log(`Documents: ${String(docs.length)}`);
1518
+ for (const entry of docs.sort()) {
1519
+ console.log(`- ${entry}`);
1520
+ }
1521
+ }
1522
+ return 0;
1523
+ }
1192
1524
  console.error(`Unknown remote subcommand: ${subcommand}`);
1193
1525
  console.error(remoteUsage());
1194
1526
  return 1;
1195
1527
  }
1196
1528
  catch (error) {
1197
- const message = error instanceof Error ? error.message : String(error);
1198
- if (output === "json") {
1199
- printResult(output, {
1200
- status: "error",
1201
- command: "remote",
1202
- subcommand,
1203
- error: message,
1204
- });
1205
- }
1206
- else {
1207
- console.error(message);
1208
- }
1209
- return 1;
1529
+ return reportError(subcommand, error);
1210
1530
  }
1211
1531
  }
1212
1532
  if (command === "plan") {