reffy-cli 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -38
- package/dist/cli.js +498 -178
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan.js +1 -1
- package/dist/remote.d.ts +131 -49
- package/dist/remote.js +294 -99
- package/dist/types.d.ts +5 -2
- package/package.json +1 -1
package/dist/remote.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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 = 4;
|
|
5
5
|
export const REMOTE_PROVIDER = "paseo";
|
|
6
6
|
export const REMOTE_BACKEND_ACTOR_TYPE = "reffyRemoteBackend";
|
|
7
7
|
export const REMOTE_BACKEND_VERSION = "v2";
|
|
8
|
+
export const REMOTE_MANAGER_ACTOR_TYPE = "reffyWorkspaceManager";
|
|
9
|
+
export const REMOTE_MANAGER_VERSION = "v1";
|
|
8
10
|
const REMOTE_STATE_DIR = path.join("state");
|
|
9
11
|
const REMOTE_CONFIG_FILE = "remote.json";
|
|
10
12
|
function isObject(value) {
|
|
@@ -16,6 +18,18 @@ function isNonEmptyString(value) {
|
|
|
16
18
|
function normalizeSeparators(value) {
|
|
17
19
|
return value.split(path.sep).join("/");
|
|
18
20
|
}
|
|
21
|
+
function readActorIdentity(value, label, configPath) {
|
|
22
|
+
if (!isObject(value)) {
|
|
23
|
+
throw new Error(`Remote config at ${configPath} must define ${label} as an object with pod_name and actor_id`);
|
|
24
|
+
}
|
|
25
|
+
if (!isNonEmptyString(value.pod_name)) {
|
|
26
|
+
throw new Error(`Remote config at ${configPath} must define ${label}.pod_name`);
|
|
27
|
+
}
|
|
28
|
+
if (!isNonEmptyString(value.actor_id)) {
|
|
29
|
+
throw new Error(`Remote config at ${configPath} must define ${label}.actor_id`);
|
|
30
|
+
}
|
|
31
|
+
return { pod_name: value.pod_name.trim(), actor_id: value.actor_id.trim() };
|
|
32
|
+
}
|
|
19
33
|
export function resolveRemoteConfigPath(repoRoot) {
|
|
20
34
|
return path.join(resolveRefsDir(repoRoot), REMOTE_STATE_DIR, REMOTE_CONFIG_FILE);
|
|
21
35
|
}
|
|
@@ -70,15 +84,19 @@ export async function readRemoteConfig(repoRoot) {
|
|
|
70
84
|
if (parsed.provider !== REMOTE_PROVIDER) {
|
|
71
85
|
throw new Error(`Remote config at ${configPath} must use provider "${REMOTE_PROVIDER}"`);
|
|
72
86
|
}
|
|
73
|
-
if (!isNonEmptyString(parsed.endpoint)) {
|
|
74
|
-
throw new Error(`Remote config at ${configPath} must define endpoint`);
|
|
75
|
-
}
|
|
76
87
|
if (parsed.version === 1) {
|
|
77
|
-
throw new Error(`Remote config at ${configPath} uses the legacy v1 single-target shape. Run \`reffy remote init
|
|
88
|
+
throw new Error(`Remote config at ${configPath} uses the legacy v1 single-target shape. Run \`reffy remote init\` against the workspace manager actor (reffyWorkspaceManager.v1) to reinitialize, then repush with \`reffy remote push\`.`);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.version === 2) {
|
|
91
|
+
throw new Error(`Remote config at ${configPath} uses the legacy v2 combined-backend shape. The Paseo backend now splits into reffyWorkspaceManager.v1 (control plane) and reffyRemoteBackend.v2 (per-workspace storage). Run \`reffy remote init --manager-pod <pod> --manager-actor <actor>\` (or \`--provision\`) to reinitialize, then repush with \`reffy remote push\`.`);
|
|
92
|
+
}
|
|
93
|
+
if (parsed.version === 3) {
|
|
94
|
+
throw new Error(`Remote config at ${configPath} uses the legacy v3 shape that persisted the Paseo endpoint and pre-dates bearer-token auth. Set PASEO_ENDPOINT and PASEO_TOKEN in your .env, then run \`reffy remote init\` (with \`--provision\` for fresh setups) to reinitialize, and repush with \`reffy remote push\`.`);
|
|
78
95
|
}
|
|
79
96
|
if (parsed.version !== REMOTE_CONFIG_VERSION) {
|
|
80
97
|
throw new Error(`Remote config at ${configPath} must use version ${String(REMOTE_CONFIG_VERSION)}`);
|
|
81
98
|
}
|
|
99
|
+
const manager = readActorIdentity(parsed.manager, "manager", configPath);
|
|
82
100
|
if (!isObject(parsed.targets)) {
|
|
83
101
|
throw new Error(`Remote config at ${configPath} must define targets as an object keyed by workspace_id`);
|
|
84
102
|
}
|
|
@@ -87,22 +105,16 @@ export async function readRemoteConfig(repoRoot) {
|
|
|
87
105
|
if (!isObject(entry)) {
|
|
88
106
|
throw new Error(`Remote config at ${configPath} has invalid target for workspace_id "${workspaceId}"`);
|
|
89
107
|
}
|
|
90
|
-
|
|
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
|
-
}
|
|
108
|
+
const workspace_backend = readActorIdentity(entry.workspace_backend, `targets.${workspaceId}.workspace_backend`, configPath);
|
|
96
109
|
targets[workspaceId] = {
|
|
97
|
-
|
|
98
|
-
actor_id: entry.actor_id.trim(),
|
|
110
|
+
workspace_backend,
|
|
99
111
|
last_imported_at: isNonEmptyString(entry.last_imported_at) ? entry.last_imported_at.trim() : undefined,
|
|
100
112
|
};
|
|
101
113
|
}
|
|
102
114
|
return {
|
|
103
115
|
version: REMOTE_CONFIG_VERSION,
|
|
104
116
|
provider: REMOTE_PROVIDER,
|
|
105
|
-
|
|
117
|
+
manager,
|
|
106
118
|
targets,
|
|
107
119
|
};
|
|
108
120
|
}
|
|
@@ -129,33 +141,79 @@ export async function updateRemoteConfigMetadata(repoRoot, workspaceId, patch) {
|
|
|
129
141
|
await writeRemoteConfig(repoRoot, next);
|
|
130
142
|
return next;
|
|
131
143
|
}
|
|
132
|
-
export function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
export async function removeWorkspaceTarget(repoRoot, existing, workspaceId) {
|
|
145
|
+
const existed = Object.prototype.hasOwnProperty.call(existing.targets, workspaceId);
|
|
146
|
+
if (!existed) {
|
|
147
|
+
return { config: existing, existed: false };
|
|
148
|
+
}
|
|
149
|
+
const nextTargets = { ...existing.targets };
|
|
150
|
+
delete nextTargets[workspaceId];
|
|
151
|
+
const next = { ...existing, targets: nextTargets };
|
|
152
|
+
await writeRemoteConfig(repoRoot, next);
|
|
153
|
+
return { config: next, existed: true };
|
|
154
|
+
}
|
|
155
|
+
export async function upsertWorkspaceTarget(repoRoot, existing, workspaceId, workspaceBackend) {
|
|
156
|
+
const priorTarget = existing.targets[workspaceId];
|
|
157
|
+
const next = {
|
|
158
|
+
...existing,
|
|
159
|
+
targets: {
|
|
160
|
+
...existing.targets,
|
|
161
|
+
[workspaceId]: {
|
|
162
|
+
workspace_backend: workspaceBackend,
|
|
163
|
+
last_imported_at: priorTarget?.last_imported_at,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
await writeRemoteConfig(repoRoot, next);
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
export function resolveRemoteTarget(stored, workspaceId, endpoint) {
|
|
171
|
+
if (!stored)
|
|
172
|
+
return null;
|
|
173
|
+
const target = stored.targets[workspaceId];
|
|
174
|
+
if (!target)
|
|
138
175
|
return null;
|
|
139
|
-
}
|
|
140
176
|
return {
|
|
141
177
|
endpoint,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
last_imported_at: target
|
|
178
|
+
manager: stored.manager,
|
|
179
|
+
workspace_backend: target.workspace_backend,
|
|
180
|
+
last_imported_at: target.last_imported_at,
|
|
145
181
|
};
|
|
146
182
|
}
|
|
147
183
|
export function describeRemoteLinkage(linkage) {
|
|
148
|
-
return
|
|
184
|
+
return [
|
|
185
|
+
`endpoint=${linkage.endpoint}`,
|
|
186
|
+
`manager=${linkage.manager.pod_name}/${linkage.manager.actor_id}`,
|
|
187
|
+
`workspace_backend=${linkage.workspace_backend.pod_name}/${linkage.workspace_backend.actor_id}`,
|
|
188
|
+
`workspace=${linkage.workspace_id}`,
|
|
189
|
+
].join(" ");
|
|
190
|
+
}
|
|
191
|
+
export class RemoteHttpError extends Error {
|
|
192
|
+
status;
|
|
193
|
+
body;
|
|
194
|
+
url;
|
|
195
|
+
method;
|
|
196
|
+
constructor(status, statusText, body, url, method) {
|
|
197
|
+
super(`${String(status)} ${statusText} - ${body}`);
|
|
198
|
+
this.status = status;
|
|
199
|
+
this.body = body;
|
|
200
|
+
this.url = url;
|
|
201
|
+
this.method = method;
|
|
202
|
+
}
|
|
149
203
|
}
|
|
150
204
|
async function http(url, init = {}) {
|
|
151
|
-
const headers
|
|
152
|
-
|
|
205
|
+
const { token, headers: rawHeaders, ...rest } = init;
|
|
206
|
+
const headers = new Headers(rawHeaders ?? {});
|
|
207
|
+
if (!headers.has("Content-Type") && rest.body) {
|
|
153
208
|
headers.set("Content-Type", "application/json");
|
|
154
209
|
}
|
|
155
|
-
|
|
210
|
+
if (token && !headers.has("Authorization")) {
|
|
211
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
212
|
+
}
|
|
213
|
+
const response = await fetch(url, { ...rest, headers });
|
|
156
214
|
if (!response.ok) {
|
|
157
215
|
const text = await response.text().catch(() => "");
|
|
158
|
-
throw new
|
|
216
|
+
throw new RemoteHttpError(response.status, response.statusText, text, url, rest.method ?? "GET");
|
|
159
217
|
}
|
|
160
218
|
return response;
|
|
161
219
|
}
|
|
@@ -163,136 +221,273 @@ async function httpJson(url, init = {}) {
|
|
|
163
221
|
const response = await http(url, init);
|
|
164
222
|
return (await response.json());
|
|
165
223
|
}
|
|
166
|
-
function actorBase(
|
|
167
|
-
return `${
|
|
168
|
-
}
|
|
169
|
-
function workspaceBase(config, workspaceId) {
|
|
170
|
-
return `${actorBase(config)}/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
224
|
+
function actorBase(endpoint, identity) {
|
|
225
|
+
return `${endpoint}/pods/${identity.pod_name}/actors/${identity.actor_id}`;
|
|
171
226
|
}
|
|
172
|
-
export class
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
227
|
+
export class PaseoManagerClient {
|
|
228
|
+
endpoint;
|
|
229
|
+
identity;
|
|
230
|
+
token;
|
|
231
|
+
constructor(endpoint, identity, token) {
|
|
232
|
+
this.endpoint = endpoint;
|
|
233
|
+
this.identity = identity;
|
|
234
|
+
this.token = token;
|
|
235
|
+
}
|
|
236
|
+
base() {
|
|
237
|
+
return actorBase(this.endpoint, this.identity);
|
|
238
|
+
}
|
|
239
|
+
requireToken() {
|
|
240
|
+
if (!this.token) {
|
|
241
|
+
throw new Error("Manager request requires PASEO_TOKEN to be set in the environment. Add it to your .env (or export it) and retry.");
|
|
242
|
+
}
|
|
243
|
+
return this.token;
|
|
176
244
|
}
|
|
177
245
|
async createPod() {
|
|
178
|
-
const result = await httpJson(`${this.
|
|
246
|
+
const result = await httpJson(`${this.endpoint}/pods`, { method: "POST" });
|
|
179
247
|
if (!isNonEmptyString(result.podName)) {
|
|
180
248
|
throw new Error("Paseo create pod response missing podName");
|
|
181
249
|
}
|
|
182
250
|
return result.podName.trim();
|
|
183
251
|
}
|
|
184
|
-
async
|
|
185
|
-
const result = await httpJson(`${this.
|
|
252
|
+
async createManagerActor(podName) {
|
|
253
|
+
const result = await httpJson(`${this.endpoint}/pods/${podName}/actors`, {
|
|
186
254
|
method: "POST",
|
|
187
255
|
body: JSON.stringify({
|
|
188
256
|
config: {
|
|
189
|
-
actorType:
|
|
190
|
-
version:
|
|
257
|
+
actorType: REMOTE_MANAGER_ACTOR_TYPE,
|
|
258
|
+
version: REMOTE_MANAGER_VERSION,
|
|
191
259
|
schema: { type: "object" },
|
|
192
|
-
params: {
|
|
193
|
-
project_id: identity.project_id,
|
|
194
|
-
workspace_ids: [identity.workspace_id],
|
|
195
|
-
default_lock_ttl_seconds: 120,
|
|
196
|
-
max_snapshot_documents: 1000,
|
|
197
|
-
},
|
|
260
|
+
params: {},
|
|
198
261
|
},
|
|
199
262
|
}),
|
|
200
263
|
});
|
|
201
264
|
if (!isNonEmptyString(result.actorId)) {
|
|
202
|
-
throw new Error("Paseo create actor response missing actorId");
|
|
265
|
+
throw new Error("Paseo create manager actor response missing actorId");
|
|
203
266
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
267
|
+
if (!isNonEmptyString(result.managerAuthToken)) {
|
|
268
|
+
throw new Error("Paseo create manager actor response missing managerAuthToken. The backend may not yet expose bearer-token issuance for reffyWorkspaceManager.v1.");
|
|
269
|
+
}
|
|
270
|
+
return { actorId: result.actorId.trim(), managerAuthToken: result.managerAuthToken.trim() };
|
|
208
271
|
}
|
|
209
|
-
async
|
|
210
|
-
|
|
272
|
+
async createWorkspace(workspaceId, metadata) {
|
|
273
|
+
const result = await httpJson(`${this.base()}/workspaces`, {
|
|
211
274
|
method: "POST",
|
|
275
|
+
token: this.requireToken(),
|
|
212
276
|
body: JSON.stringify({
|
|
213
|
-
|
|
214
|
-
|
|
277
|
+
workspace_id: workspaceId,
|
|
278
|
+
metadata: metadata ?? {},
|
|
279
|
+
controller: { client: "reffy-cli" },
|
|
215
280
|
}),
|
|
216
281
|
});
|
|
282
|
+
return extractWorkspaceBackendIdentity(result, workspaceId, "createWorkspace");
|
|
217
283
|
}
|
|
218
|
-
async
|
|
219
|
-
const
|
|
220
|
-
return
|
|
284
|
+
async getWorkspace(workspaceId) {
|
|
285
|
+
const result = await httpJson(`${this.base()}/workspaces/${encodeURIComponent(workspaceId)}`, { token: this.requireToken() });
|
|
286
|
+
return extractWorkspaceBackendIdentity(result, workspaceId, "getWorkspace");
|
|
287
|
+
}
|
|
288
|
+
async registerProject(workspaceId, projectId) {
|
|
289
|
+
try {
|
|
290
|
+
await http(`${this.base()}/workspaces/${encodeURIComponent(workspaceId)}/projects/${encodeURIComponent(projectId)}`, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
token: this.requireToken(),
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
owner: { source: "reffy-cli" },
|
|
295
|
+
metadata: { source: "reffy-cli" },
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
return { alreadyRegistered: false };
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof RemoteHttpError && error.status === 409) {
|
|
302
|
+
return { alreadyRegistered: true };
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
221
306
|
}
|
|
222
|
-
async
|
|
223
|
-
return await httpJson(`${
|
|
307
|
+
async listProjects(workspaceId) {
|
|
308
|
+
return await httpJson(`${this.base()}/workspaces/${encodeURIComponent(workspaceId)}/projects`, { token: this.requireToken() });
|
|
309
|
+
}
|
|
310
|
+
async getProject(workspaceId, projectId) {
|
|
311
|
+
return await httpJson(`${this.base()}/workspaces/${encodeURIComponent(workspaceId)}/projects/${encodeURIComponent(projectId)}`, { token: this.requireToken() });
|
|
312
|
+
}
|
|
313
|
+
async deleteWorkspace(workspaceId) {
|
|
314
|
+
try {
|
|
315
|
+
await http(`${this.base()}/workspaces/${encodeURIComponent(workspaceId)}`, {
|
|
316
|
+
method: "DELETE",
|
|
317
|
+
token: this.requireToken(),
|
|
318
|
+
});
|
|
319
|
+
return { alreadyAbsent: false };
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof RemoteHttpError && error.status === 404) {
|
|
323
|
+
return { alreadyAbsent: true };
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
224
327
|
}
|
|
225
328
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
329
|
+
function extractWorkspaceBackendIdentity(response, workspaceId, context) {
|
|
330
|
+
const workspace = response.workspace;
|
|
331
|
+
if (!isObject(workspace)) {
|
|
332
|
+
throw new Error(`Manager ${context} response missing workspace envelope for "${workspaceId}"`);
|
|
333
|
+
}
|
|
334
|
+
const backend = workspace.backend;
|
|
335
|
+
if (!isObject(backend)) {
|
|
336
|
+
throw new Error(`Manager ${context} response missing workspace.backend for "${workspaceId}"`);
|
|
337
|
+
}
|
|
338
|
+
if (!isNonEmptyString(backend.pod_name) || !isNonEmptyString(backend.actor_id)) {
|
|
339
|
+
throw new Error(`Manager ${context} response missing workspace.backend.pod_name or actor_id for "${workspaceId}"`);
|
|
340
|
+
}
|
|
341
|
+
return { pod_name: backend.pod_name.trim(), actor_id: backend.actor_id.trim() };
|
|
342
|
+
}
|
|
343
|
+
export class PaseoWorkspaceBackendClient {
|
|
344
|
+
endpoint;
|
|
345
|
+
identity;
|
|
346
|
+
token;
|
|
347
|
+
constructor(endpoint, identity, token) {
|
|
348
|
+
if (!token) {
|
|
349
|
+
throw new Error("PaseoWorkspaceBackendClient requires a bearer token. Set PASEO_TOKEN in your .env (or export it) before invoking remote commands.");
|
|
350
|
+
}
|
|
351
|
+
this.endpoint = endpoint;
|
|
352
|
+
this.identity = identity;
|
|
353
|
+
this.token = token;
|
|
354
|
+
}
|
|
355
|
+
base() {
|
|
356
|
+
return `${actorBase(this.endpoint, this.identity)}/workspace`;
|
|
357
|
+
}
|
|
358
|
+
async getWorkspace() {
|
|
359
|
+
return await httpJson(this.base(), { token: this.token });
|
|
360
|
+
}
|
|
361
|
+
async importProject(projectId, documents, replaceMissing = true) {
|
|
362
|
+
return await httpJson(`${this.base()}/projects/${encodeURIComponent(projectId)}/import`, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
token: this.token,
|
|
365
|
+
body: JSON.stringify({ documents, replace_missing: replaceMissing }),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async listProjectDocuments(projectId, options = {}) {
|
|
369
|
+
const params = new URLSearchParams();
|
|
370
|
+
if (options.prefix)
|
|
371
|
+
params.set("prefix", options.prefix);
|
|
372
|
+
const query = params.toString();
|
|
373
|
+
return await httpJson(`${this.base()}/projects/${encodeURIComponent(projectId)}/documents${query ? `?${query}` : ""}`, { token: this.token });
|
|
374
|
+
}
|
|
375
|
+
async getProjectDocument(projectId, documentPath) {
|
|
376
|
+
return await httpJson(`${this.base()}/projects/${encodeURIComponent(projectId)}/documents?path=${encodeURIComponent(documentPath)}`, { token: this.token });
|
|
377
|
+
}
|
|
378
|
+
async getProjectSnapshot(projectId) {
|
|
379
|
+
return await httpJson(`${this.base()}/projects/${encodeURIComponent(projectId)}/snapshot`, { token: this.token });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export async function ensureManagerInit(repoRoot, options) {
|
|
229
383
|
const endpoint = options.endpoint.trim();
|
|
230
384
|
if (!endpoint) {
|
|
231
385
|
throw new Error("Remote init requires an endpoint");
|
|
232
386
|
}
|
|
233
|
-
const
|
|
234
|
-
let podName = options.
|
|
235
|
-
let actorId = options.
|
|
387
|
+
const prior = options.existingConfig;
|
|
388
|
+
let podName = (options.managerPodName ?? prior?.manager.pod_name ?? "").trim();
|
|
389
|
+
let actorId = (options.managerActorId ?? prior?.manager.actor_id ?? "").trim();
|
|
390
|
+
let created_pod = false;
|
|
391
|
+
let created_actor = false;
|
|
392
|
+
let manager_auth_token;
|
|
236
393
|
if (!podName && !options.provision) {
|
|
237
|
-
throw new Error("Remote init requires --pod
|
|
394
|
+
throw new Error("Remote init requires --manager-pod (or --provision to create a fresh pod and manager actor)");
|
|
238
395
|
}
|
|
239
|
-
|
|
240
|
-
|
|
396
|
+
// createPod and createManagerActor do not require a bearer token; the manager actor itself issues the token.
|
|
397
|
+
const provisioner = new PaseoManagerClient(endpoint, {
|
|
241
398
|
pod_name: podName || "pending-pod",
|
|
242
399
|
actor_id: actorId || "pending-actor",
|
|
243
400
|
});
|
|
244
401
|
if (!podName) {
|
|
245
|
-
podName = await
|
|
402
|
+
podName = await provisioner.createPod();
|
|
246
403
|
created_pod = true;
|
|
247
404
|
}
|
|
248
405
|
if (!actorId) {
|
|
249
406
|
if (!options.provision) {
|
|
250
|
-
throw new Error("Remote init requires --actor
|
|
407
|
+
throw new Error("Remote init requires --manager-actor unless --provision is set so the CLI can create a fresh manager actor");
|
|
251
408
|
}
|
|
252
|
-
const
|
|
253
|
-
endpoint,
|
|
409
|
+
const result = await new PaseoManagerClient(endpoint, {
|
|
254
410
|
pod_name: podName,
|
|
255
411
|
actor_id: "pending-actor",
|
|
256
|
-
});
|
|
257
|
-
actorId =
|
|
412
|
+
}).createManagerActor(podName);
|
|
413
|
+
actorId = result.actorId;
|
|
414
|
+
manager_auth_token = result.managerAuthToken;
|
|
258
415
|
created_actor = true;
|
|
259
416
|
}
|
|
260
|
-
const nextTarget = {
|
|
261
|
-
pod_name: podName,
|
|
262
|
-
actor_id: actorId,
|
|
263
|
-
last_imported_at: priorTarget?.last_imported_at,
|
|
264
|
-
};
|
|
265
417
|
const config = {
|
|
266
418
|
version: REMOTE_CONFIG_VERSION,
|
|
267
419
|
provider: REMOTE_PROVIDER,
|
|
268
|
-
|
|
269
|
-
targets: {
|
|
270
|
-
...(options.existingConfig?.targets ?? {}),
|
|
271
|
-
[options.identity.workspace_id]: nextTarget,
|
|
272
|
-
},
|
|
420
|
+
manager: { pod_name: podName, actor_id: actorId },
|
|
421
|
+
targets: prior?.targets ?? {},
|
|
273
422
|
};
|
|
274
423
|
await writeRemoteConfig(repoRoot, config);
|
|
275
|
-
return { config,
|
|
424
|
+
return { config, created_pod, created_actor, manager_auth_token };
|
|
425
|
+
}
|
|
426
|
+
export async function ensureWorkspaceTarget(repoRoot, config, options) {
|
|
427
|
+
const manager = new PaseoManagerClient(options.endpoint, config.manager, options.token);
|
|
428
|
+
const existing = config.targets[options.workspaceId];
|
|
429
|
+
if (options.mode === "resolve" || (options.mode === "auto" && existing)) {
|
|
430
|
+
try {
|
|
431
|
+
const backend = await manager.getWorkspace(options.workspaceId);
|
|
432
|
+
const nextConfig = await upsertWorkspaceTarget(repoRoot, config, options.workspaceId, backend);
|
|
433
|
+
return { config: nextConfig, workspace_backend: backend, outcome: existing ? "reused" : "resolved" };
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (options.mode === "resolve")
|
|
437
|
+
throw error;
|
|
438
|
+
// fall through to create on auto
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// create (or auto with no existing)
|
|
442
|
+
try {
|
|
443
|
+
const backend = await manager.createWorkspace(options.workspaceId, options.metadata);
|
|
444
|
+
const nextConfig = await upsertWorkspaceTarget(repoRoot, config, options.workspaceId, backend);
|
|
445
|
+
return { config: nextConfig, workspace_backend: backend, outcome: "created" };
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (error instanceof RemoteHttpError && error.status === 409) {
|
|
449
|
+
const backend = await manager.getWorkspace(options.workspaceId);
|
|
450
|
+
const nextConfig = await upsertWorkspaceTarget(repoRoot, config, options.workspaceId, backend);
|
|
451
|
+
return { config: nextConfig, workspace_backend: backend, outcome: "resolved" };
|
|
452
|
+
}
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
276
455
|
}
|
|
277
|
-
export function
|
|
456
|
+
export function extractWorkspaceSummaryIdentity(summary) {
|
|
457
|
+
const top = summary;
|
|
278
458
|
const source = isObject(summary.source) ? summary.source : {};
|
|
279
459
|
const workspace = isObject(summary.workspace) ? summary.workspace : {};
|
|
280
460
|
const stats = isObject(summary.stats) ? summary.stats : {};
|
|
461
|
+
const actorType = typeof source.actor_type === "string"
|
|
462
|
+
? source.actor_type
|
|
463
|
+
: typeof top.actor_type === "string"
|
|
464
|
+
? top.actor_type
|
|
465
|
+
: null;
|
|
466
|
+
const backendVersion = typeof source.version === "string"
|
|
467
|
+
? source.version
|
|
468
|
+
: typeof top.version === "string"
|
|
469
|
+
? top.version
|
|
470
|
+
: null;
|
|
471
|
+
const documentCount = typeof stats.document_count === "number"
|
|
472
|
+
? stats.document_count
|
|
473
|
+
: typeof stats.source_document_count === "number"
|
|
474
|
+
? stats.source_document_count
|
|
475
|
+
: null;
|
|
281
476
|
return {
|
|
282
|
-
project_id: typeof source.project_id === "string" ? source.project_id : null,
|
|
283
477
|
workspace_id: typeof workspace.workspace_id === "string" ? workspace.workspace_id : null,
|
|
284
|
-
actor_type:
|
|
285
|
-
backend_version:
|
|
286
|
-
document_count:
|
|
478
|
+
actor_type: actorType,
|
|
479
|
+
backend_version: backendVersion,
|
|
480
|
+
document_count: documentCount,
|
|
481
|
+
registered_project_count: typeof stats.registered_project_count === "number" ? stats.registered_project_count : null,
|
|
287
482
|
};
|
|
288
483
|
}
|
|
289
|
-
export function
|
|
290
|
-
const remote =
|
|
484
|
+
export function assertWorkspaceSummaryIdentity(summary, expectedWorkspaceId) {
|
|
485
|
+
const remote = extractWorkspaceSummaryIdentity(summary);
|
|
291
486
|
if (!remote.workspace_id) {
|
|
292
|
-
throw new Error("Remote
|
|
487
|
+
throw new Error("Remote workspace summary did not include a workspace.workspace_id envelope. The linked actor may be a legacy v1 backend or a non-workspace actor. Reinitialize against reffyWorkspaceManager.v1 + reffyRemoteBackend.v2 (`reffy remote init`) and repush with `reffy remote push`.");
|
|
293
488
|
}
|
|
294
|
-
if (remote.
|
|
295
|
-
throw new Error(`Remote identity mismatch:
|
|
489
|
+
if (remote.workspace_id !== expectedWorkspaceId) {
|
|
490
|
+
throw new Error(`Remote identity mismatch: expected workspace_id=${expectedWorkspaceId} but workspace backend reported workspace_id=${String(remote.workspace_id)}. Re-resolve the workspace through the manager (\`reffy remote workspace get ${expectedWorkspaceId}\`) or reinitialize linkage.`);
|
|
296
491
|
}
|
|
297
492
|
}
|
|
298
493
|
export function validateImportResult(result) {
|
package/dist/types.d.ts
CHANGED
|
@@ -22,15 +22,18 @@ export interface Manifest {
|
|
|
22
22
|
workspace_name?: string;
|
|
23
23
|
artifacts: Artifact[];
|
|
24
24
|
}
|
|
25
|
-
export interface
|
|
25
|
+
export interface RemoteActorIdentity {
|
|
26
26
|
pod_name: string;
|
|
27
27
|
actor_id: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RemoteTarget {
|
|
30
|
+
workspace_backend: RemoteActorIdentity;
|
|
28
31
|
last_imported_at?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface RemoteLinkConfig {
|
|
31
34
|
version: number;
|
|
32
35
|
provider: "paseo";
|
|
33
|
-
|
|
36
|
+
manager: RemoteActorIdentity;
|
|
34
37
|
targets: Record<string, RemoteTarget>;
|
|
35
38
|
}
|
|
36
39
|
export interface RemoteDocument {
|