@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.
@@ -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 ? (() => {
@@ -249,6 +293,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
249
293
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
250
294
  if (server.authToken)
251
295
  url.searchParams.set("token", server.authToken);
296
+ if (server.serverProjectRoot)
297
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
252
298
  return url.toString();
253
299
  }
254
300
 
@@ -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,28 @@ 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 checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
179
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
180
+ if (path)
181
+ writeRepoServerProjectRoot(projectRoot, path);
182
+ return path;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
146
187
  function mergeHeaders(headers, authToken) {
147
188
  const merged = new Headers(headers);
148
189
  if (authToken) {
@@ -167,9 +208,12 @@ function diagnosticMessage(payload) {
167
208
  }
168
209
  async function requestServerJson(context, pathname, init = {}) {
169
210
  const server = await ensureServerForCli(context.projectRoot);
211
+ const headers = mergeHeaders(init.headers, server.authToken);
212
+ if (server.serverProjectRoot)
213
+ headers.set("x-rig-project-root", server.serverProjectRoot);
170
214
  const response = await fetch(`${server.baseUrl}${pathname}`, {
171
215
  ...init,
172
- headers: mergeHeaders(init.headers, server.authToken)
216
+ headers
173
217
  });
174
218
  const text = await response.text();
175
219
  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,28 @@ 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 checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
184
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
185
+ if (path)
186
+ writeRepoServerProjectRoot(projectRoot, path);
187
+ return path;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
151
192
  function appendTaskFilterParams(url, filters) {
152
193
  if (filters.assignee)
153
194
  url.searchParams.set("assignee", filters.assignee);
@@ -182,9 +223,12 @@ function diagnosticMessage(payload) {
182
223
  }
183
224
  async function requestServerJson(context, pathname, init = {}) {
184
225
  const server = await ensureServerForCli(context.projectRoot);
226
+ const headers = mergeHeaders(init.headers, server.authToken);
227
+ if (server.serverProjectRoot)
228
+ headers.set("x-rig-project-root", server.serverProjectRoot);
185
229
  const response = await fetch(`${server.baseUrl}${pathname}`, {
186
230
  ...init,
187
- headers: mergeHeaders(init.headers, server.authToken)
231
+ headers
188
232
  });
189
233
  const text = await response.text();
190
234
  const payload = text.trim().length > 0 ? (() => {
@@ -291,22 +335,12 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
291
335
  if (!switched) {
292
336
  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
337
  }
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);
338
+ const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
339
+ if (record.ok === true) {
340
+ writeRepoServerProjectRoot(context.projectRoot, projectRoot);
341
+ return { ok: true, switched: record };
308
342
  }
309
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
343
+ throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
310
344
  }
311
345
  async function listRunsViaServer(context, options = {}) {
312
346
  const url = new URL("http://rig.local/api/runs");
@@ -438,6 +472,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
438
472
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
439
473
  if (server.authToken)
440
474
  url.searchParams.set("token", server.authToken);
475
+ if (server.serverProjectRoot)
476
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
441
477
  return url.toString();
442
478
  }
443
479
  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,28 @@ 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 checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
184
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
185
+ if (path)
186
+ writeRepoServerProjectRoot(projectRoot, path);
187
+ return path;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
151
192
  function mergeHeaders(headers, authToken) {
152
193
  const merged = new Headers(headers);
153
194
  if (authToken) {
@@ -172,9 +213,12 @@ function diagnosticMessage(payload) {
172
213
  }
173
214
  async function requestServerJson(context, pathname, init = {}) {
174
215
  const server = await ensureServerForCli(context.projectRoot);
216
+ const headers = mergeHeaders(init.headers, server.authToken);
217
+ if (server.serverProjectRoot)
218
+ headers.set("x-rig-project-root", server.serverProjectRoot);
175
219
  const response = await fetch(`${server.baseUrl}${pathname}`, {
176
220
  ...init,
177
- headers: mergeHeaders(init.headers, server.authToken)
221
+ headers
178
222
  });
179
223
  const text = await response.text();
180
224
  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,28 @@ 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 checkout = project?.checkout && typeof project.checkout === "object" && !Array.isArray(project.checkout) ? project.checkout : null;
189
+ const path = typeof checkout?.path === "string" && checkout.path.trim() ? checkout.path.trim() : typeof project?.path === "string" && project.path.trim() ? project.path.trim() : null;
190
+ if (path)
191
+ writeRepoServerProjectRoot(projectRoot, path);
192
+ return path;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
156
197
  function mergeHeaders(headers, authToken) {
157
198
  const merged = new Headers(headers);
158
199
  if (authToken) {
@@ -177,9 +218,12 @@ function diagnosticMessage(payload) {
177
218
  }
178
219
  async function requestServerJson(context, pathname, init = {}) {
179
220
  const server = await ensureServerForCli(context.projectRoot);
221
+ const headers = mergeHeaders(init.headers, server.authToken);
222
+ if (server.serverProjectRoot)
223
+ headers.set("x-rig-project-root", server.serverProjectRoot);
180
224
  const response = await fetch(`${server.baseUrl}${pathname}`, {
181
225
  ...init,
182
- headers: mergeHeaders(init.headers, server.authToken)
226
+ headers
183
227
  });
184
228
  const text = await response.text();
185
229
  const payload = text.trim().length > 0 ? (() => {