reffy-cli 1.4.0 → 1.4.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 +21 -2
- package/dist/cli.js +82 -20
- package/dist/remote.d.ts +9 -0
- package/dist/remote.js +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,8 +99,8 @@ The current remote flow is:
|
|
|
99
99
|
1. Reffy reads `project_id` and `workspace_name` from `.reffy/manifest.json`.
|
|
100
100
|
2. Reffy connects to Paseo using `PASEO_ENDPOINT` or `--endpoint`.
|
|
101
101
|
3. `reffy remote init --provision` creates a pod and actor when needed, then writes local linkage state to `.reffy/state/remote.json`.
|
|
102
|
-
4. `reffy remote push` publishes the local workspace to that linked actor.
|
|
103
|
-
5. `reffy remote status|ls|cat` inspects the linked remote workspace.
|
|
102
|
+
4. `reffy remote push` publishes the local workspace to that linked actor with `replace_missing=true` by default, so remote reflects local.
|
|
103
|
+
5. `reffy remote status|ls|cat` inspects the linked remote workspace using the saved linkage state unless you override it.
|
|
104
104
|
|
|
105
105
|
### Minimal connection requirement
|
|
106
106
|
|
|
@@ -141,6 +141,8 @@ That file contains the connection tuple Reffy uses to reach the backend:
|
|
|
141
141
|
|
|
142
142
|
This file is local runtime state. It is not part of the synced remote workspace and is intentionally excluded from `reffy remote push`.
|
|
143
143
|
|
|
144
|
+
The file can also record local sync metadata such as the last successful import timestamp. That metadata is for local diagnostics and does not become part of the remote workspace contract.
|
|
145
|
+
|
|
144
146
|
### Example flow
|
|
145
147
|
|
|
146
148
|
```bash
|
|
@@ -152,6 +154,23 @@ reffy remote ls
|
|
|
152
154
|
reffy remote cat .reffy/manifest.json
|
|
153
155
|
```
|
|
154
156
|
|
|
157
|
+
`reffy remote status` is the primary diagnostic command for this flow. It reports:
|
|
158
|
+
|
|
159
|
+
- the saved linkage being used
|
|
160
|
+
- the local manifest identity
|
|
161
|
+
- the remote workspace identity
|
|
162
|
+
- remote document counts when reachable
|
|
163
|
+
|
|
164
|
+
`reffy remote push` reports:
|
|
165
|
+
|
|
166
|
+
- local document count
|
|
167
|
+
- imported count
|
|
168
|
+
- created count
|
|
169
|
+
- updated count
|
|
170
|
+
- deleted count
|
|
171
|
+
|
|
172
|
+
That makes the default prune/import behavior auditable without dropping to direct backend API calls.
|
|
173
|
+
|
|
155
174
|
## Manifest Contract
|
|
156
175
|
|
|
157
176
|
Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests include both core timestamps and workspace identity:
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { archivePlanningChange } from "./plan-archive.js";
|
|
|
9
9
|
import { createPlanScaffold } from "./plan.js";
|
|
10
10
|
import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPlanningPath } from "./planning-paths.js";
|
|
11
11
|
import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
|
|
12
|
-
import { assertRemoteIdentity, collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, mergeRemoteConfig, PaseoRemoteClient, readRemoteConfig, requireWorkspaceIdentity, resolveRemoteConfigPath, } from "./remote.js";
|
|
12
|
+
import { assertRemoteIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureRemoteInit, extractWorkspaceIdentity, mergeRemoteConfig, PaseoRemoteClient, readRemoteConfig, requireWorkspaceIdentity, resolveRemoteConfigPath, updateRemoteConfigMetadata, validateImportResult, } from "./remote.js";
|
|
13
13
|
import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
|
|
14
14
|
import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
|
|
15
15
|
import { ReferencesStore } from "./storage.js";
|
|
@@ -813,17 +813,25 @@ async function resolveRemoteContext(args) {
|
|
|
813
813
|
const store = new ReferencesStore(args.repoRoot);
|
|
814
814
|
const identity = requireWorkspaceIdentity(await store.getWorkspaceIdentity());
|
|
815
815
|
const savedConfig = await readRemoteConfig(args.repoRoot);
|
|
816
|
-
const
|
|
816
|
+
const overrideValues = {
|
|
817
817
|
endpoint: args.endpoint ?? process.env.PASEO_ENDPOINT,
|
|
818
818
|
pod_name: args.podName ?? process.env.PASEO_POD_NAME,
|
|
819
819
|
actor_id: args.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
820
|
-
}
|
|
820
|
+
};
|
|
821
|
+
const mergedConfig = mergeRemoteConfig(savedConfig, overrideValues);
|
|
822
|
+
const hasOverrides = Boolean(overrideValues.endpoint || overrideValues.pod_name || overrideValues.actor_id);
|
|
823
|
+
const linkageSource = mergedConfig
|
|
824
|
+
? savedConfig && !hasOverrides
|
|
825
|
+
? "saved_config"
|
|
826
|
+
: "cli_or_env_overrides"
|
|
827
|
+
: "unconfigured";
|
|
821
828
|
return {
|
|
822
829
|
store,
|
|
823
830
|
identity,
|
|
824
831
|
configPath: resolveRemoteConfigPath(args.repoRoot),
|
|
825
832
|
savedConfig,
|
|
826
833
|
mergedConfig,
|
|
834
|
+
linkageSource,
|
|
827
835
|
};
|
|
828
836
|
}
|
|
829
837
|
async function main() {
|
|
@@ -885,7 +893,7 @@ async function main() {
|
|
|
885
893
|
const parsed = parseRemoteArgs(remoteArgs);
|
|
886
894
|
try {
|
|
887
895
|
if (subcommand === "init") {
|
|
888
|
-
const { identity } = await resolveRemoteContext(parsed);
|
|
896
|
+
const { identity, configPath } = await resolveRemoteContext(parsed);
|
|
889
897
|
const endpoint = parsed.endpoint ?? process.env.PASEO_ENDPOINT;
|
|
890
898
|
if (!endpoint) {
|
|
891
899
|
throw new Error("Remote init requires --endpoint or PASEO_ENDPOINT (optionally loaded from .env)");
|
|
@@ -902,7 +910,7 @@ async function main() {
|
|
|
902
910
|
command: "remote",
|
|
903
911
|
subcommand: "init",
|
|
904
912
|
provider: result.config.provider,
|
|
905
|
-
config_path:
|
|
913
|
+
config_path: configPath,
|
|
906
914
|
endpoint: result.config.endpoint,
|
|
907
915
|
pod_name: result.config.pod_name,
|
|
908
916
|
actor_id: result.config.actor_id,
|
|
@@ -910,6 +918,7 @@ async function main() {
|
|
|
910
918
|
created_actor: result.created_actor,
|
|
911
919
|
project_id: identity.project_id,
|
|
912
920
|
workspace_name: identity.workspace_name,
|
|
921
|
+
linkage_mode: result.created_pod || result.created_actor ? "provisioned" : "existing",
|
|
913
922
|
};
|
|
914
923
|
if (output === "json") {
|
|
915
924
|
printResult(output, payload);
|
|
@@ -924,6 +933,7 @@ async function main() {
|
|
|
924
933
|
console.log(`Workspace name: ${payload.workspace_name}`);
|
|
925
934
|
console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
|
|
926
935
|
console.log(`Created actor: ${payload.created_actor ? "yes" : "no"}`);
|
|
936
|
+
console.log(`Linkage mode: ${payload.linkage_mode}`);
|
|
927
937
|
}
|
|
928
938
|
return 0;
|
|
929
939
|
}
|
|
@@ -933,7 +943,14 @@ async function main() {
|
|
|
933
943
|
}
|
|
934
944
|
const client = new PaseoRemoteClient(context.mergedConfig);
|
|
935
945
|
if (subcommand === "status") {
|
|
936
|
-
|
|
946
|
+
let workspace;
|
|
947
|
+
try {
|
|
948
|
+
workspace = await client.getWorkspace();
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
952
|
+
throw new Error(`Remote status failed using ${describeRemoteLinkage(context.mergedConfig)}: ${message}`);
|
|
953
|
+
}
|
|
937
954
|
assertRemoteIdentity(workspace, context.identity);
|
|
938
955
|
const remoteIdentity = extractWorkspaceIdentity(workspace);
|
|
939
956
|
const payload = {
|
|
@@ -945,12 +962,14 @@ async function main() {
|
|
|
945
962
|
endpoint: context.mergedConfig.endpoint,
|
|
946
963
|
pod_name: context.mergedConfig.pod_name,
|
|
947
964
|
actor_id: context.mergedConfig.actor_id,
|
|
965
|
+
linkage_source: context.linkageSource,
|
|
948
966
|
local_identity: context.identity,
|
|
949
967
|
remote_identity: {
|
|
950
968
|
project_id: remoteIdentity.project_id,
|
|
951
969
|
workspace_name: remoteIdentity.workspace_name,
|
|
952
970
|
},
|
|
953
971
|
document_count: remoteIdentity.document_count,
|
|
972
|
+
last_imported_at: context.savedConfig?.last_imported_at ?? null,
|
|
954
973
|
workspace,
|
|
955
974
|
};
|
|
956
975
|
if (output === "json") {
|
|
@@ -962,17 +981,35 @@ async function main() {
|
|
|
962
981
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
963
982
|
console.log(`Pod: ${payload.pod_name}`);
|
|
964
983
|
console.log(`Actor: ${payload.actor_id}`);
|
|
984
|
+
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
965
985
|
console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.workspace_name}`);
|
|
966
986
|
console.log(`Remote identity: ${String(payload.remote_identity.project_id)}/${String(payload.remote_identity.workspace_name)}`);
|
|
967
987
|
console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
|
|
988
|
+
console.log(`Last imported at: ${String(payload.last_imported_at ?? "never")}`);
|
|
968
989
|
}
|
|
969
990
|
return 0;
|
|
970
991
|
}
|
|
971
992
|
if (subcommand === "push") {
|
|
972
993
|
const documents = await collectWorkspaceDocuments(parsed.repoRoot);
|
|
973
|
-
|
|
974
|
-
|
|
994
|
+
let importResult;
|
|
995
|
+
try {
|
|
996
|
+
importResult = validateImportResult(await client.importWorkspace(documents, true));
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1000
|
+
throw new Error(`Remote push failed using ${describeRemoteLinkage(context.mergedConfig)}: ${message}`);
|
|
1001
|
+
}
|
|
1002
|
+
let workspace;
|
|
1003
|
+
try {
|
|
1004
|
+
workspace = await client.getWorkspace();
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1008
|
+
throw new Error(`Remote push could not verify remote state using ${describeRemoteLinkage(context.mergedConfig)}: ${message}`);
|
|
1009
|
+
}
|
|
975
1010
|
assertRemoteIdentity(workspace, context.identity);
|
|
1011
|
+
const importedAt = new Date().toISOString();
|
|
1012
|
+
await updateRemoteConfigMetadata(parsed.repoRoot, { last_imported_at: importedAt });
|
|
976
1013
|
const payload = {
|
|
977
1014
|
status: "ok",
|
|
978
1015
|
command: "remote",
|
|
@@ -980,13 +1017,16 @@ async function main() {
|
|
|
980
1017
|
endpoint: context.mergedConfig.endpoint,
|
|
981
1018
|
pod_name: context.mergedConfig.pod_name,
|
|
982
1019
|
actor_id: context.mergedConfig.actor_id,
|
|
1020
|
+
linkage_source: context.linkageSource,
|
|
983
1021
|
project_id: context.identity.project_id,
|
|
984
1022
|
workspace_name: context.identity.workspace_name,
|
|
985
1023
|
local_document_count: documents.length,
|
|
986
|
-
imported: importResult.imported
|
|
987
|
-
created: importResult.created
|
|
988
|
-
updated: importResult.updated
|
|
989
|
-
deleted: importResult.deleted
|
|
1024
|
+
imported: importResult.imported,
|
|
1025
|
+
created: importResult.created,
|
|
1026
|
+
updated: importResult.updated,
|
|
1027
|
+
deleted: importResult.deleted,
|
|
1028
|
+
replace_missing: true,
|
|
1029
|
+
last_imported_at: importedAt,
|
|
990
1030
|
remote_document_count: extractWorkspaceIdentity(workspace).document_count,
|
|
991
1031
|
};
|
|
992
1032
|
if (output === "json") {
|
|
@@ -997,17 +1037,27 @@ async function main() {
|
|
|
997
1037
|
console.log(`Endpoint: ${payload.endpoint}`);
|
|
998
1038
|
console.log(`Pod: ${payload.pod_name}`);
|
|
999
1039
|
console.log(`Actor: ${payload.actor_id}`);
|
|
1040
|
+
console.log(`Linkage source: ${payload.linkage_source}`);
|
|
1041
|
+
console.log("Mode: remote reflects local (replace_missing=true)");
|
|
1000
1042
|
console.log(`Local document count: ${String(payload.local_document_count)}`);
|
|
1001
|
-
console.log(`Imported: ${String(payload.imported
|
|
1002
|
-
console.log(`Created: ${String(payload.created
|
|
1003
|
-
console.log(`Updated: ${String(payload.updated
|
|
1004
|
-
console.log(`Deleted: ${String(payload.deleted
|
|
1043
|
+
console.log(`Imported: ${String(payload.imported)}`);
|
|
1044
|
+
console.log(`Created: ${String(payload.created)}`);
|
|
1045
|
+
console.log(`Updated: ${String(payload.updated)}`);
|
|
1046
|
+
console.log(`Deleted: ${String(payload.deleted)}`);
|
|
1005
1047
|
console.log(`Remote document count: ${String(payload.remote_document_count ?? "unknown")}`);
|
|
1048
|
+
console.log(`Last imported at: ${payload.last_imported_at}`);
|
|
1006
1049
|
}
|
|
1007
1050
|
return 0;
|
|
1008
1051
|
}
|
|
1009
1052
|
if (subcommand === "ls") {
|
|
1010
|
-
|
|
1053
|
+
let snapshot;
|
|
1054
|
+
try {
|
|
1055
|
+
snapshot = await client.getSnapshot(parsed.prefix);
|
|
1056
|
+
}
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1059
|
+
throw new Error(`Remote ls failed using ${describeRemoteLinkage(context.mergedConfig)}: ${message}`);
|
|
1060
|
+
}
|
|
1011
1061
|
const paths = (snapshot.documents ?? [])
|
|
1012
1062
|
.map((document) => (typeof document.path === "string" ? document.path : ""))
|
|
1013
1063
|
.filter(Boolean)
|
|
@@ -1019,6 +1069,7 @@ async function main() {
|
|
|
1019
1069
|
endpoint: context.mergedConfig.endpoint,
|
|
1020
1070
|
pod_name: context.mergedConfig.pod_name,
|
|
1021
1071
|
actor_id: context.mergedConfig.actor_id,
|
|
1072
|
+
linkage_source: context.linkageSource,
|
|
1022
1073
|
prefix: parsed.prefix ?? null,
|
|
1023
1074
|
paths,
|
|
1024
1075
|
};
|
|
@@ -1043,7 +1094,17 @@ async function main() {
|
|
|
1043
1094
|
if (!documentPath.startsWith(".reffy/")) {
|
|
1044
1095
|
throw new Error("reffy remote cat requires a canonical path rooted at .reffy/");
|
|
1045
1096
|
}
|
|
1046
|
-
|
|
1097
|
+
let document;
|
|
1098
|
+
try {
|
|
1099
|
+
document = await client.getDocument(documentPath);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1103
|
+
throw new Error(`Remote cat failed using ${describeRemoteLinkage(context.mergedConfig)}: ${message}`);
|
|
1104
|
+
}
|
|
1105
|
+
if (!document.document) {
|
|
1106
|
+
throw new Error(`Remote document not found using ${describeRemoteLinkage(context.mergedConfig)}: ${documentPath}`);
|
|
1107
|
+
}
|
|
1047
1108
|
const payload = {
|
|
1048
1109
|
status: "ok",
|
|
1049
1110
|
command: "remote",
|
|
@@ -1051,13 +1112,14 @@ async function main() {
|
|
|
1051
1112
|
endpoint: context.mergedConfig.endpoint,
|
|
1052
1113
|
pod_name: context.mergedConfig.pod_name,
|
|
1053
1114
|
actor_id: context.mergedConfig.actor_id,
|
|
1054
|
-
|
|
1115
|
+
linkage_source: context.linkageSource,
|
|
1116
|
+
document: document.document,
|
|
1055
1117
|
};
|
|
1056
1118
|
if (output === "json") {
|
|
1057
1119
|
printResult(output, payload);
|
|
1058
1120
|
}
|
|
1059
1121
|
else {
|
|
1060
|
-
const content = typeof document.document
|
|
1122
|
+
const content = typeof document.document.content === "string" ? document.document.content : "";
|
|
1061
1123
|
process.stdout.write(content);
|
|
1062
1124
|
if (!content.endsWith("\n")) {
|
|
1063
1125
|
process.stdout.write("\n");
|
package/dist/remote.d.ts
CHANGED
|
@@ -17,10 +17,18 @@ export interface EnsureRemoteInitResult {
|
|
|
17
17
|
created_pod: boolean;
|
|
18
18
|
created_actor: boolean;
|
|
19
19
|
}
|
|
20
|
+
export interface ValidatedImportResult {
|
|
21
|
+
imported: number;
|
|
22
|
+
created: number;
|
|
23
|
+
updated: number;
|
|
24
|
+
deleted: number;
|
|
25
|
+
}
|
|
20
26
|
export declare function resolveRemoteConfigPath(repoRoot: string): string;
|
|
21
27
|
export declare function readRemoteConfig(repoRoot: string): Promise<RemoteLinkConfig | null>;
|
|
22
28
|
export declare function writeRemoteConfig(repoRoot: string, config: RemoteLinkConfig): Promise<string>;
|
|
29
|
+
export declare function updateRemoteConfigMetadata(repoRoot: string, patch: Partial<Pick<RemoteLinkConfig, "last_imported_at">>): Promise<RemoteLinkConfig | null>;
|
|
23
30
|
export declare function mergeRemoteConfig(stored: RemoteLinkConfig | null, overrides: Partial<Pick<RemoteLinkConfig, "endpoint" | "pod_name" | "actor_id">>): RemoteLinkConfig | null;
|
|
31
|
+
export declare function describeRemoteLinkage(config: Pick<RemoteLinkConfig, "endpoint" | "pod_name" | "actor_id">): string;
|
|
24
32
|
export declare class PaseoRemoteClient {
|
|
25
33
|
private readonly config;
|
|
26
34
|
constructor(config: Pick<RemoteLinkConfig, "endpoint" | "pod_name" | "actor_id">);
|
|
@@ -52,5 +60,6 @@ export declare function extractWorkspaceIdentity(summary: RemoteWorkspaceSummary
|
|
|
52
60
|
document_count?: number | null;
|
|
53
61
|
};
|
|
54
62
|
export declare function assertRemoteIdentity(summary: RemoteWorkspaceSummary, identity: WorkspaceIdentity): void;
|
|
63
|
+
export declare function validateImportResult(result: RemoteImportResult): ValidatedImportResult;
|
|
55
64
|
export declare function toCanonicalRemotePath(refsDir: string, filePath: string): string;
|
|
56
65
|
export declare function collectWorkspaceDocuments(repoRoot: string): Promise<RemoteDocument[]>;
|
package/dist/remote.js
CHANGED
|
@@ -62,6 +62,17 @@ export async function writeRemoteConfig(repoRoot, config) {
|
|
|
62
62
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
63
63
|
return configPath;
|
|
64
64
|
}
|
|
65
|
+
export async function updateRemoteConfigMetadata(repoRoot, patch) {
|
|
66
|
+
const existing = await readRemoteConfig(repoRoot);
|
|
67
|
+
if (!existing)
|
|
68
|
+
return null;
|
|
69
|
+
const next = {
|
|
70
|
+
...existing,
|
|
71
|
+
...patch,
|
|
72
|
+
};
|
|
73
|
+
await writeRemoteConfig(repoRoot, next);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
65
76
|
export function mergeRemoteConfig(stored, overrides) {
|
|
66
77
|
if (!stored && !overrides.endpoint && !overrides.pod_name && !overrides.actor_id) {
|
|
67
78
|
return null;
|
|
@@ -81,6 +92,9 @@ export function mergeRemoteConfig(stored, overrides) {
|
|
|
81
92
|
last_imported_at: stored?.last_imported_at,
|
|
82
93
|
};
|
|
83
94
|
}
|
|
95
|
+
export function describeRemoteLinkage(config) {
|
|
96
|
+
return `endpoint=${config.endpoint} pod=${config.pod_name} actor=${config.actor_id}`;
|
|
97
|
+
}
|
|
84
98
|
async function http(url, init = {}) {
|
|
85
99
|
const headers = new Headers(init.headers ?? {});
|
|
86
100
|
if (!headers.has("Content-Type") && init.body) {
|
|
@@ -219,6 +233,19 @@ export function assertRemoteIdentity(summary, identity) {
|
|
|
219
233
|
throw new Error(`Remote identity mismatch: local=${identity.project_id}/${identity.workspace_name} remote=${String(remote.project_id)}/${String(remote.workspace_name)}`);
|
|
220
234
|
}
|
|
221
235
|
}
|
|
236
|
+
export function validateImportResult(result) {
|
|
237
|
+
const requiredKeys = ["imported", "created", "updated", "deleted"];
|
|
238
|
+
const missing = requiredKeys.filter((key) => typeof result[key] !== "number");
|
|
239
|
+
if (missing.length > 0) {
|
|
240
|
+
throw new Error(`Remote import response missing numeric field(s): ${missing.join(", ")}`);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
imported: result.imported,
|
|
244
|
+
created: result.created,
|
|
245
|
+
updated: result.updated,
|
|
246
|
+
deleted: result.deleted,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
222
249
|
async function collectFiles(dirPath) {
|
|
223
250
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
224
251
|
const files = [];
|