@h-rig/cli 0.0.6-alpha.44 → 0.0.6-alpha.46

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/dist/bin/rig.js CHANGED
@@ -2888,7 +2888,8 @@ function readRepoConnection(projectRoot) {
2888
2888
  return {
2889
2889
  selected,
2890
2890
  project: typeof record.project === "string" ? record.project : undefined,
2891
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
2891
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
2892
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
2892
2893
  };
2893
2894
  }
2894
2895
  function writeRepoConnection(projectRoot, state) {
@@ -2899,13 +2900,19 @@ function resolveSelectedConnection(projectRoot, options = {}) {
2899
2900
  if (!repo)
2900
2901
  return null;
2901
2902
  if (repo.selected === "local")
2902
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
2903
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
2903
2904
  const global = readGlobalConnections(options);
2904
2905
  const connection = global.connections[repo.selected];
2905
2906
  if (!connection) {
2906
2907
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
2907
2908
  }
2908
- return { alias: repo.selected, connection };
2909
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
2910
+ }
2911
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
2912
+ const repo = readRepoConnection(projectRoot);
2913
+ if (!repo)
2914
+ return;
2915
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
2909
2916
  }
2910
2917
 
2911
2918
  // packages/cli/src/commands/_server-client.ts
@@ -2946,17 +2953,21 @@ async function ensureServerForCli(projectRoot) {
2946
2953
  try {
2947
2954
  const selected = resolveSelectedConnection(projectRoot);
2948
2955
  if (selected?.connection.kind === "remote") {
2956
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
2957
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
2949
2958
  return {
2950
2959
  baseUrl: selected.connection.baseUrl,
2951
- authToken: readGitHubBearerTokenForRemote(projectRoot),
2952
- connectionKind: "remote"
2960
+ authToken,
2961
+ connectionKind: "remote",
2962
+ serverProjectRoot
2953
2963
  };
2954
2964
  }
2955
2965
  const connection = await ensureLocalRigServerConnection(projectRoot);
2956
2966
  return {
2957
2967
  baseUrl: connection.baseUrl,
2958
2968
  authToken: connection.authToken,
2959
- connectionKind: "local"
2969
+ connectionKind: "local",
2970
+ serverProjectRoot: resolve11(projectRoot)
2960
2971
  };
2961
2972
  } catch (error) {
2962
2973
  if (error instanceof Error) {
@@ -2965,6 +2976,28 @@ async function ensureServerForCli(projectRoot) {
2965
2976
  throw error;
2966
2977
  }
2967
2978
  }
2979
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
2980
+ const repo = readRepoConnection(projectRoot);
2981
+ const slug = repo?.project?.trim();
2982
+ if (!slug)
2983
+ return null;
2984
+ try {
2985
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
2986
+ headers: mergeHeaders(undefined, authToken)
2987
+ });
2988
+ if (!response.ok)
2989
+ return null;
2990
+ const payload = await response.json();
2991
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
2992
+ const checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
2993
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
2994
+ if (path)
2995
+ writeRepoServerProjectRoot(projectRoot, path);
2996
+ return path;
2997
+ } catch {
2998
+ return null;
2999
+ }
3000
+ }
2968
3001
  function appendTaskFilterParams(url, filters) {
2969
3002
  if (filters.assignee)
2970
3003
  url.searchParams.set("assignee", filters.assignee);
@@ -2999,9 +3032,12 @@ function diagnosticMessage(payload) {
2999
3032
  }
3000
3033
  async function requestServerJson(context, pathname, init = {}) {
3001
3034
  const server = await ensureServerForCli(context.projectRoot);
3035
+ const headers = mergeHeaders(init.headers, server.authToken);
3036
+ if (server.serverProjectRoot)
3037
+ headers.set("x-rig-project-root", server.serverProjectRoot);
3002
3038
  const response = await fetch(`${server.baseUrl}${pathname}`, {
3003
3039
  ...init,
3004
- headers: mergeHeaders(init.headers, server.authToken)
3040
+ headers
3005
3041
  });
3006
3042
  const text2 = await response.text();
3007
3043
  const payload = text2.trim().length > 0 ? (() => {
@@ -3108,22 +3144,12 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
3108
3144
  if (!switched) {
3109
3145
  throw new CliError2(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
3110
3146
  }
3111
- while (Date.now() < deadline) {
3112
- try {
3113
- const status = await requestServerJson(context, "/api/server/status");
3114
- if (status && typeof status === "object" && !Array.isArray(status)) {
3115
- const record = status;
3116
- if (record.projectRoot === projectRoot) {
3117
- return { ok: true, switched, status: record };
3118
- }
3119
- lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
3120
- }
3121
- } catch (error) {
3122
- lastError = error;
3123
- }
3124
- await sleep(pollMs);
3147
+ const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
3148
+ if (record.ok === true) {
3149
+ writeRepoServerProjectRoot(context.projectRoot, projectRoot);
3150
+ return { ok: true, switched: record };
3125
3151
  }
3126
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
3152
+ throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
3127
3153
  }
3128
3154
  async function listRunsViaServer(context, options = {}) {
3129
3155
  const url = new URL("http://rig.local/api/runs");
@@ -3255,6 +3281,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
3255
3281
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
3256
3282
  if (server.authToken)
3257
3283
  url.searchParams.set("token", server.authToken);
3284
+ if (server.serverProjectRoot)
3285
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
3258
3286
  return url.toString();
3259
3287
  }
3260
3288
  async function submitTaskRunViaServer(context, input) {
@@ -90,7 +90,8 @@ function readRepoConnection(projectRoot) {
90
90
  return {
91
91
  selected,
92
92
  project: typeof record.project === "string" ? record.project : undefined,
93
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
93
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
94
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
94
95
  };
95
96
  }
96
97
  function writeRepoConnection(projectRoot, state) {
@@ -101,15 +102,22 @@ function resolveSelectedConnection(projectRoot, options = {}) {
101
102
  if (!repo)
102
103
  return null;
103
104
  if (repo.selected === "local")
104
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
105
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
105
106
  const global = readGlobalConnections(options);
106
107
  const connection = global.connections[repo.selected];
107
108
  if (!connection) {
108
109
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
109
110
  }
110
- return { alias: repo.selected, connection };
111
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
112
+ }
113
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
114
+ const repo = readRepoConnection(projectRoot);
115
+ if (!repo)
116
+ return;
117
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
111
118
  }
112
119
  export {
120
+ writeRepoServerProjectRoot,
113
121
  writeRepoConnection,
114
122
  writeGlobalConnections,
115
123
  upsertGlobalConnection,
@@ -40,6 +40,11 @@ function readJsonFile(path) {
40
40
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
41
41
  }
42
42
  }
43
+ function writeJsonFile(path, value) {
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
46
+ `, "utf8");
47
+ }
43
48
  function normalizeConnection(value) {
44
49
  if (!value || typeof value !== "object" || Array.isArray(value))
45
50
  return null;
@@ -80,21 +85,31 @@ function readRepoConnection(projectRoot) {
80
85
  return {
81
86
  selected,
82
87
  project: typeof record.project === "string" ? record.project : undefined,
83
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
88
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
89
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
84
90
  };
85
91
  }
92
+ function writeRepoConnection(projectRoot, state) {
93
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
94
+ }
86
95
  function resolveSelectedConnection(projectRoot, options = {}) {
87
96
  const repo = readRepoConnection(projectRoot);
88
97
  if (!repo)
89
98
  return null;
90
99
  if (repo.selected === "local")
91
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
100
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
92
101
  const global = readGlobalConnections(options);
93
102
  const connection = global.connections[repo.selected];
94
103
  if (!connection) {
95
104
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
96
105
  }
97
- return { alias: repo.selected, connection };
106
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
107
+ }
108
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
109
+ const repo = readRepoConnection(projectRoot);
110
+ if (!repo)
111
+ return;
112
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
98
113
  }
99
114
 
100
115
  // packages/cli/src/commands/_server-client.ts
@@ -130,17 +145,21 @@ async function ensureServerForCli(projectRoot) {
130
145
  try {
131
146
  const selected = resolveSelectedConnection(projectRoot);
132
147
  if (selected?.connection.kind === "remote") {
148
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
149
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
133
150
  return {
134
151
  baseUrl: selected.connection.baseUrl,
135
- authToken: readGitHubBearerTokenForRemote(projectRoot),
136
- connectionKind: "remote"
152
+ authToken,
153
+ connectionKind: "remote",
154
+ serverProjectRoot
137
155
  };
138
156
  }
139
157
  const connection = await ensureLocalRigServerConnection(projectRoot);
140
158
  return {
141
159
  baseUrl: connection.baseUrl,
142
160
  authToken: connection.authToken,
143
- connectionKind: "local"
161
+ connectionKind: "local",
162
+ serverProjectRoot: resolve2(projectRoot)
144
163
  };
145
164
  } catch (error) {
146
165
  if (error instanceof Error) {
@@ -149,6 +168,28 @@ async function ensureServerForCli(projectRoot) {
149
168
  throw error;
150
169
  }
151
170
  }
171
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
172
+ const repo = readRepoConnection(projectRoot);
173
+ const slug = repo?.project?.trim();
174
+ if (!slug)
175
+ return null;
176
+ try {
177
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
178
+ headers: mergeHeaders(undefined, authToken)
179
+ });
180
+ if (!response.ok)
181
+ return null;
182
+ const payload = await response.json();
183
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
184
+ const checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
185
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
186
+ if (path)
187
+ writeRepoServerProjectRoot(projectRoot, path);
188
+ return path;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
152
193
  function mergeHeaders(headers, authToken) {
153
194
  const merged = new Headers(headers);
154
195
  if (authToken) {
@@ -173,9 +214,12 @@ function diagnosticMessage(payload) {
173
214
  }
174
215
  async function requestServerJson(context, pathname, init = {}) {
175
216
  const server = await ensureServerForCli(context.projectRoot);
217
+ const headers = mergeHeaders(init.headers, server.authToken);
218
+ if (server.serverProjectRoot)
219
+ headers.set("x-rig-project-root", server.serverProjectRoot);
176
220
  const response = await fetch(`${server.baseUrl}${pathname}`, {
177
221
  ...init,
178
- headers: mergeHeaders(init.headers, server.authToken)
222
+ headers
179
223
  });
180
224
  const text = await response.text();
181
225
  const payload = text.trim().length > 0 ? (() => {
@@ -38,6 +38,11 @@ function readJsonFile(path) {
38
38
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
39
39
  }
40
40
  }
41
+ function writeJsonFile(path, value) {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
44
+ `, "utf8");
45
+ }
41
46
  function normalizeConnection(value) {
42
47
  if (!value || typeof value !== "object" || Array.isArray(value))
43
48
  return null;
@@ -78,21 +83,31 @@ function readRepoConnection(projectRoot) {
78
83
  return {
79
84
  selected,
80
85
  project: typeof record.project === "string" ? record.project : undefined,
81
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
86
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
87
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
82
88
  };
83
89
  }
90
+ function writeRepoConnection(projectRoot, state) {
91
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
92
+ }
84
93
  function resolveSelectedConnection(projectRoot, options = {}) {
85
94
  const repo = readRepoConnection(projectRoot);
86
95
  if (!repo)
87
96
  return null;
88
97
  if (repo.selected === "local")
89
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
98
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
90
99
  const global = readGlobalConnections(options);
91
100
  const connection = global.connections[repo.selected];
92
101
  if (!connection) {
93
102
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
94
103
  }
95
- return { alias: repo.selected, connection };
104
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
105
+ }
106
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
107
+ const repo = readRepoConnection(projectRoot);
108
+ if (!repo)
109
+ return;
110
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
96
111
  }
97
112
 
98
113
  // packages/cli/src/commands/_server-client.ts
@@ -125,17 +140,21 @@ async function ensureServerForCli(projectRoot) {
125
140
  try {
126
141
  const selected = resolveSelectedConnection(projectRoot);
127
142
  if (selected?.connection.kind === "remote") {
143
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
144
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
128
145
  return {
129
146
  baseUrl: selected.connection.baseUrl,
130
- authToken: readGitHubBearerTokenForRemote(projectRoot),
131
- connectionKind: "remote"
147
+ authToken,
148
+ connectionKind: "remote",
149
+ serverProjectRoot
132
150
  };
133
151
  }
134
152
  const connection = await ensureLocalRigServerConnection(projectRoot);
135
153
  return {
136
154
  baseUrl: connection.baseUrl,
137
155
  authToken: connection.authToken,
138
- connectionKind: "local"
156
+ connectionKind: "local",
157
+ serverProjectRoot: resolve2(projectRoot)
139
158
  };
140
159
  } catch (error) {
141
160
  if (error instanceof Error) {
@@ -144,6 +163,28 @@ async function ensureServerForCli(projectRoot) {
144
163
  throw error;
145
164
  }
146
165
  }
166
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
167
+ const repo = readRepoConnection(projectRoot);
168
+ const slug = repo?.project?.trim();
169
+ if (!slug)
170
+ return null;
171
+ try {
172
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
173
+ headers: mergeHeaders(undefined, authToken)
174
+ });
175
+ if (!response.ok)
176
+ return null;
177
+ const payload = await response.json();
178
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
179
+ const checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
180
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
181
+ if (path)
182
+ writeRepoServerProjectRoot(projectRoot, path);
183
+ return path;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
147
188
  function mergeHeaders(headers, authToken) {
148
189
  const merged = new Headers(headers);
149
190
  if (authToken) {
@@ -168,9 +209,12 @@ function diagnosticMessage(payload) {
168
209
  }
169
210
  async function requestServerJson(context, pathname, init = {}) {
170
211
  const server = await ensureServerForCli(context.projectRoot);
212
+ const headers = mergeHeaders(init.headers, server.authToken);
213
+ if (server.serverProjectRoot)
214
+ headers.set("x-rig-project-root", server.serverProjectRoot);
171
215
  const response = await fetch(`${server.baseUrl}${pathname}`, {
172
216
  ...init,
173
- headers: mergeHeaders(init.headers, server.authToken)
217
+ headers
174
218
  });
175
219
  const text = await response.text();
176
220
  const payload = text.trim().length > 0 ? (() => {
@@ -287,6 +331,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
287
331
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
288
332
  if (server.authToken)
289
333
  url.searchParams.set("token", server.authToken);
334
+ if (server.serverProjectRoot)
335
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
290
336
  return url.toString();
291
337
  }
292
338
 
@@ -48,6 +48,11 @@ function readJsonFile(path) {
48
48
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
49
49
  }
50
50
  }
51
+ function writeJsonFile(path, value) {
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
54
+ `, "utf8");
55
+ }
51
56
  function normalizeConnection(value) {
52
57
  if (!value || typeof value !== "object" || Array.isArray(value))
53
58
  return null;
@@ -88,21 +93,31 @@ function readRepoConnection(projectRoot) {
88
93
  return {
89
94
  selected,
90
95
  project: typeof record.project === "string" ? record.project : undefined,
91
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
96
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
97
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
92
98
  };
93
99
  }
100
+ function writeRepoConnection(projectRoot, state) {
101
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
102
+ }
94
103
  function resolveSelectedConnection(projectRoot, options = {}) {
95
104
  const repo = readRepoConnection(projectRoot);
96
105
  if (!repo)
97
106
  return null;
98
107
  if (repo.selected === "local")
99
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
108
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
100
109
  const global = readGlobalConnections(options);
101
110
  const connection = global.connections[repo.selected];
102
111
  if (!connection) {
103
112
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
104
113
  }
105
- return { alias: repo.selected, connection };
114
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
115
+ }
116
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
117
+ const repo = readRepoConnection(projectRoot);
118
+ if (!repo)
119
+ return;
120
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
106
121
  }
107
122
 
108
123
  // packages/cli/src/commands/_server-client.ts
@@ -135,17 +150,21 @@ async function ensureServerForCli(projectRoot) {
135
150
  try {
136
151
  const selected = resolveSelectedConnection(projectRoot);
137
152
  if (selected?.connection.kind === "remote") {
153
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
154
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
138
155
  return {
139
156
  baseUrl: selected.connection.baseUrl,
140
- authToken: readGitHubBearerTokenForRemote(projectRoot),
141
- connectionKind: "remote"
157
+ authToken,
158
+ connectionKind: "remote",
159
+ serverProjectRoot
142
160
  };
143
161
  }
144
162
  const connection = await ensureLocalRigServerConnection(projectRoot);
145
163
  return {
146
164
  baseUrl: connection.baseUrl,
147
165
  authToken: connection.authToken,
148
- connectionKind: "local"
166
+ connectionKind: "local",
167
+ serverProjectRoot: resolve2(projectRoot)
149
168
  };
150
169
  } catch (error) {
151
170
  if (error instanceof Error) {
@@ -154,6 +173,28 @@ async function ensureServerForCli(projectRoot) {
154
173
  throw error;
155
174
  }
156
175
  }
176
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
177
+ const repo = readRepoConnection(projectRoot);
178
+ const slug = repo?.project?.trim();
179
+ if (!slug)
180
+ return null;
181
+ try {
182
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
183
+ headers: mergeHeaders(undefined, authToken)
184
+ });
185
+ if (!response.ok)
186
+ return null;
187
+ const payload = await response.json();
188
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
189
+ const checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
190
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
191
+ if (path)
192
+ writeRepoServerProjectRoot(projectRoot, path);
193
+ return path;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
157
198
  function mergeHeaders(headers, authToken) {
158
199
  const merged = new Headers(headers);
159
200
  if (authToken) {
@@ -178,9 +219,12 @@ function diagnosticMessage(payload) {
178
219
  }
179
220
  async function requestServerJson(context, pathname, init = {}) {
180
221
  const server = await ensureServerForCli(context.projectRoot);
222
+ const headers = mergeHeaders(init.headers, server.authToken);
223
+ if (server.serverProjectRoot)
224
+ headers.set("x-rig-project-root", server.serverProjectRoot);
181
225
  const response = await fetch(`${server.baseUrl}${pathname}`, {
182
226
  ...init,
183
- headers: mergeHeaders(init.headers, server.authToken)
227
+ headers
184
228
  });
185
229
  const text = await response.text();
186
230
  const payload = text.trim().length > 0 ? (() => {
@@ -263,6 +307,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
263
307
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
264
308
  if (server.authToken)
265
309
  url.searchParams.set("token", server.authToken);
310
+ if (server.serverProjectRoot)
311
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
266
312
  return url.toString();
267
313
  }
268
314
 
@@ -41,6 +41,11 @@ function readJsonFile(path) {
41
41
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
42
42
  }
43
43
  }
44
+ function writeJsonFile(path, value) {
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
47
+ `, "utf8");
48
+ }
44
49
  function normalizeConnection(value) {
45
50
  if (!value || typeof value !== "object" || Array.isArray(value))
46
51
  return null;
@@ -81,21 +86,31 @@ function readRepoConnection(projectRoot) {
81
86
  return {
82
87
  selected,
83
88
  project: typeof record.project === "string" ? record.project : undefined,
84
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
89
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
90
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
85
91
  };
86
92
  }
93
+ function writeRepoConnection(projectRoot, state) {
94
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
95
+ }
87
96
  function resolveSelectedConnection(projectRoot, options = {}) {
88
97
  const repo = readRepoConnection(projectRoot);
89
98
  if (!repo)
90
99
  return null;
91
100
  if (repo.selected === "local")
92
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
101
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
93
102
  const global = readGlobalConnections(options);
94
103
  const connection = global.connections[repo.selected];
95
104
  if (!connection) {
96
105
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
97
106
  }
98
- return { alias: repo.selected, connection };
107
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
108
+ }
109
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
110
+ const repo = readRepoConnection(projectRoot);
111
+ if (!repo)
112
+ return;
113
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
99
114
  }
100
115
 
101
116
  // packages/cli/src/commands/_server-client.ts
@@ -128,17 +143,21 @@ async function ensureServerForCli(projectRoot) {
128
143
  try {
129
144
  const selected = resolveSelectedConnection(projectRoot);
130
145
  if (selected?.connection.kind === "remote") {
146
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
147
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
131
148
  return {
132
149
  baseUrl: selected.connection.baseUrl,
133
- authToken: readGitHubBearerTokenForRemote(projectRoot),
134
- connectionKind: "remote"
150
+ authToken,
151
+ connectionKind: "remote",
152
+ serverProjectRoot
135
153
  };
136
154
  }
137
155
  const connection = await ensureLocalRigServerConnection(projectRoot);
138
156
  return {
139
157
  baseUrl: connection.baseUrl,
140
158
  authToken: connection.authToken,
141
- connectionKind: "local"
159
+ connectionKind: "local",
160
+ serverProjectRoot: resolve2(projectRoot)
142
161
  };
143
162
  } catch (error) {
144
163
  if (error instanceof Error) {
@@ -147,6 +166,28 @@ async function ensureServerForCli(projectRoot) {
147
166
  throw error;
148
167
  }
149
168
  }
169
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
170
+ const repo = readRepoConnection(projectRoot);
171
+ const slug = repo?.project?.trim();
172
+ if (!slug)
173
+ return null;
174
+ try {
175
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
176
+ headers: mergeHeaders(undefined, authToken)
177
+ });
178
+ if (!response.ok)
179
+ return null;
180
+ const payload = await response.json();
181
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
182
+ const checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
183
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
184
+ if (path)
185
+ writeRepoServerProjectRoot(projectRoot, path);
186
+ return path;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
150
191
  function mergeHeaders(headers, authToken) {
151
192
  const merged = new Headers(headers);
152
193
  if (authToken) {
@@ -171,9 +212,12 @@ function diagnosticMessage(payload) {
171
212
  }
172
213
  async function requestServerJson(context, pathname, init = {}) {
173
214
  const server = await ensureServerForCli(context.projectRoot);
215
+ const headers = mergeHeaders(init.headers, server.authToken);
216
+ if (server.serverProjectRoot)
217
+ headers.set("x-rig-project-root", server.serverProjectRoot);
174
218
  const response = await fetch(`${server.baseUrl}${pathname}`, {
175
219
  ...init,
176
- headers: mergeHeaders(init.headers, server.authToken)
220
+ headers
177
221
  });
178
222
  const text = await response.text();
179
223
  const payload = text.trim().length > 0 ? (() => {
@@ -244,6 +288,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
244
288
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
245
289
  if (server.authToken)
246
290
  url.searchParams.set("token", server.authToken);
291
+ if (server.serverProjectRoot)
292
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
247
293
  return url.toString();
248
294
  }
249
295