reffy-cli 1.3.0 → 1.4.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 +93 -0
- package/dist/cli.js +372 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/remote.d.ts +56 -0
- package/dist/remote.js +275 -0
- package/dist/types.d.ts +24 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Command summary:
|
|
|
33
33
|
- `reffy plan create`: generates proposal, task, design, and spec scaffolds from indexed Reffy artifacts.
|
|
34
34
|
- `reffy plan validate|list|show|archive`: manages the planning lifecycle under `.reffy/reffyspec/`.
|
|
35
35
|
- `reffy spec list|show`: inspects current spec state under `.reffy/reffyspec/`.
|
|
36
|
+
- `reffy remote init|status|push|ls|cat`: links, publishes, and inspects a Paseo-backed remote `.reffy/` workspace.
|
|
36
37
|
- `reffy diagram render`: renders Mermaid diagrams as SVG or ASCII, including spec-aware generation from compatible `spec.md` files.
|
|
37
38
|
|
|
38
39
|
Output modes:
|
|
@@ -54,11 +55,103 @@ reffy plan create --change-id add-login-flow --artifacts login-idea.md
|
|
|
54
55
|
reffy plan list --output json
|
|
55
56
|
reffy plan archive add-login-flow
|
|
56
57
|
reffy spec show auth --output json
|
|
58
|
+
reffy remote init --endpoint https://your-paseo-endpoint.example --provision
|
|
59
|
+
reffy remote status --output json
|
|
60
|
+
reffy remote push
|
|
61
|
+
reffy remote ls
|
|
62
|
+
reffy remote cat .reffy/manifest.json
|
|
57
63
|
reffy diagram render --stdin --format svg < diagram.mmd
|
|
58
64
|
reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format ascii
|
|
59
65
|
reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
|
|
60
66
|
```
|
|
61
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
|
+
## Remote Sync
|
|
94
|
+
|
|
95
|
+
Reffy can publish the local `.reffy/` workspace to a Paseo-backed remote actor and inspect it later with native CLI commands.
|
|
96
|
+
|
|
97
|
+
The current remote flow is:
|
|
98
|
+
|
|
99
|
+
1. Reffy reads `project_id` and `workspace_name` from `.reffy/manifest.json`.
|
|
100
|
+
2. Reffy connects to Paseo using `PASEO_ENDPOINT` or `--endpoint`.
|
|
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.
|
|
104
|
+
|
|
105
|
+
### Minimal connection requirement
|
|
106
|
+
|
|
107
|
+
For the fresh-provision path, the only required connection value is:
|
|
108
|
+
|
|
109
|
+
- `PASEO_ENDPOINT`
|
|
110
|
+
|
|
111
|
+
Example `.env`:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
PASEO_ENDPOINT="https://your-paseo-endpoint.example"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
That is enough for:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
reffy remote init --provision
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Optional overrides:
|
|
124
|
+
|
|
125
|
+
- `PASEO_POD_NAME` if you want to reuse an existing Paseo pod
|
|
126
|
+
- `PASEO_ACTOR_ID` if you want to reuse an existing backend actor
|
|
127
|
+
|
|
128
|
+
Reffy does not require separate `REFFY_PROJECT_ID` or `REFFY_WORKSPACE_NAME` values for the normal case because those come from `.reffy/manifest.json`.
|
|
129
|
+
|
|
130
|
+
### Saved remote linkage
|
|
131
|
+
|
|
132
|
+
After `reffy remote init`, Reffy stores the local pointer to the linked backend actor in:
|
|
133
|
+
|
|
134
|
+
- `.reffy/state/remote.json`
|
|
135
|
+
|
|
136
|
+
That file contains the connection tuple Reffy uses to reach the backend:
|
|
137
|
+
|
|
138
|
+
- `endpoint`
|
|
139
|
+
- `pod_name`
|
|
140
|
+
- `actor_id`
|
|
141
|
+
|
|
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
|
+
|
|
144
|
+
### Example flow
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
reffy init
|
|
148
|
+
reffy remote init --provision
|
|
149
|
+
reffy remote status
|
|
150
|
+
reffy remote push
|
|
151
|
+
reffy remote ls
|
|
152
|
+
reffy remote cat .reffy/manifest.json
|
|
153
|
+
```
|
|
154
|
+
|
|
62
155
|
## Manifest Contract
|
|
63
156
|
|
|
64
157
|
Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests include both core timestamps and workspace identity:
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,12 @@ import { existsSync, promises as fs, statSync } from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { renderDiagram } from "./diagram.js";
|
|
6
6
|
import { runDoctor } from "./doctor.js";
|
|
7
|
+
import { loadDotEnvIfPresent } from "./env.js";
|
|
7
8
|
import { archivePlanningChange } from "./plan-archive.js";
|
|
8
9
|
import { createPlanScaffold } from "./plan.js";
|
|
9
10
|
import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPlanningPath } from "./planning-paths.js";
|
|
10
11
|
import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
|
|
12
|
+
import { assertRemoteIdentity, collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, mergeRemoteConfig, PaseoRemoteClient, readRemoteConfig, requireWorkspaceIdentity, resolveRemoteConfigPath, } from "./remote.js";
|
|
11
13
|
import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
|
|
12
14
|
import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
|
|
13
15
|
import { ReferencesStore } from "./storage.js";
|
|
@@ -431,6 +433,7 @@ function usage() {
|
|
|
431
433
|
" summarize Generate a read-only summary of indexed Reffy artifacts.",
|
|
432
434
|
" plan Generate and manage ReffySpec planning scaffolds from indexed Reffy artifacts.",
|
|
433
435
|
" spec Inspect current specs from the ReffySpec layout.",
|
|
436
|
+
" remote Link, publish, and inspect a Paseo-backed remote Reffy workspace.",
|
|
434
437
|
" diagram Render Mermaid diagrams (supports SVG and ASCII).",
|
|
435
438
|
].join("\n");
|
|
436
439
|
}
|
|
@@ -478,6 +481,24 @@ function planUsage() {
|
|
|
478
481
|
" --force Overwrite an existing non-empty change directory",
|
|
479
482
|
].join("\n");
|
|
480
483
|
}
|
|
484
|
+
function remoteUsage() {
|
|
485
|
+
return [
|
|
486
|
+
"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]",
|
|
492
|
+
"",
|
|
493
|
+
"Options:",
|
|
494
|
+
" --endpoint URL Paseo base URL. Defaults to saved config or PASEO_ENDPOINT/.env.",
|
|
495
|
+
" --pod-name NAME Paseo pod name. Defaults to saved config or PASEO_POD_NAME/.env.",
|
|
496
|
+
" --actor-id ID Paseo actor id. Defaults to saved config or PASEO_ACTOR_ID/.env.",
|
|
497
|
+
" --provision Create missing pod/actor during `remote init`.",
|
|
498
|
+
" --prefix PATH Prefix filter for `remote ls`.",
|
|
499
|
+
" --env-file PATH Dotenv file to load before resolving env-backed options. Defaults to .env.",
|
|
500
|
+
].join("\n");
|
|
501
|
+
}
|
|
481
502
|
function parseDiagramArgs(argv) {
|
|
482
503
|
const repoRoot = parseRepoArg(argv);
|
|
483
504
|
const args = {
|
|
@@ -654,6 +675,129 @@ function parsePlanArgs(argv) {
|
|
|
654
675
|
}
|
|
655
676
|
return args;
|
|
656
677
|
}
|
|
678
|
+
function parseRemoteArgs(argv) {
|
|
679
|
+
const repoRoot = parseRepoArg(argv);
|
|
680
|
+
const args = {
|
|
681
|
+
repoRoot,
|
|
682
|
+
provision: false,
|
|
683
|
+
};
|
|
684
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
685
|
+
const arg = argv[i];
|
|
686
|
+
if (arg === "--repo") {
|
|
687
|
+
i += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (arg.startsWith("--repo="))
|
|
691
|
+
continue;
|
|
692
|
+
if (arg === "--json")
|
|
693
|
+
continue;
|
|
694
|
+
if (arg === "--output") {
|
|
695
|
+
i += 1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (arg.startsWith("--output="))
|
|
699
|
+
continue;
|
|
700
|
+
if (arg === "--endpoint") {
|
|
701
|
+
const value = argv[i + 1];
|
|
702
|
+
if (!value)
|
|
703
|
+
throw new Error("--endpoint requires a value");
|
|
704
|
+
args.endpoint = value;
|
|
705
|
+
i += 1;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (arg.startsWith("--endpoint=")) {
|
|
709
|
+
args.endpoint = arg.split("=", 2)[1];
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (arg === "--pod-name") {
|
|
713
|
+
const value = argv[i + 1];
|
|
714
|
+
if (!value)
|
|
715
|
+
throw new Error("--pod-name requires a value");
|
|
716
|
+
args.podName = value;
|
|
717
|
+
i += 1;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (arg.startsWith("--pod-name=")) {
|
|
721
|
+
args.podName = arg.split("=", 2)[1];
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (arg === "--actor-id") {
|
|
725
|
+
const value = argv[i + 1];
|
|
726
|
+
if (!value)
|
|
727
|
+
throw new Error("--actor-id requires a value");
|
|
728
|
+
args.actorId = value;
|
|
729
|
+
i += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (arg.startsWith("--actor-id=")) {
|
|
733
|
+
args.actorId = arg.split("=", 2)[1];
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (arg === "--env-file") {
|
|
737
|
+
const value = argv[i + 1];
|
|
738
|
+
if (!value)
|
|
739
|
+
throw new Error("--env-file requires a value");
|
|
740
|
+
args.envFile = value;
|
|
741
|
+
i += 1;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (arg.startsWith("--env-file=")) {
|
|
745
|
+
args.envFile = arg.split("=", 2)[1];
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (arg === "--prefix") {
|
|
749
|
+
const value = argv[i + 1];
|
|
750
|
+
if (!value)
|
|
751
|
+
throw new Error("--prefix requires a value");
|
|
752
|
+
args.prefix = value;
|
|
753
|
+
i += 1;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (arg.startsWith("--prefix=")) {
|
|
757
|
+
args.prefix = arg.split("=", 2)[1];
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (arg === "--provision") {
|
|
761
|
+
args.provision = true;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (arg.startsWith("--")) {
|
|
765
|
+
throw new Error(`Unknown remote option: ${arg}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return args;
|
|
769
|
+
}
|
|
770
|
+
function getRemotePositionalArgs(argv) {
|
|
771
|
+
const positionals = [];
|
|
772
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
773
|
+
const arg = argv[i];
|
|
774
|
+
if (arg === "--repo" ||
|
|
775
|
+
arg === "--output" ||
|
|
776
|
+
arg === "--endpoint" ||
|
|
777
|
+
arg === "--pod-name" ||
|
|
778
|
+
arg === "--actor-id" ||
|
|
779
|
+
arg === "--env-file" ||
|
|
780
|
+
arg === "--prefix") {
|
|
781
|
+
i += 1;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (arg.startsWith("--repo=") ||
|
|
785
|
+
arg.startsWith("--output=") ||
|
|
786
|
+
arg.startsWith("--endpoint=") ||
|
|
787
|
+
arg.startsWith("--pod-name=") ||
|
|
788
|
+
arg.startsWith("--actor-id=") ||
|
|
789
|
+
arg.startsWith("--env-file=") ||
|
|
790
|
+
arg.startsWith("--prefix=") ||
|
|
791
|
+
arg === "--json" ||
|
|
792
|
+
arg === "--provision") {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (arg.startsWith("--"))
|
|
796
|
+
continue;
|
|
797
|
+
positionals.push(arg);
|
|
798
|
+
}
|
|
799
|
+
return positionals;
|
|
800
|
+
}
|
|
657
801
|
function printSection(title, values) {
|
|
658
802
|
console.log(`${title}:`);
|
|
659
803
|
if (values.length === 0) {
|
|
@@ -664,6 +808,24 @@ function printSection(title, values) {
|
|
|
664
808
|
console.log(`- ${value}`);
|
|
665
809
|
}
|
|
666
810
|
}
|
|
811
|
+
async function resolveRemoteContext(args) {
|
|
812
|
+
await loadDotEnvIfPresent(args.repoRoot, args.envFile ?? ".env");
|
|
813
|
+
const store = new ReferencesStore(args.repoRoot);
|
|
814
|
+
const identity = requireWorkspaceIdentity(await store.getWorkspaceIdentity());
|
|
815
|
+
const savedConfig = await readRemoteConfig(args.repoRoot);
|
|
816
|
+
const mergedConfig = mergeRemoteConfig(savedConfig, {
|
|
817
|
+
endpoint: args.endpoint ?? process.env.PASEO_ENDPOINT,
|
|
818
|
+
pod_name: args.podName ?? process.env.PASEO_POD_NAME,
|
|
819
|
+
actor_id: args.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
store,
|
|
823
|
+
identity,
|
|
824
|
+
configPath: resolveRemoteConfigPath(args.repoRoot),
|
|
825
|
+
savedConfig,
|
|
826
|
+
mergedConfig,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
667
829
|
async function main() {
|
|
668
830
|
const [, , command, ...rest] = process.argv;
|
|
669
831
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -713,6 +875,216 @@ async function main() {
|
|
|
713
875
|
}
|
|
714
876
|
return 0;
|
|
715
877
|
}
|
|
878
|
+
if (command === "remote") {
|
|
879
|
+
const [subcommand, ...remoteArgs] = rest;
|
|
880
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
881
|
+
console.error(remoteUsage());
|
|
882
|
+
return 1;
|
|
883
|
+
}
|
|
884
|
+
const output = parseOutputMode(remoteArgs);
|
|
885
|
+
const parsed = parseRemoteArgs(remoteArgs);
|
|
886
|
+
try {
|
|
887
|
+
if (subcommand === "init") {
|
|
888
|
+
const { identity } = await resolveRemoteContext(parsed);
|
|
889
|
+
const endpoint = parsed.endpoint ?? process.env.PASEO_ENDPOINT;
|
|
890
|
+
if (!endpoint) {
|
|
891
|
+
throw new Error("Remote init requires --endpoint or PASEO_ENDPOINT (optionally loaded from .env)");
|
|
892
|
+
}
|
|
893
|
+
const result = await ensureRemoteInit(parsed.repoRoot, {
|
|
894
|
+
endpoint,
|
|
895
|
+
podName: parsed.podName ?? process.env.PASEO_POD_NAME,
|
|
896
|
+
actorId: parsed.actorId ?? process.env.PASEO_ACTOR_ID,
|
|
897
|
+
provision: parsed.provision,
|
|
898
|
+
identity,
|
|
899
|
+
});
|
|
900
|
+
const payload = {
|
|
901
|
+
status: "ok",
|
|
902
|
+
command: "remote",
|
|
903
|
+
subcommand: "init",
|
|
904
|
+
provider: result.config.provider,
|
|
905
|
+
config_path: resolveRemoteConfigPath(parsed.repoRoot),
|
|
906
|
+
endpoint: result.config.endpoint,
|
|
907
|
+
pod_name: result.config.pod_name,
|
|
908
|
+
actor_id: result.config.actor_id,
|
|
909
|
+
created_pod: result.created_pod,
|
|
910
|
+
created_actor: result.created_actor,
|
|
911
|
+
project_id: identity.project_id,
|
|
912
|
+
workspace_name: identity.workspace_name,
|
|
913
|
+
};
|
|
914
|
+
if (output === "json") {
|
|
915
|
+
printResult(output, payload);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
console.log("Remote linkage ready");
|
|
919
|
+
console.log(`Config: ${payload.config_path}`);
|
|
920
|
+
console.log(`Endpoint: ${payload.endpoint}`);
|
|
921
|
+
console.log(`Pod: ${payload.pod_name}`);
|
|
922
|
+
console.log(`Actor: ${payload.actor_id}`);
|
|
923
|
+
console.log(`Project identity: ${payload.project_id}`);
|
|
924
|
+
console.log(`Workspace name: ${payload.workspace_name}`);
|
|
925
|
+
console.log(`Created pod: ${payload.created_pod ? "yes" : "no"}`);
|
|
926
|
+
console.log(`Created actor: ${payload.created_actor ? "yes" : "no"}`);
|
|
927
|
+
}
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
const context = await resolveRemoteContext(parsed);
|
|
931
|
+
if (!context.mergedConfig) {
|
|
932
|
+
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}`);
|
|
933
|
+
}
|
|
934
|
+
const client = new PaseoRemoteClient(context.mergedConfig);
|
|
935
|
+
if (subcommand === "status") {
|
|
936
|
+
const workspace = await client.getWorkspace();
|
|
937
|
+
assertRemoteIdentity(workspace, context.identity);
|
|
938
|
+
const remoteIdentity = extractWorkspaceIdentity(workspace);
|
|
939
|
+
const payload = {
|
|
940
|
+
status: "ok",
|
|
941
|
+
command: "remote",
|
|
942
|
+
subcommand: "status",
|
|
943
|
+
config_path: context.configPath,
|
|
944
|
+
provider: context.mergedConfig.provider,
|
|
945
|
+
endpoint: context.mergedConfig.endpoint,
|
|
946
|
+
pod_name: context.mergedConfig.pod_name,
|
|
947
|
+
actor_id: context.mergedConfig.actor_id,
|
|
948
|
+
local_identity: context.identity,
|
|
949
|
+
remote_identity: {
|
|
950
|
+
project_id: remoteIdentity.project_id,
|
|
951
|
+
workspace_name: remoteIdentity.workspace_name,
|
|
952
|
+
},
|
|
953
|
+
document_count: remoteIdentity.document_count,
|
|
954
|
+
workspace,
|
|
955
|
+
};
|
|
956
|
+
if (output === "json") {
|
|
957
|
+
printResult(output, payload);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
console.log("Remote workspace reachable");
|
|
961
|
+
console.log(`Config: ${payload.config_path}`);
|
|
962
|
+
console.log(`Endpoint: ${payload.endpoint}`);
|
|
963
|
+
console.log(`Pod: ${payload.pod_name}`);
|
|
964
|
+
console.log(`Actor: ${payload.actor_id}`);
|
|
965
|
+
console.log(`Local identity: ${payload.local_identity.project_id}/${payload.local_identity.workspace_name}`);
|
|
966
|
+
console.log(`Remote identity: ${String(payload.remote_identity.project_id)}/${String(payload.remote_identity.workspace_name)}`);
|
|
967
|
+
console.log(`Remote document count: ${String(payload.document_count ?? "unknown")}`);
|
|
968
|
+
}
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
if (subcommand === "push") {
|
|
972
|
+
const documents = await collectWorkspaceDocuments(parsed.repoRoot);
|
|
973
|
+
const importResult = await client.importWorkspace(documents, true);
|
|
974
|
+
const workspace = await client.getWorkspace();
|
|
975
|
+
assertRemoteIdentity(workspace, context.identity);
|
|
976
|
+
const payload = {
|
|
977
|
+
status: "ok",
|
|
978
|
+
command: "remote",
|
|
979
|
+
subcommand: "push",
|
|
980
|
+
endpoint: context.mergedConfig.endpoint,
|
|
981
|
+
pod_name: context.mergedConfig.pod_name,
|
|
982
|
+
actor_id: context.mergedConfig.actor_id,
|
|
983
|
+
project_id: context.identity.project_id,
|
|
984
|
+
workspace_name: context.identity.workspace_name,
|
|
985
|
+
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,
|
|
990
|
+
remote_document_count: extractWorkspaceIdentity(workspace).document_count,
|
|
991
|
+
};
|
|
992
|
+
if (output === "json") {
|
|
993
|
+
printResult(output, payload);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
console.log("Remote push complete");
|
|
997
|
+
console.log(`Endpoint: ${payload.endpoint}`);
|
|
998
|
+
console.log(`Pod: ${payload.pod_name}`);
|
|
999
|
+
console.log(`Actor: ${payload.actor_id}`);
|
|
1000
|
+
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")}`);
|
|
1005
|
+
console.log(`Remote document count: ${String(payload.remote_document_count ?? "unknown")}`);
|
|
1006
|
+
}
|
|
1007
|
+
return 0;
|
|
1008
|
+
}
|
|
1009
|
+
if (subcommand === "ls") {
|
|
1010
|
+
const snapshot = await client.getSnapshot(parsed.prefix);
|
|
1011
|
+
const paths = (snapshot.documents ?? [])
|
|
1012
|
+
.map((document) => (typeof document.path === "string" ? document.path : ""))
|
|
1013
|
+
.filter(Boolean)
|
|
1014
|
+
.sort();
|
|
1015
|
+
const payload = {
|
|
1016
|
+
status: "ok",
|
|
1017
|
+
command: "remote",
|
|
1018
|
+
subcommand: "ls",
|
|
1019
|
+
endpoint: context.mergedConfig.endpoint,
|
|
1020
|
+
pod_name: context.mergedConfig.pod_name,
|
|
1021
|
+
actor_id: context.mergedConfig.actor_id,
|
|
1022
|
+
prefix: parsed.prefix ?? null,
|
|
1023
|
+
paths,
|
|
1024
|
+
};
|
|
1025
|
+
if (output === "json") {
|
|
1026
|
+
printResult(output, payload);
|
|
1027
|
+
}
|
|
1028
|
+
else if (paths.length === 0) {
|
|
1029
|
+
console.log("(none)");
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
for (const entry of paths) {
|
|
1033
|
+
console.log(entry);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
if (subcommand === "cat") {
|
|
1039
|
+
const [documentPath] = getRemotePositionalArgs(remoteArgs);
|
|
1040
|
+
if (!documentPath) {
|
|
1041
|
+
throw new Error("reffy remote cat requires a document path");
|
|
1042
|
+
}
|
|
1043
|
+
if (!documentPath.startsWith(".reffy/")) {
|
|
1044
|
+
throw new Error("reffy remote cat requires a canonical path rooted at .reffy/");
|
|
1045
|
+
}
|
|
1046
|
+
const document = await client.getDocument(documentPath);
|
|
1047
|
+
const payload = {
|
|
1048
|
+
status: "ok",
|
|
1049
|
+
command: "remote",
|
|
1050
|
+
subcommand: "cat",
|
|
1051
|
+
endpoint: context.mergedConfig.endpoint,
|
|
1052
|
+
pod_name: context.mergedConfig.pod_name,
|
|
1053
|
+
actor_id: context.mergedConfig.actor_id,
|
|
1054
|
+
document: document.document ?? null,
|
|
1055
|
+
};
|
|
1056
|
+
if (output === "json") {
|
|
1057
|
+
printResult(output, payload);
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
const content = typeof document.document?.content === "string" ? document.document.content : "";
|
|
1061
|
+
process.stdout.write(content);
|
|
1062
|
+
if (!content.endsWith("\n")) {
|
|
1063
|
+
process.stdout.write("\n");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1068
|
+
console.error(`Unknown remote subcommand: ${subcommand}`);
|
|
1069
|
+
console.error(remoteUsage());
|
|
1070
|
+
return 1;
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
if (output === "json") {
|
|
1075
|
+
printResult(output, {
|
|
1076
|
+
status: "error",
|
|
1077
|
+
command: "remote",
|
|
1078
|
+
subcommand,
|
|
1079
|
+
error: message,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
console.error(message);
|
|
1084
|
+
}
|
|
1085
|
+
return 1;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
716
1088
|
if (command === "plan") {
|
|
717
1089
|
const [subcommand, ...planArgs] = rest;
|
|
718
1090
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
package/dist/env.d.ts
ADDED
package/dist/env.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function stripQuotes(value) {
|
|
4
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
5
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
6
|
+
return value.slice(1, -1);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function parseDotEnv(content) {
|
|
11
|
+
const parsed = {};
|
|
12
|
+
for (const line of content.split(/\r?\n/)) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
15
|
+
continue;
|
|
16
|
+
const normalized = trimmed.startsWith("export ")
|
|
17
|
+
? trimmed.slice("export ".length).trim()
|
|
18
|
+
: trimmed;
|
|
19
|
+
const equalsIndex = normalized.indexOf("=");
|
|
20
|
+
if (equalsIndex <= 0)
|
|
21
|
+
continue;
|
|
22
|
+
const key = normalized.slice(0, equalsIndex).trim();
|
|
23
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
24
|
+
continue;
|
|
25
|
+
const rawValue = normalized.slice(equalsIndex + 1).trim();
|
|
26
|
+
parsed[key] = stripQuotes(rawValue);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
export async function loadDotEnvIfPresent(repoRoot, envFile = ".env") {
|
|
31
|
+
const envPath = path.isAbsolute(envFile) ? envFile : path.join(repoRoot, envFile);
|
|
32
|
+
const text = await fs.readFile(envPath, "utf8").catch(() => null);
|
|
33
|
+
if (text === null) {
|
|
34
|
+
return { loaded: false, path: envPath };
|
|
35
|
+
}
|
|
36
|
+
const parsed = parseDotEnv(text);
|
|
37
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
38
|
+
if (process.env[key] === undefined) {
|
|
39
|
+
process.env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { loaded: true, path: envPath };
|
|
43
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,6 @@ export { ReferencesStore } from "./storage.js";
|
|
|
2
2
|
export { MANIFEST_VERSION, allowedKindExtensions, validateManifest } from "./manifest.js";
|
|
3
3
|
export { runDoctor } from "./doctor.js";
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
|
+
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, mergeRemoteConfig, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
5
7
|
export { listSpecs, showSpec } from "./spec-runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { ReferencesStore } from "./storage.js";
|
|
|
2
2
|
export { MANIFEST_VERSION, allowedKindExtensions, validateManifest } from "./manifest.js";
|
|
3
3
|
export { runDoctor } from "./doctor.js";
|
|
4
4
|
export { createPlanScaffold } from "./plan.js";
|
|
5
|
+
export { loadDotEnvIfPresent, parseDotEnv } from "./env.js";
|
|
6
|
+
export { collectWorkspaceDocuments, ensureRemoteInit, extractWorkspaceIdentity, mergeRemoteConfig, PaseoRemoteClient, readRemoteConfig, resolveRemoteConfigPath, toCanonicalRemotePath, writeRemoteConfig, } from "./remote.js";
|
|
5
7
|
export { listSpecs, showSpec } from "./spec-runtime.js";
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RemoteDocument, RemoteImportResult, RemoteLinkConfig, RemoteWorkspaceSummary } from "./types.js";
|
|
2
|
+
export declare const REMOTE_CONFIG_VERSION = 1;
|
|
3
|
+
export declare const REMOTE_PROVIDER = "paseo";
|
|
4
|
+
export interface WorkspaceIdentity {
|
|
5
|
+
project_id: string;
|
|
6
|
+
workspace_name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EnsureRemoteInitOptions {
|
|
9
|
+
endpoint: string;
|
|
10
|
+
podName?: string;
|
|
11
|
+
actorId?: string;
|
|
12
|
+
provision: boolean;
|
|
13
|
+
identity: WorkspaceIdentity;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureRemoteInitResult {
|
|
16
|
+
config: RemoteLinkConfig;
|
|
17
|
+
created_pod: boolean;
|
|
18
|
+
created_actor: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveRemoteConfigPath(repoRoot: string): string;
|
|
21
|
+
export declare function readRemoteConfig(repoRoot: string): Promise<RemoteLinkConfig | null>;
|
|
22
|
+
export declare function writeRemoteConfig(repoRoot: string, config: RemoteLinkConfig): Promise<string>;
|
|
23
|
+
export declare function mergeRemoteConfig(stored: RemoteLinkConfig | null, overrides: Partial<Pick<RemoteLinkConfig, "endpoint" | "pod_name" | "actor_id">>): RemoteLinkConfig | null;
|
|
24
|
+
export declare class PaseoRemoteClient {
|
|
25
|
+
private readonly config;
|
|
26
|
+
constructor(config: Pick<RemoteLinkConfig, "endpoint" | "pod_name" | "actor_id">);
|
|
27
|
+
createPod(): Promise<string>;
|
|
28
|
+
createActor(identity: WorkspaceIdentity): Promise<string>;
|
|
29
|
+
getWorkspace(): Promise<RemoteWorkspaceSummary>;
|
|
30
|
+
importWorkspace(documents: RemoteDocument[], replaceMissing?: boolean): Promise<RemoteImportResult>;
|
|
31
|
+
getSnapshot(prefix?: string): Promise<{
|
|
32
|
+
documents?: Array<{
|
|
33
|
+
path?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
getDocument(documentPath: string): Promise<{
|
|
37
|
+
document?: {
|
|
38
|
+
path?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
content_type?: string;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export declare function ensureRemoteInit(repoRoot: string, options: EnsureRemoteInitOptions): Promise<EnsureRemoteInitResult>;
|
|
45
|
+
export declare function requireWorkspaceIdentity(identity: {
|
|
46
|
+
project_id?: string;
|
|
47
|
+
workspace_name?: string;
|
|
48
|
+
}): WorkspaceIdentity;
|
|
49
|
+
export declare function extractWorkspaceIdentity(summary: RemoteWorkspaceSummary): {
|
|
50
|
+
project_id?: string | null;
|
|
51
|
+
workspace_name?: string | null;
|
|
52
|
+
document_count?: number | null;
|
|
53
|
+
};
|
|
54
|
+
export declare function assertRemoteIdentity(summary: RemoteWorkspaceSummary, identity: WorkspaceIdentity): void;
|
|
55
|
+
export declare function toCanonicalRemotePath(refsDir: string, filePath: string): string;
|
|
56
|
+
export declare function collectWorkspaceDocuments(repoRoot: string): Promise<RemoteDocument[]>;
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRefsDir } from "./refs-paths.js";
|
|
4
|
+
export const REMOTE_CONFIG_VERSION = 1;
|
|
5
|
+
export const REMOTE_PROVIDER = "paseo";
|
|
6
|
+
const REMOTE_STATE_DIR = path.join("state");
|
|
7
|
+
const REMOTE_CONFIG_FILE = "remote.json";
|
|
8
|
+
function isObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
13
|
+
}
|
|
14
|
+
function normalizeSeparators(value) {
|
|
15
|
+
return value.split(path.sep).join("/");
|
|
16
|
+
}
|
|
17
|
+
export function resolveRemoteConfigPath(repoRoot) {
|
|
18
|
+
return path.join(resolveRefsDir(repoRoot), REMOTE_STATE_DIR, REMOTE_CONFIG_FILE);
|
|
19
|
+
}
|
|
20
|
+
export async function readRemoteConfig(repoRoot) {
|
|
21
|
+
const configPath = resolveRemoteConfigPath(repoRoot);
|
|
22
|
+
const raw = await fs.readFile(configPath, "utf8").catch(() => null);
|
|
23
|
+
if (raw === null)
|
|
24
|
+
return null;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Failed to parse remote config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
if (!isObject(parsed)) {
|
|
33
|
+
throw new Error(`Remote config at ${configPath} must be an object`);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.version !== REMOTE_CONFIG_VERSION) {
|
|
36
|
+
throw new Error(`Remote config at ${configPath} must use version ${String(REMOTE_CONFIG_VERSION)}`);
|
|
37
|
+
}
|
|
38
|
+
if (parsed.provider !== REMOTE_PROVIDER) {
|
|
39
|
+
throw new Error(`Remote config at ${configPath} must use provider "${REMOTE_PROVIDER}"`);
|
|
40
|
+
}
|
|
41
|
+
if (!isNonEmptyString(parsed.endpoint)) {
|
|
42
|
+
throw new Error(`Remote config at ${configPath} must define endpoint`);
|
|
43
|
+
}
|
|
44
|
+
if (!isNonEmptyString(parsed.pod_name)) {
|
|
45
|
+
throw new Error(`Remote config at ${configPath} must define pod_name`);
|
|
46
|
+
}
|
|
47
|
+
if (!isNonEmptyString(parsed.actor_id)) {
|
|
48
|
+
throw new Error(`Remote config at ${configPath} must define actor_id`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
version: REMOTE_CONFIG_VERSION,
|
|
52
|
+
provider: REMOTE_PROVIDER,
|
|
53
|
+
endpoint: parsed.endpoint.trim(),
|
|
54
|
+
pod_name: parsed.pod_name.trim(),
|
|
55
|
+
actor_id: parsed.actor_id.trim(),
|
|
56
|
+
last_imported_at: isNonEmptyString(parsed.last_imported_at) ? parsed.last_imported_at.trim() : undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function writeRemoteConfig(repoRoot, config) {
|
|
60
|
+
const configPath = resolveRemoteConfigPath(repoRoot);
|
|
61
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
62
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
63
|
+
return configPath;
|
|
64
|
+
}
|
|
65
|
+
export function mergeRemoteConfig(stored, overrides) {
|
|
66
|
+
if (!stored && !overrides.endpoint && !overrides.pod_name && !overrides.actor_id) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const endpoint = overrides.endpoint ?? stored?.endpoint;
|
|
70
|
+
const pod_name = overrides.pod_name ?? stored?.pod_name;
|
|
71
|
+
const actor_id = overrides.actor_id ?? stored?.actor_id;
|
|
72
|
+
if (!endpoint || !pod_name || !actor_id) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
version: REMOTE_CONFIG_VERSION,
|
|
77
|
+
provider: REMOTE_PROVIDER,
|
|
78
|
+
endpoint,
|
|
79
|
+
pod_name,
|
|
80
|
+
actor_id,
|
|
81
|
+
last_imported_at: stored?.last_imported_at,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function http(url, init = {}) {
|
|
85
|
+
const headers = new Headers(init.headers ?? {});
|
|
86
|
+
if (!headers.has("Content-Type") && init.body) {
|
|
87
|
+
headers.set("Content-Type", "application/json");
|
|
88
|
+
}
|
|
89
|
+
const response = await fetch(url, { ...init, headers });
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const text = await response.text().catch(() => "");
|
|
92
|
+
throw new Error(`${response.status} ${response.statusText} - ${text}`);
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
}
|
|
96
|
+
async function httpJson(url, init = {}) {
|
|
97
|
+
const response = await http(url, init);
|
|
98
|
+
return (await response.json());
|
|
99
|
+
}
|
|
100
|
+
function actorBase(config) {
|
|
101
|
+
return `${config.endpoint}/pods/${config.pod_name}/actors/${config.actor_id}`;
|
|
102
|
+
}
|
|
103
|
+
export class PaseoRemoteClient {
|
|
104
|
+
config;
|
|
105
|
+
constructor(config) {
|
|
106
|
+
this.config = config;
|
|
107
|
+
}
|
|
108
|
+
async createPod() {
|
|
109
|
+
const result = await httpJson(`${this.config.endpoint}/pods`, { method: "POST" });
|
|
110
|
+
if (!isNonEmptyString(result.podName)) {
|
|
111
|
+
throw new Error("Paseo create pod response missing podName");
|
|
112
|
+
}
|
|
113
|
+
return result.podName.trim();
|
|
114
|
+
}
|
|
115
|
+
async createActor(identity) {
|
|
116
|
+
const result = await httpJson(`${this.config.endpoint}/pods/${this.config.pod_name}/actors`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
config: {
|
|
120
|
+
actorType: "reffyRemoteBackend",
|
|
121
|
+
version: "v1",
|
|
122
|
+
schema: { type: "object" },
|
|
123
|
+
params: {
|
|
124
|
+
project_id: identity.project_id,
|
|
125
|
+
workspace_name: identity.workspace_name,
|
|
126
|
+
default_lock_ttl_seconds: 120,
|
|
127
|
+
max_snapshot_documents: 1000,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
if (!isNonEmptyString(result.actorId)) {
|
|
133
|
+
throw new Error("Paseo create actor response missing actorId");
|
|
134
|
+
}
|
|
135
|
+
return result.actorId.trim();
|
|
136
|
+
}
|
|
137
|
+
async getWorkspace() {
|
|
138
|
+
return await httpJson(`${actorBase(this.config)}/workspace`);
|
|
139
|
+
}
|
|
140
|
+
async importWorkspace(documents, replaceMissing = true) {
|
|
141
|
+
return await httpJson(`${actorBase(this.config)}/workspace/import`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
documents,
|
|
145
|
+
replace_missing: replaceMissing,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async getSnapshot(prefix) {
|
|
150
|
+
const query = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
|
|
151
|
+
return await httpJson(`${actorBase(this.config)}/workspace/snapshot${query}`);
|
|
152
|
+
}
|
|
153
|
+
async getDocument(documentPath) {
|
|
154
|
+
return await httpJson(`${actorBase(this.config)}/workspace/documents?path=${encodeURIComponent(documentPath)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function ensureRemoteInit(repoRoot, options) {
|
|
158
|
+
let created_pod = false;
|
|
159
|
+
let created_actor = false;
|
|
160
|
+
let podName = options.podName?.trim() || "";
|
|
161
|
+
let actorId = options.actorId?.trim() || "";
|
|
162
|
+
if (!options.endpoint.trim()) {
|
|
163
|
+
throw new Error("Remote init requires an endpoint");
|
|
164
|
+
}
|
|
165
|
+
if (!podName && !options.provision) {
|
|
166
|
+
throw new Error("Remote init requires --pod-name unless --provision is set");
|
|
167
|
+
}
|
|
168
|
+
const baseClient = new PaseoRemoteClient({
|
|
169
|
+
endpoint: options.endpoint.trim(),
|
|
170
|
+
pod_name: podName || "pending-pod",
|
|
171
|
+
actor_id: actorId || "pending-actor",
|
|
172
|
+
});
|
|
173
|
+
if (!podName) {
|
|
174
|
+
podName = await baseClient.createPod();
|
|
175
|
+
created_pod = true;
|
|
176
|
+
}
|
|
177
|
+
if (!actorId) {
|
|
178
|
+
if (!options.provision) {
|
|
179
|
+
throw new Error("Remote init requires --actor-id unless --provision is set");
|
|
180
|
+
}
|
|
181
|
+
const client = new PaseoRemoteClient({
|
|
182
|
+
endpoint: options.endpoint.trim(),
|
|
183
|
+
pod_name: podName,
|
|
184
|
+
actor_id: "pending-actor",
|
|
185
|
+
});
|
|
186
|
+
actorId = await client.createActor(options.identity);
|
|
187
|
+
created_actor = true;
|
|
188
|
+
}
|
|
189
|
+
const config = {
|
|
190
|
+
version: REMOTE_CONFIG_VERSION,
|
|
191
|
+
provider: REMOTE_PROVIDER,
|
|
192
|
+
endpoint: options.endpoint.trim(),
|
|
193
|
+
pod_name: podName,
|
|
194
|
+
actor_id: actorId,
|
|
195
|
+
};
|
|
196
|
+
await writeRemoteConfig(repoRoot, config);
|
|
197
|
+
return { config, created_pod, created_actor };
|
|
198
|
+
}
|
|
199
|
+
export function requireWorkspaceIdentity(identity) {
|
|
200
|
+
const project_id = identity.project_id?.trim();
|
|
201
|
+
const workspace_name = identity.workspace_name?.trim();
|
|
202
|
+
if (!project_id || !workspace_name) {
|
|
203
|
+
throw new Error("Remote sync requires .reffy/manifest.json to define project_id and workspace_name");
|
|
204
|
+
}
|
|
205
|
+
return { project_id, workspace_name };
|
|
206
|
+
}
|
|
207
|
+
export function extractWorkspaceIdentity(summary) {
|
|
208
|
+
const workspace = isObject(summary.workspace) ? summary.workspace : {};
|
|
209
|
+
const stats = isObject(summary.stats) ? summary.stats : {};
|
|
210
|
+
return {
|
|
211
|
+
project_id: typeof workspace.project_id === "string" ? workspace.project_id : null,
|
|
212
|
+
workspace_name: typeof workspace.workspace_name === "string" ? workspace.workspace_name : null,
|
|
213
|
+
document_count: typeof stats.document_count === "number" ? stats.document_count : null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function assertRemoteIdentity(summary, identity) {
|
|
217
|
+
const remote = extractWorkspaceIdentity(summary);
|
|
218
|
+
if (remote.project_id !== identity.project_id || remote.workspace_name !== identity.workspace_name) {
|
|
219
|
+
throw new Error(`Remote identity mismatch: local=${identity.project_id}/${identity.workspace_name} remote=${String(remote.project_id)}/${String(remote.workspace_name)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function collectFiles(dirPath) {
|
|
223
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
224
|
+
const files = [];
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
if (entry.name === "state")
|
|
229
|
+
continue;
|
|
230
|
+
files.push(...(await collectFiles(fullPath)));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (entry.isFile()) {
|
|
234
|
+
files.push(fullPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return files;
|
|
238
|
+
}
|
|
239
|
+
function inferContentType(filePath) {
|
|
240
|
+
if (filePath.endsWith(".json"))
|
|
241
|
+
return "application/json";
|
|
242
|
+
if (filePath.endsWith(".md"))
|
|
243
|
+
return "text/markdown";
|
|
244
|
+
return "text/plain";
|
|
245
|
+
}
|
|
246
|
+
export function toCanonicalRemotePath(refsDir, filePath) {
|
|
247
|
+
const relative = normalizeSeparators(path.relative(refsDir, filePath));
|
|
248
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
249
|
+
throw new Error(`Cannot convert ${filePath} into a canonical .reffy path`);
|
|
250
|
+
}
|
|
251
|
+
return `.reffy/${relative}`;
|
|
252
|
+
}
|
|
253
|
+
export async function collectWorkspaceDocuments(repoRoot) {
|
|
254
|
+
const refsDir = resolveRefsDir(repoRoot);
|
|
255
|
+
const stat = await fs.stat(refsDir).catch(() => null);
|
|
256
|
+
if (!stat || !stat.isDirectory()) {
|
|
257
|
+
throw new Error(`Reffy directory not found: ${refsDir}`);
|
|
258
|
+
}
|
|
259
|
+
const files = await collectFiles(refsDir);
|
|
260
|
+
const documents = [];
|
|
261
|
+
for (const fullPath of files.sort()) {
|
|
262
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
263
|
+
const remotePath = toCanonicalRemotePath(refsDir, fullPath);
|
|
264
|
+
documents.push({
|
|
265
|
+
path: remotePath,
|
|
266
|
+
content,
|
|
267
|
+
content_type: inferContentType(remotePath),
|
|
268
|
+
metadata: {
|
|
269
|
+
source: "local-reffy-import",
|
|
270
|
+
size_bytes: Buffer.byteLength(content, "utf8"),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return documents;
|
|
275
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -20,3 +20,27 @@ export interface Manifest {
|
|
|
20
20
|
workspace_name?: string;
|
|
21
21
|
artifacts: Artifact[];
|
|
22
22
|
}
|
|
23
|
+
export interface RemoteLinkConfig {
|
|
24
|
+
version: number;
|
|
25
|
+
provider: "paseo";
|
|
26
|
+
endpoint: string;
|
|
27
|
+
pod_name: string;
|
|
28
|
+
actor_id: string;
|
|
29
|
+
last_imported_at?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface RemoteDocument {
|
|
32
|
+
path: string;
|
|
33
|
+
content: string;
|
|
34
|
+
content_type: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export interface RemoteImportResult {
|
|
38
|
+
imported?: number;
|
|
39
|
+
created?: number;
|
|
40
|
+
updated?: number;
|
|
41
|
+
deleted?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface RemoteWorkspaceSummary {
|
|
44
|
+
workspace?: Record<string, unknown>;
|
|
45
|
+
stats?: Record<string, unknown>;
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reffy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Planning system for AI assisted development.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reffy",
|
|
@@ -35,6 +35,7 @@
|
|
|
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",
|
|
38
39
|
"test": "npm run build && vitest run --coverage",
|
|
39
40
|
"test:watch": "vitest"
|
|
40
41
|
},
|