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 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 mergedConfig = mergeRemoteConfig(savedConfig, {
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: resolveRemoteConfigPath(parsed.repoRoot),
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
- const workspace = await client.getWorkspace();
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
- const importResult = await client.importWorkspace(documents, true);
974
- const workspace = await client.getWorkspace();
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 ?? null,
987
- created: importResult.created ?? null,
988
- updated: importResult.updated ?? null,
989
- deleted: importResult.deleted ?? null,
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 ?? "unknown")}`);
1002
- console.log(`Created: ${String(payload.created ?? "unknown")}`);
1003
- console.log(`Updated: ${String(payload.updated ?? "unknown")}`);
1004
- console.log(`Deleted: ${String(payload.deleted ?? "unknown")}`);
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
- const snapshot = await client.getSnapshot(parsed.prefix);
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
- const document = await client.getDocument(documentPath);
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
- document: document.document ?? null,
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?.content === "string" ? document.document.content : "";
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reffy-cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Planning system for AI assisted development.",
5
5
  "keywords": [
6
6
  "reffy",