reffy-cli 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -36
- package/dist/cli.js +498 -178
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/remote.d.ts +131 -49
- package/dist/remote.js +294 -99
- package/dist/types.d.ts +5 -2
- package/package.json +1 -1
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 {
|
|
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
|
|
489
|
-
" reffy remote status [--repo PATH] [--output text|json] [--workspace-id ID] [--
|
|
490
|
-
" reffy remote push [--repo PATH] [--output text|json] [--workspace-id ID] [--
|
|
491
|
-
" reffy remote ls [--repo PATH] [--output text|json] [--workspace-id ID] [--
|
|
492
|
-
" reffy remote cat <path> [--repo PATH] [--output text|json] [--workspace-id ID] [--
|
|
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
|
|
498
|
-
" --actor
|
|
499
|
-
" --provision
|
|
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
|
-
|
|
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
|
|
715
|
-
|
|
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
|
|
723
|
-
args.
|
|
740
|
+
if (arg.startsWith("--manager-pod=")) {
|
|
741
|
+
args.managerPod = arg.split("=", 2)[1];
|
|
724
742
|
continue;
|
|
725
743
|
}
|
|
726
|
-
if (arg === "--actor
|
|
727
|
-
|
|
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
|
|
735
|
-
args.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
800
|
-
|
|
801
|
-
|
|
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.
|
|
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
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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, {
|
|
931
|
-
const endpoint = parsed
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
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
|
-
|
|
938
|
-
|
|
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:
|
|
1223
|
+
provider: targetResult.config.provider,
|
|
949
1224
|
config_path: configPath,
|
|
950
|
-
endpoint
|
|
1225
|
+
endpoint,
|
|
951
1226
|
workspace_id: workspaceId,
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
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(`
|
|
975
|
-
console.log(`
|
|
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.
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
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:
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
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
|
-
|
|
1008
|
-
const
|
|
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:
|
|
1309
|
+
endpoint: resolvedTarget.endpoint,
|
|
1016
1310
|
workspace_id: context.workspaceId,
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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:
|
|
1028
|
-
|
|
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(`
|
|
1040
|
-
|
|
1041
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
1375
|
+
endpoint: resolvedTarget.endpoint,
|
|
1077
1376
|
workspace_id: context.workspaceId,
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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:
|
|
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(`
|
|
1099
|
-
console.log(`
|
|
1100
|
-
console.log(`
|
|
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
|
|
1415
|
+
let listing;
|
|
1114
1416
|
try {
|
|
1115
|
-
|
|
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 = (
|
|
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:
|
|
1431
|
+
endpoint: resolvedTarget.endpoint,
|
|
1130
1432
|
workspace_id: context.workspaceId,
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
|
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:
|
|
1474
|
+
endpoint: resolvedTarget.endpoint,
|
|
1174
1475
|
workspace_id: context.workspaceId,
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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") {
|