@h-rig/cli 0.0.6-alpha.45 → 0.0.6-alpha.47

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.
@@ -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,29 @@ 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 checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
180
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
181
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
182
+ if (path)
183
+ writeRepoServerProjectRoot(projectRoot, path);
184
+ return path;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
147
189
  function mergeHeaders(headers, authToken) {
148
190
  const merged = new Headers(headers);
149
191
  if (authToken) {
@@ -168,9 +210,12 @@ function diagnosticMessage(payload) {
168
210
  }
169
211
  async function requestServerJson(context, pathname, init = {}) {
170
212
  const server = await ensureServerForCli(context.projectRoot);
213
+ const headers = mergeHeaders(init.headers, server.authToken);
214
+ if (server.serverProjectRoot)
215
+ headers.set("x-rig-project-root", server.serverProjectRoot);
171
216
  const response = await fetch(`${server.baseUrl}${pathname}`, {
172
217
  ...init,
173
- headers: mergeHeaders(init.headers, server.authToken)
218
+ headers
174
219
  });
175
220
  const text = await response.text();
176
221
  const payload = text.trim().length > 0 ? (() => {
@@ -249,6 +294,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
249
294
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
250
295
  if (server.authToken)
251
296
  url.searchParams.set("token", server.authToken);
297
+ if (server.serverProjectRoot)
298
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
252
299
  return url.toString();
253
300
  }
254
301
 
@@ -34,6 +34,11 @@ function readJsonFile(path) {
34
34
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
35
35
  }
36
36
  }
37
+ function writeJsonFile(path, value) {
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
40
+ `, "utf8");
41
+ }
37
42
  function normalizeConnection(value) {
38
43
  if (!value || typeof value !== "object" || Array.isArray(value))
39
44
  return null;
@@ -74,21 +79,31 @@ function readRepoConnection(projectRoot) {
74
79
  return {
75
80
  selected,
76
81
  project: typeof record.project === "string" ? record.project : undefined,
77
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
82
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
83
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
78
84
  };
79
85
  }
86
+ function writeRepoConnection(projectRoot, state) {
87
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
88
+ }
80
89
  function resolveSelectedConnection(projectRoot, options = {}) {
81
90
  const repo = readRepoConnection(projectRoot);
82
91
  if (!repo)
83
92
  return null;
84
93
  if (repo.selected === "local")
85
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
94
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
86
95
  const global = readGlobalConnections(options);
87
96
  const connection = global.connections[repo.selected];
88
97
  if (!connection) {
89
98
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
90
99
  }
91
- return { alias: repo.selected, connection };
100
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
101
+ }
102
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
103
+ const repo = readRepoConnection(projectRoot);
104
+ if (!repo)
105
+ return;
106
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
92
107
  }
93
108
 
94
109
  // packages/cli/src/commands/_server-client.ts
@@ -124,17 +139,21 @@ async function ensureServerForCli(projectRoot) {
124
139
  try {
125
140
  const selected = resolveSelectedConnection(projectRoot);
126
141
  if (selected?.connection.kind === "remote") {
142
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
143
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
127
144
  return {
128
145
  baseUrl: selected.connection.baseUrl,
129
- authToken: readGitHubBearerTokenForRemote(projectRoot),
130
- connectionKind: "remote"
146
+ authToken,
147
+ connectionKind: "remote",
148
+ serverProjectRoot
131
149
  };
132
150
  }
133
151
  const connection = await ensureLocalRigServerConnection(projectRoot);
134
152
  return {
135
153
  baseUrl: connection.baseUrl,
136
154
  authToken: connection.authToken,
137
- connectionKind: "local"
155
+ connectionKind: "local",
156
+ serverProjectRoot: resolve2(projectRoot)
138
157
  };
139
158
  } catch (error) {
140
159
  if (error instanceof Error) {
@@ -143,6 +162,29 @@ async function ensureServerForCli(projectRoot) {
143
162
  throw error;
144
163
  }
145
164
  }
165
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
166
+ const repo = readRepoConnection(projectRoot);
167
+ const slug = repo?.project?.trim();
168
+ if (!slug)
169
+ return null;
170
+ try {
171
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
172
+ headers: mergeHeaders(undefined, authToken)
173
+ });
174
+ if (!response.ok)
175
+ return null;
176
+ const payload = await response.json();
177
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
178
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
179
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
180
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
181
+ if (path)
182
+ writeRepoServerProjectRoot(projectRoot, path);
183
+ return path;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
146
188
  function mergeHeaders(headers, authToken) {
147
189
  const merged = new Headers(headers);
148
190
  if (authToken) {
@@ -167,9 +209,12 @@ function diagnosticMessage(payload) {
167
209
  }
168
210
  async function requestServerJson(context, pathname, init = {}) {
169
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);
170
215
  const response = await fetch(`${server.baseUrl}${pathname}`, {
171
216
  ...init,
172
- headers: mergeHeaders(init.headers, server.authToken)
217
+ headers
173
218
  });
174
219
  const text = await response.text();
175
220
  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
@@ -129,17 +144,21 @@ async function ensureServerForCli(projectRoot) {
129
144
  try {
130
145
  const selected = resolveSelectedConnection(projectRoot);
131
146
  if (selected?.connection.kind === "remote") {
147
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
148
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
132
149
  return {
133
150
  baseUrl: selected.connection.baseUrl,
134
- authToken: readGitHubBearerTokenForRemote(projectRoot),
135
- connectionKind: "remote"
151
+ authToken,
152
+ connectionKind: "remote",
153
+ serverProjectRoot
136
154
  };
137
155
  }
138
156
  const connection = await ensureLocalRigServerConnection(projectRoot);
139
157
  return {
140
158
  baseUrl: connection.baseUrl,
141
159
  authToken: connection.authToken,
142
- connectionKind: "local"
160
+ connectionKind: "local",
161
+ serverProjectRoot: resolve2(projectRoot)
143
162
  };
144
163
  } catch (error) {
145
164
  if (error instanceof Error) {
@@ -148,6 +167,29 @@ async function ensureServerForCli(projectRoot) {
148
167
  throw error;
149
168
  }
150
169
  }
170
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
171
+ const repo = readRepoConnection(projectRoot);
172
+ const slug = repo?.project?.trim();
173
+ if (!slug)
174
+ return null;
175
+ try {
176
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
177
+ headers: mergeHeaders(undefined, authToken)
178
+ });
179
+ if (!response.ok)
180
+ return null;
181
+ const payload = await response.json();
182
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
183
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
184
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
185
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
186
+ if (path)
187
+ writeRepoServerProjectRoot(projectRoot, path);
188
+ return path;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
151
193
  function appendTaskFilterParams(url, filters) {
152
194
  if (filters.assignee)
153
195
  url.searchParams.set("assignee", filters.assignee);
@@ -182,9 +224,12 @@ function diagnosticMessage(payload) {
182
224
  }
183
225
  async function requestServerJson(context, pathname, init = {}) {
184
226
  const server = await ensureServerForCli(context.projectRoot);
227
+ const headers = mergeHeaders(init.headers, server.authToken);
228
+ if (server.serverProjectRoot)
229
+ headers.set("x-rig-project-root", server.serverProjectRoot);
185
230
  const response = await fetch(`${server.baseUrl}${pathname}`, {
186
231
  ...init,
187
- headers: mergeHeaders(init.headers, server.authToken)
232
+ headers
188
233
  });
189
234
  const text = await response.text();
190
235
  const payload = text.trim().length > 0 ? (() => {
@@ -291,22 +336,12 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
291
336
  if (!switched) {
292
337
  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);
293
338
  }
294
- while (Date.now() < deadline) {
295
- try {
296
- const status = await requestServerJson(context, "/api/server/status");
297
- if (status && typeof status === "object" && !Array.isArray(status)) {
298
- const record = status;
299
- if (record.projectRoot === projectRoot) {
300
- return { ok: true, switched, status: record };
301
- }
302
- lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
303
- }
304
- } catch (error) {
305
- lastError = error;
306
- }
307
- await sleep(pollMs);
339
+ const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
340
+ if (record.ok === true) {
341
+ writeRepoServerProjectRoot(context.projectRoot, projectRoot);
342
+ return { ok: true, switched: record };
308
343
  }
309
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
344
+ throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
310
345
  }
311
346
  async function listRunsViaServer(context, options = {}) {
312
347
  const url = new URL("http://rig.local/api/runs");
@@ -438,6 +473,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
438
473
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
439
474
  if (server.authToken)
440
475
  url.searchParams.set("token", server.authToken);
476
+ if (server.serverProjectRoot)
477
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
441
478
  return url.toString();
442
479
  }
443
480
  async function submitTaskRunViaServer(context, input) {
@@ -42,6 +42,11 @@ function readJsonFile(path) {
42
42
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
43
43
  }
44
44
  }
45
+ function writeJsonFile(path, value) {
46
+ mkdirSync(dirname(path), { recursive: true });
47
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
48
+ `, "utf8");
49
+ }
45
50
  function normalizeConnection(value) {
46
51
  if (!value || typeof value !== "object" || Array.isArray(value))
47
52
  return null;
@@ -82,21 +87,31 @@ function readRepoConnection(projectRoot) {
82
87
  return {
83
88
  selected,
84
89
  project: typeof record.project === "string" ? record.project : undefined,
85
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
90
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
91
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
86
92
  };
87
93
  }
94
+ function writeRepoConnection(projectRoot, state) {
95
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
96
+ }
88
97
  function resolveSelectedConnection(projectRoot, options = {}) {
89
98
  const repo = readRepoConnection(projectRoot);
90
99
  if (!repo)
91
100
  return null;
92
101
  if (repo.selected === "local")
93
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
102
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
94
103
  const global = readGlobalConnections(options);
95
104
  const connection = global.connections[repo.selected];
96
105
  if (!connection) {
97
106
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
98
107
  }
99
- return { alias: repo.selected, connection };
108
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
109
+ }
110
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
111
+ const repo = readRepoConnection(projectRoot);
112
+ if (!repo)
113
+ return;
114
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
100
115
  }
101
116
 
102
117
  // packages/cli/src/commands/_server-client.ts
@@ -129,17 +144,21 @@ async function ensureServerForCli(projectRoot) {
129
144
  try {
130
145
  const selected = resolveSelectedConnection(projectRoot);
131
146
  if (selected?.connection.kind === "remote") {
147
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
148
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
132
149
  return {
133
150
  baseUrl: selected.connection.baseUrl,
134
- authToken: readGitHubBearerTokenForRemote(projectRoot),
135
- connectionKind: "remote"
151
+ authToken,
152
+ connectionKind: "remote",
153
+ serverProjectRoot
136
154
  };
137
155
  }
138
156
  const connection = await ensureLocalRigServerConnection(projectRoot);
139
157
  return {
140
158
  baseUrl: connection.baseUrl,
141
159
  authToken: connection.authToken,
142
- connectionKind: "local"
160
+ connectionKind: "local",
161
+ serverProjectRoot: resolve2(projectRoot)
143
162
  };
144
163
  } catch (error) {
145
164
  if (error instanceof Error) {
@@ -148,6 +167,29 @@ async function ensureServerForCli(projectRoot) {
148
167
  throw error;
149
168
  }
150
169
  }
170
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
171
+ const repo = readRepoConnection(projectRoot);
172
+ const slug = repo?.project?.trim();
173
+ if (!slug)
174
+ return null;
175
+ try {
176
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
177
+ headers: mergeHeaders(undefined, authToken)
178
+ });
179
+ if (!response.ok)
180
+ return null;
181
+ const payload = await response.json();
182
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
183
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
184
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
185
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
186
+ if (path)
187
+ writeRepoServerProjectRoot(projectRoot, path);
188
+ return path;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
151
193
  function mergeHeaders(headers, authToken) {
152
194
  const merged = new Headers(headers);
153
195
  if (authToken) {
@@ -172,9 +214,12 @@ function diagnosticMessage(payload) {
172
214
  }
173
215
  async function requestServerJson(context, pathname, init = {}) {
174
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);
175
220
  const response = await fetch(`${server.baseUrl}${pathname}`, {
176
221
  ...init,
177
- headers: mergeHeaders(init.headers, server.authToken)
222
+ headers
178
223
  });
179
224
  const text = await response.text();
180
225
  const payload = text.trim().length > 0 ? (() => {
@@ -97,7 +97,8 @@ function readRepoConnection(projectRoot) {
97
97
  return {
98
98
  selected,
99
99
  project: typeof record.project === "string" ? record.project : undefined,
100
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
100
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
101
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
101
102
  };
102
103
  }
103
104
  function writeRepoConnection(projectRoot, state) {
@@ -44,6 +44,11 @@ function readJsonFile(path) {
44
44
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
45
45
  }
46
46
  }
47
+ function writeJsonFile(path, value) {
48
+ mkdirSync(dirname(path), { recursive: true });
49
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
50
+ `, "utf8");
51
+ }
47
52
  function normalizeConnection(value) {
48
53
  if (!value || typeof value !== "object" || Array.isArray(value))
49
54
  return null;
@@ -84,21 +89,31 @@ function readRepoConnection(projectRoot) {
84
89
  return {
85
90
  selected,
86
91
  project: typeof record.project === "string" ? record.project : undefined,
87
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
92
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
93
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
88
94
  };
89
95
  }
96
+ function writeRepoConnection(projectRoot, state) {
97
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
98
+ }
90
99
  function resolveSelectedConnection(projectRoot, options = {}) {
91
100
  const repo = readRepoConnection(projectRoot);
92
101
  if (!repo)
93
102
  return null;
94
103
  if (repo.selected === "local")
95
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
104
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
96
105
  const global = readGlobalConnections(options);
97
106
  const connection = global.connections[repo.selected];
98
107
  if (!connection) {
99
108
  throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
100
109
  }
101
- return { alias: repo.selected, connection };
110
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
111
+ }
112
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
113
+ const repo = readRepoConnection(projectRoot);
114
+ if (!repo)
115
+ return;
116
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
102
117
  }
103
118
 
104
119
  // packages/cli/src/commands/_server-client.ts
@@ -134,17 +149,21 @@ async function ensureServerForCli(projectRoot) {
134
149
  try {
135
150
  const selected = resolveSelectedConnection(projectRoot);
136
151
  if (selected?.connection.kind === "remote") {
152
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
153
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
137
154
  return {
138
155
  baseUrl: selected.connection.baseUrl,
139
- authToken: readGitHubBearerTokenForRemote(projectRoot),
140
- connectionKind: "remote"
156
+ authToken,
157
+ connectionKind: "remote",
158
+ serverProjectRoot
141
159
  };
142
160
  }
143
161
  const connection = await ensureLocalRigServerConnection(projectRoot);
144
162
  return {
145
163
  baseUrl: connection.baseUrl,
146
164
  authToken: connection.authToken,
147
- connectionKind: "local"
165
+ connectionKind: "local",
166
+ serverProjectRoot: resolve2(projectRoot)
148
167
  };
149
168
  } catch (error) {
150
169
  if (error instanceof Error) {
@@ -153,6 +172,29 @@ async function ensureServerForCli(projectRoot) {
153
172
  throw error;
154
173
  }
155
174
  }
175
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
176
+ const repo = readRepoConnection(projectRoot);
177
+ const slug = repo?.project?.trim();
178
+ if (!slug)
179
+ return null;
180
+ try {
181
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
182
+ headers: mergeHeaders(undefined, authToken)
183
+ });
184
+ if (!response.ok)
185
+ return null;
186
+ const payload = await response.json();
187
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
188
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
189
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
190
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
191
+ if (path)
192
+ writeRepoServerProjectRoot(projectRoot, path);
193
+ return path;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
156
198
  function mergeHeaders(headers, authToken) {
157
199
  const merged = new Headers(headers);
158
200
  if (authToken) {
@@ -177,9 +219,12 @@ function diagnosticMessage(payload) {
177
219
  }
178
220
  async function requestServerJson(context, pathname, init = {}) {
179
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);
180
225
  const response = await fetch(`${server.baseUrl}${pathname}`, {
181
226
  ...init,
182
- headers: mergeHeaders(init.headers, server.authToken)
227
+ headers
183
228
  });
184
229
  const text = await response.text();
185
230
  const payload = text.trim().length > 0 ? (() => {