reffy-cli 1.4.2 → 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 +100 -58
- package/dist/cli.js +547 -172
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +5 -1
- package/dist/manifest.js +59 -6
- package/dist/remote.d.ts +150 -41
- package/dist/remote.js +356 -99
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +13 -4
- package/dist/workspace.js +6 -4
- package/package.json +1 -2
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,17 +485,29 @@ function planUsage() {
|
|
|
485
485
|
function remoteUsage() {
|
|
486
486
|
return [
|
|
487
487
|
"Usage:",
|
|
488
|
-
" reffy remote init [--repo PATH] [--output text|json] [--endpoint URL] [--pod
|
|
489
|
-
" reffy remote status [--repo PATH] [--output text|json] [--
|
|
490
|
-
" reffy remote push [--repo PATH] [--output text|json] [--
|
|
491
|
-
" reffy remote ls [--repo PATH] [--output text|json] [--
|
|
492
|
-
" reffy remote cat <path> [--repo PATH] [--output text|json] [--
|
|
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:",
|
|
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.",
|
|
495
503
|
" --endpoint URL Paseo base URL. Defaults to saved config or PASEO_ENDPOINT/.env.",
|
|
496
|
-
" --pod
|
|
497
|
-
" --actor
|
|
498
|
-
" --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`).",
|
|
499
511
|
" --prefix PATH Prefix filter for `remote ls`.",
|
|
500
512
|
" --env-file PATH Dotenv file to load before resolving env-backed options. Defaults to .env.",
|
|
501
513
|
].join("\n");
|
|
@@ -676,70 +688,69 @@ function parsePlanArgs(argv) {
|
|
|
676
688
|
}
|
|
677
689
|
return args;
|
|
678
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"]);
|
|
679
704
|
function parseRemoteArgs(argv) {
|
|
680
705
|
const repoRoot = parseRepoArg(argv);
|
|
681
706
|
const args = {
|
|
682
707
|
repoRoot,
|
|
683
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;
|
|
684
717
|
};
|
|
685
718
|
for (let i = 0; i < argv.length; i += 1) {
|
|
686
719
|
const arg = argv[i];
|
|
687
|
-
if (arg === "--repo") {
|
|
720
|
+
if (arg === "--repo" || arg === "--output") {
|
|
688
721
|
i += 1;
|
|
689
722
|
continue;
|
|
690
723
|
}
|
|
691
|
-
if (arg.startsWith("--repo="))
|
|
724
|
+
if (arg.startsWith("--repo=") || arg.startsWith("--output="))
|
|
692
725
|
continue;
|
|
693
726
|
if (arg === "--json")
|
|
694
727
|
continue;
|
|
695
|
-
if (arg === "--output") {
|
|
696
|
-
i += 1;
|
|
697
|
-
continue;
|
|
698
|
-
}
|
|
699
|
-
if (arg.startsWith("--output="))
|
|
700
|
-
continue;
|
|
701
728
|
if (arg === "--endpoint") {
|
|
702
|
-
|
|
703
|
-
if (!value)
|
|
704
|
-
throw new Error("--endpoint requires a value");
|
|
705
|
-
args.endpoint = value;
|
|
706
|
-
i += 1;
|
|
729
|
+
args.endpoint = setStr(arg, argv[++i]);
|
|
707
730
|
continue;
|
|
708
731
|
}
|
|
709
732
|
if (arg.startsWith("--endpoint=")) {
|
|
710
733
|
args.endpoint = arg.split("=", 2)[1];
|
|
711
734
|
continue;
|
|
712
735
|
}
|
|
713
|
-
if (arg === "--pod
|
|
714
|
-
|
|
715
|
-
if (!value)
|
|
716
|
-
throw new Error("--pod-name requires a value");
|
|
717
|
-
args.podName = value;
|
|
718
|
-
i += 1;
|
|
736
|
+
if (arg === "--manager-pod") {
|
|
737
|
+
args.managerPod = setStr(arg, argv[++i]);
|
|
719
738
|
continue;
|
|
720
739
|
}
|
|
721
|
-
if (arg.startsWith("--pod
|
|
722
|
-
args.
|
|
740
|
+
if (arg.startsWith("--manager-pod=")) {
|
|
741
|
+
args.managerPod = arg.split("=", 2)[1];
|
|
723
742
|
continue;
|
|
724
743
|
}
|
|
725
|
-
if (arg === "--actor
|
|
726
|
-
|
|
727
|
-
if (!value)
|
|
728
|
-
throw new Error("--actor-id requires a value");
|
|
729
|
-
args.actorId = value;
|
|
730
|
-
i += 1;
|
|
744
|
+
if (arg === "--manager-actor") {
|
|
745
|
+
args.managerActor = setStr(arg, argv[++i]);
|
|
731
746
|
continue;
|
|
732
747
|
}
|
|
733
|
-
if (arg.startsWith("--actor
|
|
734
|
-
args.
|
|
748
|
+
if (arg.startsWith("--manager-actor=")) {
|
|
749
|
+
args.managerActor = arg.split("=", 2)[1];
|
|
735
750
|
continue;
|
|
736
751
|
}
|
|
737
752
|
if (arg === "--env-file") {
|
|
738
|
-
|
|
739
|
-
if (!value)
|
|
740
|
-
throw new Error("--env-file requires a value");
|
|
741
|
-
args.envFile = value;
|
|
742
|
-
i += 1;
|
|
753
|
+
args.envFile = setStr(arg, argv[++i]);
|
|
743
754
|
continue;
|
|
744
755
|
}
|
|
745
756
|
if (arg.startsWith("--env-file=")) {
|
|
@@ -747,52 +758,74 @@ function parseRemoteArgs(argv) {
|
|
|
747
758
|
continue;
|
|
748
759
|
}
|
|
749
760
|
if (arg === "--prefix") {
|
|
750
|
-
|
|
751
|
-
if (!value)
|
|
752
|
-
throw new Error("--prefix requires a value");
|
|
753
|
-
args.prefix = value;
|
|
754
|
-
i += 1;
|
|
761
|
+
args.prefix = setStr(arg, argv[++i]);
|
|
755
762
|
continue;
|
|
756
763
|
}
|
|
757
764
|
if (arg.startsWith("--prefix=")) {
|
|
758
765
|
args.prefix = arg.split("=", 2)[1];
|
|
759
766
|
continue;
|
|
760
767
|
}
|
|
768
|
+
if (arg === "--workspace-id") {
|
|
769
|
+
args.workspaceId = setStr(arg, argv[++i]);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (arg.startsWith("--workspace-id=")) {
|
|
773
|
+
args.workspaceId = arg.split("=", 2)[1];
|
|
774
|
+
continue;
|
|
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
|
+
}
|
|
761
792
|
if (arg === "--provision") {
|
|
762
793
|
args.provision = true;
|
|
763
794
|
continue;
|
|
764
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
|
+
}
|
|
765
808
|
if (arg.startsWith("--")) {
|
|
766
809
|
throw new Error(`Unknown remote option: ${arg}`);
|
|
767
810
|
}
|
|
768
811
|
}
|
|
812
|
+
if (args.create && args.resolve) {
|
|
813
|
+
throw new Error("--create and --resolve are mutually exclusive");
|
|
814
|
+
}
|
|
769
815
|
return args;
|
|
770
816
|
}
|
|
771
817
|
function getRemotePositionalArgs(argv) {
|
|
772
818
|
const positionals = [];
|
|
773
819
|
for (let i = 0; i < argv.length; i += 1) {
|
|
774
820
|
const arg = argv[i];
|
|
775
|
-
if (arg
|
|
776
|
-
arg === "--output" ||
|
|
777
|
-
arg === "--endpoint" ||
|
|
778
|
-
arg === "--pod-name" ||
|
|
779
|
-
arg === "--actor-id" ||
|
|
780
|
-
arg === "--env-file" ||
|
|
781
|
-
arg === "--prefix") {
|
|
821
|
+
if (REMOTE_VALUE_FLAGS.has(arg)) {
|
|
782
822
|
i += 1;
|
|
783
823
|
continue;
|
|
784
824
|
}
|
|
785
|
-
if (arg.startsWith(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
arg.startsWith("--pod-name=") ||
|
|
789
|
-
arg.startsWith("--actor-id=") ||
|
|
790
|
-
arg.startsWith("--env-file=") ||
|
|
791
|
-
arg.startsWith("--prefix=") ||
|
|
792
|
-
arg === "--json" ||
|
|
793
|
-
arg === "--provision") {
|
|
825
|
+
if (Array.from(REMOTE_VALUE_FLAGS).some((flag) => arg.startsWith(`${flag}=`)))
|
|
826
|
+
continue;
|
|
827
|
+
if (REMOTE_BOOLEAN_FLAGS.has(arg))
|
|
794
828
|
continue;
|
|
795
|
-
}
|
|
796
829
|
if (arg.startsWith("--"))
|
|
797
830
|
continue;
|
|
798
831
|
positionals.push(arg);
|
|
@@ -809,30 +842,267 @@ function printSection(title, values) {
|
|
|
809
842
|
console.log(`- ${value}`);
|
|
810
843
|
}
|
|
811
844
|
}
|
|
812
|
-
async function resolveRemoteContext(args) {
|
|
845
|
+
async function resolveRemoteContext(args, opts = {}) {
|
|
813
846
|
await loadDotEnvIfPresent(args.repoRoot, args.envFile ?? ".env");
|
|
814
847
|
const store = new ReferencesStore(args.repoRoot);
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
:
|
|
848
|
+
const identityInfo = await store.getWorkspaceIdentity();
|
|
849
|
+
const workspaceId = resolveSelectedWorkspaceId(identityInfo, args.workspaceId);
|
|
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
|
+
}
|
|
855
|
+
let savedConfig = null;
|
|
856
|
+
let replacedLegacy = false;
|
|
857
|
+
try {
|
|
858
|
+
savedConfig = await readRemoteConfig(args.repoRoot);
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
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"))) {
|
|
866
|
+
replacedLegacy = true;
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const endpointForResolve = (args.endpoint ?? process.env.PASEO_ENDPOINT ?? "").trim();
|
|
873
|
+
const resolvedTarget = endpointForResolve
|
|
874
|
+
? resolveRemoteTarget(savedConfig, workspaceId, endpointForResolve)
|
|
875
|
+
: null;
|
|
829
876
|
return {
|
|
830
877
|
store,
|
|
831
878
|
identity,
|
|
879
|
+
workspaceId,
|
|
880
|
+
projectId,
|
|
832
881
|
configPath: resolveRemoteConfigPath(args.repoRoot),
|
|
833
882
|
savedConfig,
|
|
834
|
-
|
|
835
|
-
|
|
883
|
+
resolvedTarget,
|
|
884
|
+
replacedLegacy,
|
|
885
|
+
};
|
|
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,
|
|
836
1106
|
};
|
|
837
1107
|
}
|
|
838
1108
|
async function main() {
|
|
@@ -892,91 +1162,165 @@ async function main() {
|
|
|
892
1162
|
}
|
|
893
1163
|
const output = parseOutputMode(remoteArgs);
|
|
894
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
|
+
};
|
|
895
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
|
+
}
|
|
896
1183
|
if (subcommand === "init") {
|
|
897
|
-
const { identity, configPath } = await resolveRemoteContext(parsed);
|
|
898
|
-
const endpoint = parsed
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
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, {
|
|
903
1193
|
endpoint,
|
|
904
|
-
|
|
905
|
-
|
|
1194
|
+
managerPodName: parsed.managerPod ?? process.env.PASEO_MANAGER_POD,
|
|
1195
|
+
managerActorId: parsed.managerActor ?? process.env.PASEO_MANAGER_ACTOR,
|
|
906
1196
|
provision: parsed.provision,
|
|
907
|
-
|
|
1197
|
+
existingConfig: savedConfig,
|
|
908
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);
|
|
909
1217
|
const gitignore = await ensureGitignoreEntries(parsed.repoRoot, [".reffy/state/"]);
|
|
1218
|
+
const tokenJustIssued = managerInit.manager_auth_token != null;
|
|
910
1219
|
const payload = {
|
|
911
1220
|
status: "ok",
|
|
912
1221
|
command: "remote",
|
|
913
1222
|
subcommand: "init",
|
|
914
|
-
provider:
|
|
1223
|
+
provider: targetResult.config.provider,
|
|
915
1224
|
config_path: configPath,
|
|
916
|
-
endpoint
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1225
|
+
endpoint,
|
|
1226
|
+
workspace_id: workspaceId,
|
|
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,
|
|
924
1236
|
gitignore_path: gitignore.path,
|
|
925
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,
|
|
926
1241
|
};
|
|
927
1242
|
if (output === "json") {
|
|
928
1243
|
printResult(output, payload);
|
|
929
1244
|
}
|
|
930
1245
|
else {
|
|
1246
|
+
if (replacedLegacy) {
|
|
1247
|
+
console.log("Replacing legacy remote linkage with v4 (no endpoint, bearer-token auth)");
|
|
1248
|
+
}
|
|
931
1249
|
console.log("Remote linkage ready");
|
|
932
1250
|
console.log(`Config: ${payload.config_path}`);
|
|
933
|
-
console.log(`Endpoint: ${payload.endpoint}`);
|
|
934
|
-
console.log(`
|
|
935
|
-
console.log(`
|
|
1251
|
+
console.log(`Endpoint: ${payload.endpoint} (sourced from env, not persisted)`);
|
|
1252
|
+
console.log(`Manager: ${payload.manager.pod_name}/${payload.manager.actor_id}`);
|
|
1253
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
1254
|
+
console.log(`Workspace backend: ${payload.workspace_backend.pod_name}/${payload.workspace_backend.actor_id}`);
|
|
1255
|
+
console.log(`Workspace outcome: ${payload.workspace_outcome}`);
|
|
936
1256
|
console.log(`Project identity: ${payload.project_id}`);
|
|
937
|
-
console.log(`
|
|
1257
|
+
console.log(`Project registration: ${payload.project_registration}`);
|
|
938
1258
|
console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
|
|
939
|
-
console.log(`Created actor: ${payload.
|
|
940
|
-
console.log(`
|
|
1259
|
+
console.log(`Created manager actor: ${payload.created_manager_actor ? "yes" : "no"}`);
|
|
1260
|
+
console.log(`Configured workspace ids: ${payload.configured_workspace_ids.join(", ")}`);
|
|
941
1261
|
if (payload.gitignore_added.length > 0) {
|
|
942
1262
|
console.log(`Updated .gitignore: ${payload.gitignore_added.join(", ")}`);
|
|
943
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
|
+
}
|
|
944
1274
|
}
|
|
945
1275
|
return 0;
|
|
946
1276
|
}
|
|
947
1277
|
const context = await resolveRemoteContext(parsed);
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
|
|
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);
|
|
1286
|
+
const linkageLabel = describeRemoteLinkage({
|
|
1287
|
+
endpoint: resolvedTarget.endpoint,
|
|
1288
|
+
manager: resolvedTarget.manager,
|
|
1289
|
+
workspace_backend: resolvedTarget.workspace_backend,
|
|
1290
|
+
workspace_id: context.workspaceId,
|
|
1291
|
+
});
|
|
952
1292
|
if (subcommand === "status") {
|
|
953
1293
|
let workspace;
|
|
954
1294
|
try {
|
|
955
|
-
workspace = await
|
|
1295
|
+
workspace = await backendClient.getWorkspace();
|
|
956
1296
|
}
|
|
957
1297
|
catch (error) {
|
|
958
1298
|
const message = error instanceof Error ? error.message : String(error);
|
|
959
|
-
throw new Error(`Remote status failed using ${
|
|
1299
|
+
throw new Error(`Remote status failed using ${linkageLabel}: ${message}`, { cause: error });
|
|
960
1300
|
}
|
|
961
|
-
|
|
962
|
-
const
|
|
1301
|
+
assertWorkspaceSummaryIdentity(workspace, context.workspaceId);
|
|
1302
|
+
const summaryIdentity = extractWorkspaceSummaryIdentity(workspace);
|
|
963
1303
|
const payload = {
|
|
964
1304
|
status: "ok",
|
|
965
1305
|
command: "remote",
|
|
966
1306
|
subcommand: "status",
|
|
967
1307
|
config_path: context.configPath,
|
|
968
|
-
provider:
|
|
969
|
-
endpoint:
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1308
|
+
provider: "paseo",
|
|
1309
|
+
endpoint: resolvedTarget.endpoint,
|
|
1310
|
+
workspace_id: context.workspaceId,
|
|
1311
|
+
project_id: context.projectId,
|
|
1312
|
+
manager: resolvedTarget.manager,
|
|
1313
|
+
workspace_backend: resolvedTarget.workspace_backend,
|
|
1314
|
+
recovered_workspace_backend: recovered,
|
|
973
1315
|
local_identity: context.identity,
|
|
974
1316
|
remote_identity: {
|
|
975
|
-
|
|
976
|
-
|
|
1317
|
+
workspace_id: summaryIdentity.workspace_id,
|
|
1318
|
+
actor_type: summaryIdentity.actor_type,
|
|
1319
|
+
backend_version: summaryIdentity.backend_version,
|
|
977
1320
|
},
|
|
978
|
-
document_count:
|
|
979
|
-
|
|
1321
|
+
document_count: summaryIdentity.document_count,
|
|
1322
|
+
registered_project_count: summaryIdentity.registered_project_count,
|
|
1323
|
+
last_imported_at: resolvedTarget.last_imported_at ?? null,
|
|
980
1324
|
workspace,
|
|
981
1325
|
};
|
|
982
1326
|
if (output === "json") {
|
|
@@ -986,47 +1330,55 @@ async function main() {
|
|
|
986
1330
|
console.log("Remote workspace reachable");
|
|
987
1331
|
console.log(`Config: ${payload.config_path}`);
|
|
988
1332
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
989
|
-
console.log(`
|
|
990
|
-
console.log(`
|
|
991
|
-
console.log(`
|
|
992
|
-
|
|
993
|
-
|
|
1333
|
+
console.log(`Manager: ${payload.manager.pod_name}/${payload.manager.actor_id}`);
|
|
1334
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
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");
|
|
1338
|
+
console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.workspace_id}`);
|
|
1339
|
+
console.log(`Remote identity: workspace=${String(payload.remote_identity.workspace_id)} (${String(payload.remote_identity.actor_type)} ${String(payload.remote_identity.backend_version)})`);
|
|
994
1340
|
console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
|
|
1341
|
+
console.log(`Registered projects: ${String(payload.registered_project_count ?? "unknown")}`);
|
|
995
1342
|
console.log(`Last imported at: ${String(payload.last_imported_at ?? "never")}`);
|
|
996
1343
|
}
|
|
997
1344
|
return 0;
|
|
998
1345
|
}
|
|
999
1346
|
if (subcommand === "push") {
|
|
1000
1347
|
const documents = await collectWorkspaceDocuments(parsed.repoRoot);
|
|
1348
|
+
const registration = await managerClient.registerProject(context.workspaceId, context.projectId);
|
|
1001
1349
|
let importResult;
|
|
1002
1350
|
try {
|
|
1003
|
-
importResult = validateImportResult(await
|
|
1351
|
+
importResult = validateImportResult(await backendClient.importProject(context.projectId, documents, true));
|
|
1004
1352
|
}
|
|
1005
1353
|
catch (error) {
|
|
1006
1354
|
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
-
throw new Error(`Remote push failed using ${
|
|
1355
|
+
throw new Error(`Remote push failed using ${linkageLabel}: ${message}`, { cause: error });
|
|
1008
1356
|
}
|
|
1009
1357
|
let workspace;
|
|
1010
1358
|
try {
|
|
1011
|
-
workspace = await
|
|
1359
|
+
workspace = await backendClient.getWorkspace();
|
|
1012
1360
|
}
|
|
1013
1361
|
catch (error) {
|
|
1014
1362
|
const message = error instanceof Error ? error.message : String(error);
|
|
1015
|
-
throw new Error(`Remote push could not verify remote state using ${
|
|
1363
|
+
throw new Error(`Remote push could not verify remote state using ${linkageLabel}: ${message}`, { cause: error });
|
|
1016
1364
|
}
|
|
1017
|
-
|
|
1365
|
+
assertWorkspaceSummaryIdentity(workspace, context.workspaceId);
|
|
1018
1366
|
const importedAt = new Date().toISOString();
|
|
1019
|
-
await updateRemoteConfigMetadata(parsed.repoRoot, {
|
|
1367
|
+
await updateRemoteConfigMetadata(parsed.repoRoot, context.workspaceId, {
|
|
1368
|
+
last_imported_at: importedAt,
|
|
1369
|
+
});
|
|
1370
|
+
const summary = extractWorkspaceSummaryIdentity(workspace);
|
|
1020
1371
|
const payload = {
|
|
1021
1372
|
status: "ok",
|
|
1022
1373
|
command: "remote",
|
|
1023
1374
|
subcommand: "push",
|
|
1024
|
-
endpoint:
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1375
|
+
endpoint: resolvedTarget.endpoint,
|
|
1376
|
+
workspace_id: context.workspaceId,
|
|
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",
|
|
1030
1382
|
local_document_count: documents.length,
|
|
1031
1383
|
imported: importResult.imported,
|
|
1032
1384
|
created: importResult.created,
|
|
@@ -1034,7 +1386,7 @@ async function main() {
|
|
|
1034
1386
|
deleted: importResult.deleted,
|
|
1035
1387
|
replace_missing: true,
|
|
1036
1388
|
last_imported_at: importedAt,
|
|
1037
|
-
remote_document_count:
|
|
1389
|
+
remote_document_count: summary.document_count,
|
|
1038
1390
|
};
|
|
1039
1391
|
if (output === "json") {
|
|
1040
1392
|
printResult(output, payload);
|
|
@@ -1042,9 +1394,12 @@ async function main() {
|
|
|
1042
1394
|
else {
|
|
1043
1395
|
console.log("Remote push complete");
|
|
1044
1396
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
1045
|
-
console.log(`
|
|
1046
|
-
console.log(`
|
|
1047
|
-
console.log(`
|
|
1397
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
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");
|
|
1048
1403
|
console.log("Mode: remote reflects local (replace_missing=true)");
|
|
1049
1404
|
console.log(`Local document count: ${String(payload.local_document_count)}`);
|
|
1050
1405
|
console.log(`Imported: ${String(payload.imported)}`);
|
|
@@ -1057,15 +1412,15 @@ async function main() {
|
|
|
1057
1412
|
return 0;
|
|
1058
1413
|
}
|
|
1059
1414
|
if (subcommand === "ls") {
|
|
1060
|
-
let
|
|
1415
|
+
let listing;
|
|
1061
1416
|
try {
|
|
1062
|
-
|
|
1417
|
+
listing = await backendClient.listProjectDocuments(context.projectId, { prefix: parsed.prefix });
|
|
1063
1418
|
}
|
|
1064
1419
|
catch (error) {
|
|
1065
1420
|
const message = error instanceof Error ? error.message : String(error);
|
|
1066
|
-
throw new Error(`Remote ls failed using ${
|
|
1421
|
+
throw new Error(`Remote ls failed using ${linkageLabel}: ${message}`, { cause: error });
|
|
1067
1422
|
}
|
|
1068
|
-
const paths = (
|
|
1423
|
+
const paths = (listing.documents ?? [])
|
|
1069
1424
|
.map((document) => (typeof document.path === "string" ? document.path : ""))
|
|
1070
1425
|
.filter(Boolean)
|
|
1071
1426
|
.sort();
|
|
@@ -1073,10 +1428,10 @@ async function main() {
|
|
|
1073
1428
|
status: "ok",
|
|
1074
1429
|
command: "remote",
|
|
1075
1430
|
subcommand: "ls",
|
|
1076
|
-
endpoint:
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1431
|
+
endpoint: resolvedTarget.endpoint,
|
|
1432
|
+
workspace_id: context.workspaceId,
|
|
1433
|
+
project_id: context.projectId,
|
|
1434
|
+
workspace_backend: resolvedTarget.workspace_backend,
|
|
1080
1435
|
prefix: parsed.prefix ?? null,
|
|
1081
1436
|
paths,
|
|
1082
1437
|
};
|
|
@@ -1103,23 +1458,23 @@ async function main() {
|
|
|
1103
1458
|
}
|
|
1104
1459
|
let document;
|
|
1105
1460
|
try {
|
|
1106
|
-
document = await
|
|
1461
|
+
document = await backendClient.getProjectDocument(context.projectId, documentPath);
|
|
1107
1462
|
}
|
|
1108
1463
|
catch (error) {
|
|
1109
1464
|
const message = error instanceof Error ? error.message : String(error);
|
|
1110
|
-
throw new Error(`Remote cat failed using ${
|
|
1465
|
+
throw new Error(`Remote cat failed using ${linkageLabel}: ${message}`, { cause: error });
|
|
1111
1466
|
}
|
|
1112
1467
|
if (!document.document) {
|
|
1113
|
-
throw new Error(`Remote document not found using ${
|
|
1468
|
+
throw new Error(`Remote document not found using ${linkageLabel}: ${documentPath}`);
|
|
1114
1469
|
}
|
|
1115
1470
|
const payload = {
|
|
1116
1471
|
status: "ok",
|
|
1117
1472
|
command: "remote",
|
|
1118
1473
|
subcommand: "cat",
|
|
1119
|
-
endpoint:
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1474
|
+
endpoint: resolvedTarget.endpoint,
|
|
1475
|
+
workspace_id: context.workspaceId,
|
|
1476
|
+
project_id: context.projectId,
|
|
1477
|
+
workspace_backend: resolvedTarget.workspace_backend,
|
|
1123
1478
|
document: document.document,
|
|
1124
1479
|
};
|
|
1125
1480
|
if (output === "json") {
|
|
@@ -1134,24 +1489,44 @@ async function main() {
|
|
|
1134
1489
|
}
|
|
1135
1490
|
return 0;
|
|
1136
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
|
+
}
|
|
1137
1524
|
console.error(`Unknown remote subcommand: ${subcommand}`);
|
|
1138
1525
|
console.error(remoteUsage());
|
|
1139
1526
|
return 1;
|
|
1140
1527
|
}
|
|
1141
1528
|
catch (error) {
|
|
1142
|
-
|
|
1143
|
-
if (output === "json") {
|
|
1144
|
-
printResult(output, {
|
|
1145
|
-
status: "error",
|
|
1146
|
-
command: "remote",
|
|
1147
|
-
subcommand,
|
|
1148
|
-
error: message,
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
else {
|
|
1152
|
-
console.error(message);
|
|
1153
|
-
}
|
|
1154
|
-
return 1;
|
|
1529
|
+
return reportError(subcommand, error);
|
|
1155
1530
|
}
|
|
1156
1531
|
}
|
|
1157
1532
|
if (command === "plan") {
|