reffy-cli 1.4.1 → 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 +117 -55
- package/dist/gitignore.d.ts +4 -0
- package/dist/gitignore.js +31 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -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
|
@@ -5,11 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { renderDiagram } from "./diagram.js";
|
|
6
6
|
import { runDoctor } from "./doctor.js";
|
|
7
7
|
import { loadDotEnvIfPresent } from "./env.js";
|
|
8
|
+
import { ensureGitignoreEntries } from "./gitignore.js";
|
|
8
9
|
import { archivePlanningChange } from "./plan-archive.js";
|
|
9
10
|
import { createPlanScaffold } from "./plan.js";
|
|
10
11
|
import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPlanningPath } from "./planning-paths.js";
|
|
11
12
|
import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
|
|
12
|
-
import { assertRemoteIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureRemoteInit, extractWorkspaceIdentity,
|
|
13
|
+
import { assertRemoteIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, requireWorkspaceIdentity, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, updateRemoteConfigMetadata, validateImportResult, } from "./remote.js";
|
|
13
14
|
import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
|
|
14
15
|
import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
|
|
15
16
|
import { ReferencesStore } from "./storage.js";
|
|
@@ -484,16 +485,17 @@ function planUsage() {
|
|
|
484
485
|
function remoteUsage() {
|
|
485
486
|
return [
|
|
486
487
|
"Usage:",
|
|
487
|
-
" reffy remote init [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--provision] [--env-file PATH]",
|
|
488
|
-
" reffy remote status [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
489
|
-
" reffy remote push [--repo PATH] [--output text|json] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
490
|
-
" reffy remote ls [--repo PATH] [--output text|json] [--prefix PATH] [--endpoint URL] [--pod-name NAME] [--actor-id ID] [--env-file PATH]",
|
|
491
|
-
" 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]",
|
|
492
493
|
"",
|
|
493
494
|
"Options:",
|
|
495
|
+
" --workspace-id ID Selected workspace projection. Required when the manifest has multiple workspace_ids; otherwise inferred.",
|
|
494
496
|
" --endpoint URL Paseo base URL. Defaults to saved config or PASEO_ENDPOINT/.env.",
|
|
495
|
-
" --pod-name NAME Paseo pod name. Defaults to saved
|
|
496
|
-
" --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.",
|
|
497
499
|
" --provision Create missing pod/actor during `remote init`.",
|
|
498
500
|
" --prefix PATH Prefix filter for `remote ls`.",
|
|
499
501
|
" --env-file PATH Dotenv file to load before resolving env-backed options. Defaults to .env.",
|
|
@@ -757,6 +759,18 @@ function parseRemoteArgs(argv) {
|
|
|
757
759
|
args.prefix = arg.split("=", 2)[1];
|
|
758
760
|
continue;
|
|
759
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
|
+
}
|
|
760
774
|
if (arg === "--provision") {
|
|
761
775
|
args.provision = true;
|
|
762
776
|
continue;
|
|
@@ -777,7 +791,8 @@ function getRemotePositionalArgs(argv) {
|
|
|
777
791
|
arg === "--pod-name" ||
|
|
778
792
|
arg === "--actor-id" ||
|
|
779
793
|
arg === "--env-file" ||
|
|
780
|
-
arg === "--prefix"
|
|
794
|
+
arg === "--prefix" ||
|
|
795
|
+
arg === "--workspace-id") {
|
|
781
796
|
i += 1;
|
|
782
797
|
continue;
|
|
783
798
|
}
|
|
@@ -788,6 +803,7 @@ function getRemotePositionalArgs(argv) {
|
|
|
788
803
|
arg.startsWith("--actor-id=") ||
|
|
789
804
|
arg.startsWith("--env-file=") ||
|
|
790
805
|
arg.startsWith("--prefix=") ||
|
|
806
|
+
arg.startsWith("--workspace-id=") ||
|
|
791
807
|
arg === "--json" ||
|
|
792
808
|
arg === "--provision") {
|
|
793
809
|
continue;
|
|
@@ -808,30 +824,48 @@ function printSection(title, values) {
|
|
|
808
824
|
console.log(`- ${value}`);
|
|
809
825
|
}
|
|
810
826
|
}
|
|
811
|
-
async function resolveRemoteContext(args) {
|
|
827
|
+
async function resolveRemoteContext(args, opts = {}) {
|
|
812
828
|
await loadDotEnvIfPresent(args.repoRoot, args.envFile ?? ".env");
|
|
813
829
|
const store = new ReferencesStore(args.repoRoot);
|
|
814
|
-
const
|
|
815
|
-
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
|
+
}
|
|
816
847
|
const overrideValues = {
|
|
817
848
|
endpoint: args.endpoint ?? process.env.PASEO_ENDPOINT,
|
|
818
849
|
pod_name: args.podName ?? process.env.PASEO_POD_NAME,
|
|
819
850
|
actor_id: args.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
820
851
|
};
|
|
821
|
-
const
|
|
852
|
+
const resolvedTarget = resolveRemoteTarget(savedConfig, workspaceId, overrideValues);
|
|
822
853
|
const hasOverrides = Boolean(overrideValues.endpoint || overrideValues.pod_name || overrideValues.actor_id);
|
|
823
|
-
const
|
|
824
|
-
|
|
854
|
+
const savedHasTarget = Boolean(savedConfig?.targets?.[workspaceId]);
|
|
855
|
+
const linkageSource = resolvedTarget
|
|
856
|
+
? savedHasTarget && !hasOverrides
|
|
825
857
|
? "saved_config"
|
|
826
858
|
: "cli_or_env_overrides"
|
|
827
859
|
: "unconfigured";
|
|
828
860
|
return {
|
|
829
861
|
store,
|
|
830
862
|
identity,
|
|
863
|
+
workspaceId,
|
|
831
864
|
configPath: resolveRemoteConfigPath(args.repoRoot),
|
|
832
865
|
savedConfig,
|
|
833
|
-
|
|
866
|
+
resolvedTarget,
|
|
834
867
|
linkageSource,
|
|
868
|
+
replacedLegacy,
|
|
835
869
|
};
|
|
836
870
|
}
|
|
837
871
|
async function main() {
|
|
@@ -893,8 +927,8 @@ async function main() {
|
|
|
893
927
|
const parsed = parseRemoteArgs(remoteArgs);
|
|
894
928
|
try {
|
|
895
929
|
if (subcommand === "init") {
|
|
896
|
-
const { identity, configPath } = await resolveRemoteContext(parsed);
|
|
897
|
-
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;
|
|
898
932
|
if (!endpoint) {
|
|
899
933
|
throw new Error("Remote init requires --endpoint or PASEO_ENDPOINT (optionally loaded from .env)");
|
|
900
934
|
}
|
|
@@ -904,7 +938,9 @@ async function main() {
|
|
|
904
938
|
actorId: parsed.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
905
939
|
provision: parsed.provision,
|
|
906
940
|
identity,
|
|
941
|
+
existingConfig: savedConfig,
|
|
907
942
|
});
|
|
943
|
+
const gitignore = await ensureGitignoreEntries(parsed.repoRoot, [".reffy/state/"]);
|
|
908
944
|
const payload = {
|
|
909
945
|
status: "ok",
|
|
910
946
|
command: "remote",
|
|
@@ -912,44 +948,61 @@ async function main() {
|
|
|
912
948
|
provider: result.config.provider,
|
|
913
949
|
config_path: configPath,
|
|
914
950
|
endpoint: result.config.endpoint,
|
|
915
|
-
|
|
916
|
-
|
|
951
|
+
workspace_id: workspaceId,
|
|
952
|
+
pod_name: result.target.pod_name,
|
|
953
|
+
actor_id: result.target.actor_id,
|
|
917
954
|
created_pod: result.created_pod,
|
|
918
955
|
created_actor: result.created_actor,
|
|
919
956
|
project_id: identity.project_id,
|
|
920
|
-
workspace_name: identity.workspace_name,
|
|
921
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,
|
|
960
|
+
gitignore_path: gitignore.path,
|
|
961
|
+
gitignore_added: gitignore.added,
|
|
922
962
|
};
|
|
923
963
|
if (output === "json") {
|
|
924
964
|
printResult(output, payload);
|
|
925
965
|
}
|
|
926
966
|
else {
|
|
967
|
+
if (replacedLegacy) {
|
|
968
|
+
console.log("Replacing legacy v1 remote linkage with v2 target map");
|
|
969
|
+
}
|
|
927
970
|
console.log("Remote linkage ready");
|
|
928
971
|
console.log(`Config: ${payload.config_path}`);
|
|
929
972
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
973
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
930
974
|
console.log(`Pod: ${payload.pod_name}`);
|
|
931
975
|
console.log(`Actor: ${payload.actor_id}`);
|
|
932
976
|
console.log(`Project identity: ${payload.project_id}`);
|
|
933
|
-
console.log(`Workspace name: ${payload.workspace_name}`);
|
|
934
977
|
console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
|
|
935
978
|
console.log(`Created actor: ${payload.created_actor ? "yes" : "no"}`);
|
|
936
979
|
console.log(`Linkage mode: ${payload.linkage_mode}`);
|
|
980
|
+
console.log(`Configured workspace ids: ${payload.configured_workspace_ids.join(", ")}`);
|
|
981
|
+
if (payload.gitignore_added.length > 0) {
|
|
982
|
+
console.log(`Updated .gitignore: ${payload.gitignore_added.join(", ")}`);
|
|
983
|
+
}
|
|
937
984
|
}
|
|
938
985
|
return 0;
|
|
939
986
|
}
|
|
940
987
|
const context = await resolveRemoteContext(parsed);
|
|
941
|
-
if (!context.
|
|
942
|
-
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}`);
|
|
943
990
|
}
|
|
944
|
-
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
|
+
});
|
|
945
998
|
if (subcommand === "status") {
|
|
946
999
|
let workspace;
|
|
947
1000
|
try {
|
|
948
|
-
workspace = await client.getWorkspace();
|
|
1001
|
+
workspace = await client.getWorkspace(context.workspaceId);
|
|
949
1002
|
}
|
|
950
1003
|
catch (error) {
|
|
951
1004
|
const message = error instanceof Error ? error.message : String(error);
|
|
952
|
-
throw new Error(`Remote status failed using ${
|
|
1005
|
+
throw new Error(`Remote status failed using ${linkageLabel}: ${message}`);
|
|
953
1006
|
}
|
|
954
1007
|
assertRemoteIdentity(workspace, context.identity);
|
|
955
1008
|
const remoteIdentity = extractWorkspaceIdentity(workspace);
|
|
@@ -958,18 +1011,21 @@ async function main() {
|
|
|
958
1011
|
command: "remote",
|
|
959
1012
|
subcommand: "status",
|
|
960
1013
|
config_path: context.configPath,
|
|
961
|
-
provider:
|
|
962
|
-
endpoint: context.
|
|
963
|
-
|
|
964
|
-
|
|
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,
|
|
965
1019
|
linkage_source: context.linkageSource,
|
|
966
1020
|
local_identity: context.identity,
|
|
967
1021
|
remote_identity: {
|
|
968
1022
|
project_id: remoteIdentity.project_id,
|
|
969
|
-
|
|
1023
|
+
workspace_id: remoteIdentity.workspace_id,
|
|
1024
|
+
actor_type: remoteIdentity.actor_type,
|
|
1025
|
+
backend_version: remoteIdentity.backend_version,
|
|
970
1026
|
},
|
|
971
1027
|
document_count: remoteIdentity.document_count,
|
|
972
|
-
last_imported_at: context.
|
|
1028
|
+
last_imported_at: context.resolvedTarget.last_imported_at ?? null,
|
|
973
1029
|
workspace,
|
|
974
1030
|
};
|
|
975
1031
|
if (output === "json") {
|
|
@@ -979,11 +1035,12 @@ async function main() {
|
|
|
979
1035
|
console.log("Remote workspace reachable");
|
|
980
1036
|
console.log(`Config: ${payload.config_path}`);
|
|
981
1037
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
1038
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
982
1039
|
console.log(`Pod: ${payload.pod_name}`);
|
|
983
1040
|
console.log(`Actor: ${payload.actor_id}`);
|
|
984
1041
|
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
985
|
-
console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.
|
|
986
|
-
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)})`);
|
|
987
1044
|
console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
|
|
988
1045
|
console.log(`Last imported at: ${String(payload.last_imported_at ?? "never")}`);
|
|
989
1046
|
}
|
|
@@ -993,33 +1050,35 @@ async function main() {
|
|
|
993
1050
|
const documents = await collectWorkspaceDocuments(parsed.repoRoot);
|
|
994
1051
|
let importResult;
|
|
995
1052
|
try {
|
|
996
|
-
importResult = validateImportResult(await client.importWorkspace(documents, true));
|
|
1053
|
+
importResult = validateImportResult(await client.importWorkspace(context.workspaceId, documents, true));
|
|
997
1054
|
}
|
|
998
1055
|
catch (error) {
|
|
999
1056
|
const message = error instanceof Error ? error.message : String(error);
|
|
1000
|
-
throw new Error(`Remote push failed using ${
|
|
1057
|
+
throw new Error(`Remote push failed using ${linkageLabel}: ${message}`);
|
|
1001
1058
|
}
|
|
1002
1059
|
let workspace;
|
|
1003
1060
|
try {
|
|
1004
|
-
workspace = await client.getWorkspace();
|
|
1061
|
+
workspace = await client.getWorkspace(context.workspaceId);
|
|
1005
1062
|
}
|
|
1006
1063
|
catch (error) {
|
|
1007
1064
|
const message = error instanceof Error ? error.message : String(error);
|
|
1008
|
-
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}`);
|
|
1009
1066
|
}
|
|
1010
1067
|
assertRemoteIdentity(workspace, context.identity);
|
|
1011
1068
|
const importedAt = new Date().toISOString();
|
|
1012
|
-
await updateRemoteConfigMetadata(parsed.repoRoot, {
|
|
1069
|
+
await updateRemoteConfigMetadata(parsed.repoRoot, context.workspaceId, {
|
|
1070
|
+
last_imported_at: importedAt,
|
|
1071
|
+
});
|
|
1013
1072
|
const payload = {
|
|
1014
1073
|
status: "ok",
|
|
1015
1074
|
command: "remote",
|
|
1016
1075
|
subcommand: "push",
|
|
1017
|
-
endpoint: context.
|
|
1018
|
-
|
|
1019
|
-
|
|
1076
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1077
|
+
workspace_id: context.workspaceId,
|
|
1078
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1079
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1020
1080
|
linkage_source: context.linkageSource,
|
|
1021
1081
|
project_id: context.identity.project_id,
|
|
1022
|
-
workspace_name: context.identity.workspace_name,
|
|
1023
1082
|
local_document_count: documents.length,
|
|
1024
1083
|
imported: importResult.imported,
|
|
1025
1084
|
created: importResult.created,
|
|
@@ -1035,6 +1094,7 @@ async function main() {
|
|
|
1035
1094
|
else {
|
|
1036
1095
|
console.log("Remote push complete");
|
|
1037
1096
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
1097
|
+
console.log(`Workspace id: ${payload.workspace_id}`);
|
|
1038
1098
|
console.log(`Pod: ${payload.pod_name}`);
|
|
1039
1099
|
console.log(`Actor: ${payload.actor_id}`);
|
|
1040
1100
|
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
@@ -1052,11 +1112,11 @@ async function main() {
|
|
|
1052
1112
|
if (subcommand === "ls") {
|
|
1053
1113
|
let snapshot;
|
|
1054
1114
|
try {
|
|
1055
|
-
snapshot = await client.getSnapshot(parsed.prefix);
|
|
1115
|
+
snapshot = await client.getSnapshot(context.workspaceId, parsed.prefix);
|
|
1056
1116
|
}
|
|
1057
1117
|
catch (error) {
|
|
1058
1118
|
const message = error instanceof Error ? error.message : String(error);
|
|
1059
|
-
throw new Error(`Remote ls failed using ${
|
|
1119
|
+
throw new Error(`Remote ls failed using ${linkageLabel}: ${message}`);
|
|
1060
1120
|
}
|
|
1061
1121
|
const paths = (snapshot.documents ?? [])
|
|
1062
1122
|
.map((document) => (typeof document.path === "string" ? document.path : ""))
|
|
@@ -1066,9 +1126,10 @@ async function main() {
|
|
|
1066
1126
|
status: "ok",
|
|
1067
1127
|
command: "remote",
|
|
1068
1128
|
subcommand: "ls",
|
|
1069
|
-
endpoint: context.
|
|
1070
|
-
|
|
1071
|
-
|
|
1129
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1130
|
+
workspace_id: context.workspaceId,
|
|
1131
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1132
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1072
1133
|
linkage_source: context.linkageSource,
|
|
1073
1134
|
prefix: parsed.prefix ?? null,
|
|
1074
1135
|
paths,
|
|
@@ -1096,22 +1157,23 @@ async function main() {
|
|
|
1096
1157
|
}
|
|
1097
1158
|
let document;
|
|
1098
1159
|
try {
|
|
1099
|
-
document = await client.getDocument(documentPath);
|
|
1160
|
+
document = await client.getDocument(context.workspaceId, documentPath);
|
|
1100
1161
|
}
|
|
1101
1162
|
catch (error) {
|
|
1102
1163
|
const message = error instanceof Error ? error.message : String(error);
|
|
1103
|
-
throw new Error(`Remote cat failed using ${
|
|
1164
|
+
throw new Error(`Remote cat failed using ${linkageLabel}: ${message}`);
|
|
1104
1165
|
}
|
|
1105
1166
|
if (!document.document) {
|
|
1106
|
-
throw new Error(`Remote document not found using ${
|
|
1167
|
+
throw new Error(`Remote document not found using ${linkageLabel}: ${documentPath}`);
|
|
1107
1168
|
}
|
|
1108
1169
|
const payload = {
|
|
1109
1170
|
status: "ok",
|
|
1110
1171
|
command: "remote",
|
|
1111
1172
|
subcommand: "cat",
|
|
1112
|
-
endpoint: context.
|
|
1113
|
-
|
|
1114
|
-
|
|
1173
|
+
endpoint: context.resolvedTarget.endpoint,
|
|
1174
|
+
workspace_id: context.workspaceId,
|
|
1175
|
+
pod_name: context.resolvedTarget.pod_name,
|
|
1176
|
+
actor_id: context.resolvedTarget.actor_id,
|
|
1115
1177
|
linkage_source: context.linkageSource,
|
|
1116
1178
|
document: document.document,
|
|
1117
1179
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function normalizeLines(content) {
|
|
4
|
+
return content
|
|
5
|
+
.split(/\r?\n/)
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
export async function ensureGitignoreEntries(repoRoot, entries) {
|
|
10
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
11
|
+
const current = await fs.readFile(gitignorePath, "utf8").catch(() => "");
|
|
12
|
+
const existing = new Set(normalizeLines(current));
|
|
13
|
+
const added = [];
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const normalized = entry.trim();
|
|
16
|
+
if (!normalized || existing.has(normalized))
|
|
17
|
+
continue;
|
|
18
|
+
existing.add(normalized);
|
|
19
|
+
added.push(normalized);
|
|
20
|
+
}
|
|
21
|
+
if (added.length === 0) {
|
|
22
|
+
return { path: gitignorePath, added };
|
|
23
|
+
}
|
|
24
|
+
const nextLines = current.length > 0 ? current.replace(/\s*$/, "").split(/\r?\n/) : [];
|
|
25
|
+
if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") {
|
|
26
|
+
nextLines.push("");
|
|
27
|
+
}
|
|
28
|
+
nextLines.push(...added);
|
|
29
|
+
await fs.writeFile(gitignorePath, `${nextLines.join("\n")}\n`, "utf8");
|
|
30
|
+
return { path: gitignorePath, added };
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { MANIFEST_VERSION, allowedKindExtensions, validateManifest } from "./man
|
|
|
3
3
|
export { runDoctor } from "./doctor.js";
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
5
|
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
|
-
export {
|
|
6
|
+
export { ensureGitignoreEntries } from "./gitignore.js";
|
|
7
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
7
8
|
export { listSpecs, showSpec } from "./spec-runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { MANIFEST_VERSION, allowedKindExtensions, validateManifest } from "./man
|
|
|
3
3
|
export { runDoctor } from "./doctor.js";
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
5
|
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
|
-
export {
|
|
6
|
+
export { ensureGitignoreEntries } from "./gitignore.js";
|
|
7
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
7
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
|
},
|