reffy-cli 1.4.2 → 1.5.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 +39 -41
- package/dist/cli.js +110 -55
- 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 +43 -16
- package/dist/remote.js +118 -56
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +9 -3
- package/dist/workspace.js +6 -4
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -65,42 +65,20 @@ reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format ascii
|
|
|
65
65
|
reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
## Remote Backend Helper
|
|
69
|
-
|
|
70
|
-
Reffy includes a Paseo helper bridge at `scripts/reffy-remote-backend-demo.mjs`.
|
|
71
|
-
|
|
72
|
-
The helper:
|
|
73
|
-
|
|
74
|
-
- loads `.env` automatically when present
|
|
75
|
-
- reads `project_id` and `workspace_name` from `.reffy/manifest.json`
|
|
76
|
-
- provisions or links a Paseo `reffyRemoteBackend` actor
|
|
77
|
-
- imports the local `.reffy/` workspace
|
|
78
|
-
|
|
79
|
-
Expected environment variables:
|
|
80
|
-
|
|
81
|
-
- `PASEO_ENDPOINT` - required unless already exported in the shell
|
|
82
|
-
- `PASEO_POD_NAME` - optional existing pod override
|
|
83
|
-
- `PASEO_ACTOR_ID` - optional existing actor override
|
|
84
|
-
|
|
85
|
-
Examples:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npm run remote:backend:demo
|
|
89
|
-
reffy remote init --provision
|
|
90
|
-
reffy remote push
|
|
91
|
-
```
|
|
92
|
-
|
|
93
68
|
## Remote Sync
|
|
94
69
|
|
|
95
70
|
Reffy can publish the local `.reffy/` workspace to a Paseo-backed remote actor and inspect it later with native CLI commands.
|
|
96
71
|
|
|
97
72
|
The current remote flow is:
|
|
98
73
|
|
|
99
|
-
1. Reffy reads `project_id` and `
|
|
100
|
-
2.
|
|
101
|
-
3.
|
|
102
|
-
4. `reffy remote
|
|
103
|
-
5. `reffy remote
|
|
74
|
+
1. Reffy reads `project_id` and `workspace_ids` from `.reffy/manifest.json`.
|
|
75
|
+
2. You select one workspace projection to act on. Reffy infers the selection when `workspace_ids` has exactly one entry; otherwise pass `--workspace-id <id>`.
|
|
76
|
+
3. Reffy connects to Paseo using `PASEO_ENDPOINT` or `--endpoint`.
|
|
77
|
+
4. `reffy remote init --provision` creates a pod and a `reffyRemoteBackend.v2` actor for the selected workspace id when needed, then writes local linkage state to `.reffy/state/remote.json`.
|
|
78
|
+
5. `reffy remote push` publishes the full local `.reffy/` tree to the selected projection through `/workspaces/{workspace_id}/...` with `replace_missing=true` by default, scoped to that workspace id.
|
|
79
|
+
6. `reffy remote status|ls|cat` inspects the selected workspace projection using the saved linkage state unless you override it.
|
|
80
|
+
|
|
81
|
+
One backend actor can serve multiple workspace projections for the same source `project_id`: Reffy stores one target per `workspace_id` in `.reffy/state/remote.json`, and the backend isolates documents and locks by `(workspace_id, path)`.
|
|
104
82
|
|
|
105
83
|
### Minimal connection requirement
|
|
106
84
|
|
|
@@ -124,8 +102,9 @@ Optional overrides:
|
|
|
124
102
|
|
|
125
103
|
- `PASEO_POD_NAME` if you want to reuse an existing Paseo pod
|
|
126
104
|
- `PASEO_ACTOR_ID` if you want to reuse an existing backend actor
|
|
105
|
+
- `--workspace-id <id>` (or `PASEO_WORKSPACE_ID` for the helper script) when the manifest lists more than one `workspace_ids`
|
|
127
106
|
|
|
128
|
-
Reffy does not require separate `REFFY_PROJECT_ID`
|
|
107
|
+
Reffy does not require separate `REFFY_PROJECT_ID` values for the normal case because source identity comes from `.reffy/manifest.json`.
|
|
129
108
|
|
|
130
109
|
### Saved remote linkage
|
|
131
110
|
|
|
@@ -133,11 +112,21 @@ After `reffy remote init`, Reffy stores the local pointer to the linked backend
|
|
|
133
112
|
|
|
134
113
|
- `.reffy/state/remote.json`
|
|
135
114
|
|
|
136
|
-
That file
|
|
115
|
+
That file maps each selected `workspace_id` to the backend target Reffy should use for that projection:
|
|
137
116
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"version": 2,
|
|
120
|
+
"provider": "paseo",
|
|
121
|
+
"endpoint": "https://paseo.example",
|
|
122
|
+
"targets": {
|
|
123
|
+
"my-project": { "pod_name": "...", "actor_id": "...", "last_imported_at": "..." },
|
|
124
|
+
"portfolio-alpha": { "pod_name": "...", "actor_id": "..." }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Multiple targets can share the same `pod_name` and `actor_id` — the backend addresses each projection separately through `/workspaces/{workspace_id}/...`.
|
|
141
130
|
|
|
142
131
|
This file is local runtime state. It is not part of the synced remote workspace and is intentionally excluded from `reffy remote push`.
|
|
143
132
|
|
|
@@ -156,9 +145,9 @@ reffy remote cat .reffy/manifest.json
|
|
|
156
145
|
|
|
157
146
|
`reffy remote status` is the primary diagnostic command for this flow. It reports:
|
|
158
147
|
|
|
159
|
-
- the saved linkage being used
|
|
160
|
-
- the local manifest identity
|
|
161
|
-
- the remote workspace
|
|
148
|
+
- the saved linkage being used for the selected workspace id
|
|
149
|
+
- the local manifest identity (source `project_id` and selected `workspace_id`)
|
|
150
|
+
- the remote identity returned from v2 (`source.actor_type`, `source.version`, `source.project_id`, `workspace.workspace_id`)
|
|
162
151
|
- remote document counts when reachable
|
|
163
152
|
|
|
164
153
|
`reffy remote push` reports:
|
|
@@ -173,7 +162,7 @@ That makes the default prune/import behavior auditable without dropping to direc
|
|
|
173
162
|
|
|
174
163
|
## Manifest Contract
|
|
175
164
|
|
|
176
|
-
Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests
|
|
165
|
+
Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests separate stable source identity (`project_id`) from plural workspace membership (`workspace_ids`):
|
|
177
166
|
|
|
178
167
|
```json
|
|
179
168
|
{
|
|
@@ -181,12 +170,21 @@ Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests
|
|
|
181
170
|
"created_at": "2026-04-18T00:00:00.000Z",
|
|
182
171
|
"updated_at": "2026-04-18T00:00:00.000Z",
|
|
183
172
|
"project_id": "my-project",
|
|
184
|
-
"
|
|
173
|
+
"workspace_ids": ["my-project"],
|
|
185
174
|
"artifacts": []
|
|
186
175
|
}
|
|
187
176
|
```
|
|
188
177
|
|
|
189
|
-
|
|
178
|
+
A single `.reffy/` tree can belong to more than one planning workspace:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"project_id": "my-project",
|
|
183
|
+
"workspace_ids": ["my-project", "portfolio-alpha", "nuveris-cross-project-planning"]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Running `reffy init` populates `project_id` and `workspace_ids` with deterministic defaults derived from the repository name for any managed manifest that is missing them.
|
|
190
188
|
|
|
191
189
|
## Using Reffy With ReffySpec
|
|
192
190
|
|
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,
|
|
13
|
+
import { assertRemoteIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, 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,16 +485,17 @@ function planUsage() {
|
|
|
485
485
|
function remoteUsage() {
|
|
486
486
|
return [
|
|
487
487
|
"Usage:",
|
|
488
|
-
" reffy remote init [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--provision] [--env-file PATH]",
|
|
489
|
-
" reffy remote status [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
490
|
-
" reffy remote push [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
491
|
-
" reffy remote ls [--repo PATH] [--output text|json] [--prefix PATH] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
492
|
-
" reffy remote cat <path> [--repo PATH] [--output text|json] [--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] [--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]",
|
|
493
493
|
"",
|
|
494
494
|
"Options:",
|
|
495
|
+
" --workspace-id ID Selected workspace projection. Required when the manifest has multiple workspace_ids; otherwise inferred.",
|
|
495
496
|
" --endpoint URL Paseo base URL. Defaults to saved config or PASEO_ENDPOINT/.env.",
|
|
496
|
-
" --pod-name NAME Paseo pod name. Defaults to saved
|
|
497
|
-
" --actor-id ID Paseo actor id. Defaults to saved
|
|
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.",
|
|
498
499
|
" --provision Create missing pod/actor during `remote init`.",
|
|
499
500
|
" --prefix PATH Prefix filter for `remote ls`.",
|
|
500
501
|
" --env-file PATH Dotenv file to load before resolving env-backed options. Defaults to .env.",
|
|
@@ -758,6 +759,18 @@ function parseRemoteArgs(argv) {
|
|
|
758
759
|
args.prefix = arg.split("=", 2)[1];
|
|
759
760
|
continue;
|
|
760
761
|
}
|
|
762
|
+
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;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (arg.startsWith("--workspace-id=")) {
|
|
771
|
+
args.workspaceId = arg.split("=", 2)[1];
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
761
774
|
if (arg === "--provision") {
|
|
762
775
|
args.provision = true;
|
|
763
776
|
continue;
|
|
@@ -778,7 +791,8 @@ function getRemotePositionalArgs(argv) {
|
|
|
778
791
|
arg === "--pod-name" ||
|
|
779
792
|
arg === "--actor-id" ||
|
|
780
793
|
arg === "--env-file" ||
|
|
781
|
-
arg === "--prefix"
|
|
794
|
+
arg === "--prefix" ||
|
|
795
|
+
arg === "--workspace-id") {
|
|
782
796
|
i += 1;
|
|
783
797
|
continue;
|
|
784
798
|
}
|
|
@@ -789,6 +803,7 @@ function getRemotePositionalArgs(argv) {
|
|
|
789
803
|
arg.startsWith("--actor-id=") ||
|
|
790
804
|
arg.startsWith("--env-file=") ||
|
|
791
805
|
arg.startsWith("--prefix=") ||
|
|
806
|
+
arg.startsWith("--workspace-id=") ||
|
|
792
807
|
arg === "--json" ||
|
|
793
808
|
arg === "--provision") {
|
|
794
809
|
continue;
|
|
@@ -809,30 +824,48 @@ function printSection(title, values) {
|
|
|
809
824
|
console.log(`- ${value}`);
|
|
810
825
|
}
|
|
811
826
|
}
|
|
812
|
-
async function resolveRemoteContext(args) {
|
|
827
|
+
async function resolveRemoteContext(args, opts = {}) {
|
|
813
828
|
await loadDotEnvIfPresent(args.repoRoot, args.envFile ?? ".env");
|
|
814
829
|
const store = new ReferencesStore(args.repoRoot);
|
|
815
|
-
const
|
|
816
|
-
const
|
|
830
|
+
const identityInfo = await store.getWorkspaceIdentity();
|
|
831
|
+
const workspaceId = resolveSelectedWorkspaceId(identityInfo, args.workspaceId);
|
|
832
|
+
const identity = requireWorkspaceIdentity(identityInfo, workspaceId);
|
|
833
|
+
let savedConfig = null;
|
|
834
|
+
let replacedLegacy = false;
|
|
835
|
+
try {
|
|
836
|
+
savedConfig = await readRemoteConfig(args.repoRoot);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
840
|
+
if (opts.tolerateLegacyV1Config && message.includes("legacy v1 single-target shape")) {
|
|
841
|
+
replacedLegacy = true;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
817
847
|
const overrideValues = {
|
|
818
848
|
endpoint: args.endpoint ?? process.env.PASEO_ENDPOINT,
|
|
819
849
|
pod_name: args.podName ?? process.env.PASEO_POD_NAME,
|
|
820
850
|
actor_id: args.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
821
851
|
};
|
|
822
|
-
const
|
|
852
|
+
const resolvedTarget = resolveRemoteTarget(savedConfig, workspaceId, overrideValues);
|
|
823
853
|
const hasOverrides = Boolean(overrideValues.endpoint || overrideValues.pod_name || overrideValues.actor_id);
|
|
824
|
-
const
|
|
825
|
-
|
|
854
|
+
const savedHasTarget = Boolean(savedConfig?.targets?.[workspaceId]);
|
|
855
|
+
const linkageSource = resolvedTarget
|
|
856
|
+
? savedHasTarget && !hasOverrides
|
|
826
857
|
? "saved_config"
|
|
827
858
|
: "cli_or_env_overrides"
|
|
828
859
|
: "unconfigured";
|
|
829
860
|
return {
|
|
830
861
|
store,
|
|
831
862
|
identity,
|
|
863
|
+
workspaceId,
|
|
832
864
|
configPath: resolveRemoteConfigPath(args.repoRoot),
|
|
833
865
|
savedConfig,
|
|
834
|
-
|
|
866
|
+
resolvedTarget,
|
|
835
867
|
linkageSource,
|
|
868
|
+
replacedLegacy,
|
|
836
869
|
};
|
|
837
870
|
}
|
|
838
871
|
async function main() {
|
|
@@ -894,8 +927,8 @@ async function main() {
|
|
|
894
927
|
const parsed = parseRemoteArgs(remoteArgs);
|
|
895
928
|
try {
|
|
896
929
|
if (subcommand === "init") {
|
|
897
|
-
const { identity, configPath } = await resolveRemoteContext(parsed);
|
|
898
|
-
const endpoint = parsed.endpoint ?? process.env.PASEO_ENDPOINT;
|
|
930
|
+
const { identity, workspaceId, configPath, savedConfig, replacedLegacy } = await resolveRemoteContext(parsed, { tolerateLegacyV1Config: true });
|
|
931
|
+
const endpoint = parsed.endpoint ?? process.env.PASEO_ENDPOINT ?? savedConfig?.endpoint;
|
|
899
932
|
if (!endpoint) {
|
|
900
933
|
throw new Error("Remote init requires --endpoint or PASEO_ENDPOINT (optionally loaded from .env)");
|
|
901
934
|
}
|
|
@@ -905,6 +938,7 @@ async function main() {
|
|
|
905
938
|
actorId: parsed.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
906
939
|
provision: parsed.provision,
|
|
907
940
|
identity,
|
|
941
|
+
existingConfig: savedConfig,
|
|
908
942
|
});
|
|
909
943
|
const gitignore = await ensureGitignoreEntries(parsed.repoRoot, [".reffy/state/"]);
|
|
910
944
|
const payload = {
|
|
@@ -914,13 +948,15 @@ async function main() {
|
|
|
914
948
|
provider: result.config.provider,
|
|
915
949
|
config_path: configPath,
|
|
916
950
|
endpoint: result.config.endpoint,
|
|
917
|
-
|
|
918
|
-
|
|
951
|
+
workspace_id: workspaceId,
|
|
952
|
+
pod_name: result.target.pod_name,
|
|
953
|
+
actor_id: result.target.actor_id,
|
|
919
954
|
created_pod: result.created_pod,
|
|
920
955
|
created_actor: result.created_actor,
|
|
921
956
|
project_id: identity.project_id,
|
|
922
|
-
workspace_name: identity.workspace_name,
|
|
923
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,
|
|
924
960
|
gitignore_path: gitignore.path,
|
|
925
961
|
gitignore_added: gitignore.added,
|
|
926
962
|
};
|
|
@@ -928,16 +964,20 @@ async function main() {
|
|
|
928
964
|
printResult(output, payload);
|
|
929
965
|
}
|
|
930
966
|
else {
|
|
967
|
+
if (replacedLegacy) {
|
|
968
|
+
console.log("Replacing legacy v1 remote linkage with v2 target map");
|
|
969
|
+
}
|
|
931
970
|
console.log("Remote linkage ready");
|
|
932
971
|
console.log(`Config: ${payload.config_path}`);
|
|
933
972
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
973
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
934
974
|
console.log(`Pod: ${payload.pod_name}`);
|
|
935
975
|
console.log(`Actor: ${payload.actor_id}`);
|
|
936
976
|
console.log(`Project identity: ${payload.project_id}`);
|
|
937
|
-
console.log(`Workspace name: ${payload.workspace_name}`);
|
|
938
977
|
console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
|
|
939
978
|
console.log(`Created actor: ${payload.created_actor ? "yes" : "no"}`);
|
|
940
979
|
console.log(`Linkage mode: ${payload.linkage_mode}`);
|
|
980
|
+
console.log(`Configured workspace ids: ${payload.configured_workspace_ids.join(", ")}`);
|
|
941
981
|
if (payload.gitignore_added.length > 0) {
|
|
942
982
|
console.log(`Updated .gitignore: ${payload.gitignore_added.join(", ")}`);
|
|
943
983
|
}
|
|
@@ -945,18 +985,24 @@ async function main() {
|
|
|
945
985
|
return 0;
|
|
946
986
|
}
|
|
947
987
|
const context = await resolveRemoteContext(parsed);
|
|
948
|
-
if (!context.
|
|
949
|
-
throw new Error(`Remote linkage is not configured. Run \`reffy remote init\` or provide endpoint, pod name, and actor id. Expected config path: ${context.configPath}`);
|
|
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}`);
|
|
950
990
|
}
|
|
951
|
-
const client = new PaseoRemoteClient(context.
|
|
991
|
+
const client = new PaseoRemoteClient(context.resolvedTarget);
|
|
992
|
+
const linkageLabel = describeRemoteLinkage({
|
|
993
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
994
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
995
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
996
|
+
workspace_id: context.workspaceId,
|
|
997
|
+
});
|
|
952
998
|
if (subcommand === "status") {
|
|
953
999
|
let workspace;
|
|
954
1000
|
try {
|
|
955
|
-
workspace = await client.getWorkspace();
|
|
1001
|
+
workspace = await client.getWorkspace(context.workspaceId);
|
|
956
1002
|
}
|
|
957
1003
|
catch (error) {
|
|
958
1004
|
const message = error instanceof Error ? error.message : String(error);
|
|
959
|
-
throw new Error(`Remote status failed using ${
|
|
1005
|
+
throw new Error(`Remote status failed using ${linkageLabel}: ${message}`);
|
|
960
1006
|
}
|
|
961
1007
|
assertRemoteIdentity(workspace, context.identity);
|
|
962
1008
|
const remoteIdentity = extractWorkspaceIdentity(workspace);
|
|
@@ -965,18 +1011,21 @@ async function main() {
|
|
|
965
1011
|
command: "remote",
|
|
966
1012
|
subcommand: "status",
|
|
967
1013
|
config_path: context.configPath,
|
|
968
|
-
provider:
|
|
969
|
-
endpoint: context.
|
|
970
|
-
|
|
971
|
-
|
|
1014
|
+
provider: "paseo",
|
|
1015
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1016
|
+
workspace_id: context.workspaceId,
|
|
1017
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1018
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
972
1019
|
linkage_source: context.linkageSource,
|
|
973
1020
|
local_identity: context.identity,
|
|
974
1021
|
remote_identity: {
|
|
975
1022
|
project_id: remoteIdentity.project_id,
|
|
976
|
-
|
|
1023
|
+
workspace_id: remoteIdentity.workspace_id,
|
|
1024
|
+
actor_type: remoteIdentity.actor_type,
|
|
1025
|
+
backend_version: remoteIdentity.backend_version,
|
|
977
1026
|
},
|
|
978
1027
|
document_count: remoteIdentity.document_count,
|
|
979
|
-
last_imported_at: context.
|
|
1028
|
+
last_imported_at: context.resolvedTarget.last_imported_at ?? null,
|
|
980
1029
|
workspace,
|
|
981
1030
|
};
|
|
982
1031
|
if (output === "json") {
|
|
@@ -986,11 +1035,12 @@ async function main() {
|
|
|
986
1035
|
console.log("Remote workspace reachable");
|
|
987
1036
|
console.log(`Config: ${payload.config_path}`);
|
|
988
1037
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
1038
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
989
1039
|
console.log(`Pod: ${payload.pod_name}`);
|
|
990
1040
|
console.log(`Actor: ${payload.actor_id}`);
|
|
991
1041
|
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
992
|
-
console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.
|
|
993
|
-
console.log(`Remote identity: ${String(payload.remote_identity.project_id)}/${String(payload.remote_identity.
|
|
1042
|
+
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)})`);
|
|
994
1044
|
console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
|
|
995
1045
|
console.log(`Last imported at: ${String(payload.last_imported_at ?? "never")}`);
|
|
996
1046
|
}
|
|
@@ -1000,33 +1050,35 @@ async function main() {
|
|
|
1000
1050
|
const documents = await collectWorkspaceDocuments(parsed.repoRoot);
|
|
1001
1051
|
let importResult;
|
|
1002
1052
|
try {
|
|
1003
|
-
importResult = validateImportResult(await client.importWorkspace(documents, true));
|
|
1053
|
+
importResult = validateImportResult(await client.importWorkspace(context.workspaceId, documents, true));
|
|
1004
1054
|
}
|
|
1005
1055
|
catch (error) {
|
|
1006
1056
|
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
-
throw new Error(`Remote push failed using ${
|
|
1057
|
+
throw new Error(`Remote push failed using ${linkageLabel}: ${message}`);
|
|
1008
1058
|
}
|
|
1009
1059
|
let workspace;
|
|
1010
1060
|
try {
|
|
1011
|
-
workspace = await client.getWorkspace();
|
|
1061
|
+
workspace = await client.getWorkspace(context.workspaceId);
|
|
1012
1062
|
}
|
|
1013
1063
|
catch (error) {
|
|
1014
1064
|
const message = error instanceof Error ? error.message : String(error);
|
|
1015
|
-
throw new Error(`Remote push could not verify remote state using ${
|
|
1065
|
+
throw new Error(`Remote push could not verify remote state using ${linkageLabel}: ${message}`);
|
|
1016
1066
|
}
|
|
1017
1067
|
assertRemoteIdentity(workspace, context.identity);
|
|
1018
1068
|
const importedAt = new Date().toISOString();
|
|
1019
|
-
await updateRemoteConfigMetadata(parsed.repoRoot, {
|
|
1069
|
+
await updateRemoteConfigMetadata(parsed.repoRoot, context.workspaceId, {
|
|
1070
|
+
last_imported_at: importedAt,
|
|
1071
|
+
});
|
|
1020
1072
|
const payload = {
|
|
1021
1073
|
status: "ok",
|
|
1022
1074
|
command: "remote",
|
|
1023
1075
|
subcommand: "push",
|
|
1024
|
-
endpoint: context.
|
|
1025
|
-
|
|
1026
|
-
|
|
1076
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1077
|
+
workspace_id: context.workspaceId,
|
|
1078
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1079
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1027
1080
|
linkage_source: context.linkageSource,
|
|
1028
1081
|
project_id: context.identity.project_id,
|
|
1029
|
-
workspace_name: context.identity.workspace_name,
|
|
1030
1082
|
local_document_count: documents.length,
|
|
1031
1083
|
imported: importResult.imported,
|
|
1032
1084
|
created: importResult.created,
|
|
@@ -1042,6 +1094,7 @@ async function main() {
|
|
|
1042
1094
|
else {
|
|
1043
1095
|
console.log("Remote push complete");
|
|
1044
1096
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
1097
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
1045
1098
|
console.log(`Pod: ${payload.pod_name}`);
|
|
1046
1099
|
console.log(`Actor: ${payload.actor_id}`);
|
|
1047
1100
|
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
@@ -1059,11 +1112,11 @@ async function main() {
|
|
|
1059
1112
|
if (subcommand === "ls") {
|
|
1060
1113
|
let snapshot;
|
|
1061
1114
|
try {
|
|
1062
|
-
snapshot = await client.getSnapshot(parsed.prefix);
|
|
1115
|
+
snapshot = await client.getSnapshot(context.workspaceId, parsed.prefix);
|
|
1063
1116
|
}
|
|
1064
1117
|
catch (error) {
|
|
1065
1118
|
const message = error instanceof Error ? error.message : String(error);
|
|
1066
|
-
throw new Error(`Remote ls failed using ${
|
|
1119
|
+
throw new Error(`Remote ls failed using ${linkageLabel}: ${message}`);
|
|
1067
1120
|
}
|
|
1068
1121
|
const paths = (snapshot.documents ?? [])
|
|
1069
1122
|
.map((document) => (typeof document.path === "string" ? document.path : ""))
|
|
@@ -1073,9 +1126,10 @@ async function main() {
|
|
|
1073
1126
|
status: "ok",
|
|
1074
1127
|
command: "remote",
|
|
1075
1128
|
subcommand: "ls",
|
|
1076
|
-
endpoint: context.
|
|
1077
|
-
|
|
1078
|
-
|
|
1129
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1130
|
+
workspace_id: context.workspaceId,
|
|
1131
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1132
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1079
1133
|
linkage_source: context.linkageSource,
|
|
1080
1134
|
prefix: parsed.prefix ?? null,
|
|
1081
1135
|
paths,
|
|
@@ -1103,22 +1157,23 @@ async function main() {
|
|
|
1103
1157
|
}
|
|
1104
1158
|
let document;
|
|
1105
1159
|
try {
|
|
1106
|
-
document = await client.getDocument(documentPath);
|
|
1160
|
+
document = await client.getDocument(context.workspaceId, documentPath);
|
|
1107
1161
|
}
|
|
1108
1162
|
catch (error) {
|
|
1109
1163
|
const message = error instanceof Error ? error.message : String(error);
|
|
1110
|
-
throw new Error(`Remote cat failed using ${
|
|
1164
|
+
throw new Error(`Remote cat failed using ${linkageLabel}: ${message}`);
|
|
1111
1165
|
}
|
|
1112
1166
|
if (!document.document) {
|
|
1113
|
-
throw new Error(`Remote document not found using ${
|
|
1167
|
+
throw new Error(`Remote document not found using ${linkageLabel}: ${documentPath}`);
|
|
1114
1168
|
}
|
|
1115
1169
|
const payload = {
|
|
1116
1170
|
status: "ok",
|
|
1117
1171
|
command: "remote",
|
|
1118
1172
|
subcommand: "cat",
|
|
1119
|
-
endpoint: context.
|
|
1120
|
-
|
|
1121
|
-
|
|
1173
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1174
|
+
workspace_id: context.workspaceId,
|
|
1175
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1176
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1122
1177
|
linkage_source: context.linkageSource,
|
|
1123
1178
|
document: document.document,
|
|
1124
1179
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { runDoctor } from "./doctor.js";
|
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
5
|
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
6
|
export { ensureGitignoreEntries } from "./gitignore.js";
|
|
7
|
-
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity,
|
|
7
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
8
8
|
export { listSpecs, showSpec } from "./spec-runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,5 @@ export { runDoctor } from "./doctor.js";
|
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
5
|
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
6
|
export { ensureGitignoreEntries } from "./gitignore.js";
|
|
7
|
-
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity,
|
|
7
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
8
8
|
export { listSpecs, showSpec } from "./spec-runtime.js";
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Manifest } from "./types.js";
|
|
2
2
|
export declare const MANIFEST_VERSION = 1;
|
|
3
3
|
export declare function allowedKindExtensions(): Record<string, string[]>;
|
|
4
|
-
export
|
|
4
|
+
export interface ManifestIdentityDefaults {
|
|
5
|
+
project_id: string;
|
|
6
|
+
workspace_ids: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function deriveManifestIdentity(repoRoot: string): ManifestIdentityDefaults;
|
|
5
9
|
export declare function createManifest(repoRoot: string, now?: string): Manifest;
|
|
6
10
|
export declare function normalizeManifest(raw: unknown, repoRoot: string): Manifest;
|
|
7
11
|
export declare function inferArtifactType(filePath: string): {
|
package/dist/manifest.js
CHANGED
|
@@ -13,6 +13,7 @@ const KIND_EXTENSIONS = {
|
|
|
13
13
|
file: [],
|
|
14
14
|
};
|
|
15
15
|
const PROJECT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16
|
+
const WORKSPACE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16
17
|
const FALLBACK_WORKSPACE_ID = "reffy-workspace";
|
|
17
18
|
function isObject(value) {
|
|
18
19
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -25,11 +26,22 @@ function isIsoDate(value) {
|
|
|
25
26
|
function isProjectId(value) {
|
|
26
27
|
return typeof value === "string" && PROJECT_ID_PATTERN.test(value);
|
|
27
28
|
}
|
|
29
|
+
function isWorkspaceId(value) {
|
|
30
|
+
return typeof value === "string" && WORKSPACE_ID_PATTERN.test(value);
|
|
31
|
+
}
|
|
28
32
|
function isWorkspaceName(value) {
|
|
29
33
|
return (typeof value === "string" &&
|
|
30
34
|
value.trim().length > 0 &&
|
|
31
35
|
!/[\\/\r\n\t]/.test(value));
|
|
32
36
|
}
|
|
37
|
+
function slugifyWorkspaceId(value) {
|
|
38
|
+
const slug = value
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
42
|
+
.replace(/^-+|-+$/g, "");
|
|
43
|
+
return slug.length > 0 ? slug : null;
|
|
44
|
+
}
|
|
33
45
|
function slugifyIdentity(value) {
|
|
34
46
|
return value
|
|
35
47
|
.trim()
|
|
@@ -51,18 +63,33 @@ export function deriveManifestIdentity(repoRoot) {
|
|
|
51
63
|
const identity = candidate || FALLBACK_WORKSPACE_ID;
|
|
52
64
|
return {
|
|
53
65
|
project_id: identity,
|
|
54
|
-
|
|
66
|
+
workspace_ids: [identity],
|
|
55
67
|
};
|
|
56
68
|
}
|
|
57
69
|
export function createManifest(repoRoot, now = new Date().toISOString()) {
|
|
70
|
+
const defaults = deriveManifestIdentity(repoRoot);
|
|
58
71
|
return {
|
|
59
72
|
version: MANIFEST_VERSION,
|
|
60
73
|
created_at: now,
|
|
61
74
|
updated_at: now,
|
|
62
|
-
|
|
75
|
+
project_id: defaults.project_id,
|
|
76
|
+
workspace_ids: defaults.workspace_ids,
|
|
63
77
|
artifacts: [],
|
|
64
78
|
};
|
|
65
79
|
}
|
|
80
|
+
function normalizeWorkspaceIds(raw, fallback) {
|
|
81
|
+
const explicit = Array.isArray(raw.workspace_ids)
|
|
82
|
+
? raw.workspace_ids.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
83
|
+
: null;
|
|
84
|
+
if (explicit && explicit.length > 0) {
|
|
85
|
+
return Array.from(new Set(explicit.map((entry) => entry.trim())));
|
|
86
|
+
}
|
|
87
|
+
if (typeof raw.workspace_name === "string" && raw.workspace_name.trim().length > 0) {
|
|
88
|
+
const migrated = slugifyWorkspaceId(raw.workspace_name) ?? raw.workspace_name.trim();
|
|
89
|
+
return [migrated];
|
|
90
|
+
}
|
|
91
|
+
return [...fallback];
|
|
92
|
+
}
|
|
66
93
|
export function normalizeManifest(raw, repoRoot) {
|
|
67
94
|
const now = new Date().toISOString();
|
|
68
95
|
const defaults = deriveManifestIdentity(repoRoot);
|
|
@@ -71,7 +98,8 @@ export function normalizeManifest(raw, repoRoot) {
|
|
|
71
98
|
version: 0,
|
|
72
99
|
created_at: now,
|
|
73
100
|
updated_at: now,
|
|
74
|
-
|
|
101
|
+
project_id: defaults.project_id,
|
|
102
|
+
workspace_ids: defaults.workspace_ids,
|
|
75
103
|
artifacts: raw,
|
|
76
104
|
};
|
|
77
105
|
}
|
|
@@ -79,12 +107,13 @@ export function normalizeManifest(raw, repoRoot) {
|
|
|
79
107
|
return createManifest(repoRoot, now);
|
|
80
108
|
}
|
|
81
109
|
const artifacts = Array.isArray(raw.artifacts) ? raw.artifacts : [];
|
|
110
|
+
const workspaceIds = normalizeWorkspaceIds(raw, defaults.workspace_ids);
|
|
82
111
|
return {
|
|
83
112
|
version: typeof raw.version === "number" ? raw.version : MANIFEST_VERSION,
|
|
84
113
|
created_at: typeof raw.created_at === "string" ? raw.created_at : now,
|
|
85
114
|
updated_at: typeof raw.updated_at === "string" ? raw.updated_at : now,
|
|
86
115
|
project_id: typeof raw.project_id === "string" ? raw.project_id : defaults.project_id,
|
|
87
|
-
|
|
116
|
+
workspace_ids: workspaceIds,
|
|
88
117
|
artifacts,
|
|
89
118
|
};
|
|
90
119
|
}
|
|
@@ -183,8 +212,32 @@ export async function validateManifest(manifestPath, artifactsDir) {
|
|
|
183
212
|
if (raw.project_id !== undefined && !isProjectId(raw.project_id)) {
|
|
184
213
|
errors.push("project_id must be a non-empty kebab-case string");
|
|
185
214
|
}
|
|
186
|
-
if (raw.
|
|
187
|
-
|
|
215
|
+
if (raw.workspace_ids !== undefined) {
|
|
216
|
+
if (!Array.isArray(raw.workspace_ids) || raw.workspace_ids.length === 0) {
|
|
217
|
+
errors.push("workspace_ids must be a non-empty array of kebab-case strings when provided");
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
raw.workspace_ids.forEach((entry, index) => {
|
|
222
|
+
if (!isWorkspaceId(entry)) {
|
|
223
|
+
errors.push(`workspace_ids[${index}] must be a non-empty kebab-case string`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (seen.has(entry)) {
|
|
227
|
+
errors.push(`workspace_ids[${index}] duplicates "${entry}"`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
seen.add(entry);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (raw.workspace_name !== undefined) {
|
|
235
|
+
if (!isWorkspaceName(raw.workspace_name)) {
|
|
236
|
+
errors.push("workspace_name must be a non-empty string without path separators or control characters");
|
|
237
|
+
}
|
|
238
|
+
else if (raw.workspace_ids === undefined) {
|
|
239
|
+
warnings.push("workspace_name is deprecated; migrate to workspace_ids");
|
|
240
|
+
}
|
|
188
241
|
}
|
|
189
242
|
if (!Array.isArray(raw.artifacts)) {
|
|
190
243
|
errors.push("artifacts must be an array");
|
package/dist/remote.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import type { RemoteDocument, RemoteImportResult, RemoteLinkConfig, RemoteWorkspaceSummary } from "./types.js";
|
|
2
|
-
export declare const REMOTE_CONFIG_VERSION =
|
|
1
|
+
import type { RemoteDocument, RemoteImportResult, RemoteLinkConfig, RemoteTarget, RemoteWorkspaceSummary } from "./types.js";
|
|
2
|
+
export declare const REMOTE_CONFIG_VERSION = 2;
|
|
3
3
|
export declare const REMOTE_PROVIDER = "paseo";
|
|
4
|
+
export declare const REMOTE_BACKEND_ACTOR_TYPE = "reffyRemoteBackend";
|
|
5
|
+
export declare const REMOTE_BACKEND_VERSION = "v2";
|
|
4
6
|
export interface WorkspaceIdentity {
|
|
5
7
|
project_id: string;
|
|
6
|
-
|
|
8
|
+
workspace_id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ManifestIdentityLike {
|
|
11
|
+
project_id?: string;
|
|
12
|
+
workspace_ids?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedRemoteTarget {
|
|
15
|
+
endpoint: string;
|
|
16
|
+
pod_name: string;
|
|
17
|
+
actor_id: string;
|
|
18
|
+
last_imported_at?: string;
|
|
7
19
|
}
|
|
8
20
|
export interface EnsureRemoteInitOptions {
|
|
9
21
|
endpoint: string;
|
|
@@ -11,9 +23,11 @@ export interface EnsureRemoteInitOptions {
|
|
|
11
23
|
actorId?: string;
|
|
12
24
|
provision: boolean;
|
|
13
25
|
identity: WorkspaceIdentity;
|
|
26
|
+
existingConfig: RemoteLinkConfig | null;
|
|
14
27
|
}
|
|
15
28
|
export interface EnsureRemoteInitResult {
|
|
16
29
|
config: RemoteLinkConfig;
|
|
30
|
+
target: RemoteTarget;
|
|
17
31
|
created_pod: boolean;
|
|
18
32
|
created_actor: boolean;
|
|
19
33
|
}
|
|
@@ -24,24 +38,39 @@ export interface ValidatedImportResult {
|
|
|
24
38
|
deleted: number;
|
|
25
39
|
}
|
|
26
40
|
export declare function resolveRemoteConfigPath(repoRoot: string): string;
|
|
41
|
+
export declare function resolveSelectedWorkspaceId(identity: ManifestIdentityLike, override: string | undefined): string;
|
|
42
|
+
export declare function requireWorkspaceIdentity(identity: ManifestIdentityLike, selectedWorkspaceId: string): WorkspaceIdentity;
|
|
27
43
|
export declare function readRemoteConfig(repoRoot: string): Promise<RemoteLinkConfig | null>;
|
|
28
44
|
export declare function writeRemoteConfig(repoRoot: string, config: RemoteLinkConfig): Promise<string>;
|
|
29
|
-
export declare function updateRemoteConfigMetadata(repoRoot: string, patch: Partial<Pick<
|
|
30
|
-
export declare function
|
|
31
|
-
|
|
45
|
+
export declare function updateRemoteConfigMetadata(repoRoot: string, workspaceId: string, patch: Partial<Pick<RemoteTarget, "last_imported_at">>): Promise<RemoteLinkConfig | null>;
|
|
46
|
+
export declare function resolveRemoteTarget(stored: RemoteLinkConfig | null, workspaceId: string, overrides: {
|
|
47
|
+
endpoint?: string;
|
|
48
|
+
pod_name?: string;
|
|
49
|
+
actor_id?: string;
|
|
50
|
+
}): ResolvedRemoteTarget | null;
|
|
51
|
+
export declare function describeRemoteLinkage(linkage: {
|
|
52
|
+
endpoint: string;
|
|
53
|
+
pod_name: string;
|
|
54
|
+
actor_id: string;
|
|
55
|
+
workspace_id: string;
|
|
56
|
+
}): string;
|
|
32
57
|
export declare class PaseoRemoteClient {
|
|
33
58
|
private readonly config;
|
|
34
|
-
constructor(config:
|
|
59
|
+
constructor(config: {
|
|
60
|
+
endpoint: string;
|
|
61
|
+
pod_name: string;
|
|
62
|
+
actor_id: string;
|
|
63
|
+
});
|
|
35
64
|
createPod(): Promise<string>;
|
|
36
65
|
createActor(identity: WorkspaceIdentity): Promise<string>;
|
|
37
|
-
getWorkspace(): Promise<RemoteWorkspaceSummary>;
|
|
38
|
-
importWorkspace(documents: RemoteDocument[], replaceMissing?: boolean): Promise<RemoteImportResult>;
|
|
39
|
-
getSnapshot(prefix?: string): Promise<{
|
|
66
|
+
getWorkspace(workspaceId: string): Promise<RemoteWorkspaceSummary>;
|
|
67
|
+
importWorkspace(workspaceId: string, documents: RemoteDocument[], replaceMissing?: boolean): Promise<RemoteImportResult>;
|
|
68
|
+
getSnapshot(workspaceId: string, prefix?: string): Promise<{
|
|
40
69
|
documents?: Array<{
|
|
41
70
|
path?: string;
|
|
42
71
|
}>;
|
|
43
72
|
}>;
|
|
44
|
-
getDocument(documentPath: string): Promise<{
|
|
73
|
+
getDocument(workspaceId: string, documentPath: string): Promise<{
|
|
45
74
|
document?: {
|
|
46
75
|
path?: string;
|
|
47
76
|
content?: string;
|
|
@@ -50,13 +79,11 @@ export declare class PaseoRemoteClient {
|
|
|
50
79
|
}>;
|
|
51
80
|
}
|
|
52
81
|
export declare function ensureRemoteInit(repoRoot: string, options: EnsureRemoteInitOptions): Promise<EnsureRemoteInitResult>;
|
|
53
|
-
export declare function requireWorkspaceIdentity(identity: {
|
|
54
|
-
project_id?: string;
|
|
55
|
-
workspace_name?: string;
|
|
56
|
-
}): WorkspaceIdentity;
|
|
57
82
|
export declare function extractWorkspaceIdentity(summary: RemoteWorkspaceSummary): {
|
|
58
83
|
project_id?: string | null;
|
|
59
|
-
|
|
84
|
+
workspace_id?: string | null;
|
|
85
|
+
actor_type?: string | null;
|
|
86
|
+
backend_version?: string | null;
|
|
60
87
|
document_count?: number | null;
|
|
61
88
|
};
|
|
62
89
|
export declare function assertRemoteIdentity(summary: RemoteWorkspaceSummary, identity: WorkspaceIdentity): void;
|
package/dist/remote.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveRefsDir } from "./refs-paths.js";
|
|
4
|
-
export const REMOTE_CONFIG_VERSION =
|
|
4
|
+
export const REMOTE_CONFIG_VERSION = 2;
|
|
5
5
|
export const REMOTE_PROVIDER = "paseo";
|
|
6
|
+
export const REMOTE_BACKEND_ACTOR_TYPE = "reffyRemoteBackend";
|
|
7
|
+
export const REMOTE_BACKEND_VERSION = "v2";
|
|
6
8
|
const REMOTE_STATE_DIR = path.join("state");
|
|
7
9
|
const REMOTE_CONFIG_FILE = "remote.json";
|
|
8
10
|
function isObject(value) {
|
|
@@ -17,6 +19,39 @@ function normalizeSeparators(value) {
|
|
|
17
19
|
export function resolveRemoteConfigPath(repoRoot) {
|
|
18
20
|
return path.join(resolveRefsDir(repoRoot), REMOTE_STATE_DIR, REMOTE_CONFIG_FILE);
|
|
19
21
|
}
|
|
22
|
+
export function resolveSelectedWorkspaceId(identity, override) {
|
|
23
|
+
const workspaceIds = (identity.workspace_ids ?? [])
|
|
24
|
+
.map((entry) => entry.trim())
|
|
25
|
+
.filter((entry) => entry.length > 0);
|
|
26
|
+
if (override !== undefined) {
|
|
27
|
+
const trimmed = override.trim();
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
throw new Error("--workspace-id requires a non-empty value");
|
|
30
|
+
}
|
|
31
|
+
if (workspaceIds.length > 0 && !workspaceIds.includes(trimmed)) {
|
|
32
|
+
throw new Error(`Selected workspace id "${trimmed}" is not a member of manifest.workspace_ids: ${workspaceIds.join(", ") || "(none)"}`);
|
|
33
|
+
}
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
if (workspaceIds.length === 0) {
|
|
37
|
+
throw new Error("Remote sync requires .reffy/manifest.json to define workspace_ids. Run `reffy init` or add at least one workspace id.");
|
|
38
|
+
}
|
|
39
|
+
if (workspaceIds.length === 1) {
|
|
40
|
+
return workspaceIds[0];
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Manifest has multiple workspace_ids (${workspaceIds.join(", ")}); pass --workspace-id to select a target projection.`);
|
|
43
|
+
}
|
|
44
|
+
export function requireWorkspaceIdentity(identity, selectedWorkspaceId) {
|
|
45
|
+
const project_id = identity.project_id?.trim();
|
|
46
|
+
if (!project_id) {
|
|
47
|
+
throw new Error("Remote sync requires .reffy/manifest.json to define project_id");
|
|
48
|
+
}
|
|
49
|
+
const workspace_id = selectedWorkspaceId.trim();
|
|
50
|
+
if (!workspace_id) {
|
|
51
|
+
throw new Error("Remote sync requires a selected workspace_id");
|
|
52
|
+
}
|
|
53
|
+
return { project_id, workspace_id };
|
|
54
|
+
}
|
|
20
55
|
export async function readRemoteConfig(repoRoot) {
|
|
21
56
|
const configPath = resolveRemoteConfigPath(repoRoot);
|
|
22
57
|
const raw = await fs.readFile(configPath, "utf8").catch(() => null);
|
|
@@ -32,28 +67,43 @@ export async function readRemoteConfig(repoRoot) {
|
|
|
32
67
|
if (!isObject(parsed)) {
|
|
33
68
|
throw new Error(`Remote config at ${configPath} must be an object`);
|
|
34
69
|
}
|
|
35
|
-
if (parsed.version !== REMOTE_CONFIG_VERSION) {
|
|
36
|
-
throw new Error(`Remote config at ${configPath} must use version ${String(REMOTE_CONFIG_VERSION)}`);
|
|
37
|
-
}
|
|
38
70
|
if (parsed.provider !== REMOTE_PROVIDER) {
|
|
39
71
|
throw new Error(`Remote config at ${configPath} must use provider "${REMOTE_PROVIDER}"`);
|
|
40
72
|
}
|
|
41
73
|
if (!isNonEmptyString(parsed.endpoint)) {
|
|
42
74
|
throw new Error(`Remote config at ${configPath} must define endpoint`);
|
|
43
75
|
}
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(`Remote config at ${configPath}
|
|
76
|
+
if (parsed.version === 1) {
|
|
77
|
+
throw new Error(`Remote config at ${configPath} uses the legacy v1 single-target shape. Run \`reffy remote init --provision\` against the v2 backend to reinitialize, then repush with \`reffy remote push\`.`);
|
|
46
78
|
}
|
|
47
|
-
if (
|
|
48
|
-
throw new Error(`Remote config at ${configPath} must
|
|
79
|
+
if (parsed.version !== REMOTE_CONFIG_VERSION) {
|
|
80
|
+
throw new Error(`Remote config at ${configPath} must use version ${String(REMOTE_CONFIG_VERSION)}`);
|
|
81
|
+
}
|
|
82
|
+
if (!isObject(parsed.targets)) {
|
|
83
|
+
throw new Error(`Remote config at ${configPath} must define targets as an object keyed by workspace_id`);
|
|
84
|
+
}
|
|
85
|
+
const targets = {};
|
|
86
|
+
for (const [workspaceId, entry] of Object.entries(parsed.targets)) {
|
|
87
|
+
if (!isObject(entry)) {
|
|
88
|
+
throw new Error(`Remote config at ${configPath} has invalid target for workspace_id "${workspaceId}"`);
|
|
89
|
+
}
|
|
90
|
+
if (!isNonEmptyString(entry.pod_name)) {
|
|
91
|
+
throw new Error(`Remote config target "${workspaceId}" must define pod_name`);
|
|
92
|
+
}
|
|
93
|
+
if (!isNonEmptyString(entry.actor_id)) {
|
|
94
|
+
throw new Error(`Remote config target "${workspaceId}" must define actor_id`);
|
|
95
|
+
}
|
|
96
|
+
targets[workspaceId] = {
|
|
97
|
+
pod_name: entry.pod_name.trim(),
|
|
98
|
+
actor_id: entry.actor_id.trim(),
|
|
99
|
+
last_imported_at: isNonEmptyString(entry.last_imported_at) ? entry.last_imported_at.trim() : undefined,
|
|
100
|
+
};
|
|
49
101
|
}
|
|
50
102
|
return {
|
|
51
103
|
version: REMOTE_CONFIG_VERSION,
|
|
52
104
|
provider: REMOTE_PROVIDER,
|
|
53
105
|
endpoint: parsed.endpoint.trim(),
|
|
54
|
-
|
|
55
|
-
actor_id: parsed.actor_id.trim(),
|
|
56
|
-
last_imported_at: isNonEmptyString(parsed.last_imported_at) ? parsed.last_imported_at.trim() : undefined,
|
|
106
|
+
targets,
|
|
57
107
|
};
|
|
58
108
|
}
|
|
59
109
|
export async function writeRemoteConfig(repoRoot, config) {
|
|
@@ -62,38 +112,40 @@ export async function writeRemoteConfig(repoRoot, config) {
|
|
|
62
112
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
63
113
|
return configPath;
|
|
64
114
|
}
|
|
65
|
-
export async function updateRemoteConfigMetadata(repoRoot, patch) {
|
|
115
|
+
export async function updateRemoteConfigMetadata(repoRoot, workspaceId, patch) {
|
|
66
116
|
const existing = await readRemoteConfig(repoRoot);
|
|
67
117
|
if (!existing)
|
|
68
118
|
return null;
|
|
119
|
+
const target = existing.targets[workspaceId];
|
|
120
|
+
if (!target)
|
|
121
|
+
return null;
|
|
69
122
|
const next = {
|
|
70
123
|
...existing,
|
|
71
|
-
|
|
124
|
+
targets: {
|
|
125
|
+
...existing.targets,
|
|
126
|
+
[workspaceId]: { ...target, ...patch },
|
|
127
|
+
},
|
|
72
128
|
};
|
|
73
129
|
await writeRemoteConfig(repoRoot, next);
|
|
74
130
|
return next;
|
|
75
131
|
}
|
|
76
|
-
export function
|
|
77
|
-
if (!stored && !overrides.endpoint && !overrides.pod_name && !overrides.actor_id) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
132
|
+
export function resolveRemoteTarget(stored, workspaceId, overrides) {
|
|
80
133
|
const endpoint = overrides.endpoint ?? stored?.endpoint;
|
|
81
|
-
const
|
|
82
|
-
const
|
|
134
|
+
const target = stored?.targets?.[workspaceId];
|
|
135
|
+
const pod_name = overrides.pod_name ?? target?.pod_name;
|
|
136
|
+
const actor_id = overrides.actor_id ?? target?.actor_id;
|
|
83
137
|
if (!endpoint || !pod_name || !actor_id) {
|
|
84
138
|
return null;
|
|
85
139
|
}
|
|
86
140
|
return {
|
|
87
|
-
version: REMOTE_CONFIG_VERSION,
|
|
88
|
-
provider: REMOTE_PROVIDER,
|
|
89
141
|
endpoint,
|
|
90
142
|
pod_name,
|
|
91
143
|
actor_id,
|
|
92
|
-
last_imported_at:
|
|
144
|
+
last_imported_at: target?.last_imported_at,
|
|
93
145
|
};
|
|
94
146
|
}
|
|
95
|
-
export function describeRemoteLinkage(
|
|
96
|
-
return `endpoint=${
|
|
147
|
+
export function describeRemoteLinkage(linkage) {
|
|
148
|
+
return `endpoint=${linkage.endpoint} pod=${linkage.pod_name} actor=${linkage.actor_id} workspace=${linkage.workspace_id}`;
|
|
97
149
|
}
|
|
98
150
|
async function http(url, init = {}) {
|
|
99
151
|
const headers = new Headers(init.headers ?? {});
|
|
@@ -114,6 +166,9 @@ async function httpJson(url, init = {}) {
|
|
|
114
166
|
function actorBase(config) {
|
|
115
167
|
return `${config.endpoint}/pods/${config.pod_name}/actors/${config.actor_id}`;
|
|
116
168
|
}
|
|
169
|
+
function workspaceBase(config, workspaceId) {
|
|
170
|
+
return `${actorBase(config)}/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
171
|
+
}
|
|
117
172
|
export class PaseoRemoteClient {
|
|
118
173
|
config;
|
|
119
174
|
constructor(config) {
|
|
@@ -131,12 +186,12 @@ export class PaseoRemoteClient {
|
|
|
131
186
|
method: "POST",
|
|
132
187
|
body: JSON.stringify({
|
|
133
188
|
config: {
|
|
134
|
-
actorType:
|
|
135
|
-
version:
|
|
189
|
+
actorType: REMOTE_BACKEND_ACTOR_TYPE,
|
|
190
|
+
version: REMOTE_BACKEND_VERSION,
|
|
136
191
|
schema: { type: "object" },
|
|
137
192
|
params: {
|
|
138
193
|
project_id: identity.project_id,
|
|
139
|
-
|
|
194
|
+
workspace_ids: [identity.workspace_id],
|
|
140
195
|
default_lock_ttl_seconds: 120,
|
|
141
196
|
max_snapshot_documents: 1000,
|
|
142
197
|
},
|
|
@@ -148,11 +203,11 @@ export class PaseoRemoteClient {
|
|
|
148
203
|
}
|
|
149
204
|
return result.actorId.trim();
|
|
150
205
|
}
|
|
151
|
-
async getWorkspace() {
|
|
152
|
-
return await httpJson(
|
|
206
|
+
async getWorkspace(workspaceId) {
|
|
207
|
+
return await httpJson(workspaceBase(this.config, workspaceId));
|
|
153
208
|
}
|
|
154
|
-
async importWorkspace(documents, replaceMissing = true) {
|
|
155
|
-
return await httpJson(`${
|
|
209
|
+
async importWorkspace(workspaceId, documents, replaceMissing = true) {
|
|
210
|
+
return await httpJson(`${workspaceBase(this.config, workspaceId)}/import`, {
|
|
156
211
|
method: "POST",
|
|
157
212
|
body: JSON.stringify({
|
|
158
213
|
documents,
|
|
@@ -160,27 +215,29 @@ export class PaseoRemoteClient {
|
|
|
160
215
|
}),
|
|
161
216
|
});
|
|
162
217
|
}
|
|
163
|
-
async getSnapshot(prefix) {
|
|
218
|
+
async getSnapshot(workspaceId, prefix) {
|
|
164
219
|
const query = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
|
|
165
|
-
return await httpJson(`${
|
|
220
|
+
return await httpJson(`${workspaceBase(this.config, workspaceId)}/snapshot${query}`);
|
|
166
221
|
}
|
|
167
|
-
async getDocument(documentPath) {
|
|
168
|
-
return await httpJson(`${
|
|
222
|
+
async getDocument(workspaceId, documentPath) {
|
|
223
|
+
return await httpJson(`${workspaceBase(this.config, workspaceId)}/documents?path=${encodeURIComponent(documentPath)}`);
|
|
169
224
|
}
|
|
170
225
|
}
|
|
171
226
|
export async function ensureRemoteInit(repoRoot, options) {
|
|
172
227
|
let created_pod = false;
|
|
173
228
|
let created_actor = false;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!options.endpoint.trim()) {
|
|
229
|
+
const endpoint = options.endpoint.trim();
|
|
230
|
+
if (!endpoint) {
|
|
177
231
|
throw new Error("Remote init requires an endpoint");
|
|
178
232
|
}
|
|
233
|
+
const priorTarget = options.existingConfig?.targets[options.identity.workspace_id] ?? null;
|
|
234
|
+
let podName = options.podName?.trim() || priorTarget?.pod_name || "";
|
|
235
|
+
let actorId = options.actorId?.trim() || priorTarget?.actor_id || "";
|
|
179
236
|
if (!podName && !options.provision) {
|
|
180
237
|
throw new Error("Remote init requires --pod-name unless --provision is set");
|
|
181
238
|
}
|
|
182
239
|
const baseClient = new PaseoRemoteClient({
|
|
183
|
-
endpoint
|
|
240
|
+
endpoint,
|
|
184
241
|
pod_name: podName || "pending-pod",
|
|
185
242
|
actor_id: actorId || "pending-actor",
|
|
186
243
|
});
|
|
@@ -193,44 +250,49 @@ export async function ensureRemoteInit(repoRoot, options) {
|
|
|
193
250
|
throw new Error("Remote init requires --actor-id unless --provision is set");
|
|
194
251
|
}
|
|
195
252
|
const client = new PaseoRemoteClient({
|
|
196
|
-
endpoint
|
|
253
|
+
endpoint,
|
|
197
254
|
pod_name: podName,
|
|
198
255
|
actor_id: "pending-actor",
|
|
199
256
|
});
|
|
200
257
|
actorId = await client.createActor(options.identity);
|
|
201
258
|
created_actor = true;
|
|
202
259
|
}
|
|
260
|
+
const nextTarget = {
|
|
261
|
+
pod_name: podName,
|
|
262
|
+
actor_id: actorId,
|
|
263
|
+
last_imported_at: priorTarget?.last_imported_at,
|
|
264
|
+
};
|
|
203
265
|
const config = {
|
|
204
266
|
version: REMOTE_CONFIG_VERSION,
|
|
205
267
|
provider: REMOTE_PROVIDER,
|
|
206
|
-
endpoint
|
|
207
|
-
|
|
208
|
-
|
|
268
|
+
endpoint,
|
|
269
|
+
targets: {
|
|
270
|
+
...(options.existingConfig?.targets ?? {}),
|
|
271
|
+
[options.identity.workspace_id]: nextTarget,
|
|
272
|
+
},
|
|
209
273
|
};
|
|
210
274
|
await writeRemoteConfig(repoRoot, config);
|
|
211
|
-
return { config, created_pod, created_actor };
|
|
212
|
-
}
|
|
213
|
-
export function requireWorkspaceIdentity(identity) {
|
|
214
|
-
const project_id = identity.project_id?.trim();
|
|
215
|
-
const workspace_name = identity.workspace_name?.trim();
|
|
216
|
-
if (!project_id || !workspace_name) {
|
|
217
|
-
throw new Error("Remote sync requires .reffy/manifest.json to define project_id and workspace_name");
|
|
218
|
-
}
|
|
219
|
-
return { project_id, workspace_name };
|
|
275
|
+
return { config, target: nextTarget, created_pod, created_actor };
|
|
220
276
|
}
|
|
221
277
|
export function extractWorkspaceIdentity(summary) {
|
|
278
|
+
const source = isObject(summary.source) ? summary.source : {};
|
|
222
279
|
const workspace = isObject(summary.workspace) ? summary.workspace : {};
|
|
223
280
|
const stats = isObject(summary.stats) ? summary.stats : {};
|
|
224
281
|
return {
|
|
225
|
-
project_id: typeof
|
|
226
|
-
|
|
282
|
+
project_id: typeof source.project_id === "string" ? source.project_id : null,
|
|
283
|
+
workspace_id: typeof workspace.workspace_id === "string" ? workspace.workspace_id : null,
|
|
284
|
+
actor_type: typeof source.actor_type === "string" ? source.actor_type : null,
|
|
285
|
+
backend_version: typeof source.version === "string" ? source.version : null,
|
|
227
286
|
document_count: typeof stats.document_count === "number" ? stats.document_count : null,
|
|
228
287
|
};
|
|
229
288
|
}
|
|
230
289
|
export function assertRemoteIdentity(summary, identity) {
|
|
231
290
|
const remote = extractWorkspaceIdentity(summary);
|
|
232
|
-
if (remote.
|
|
233
|
-
throw new Error(
|
|
291
|
+
if (!remote.workspace_id) {
|
|
292
|
+
throw new Error("Remote response did not include a workspace.workspace_id envelope. This likely means the linked actor is a legacy v1 backend. Reinitialize the target against reffyRemoteBackend.v2 (`reffy remote init --provision --workspace-id <id>`) and repush with `reffy remote push`.");
|
|
293
|
+
}
|
|
294
|
+
if (remote.project_id !== identity.project_id || remote.workspace_id !== identity.workspace_id) {
|
|
295
|
+
throw new Error(`Remote identity mismatch: local=${identity.project_id}/${identity.workspace_id} remote=${String(remote.project_id)}/${String(remote.workspace_id)}`);
|
|
234
296
|
}
|
|
235
297
|
}
|
|
236
298
|
export function validateImportResult(result) {
|
package/dist/storage.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare class ReferencesStore {
|
|
|
12
12
|
listArtifacts(): Promise<Artifact[]>;
|
|
13
13
|
getWorkspaceIdentity(): Promise<{
|
|
14
14
|
project_id?: string;
|
|
15
|
-
|
|
15
|
+
workspace_ids?: string[];
|
|
16
16
|
}>;
|
|
17
17
|
getArtifact(artifactId: string): Promise<Artifact | null>;
|
|
18
18
|
getArtifactPath(artifact: Artifact): string;
|
package/dist/storage.js
CHANGED
|
@@ -51,7 +51,7 @@ export class ReferencesStore {
|
|
|
51
51
|
const manifest = await this.readManifest();
|
|
52
52
|
return {
|
|
53
53
|
project_id: manifest.project_id,
|
|
54
|
-
|
|
54
|
+
workspace_ids: manifest.workspace_ids ? [...manifest.workspace_ids] : undefined,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
async getArtifact(artifactId) {
|
package/dist/types.d.ts
CHANGED
|
@@ -17,16 +17,21 @@ export interface Manifest {
|
|
|
17
17
|
created_at: string;
|
|
18
18
|
updated_at: string;
|
|
19
19
|
project_id?: string;
|
|
20
|
+
workspace_ids?: string[];
|
|
21
|
+
/** Deprecated v1 field retained only as migration input. */
|
|
20
22
|
workspace_name?: string;
|
|
21
23
|
artifacts: Artifact[];
|
|
22
24
|
}
|
|
25
|
+
export interface RemoteTarget {
|
|
26
|
+
pod_name: string;
|
|
27
|
+
actor_id: string;
|
|
28
|
+
last_imported_at?: string;
|
|
29
|
+
}
|
|
23
30
|
export interface RemoteLinkConfig {
|
|
24
31
|
version: number;
|
|
25
32
|
provider: "paseo";
|
|
26
33
|
endpoint: string;
|
|
27
|
-
|
|
28
|
-
actor_id: string;
|
|
29
|
-
last_imported_at?: string;
|
|
34
|
+
targets: Record<string, RemoteTarget>;
|
|
30
35
|
}
|
|
31
36
|
export interface RemoteDocument {
|
|
32
37
|
path: string;
|
|
@@ -41,6 +46,7 @@ export interface RemoteImportResult {
|
|
|
41
46
|
deleted?: number;
|
|
42
47
|
}
|
|
43
48
|
export interface RemoteWorkspaceSummary {
|
|
49
|
+
source?: Record<string, unknown>;
|
|
44
50
|
workspace?: Record<string, unknown>;
|
|
45
51
|
stats?: Record<string, unknown>;
|
|
46
52
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeManifest } from "./manifest.js";
|
|
4
4
|
import { DEFAULT_REFS_DIRNAME, LEGACY_REFS_DIRNAME, detectWorkspaceState, resolveCanonicalRefsDir, } from "./refs-paths.js";
|
|
5
5
|
async function pathExists(targetPath) {
|
|
6
6
|
try {
|
|
@@ -30,11 +30,13 @@ async function ensureCanonicalStructure(repoRoot) {
|
|
|
30
30
|
const raw = JSON.parse(rawText);
|
|
31
31
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
32
32
|
const record = raw;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
33
|
+
const missingProjectId = record.project_id === undefined;
|
|
34
|
+
const missingWorkspaceIds = !Array.isArray(record.workspace_ids) || record.workspace_ids.length === 0;
|
|
35
|
+
const needsMigration = missingProjectId || missingWorkspaceIds;
|
|
36
|
+
if (needsMigration) {
|
|
36
37
|
const nextManifest = normalizeManifest(raw, repoRoot);
|
|
37
38
|
nextManifest.updated_at = new Date().toISOString();
|
|
39
|
+
delete nextManifest.workspace_name;
|
|
38
40
|
await fs.writeFile(manifestPath, JSON.stringify(nextManifest, null, 2), "utf8");
|
|
39
41
|
}
|
|
40
42
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reffy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Planning system for AI assisted development.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reffy",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
"prepare": "npm run build",
|
|
36
36
|
"dev": "tsx watch src/cli.ts",
|
|
37
37
|
"check": "tsc --noEmit",
|
|
38
|
-
"remote:backend:demo": "node scripts/reffy-remote-backend-demo.mjs",
|
|
39
38
|
"test": "npm run build && vitest run --coverage",
|
|
40
39
|
"test:watch": "vitest"
|
|
41
40
|
},
|