@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.
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,29 @@ 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 checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
2993
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
2994
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
2995
+ if (path)
2996
+ writeRepoServerProjectRoot(projectRoot, path);
2997
+ return path;
2998
+ } catch {
2999
+ return null;
3000
+ }
3001
+ }
2968
3002
  function appendTaskFilterParams(url, filters) {
2969
3003
  if (filters.assignee)
2970
3004
  url.searchParams.set("assignee", filters.assignee);
@@ -2999,9 +3033,12 @@ function diagnosticMessage(payload) {
2999
3033
  }
3000
3034
  async function requestServerJson(context, pathname, init = {}) {
3001
3035
  const server = await ensureServerForCli(context.projectRoot);
3036
+ const headers = mergeHeaders(init.headers, server.authToken);
3037
+ if (server.serverProjectRoot)
3038
+ headers.set("x-rig-project-root", server.serverProjectRoot);
3002
3039
  const response = await fetch(`${server.baseUrl}${pathname}`, {
3003
3040
  ...init,
3004
- headers: mergeHeaders(init.headers, server.authToken)
3041
+ headers
3005
3042
  });
3006
3043
  const text2 = await response.text();
3007
3044
  const payload = text2.trim().length > 0 ? (() => {
@@ -3108,22 +3145,12 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
3108
3145
  if (!switched) {
3109
3146
  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
3147
  }
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);
3148
+ const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
3149
+ if (record.ok === true) {
3150
+ writeRepoServerProjectRoot(context.projectRoot, projectRoot);
3151
+ return { ok: true, switched: record };
3125
3152
  }
3126
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
3153
+ throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
3127
3154
  }
3128
3155
  async function listRunsViaServer(context, options = {}) {
3129
3156
  const url = new URL("http://rig.local/api/runs");
@@ -3255,6 +3282,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
3255
3282
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
3256
3283
  if (server.authToken)
3257
3284
  url.searchParams.set("token", server.authToken);
3285
+ if (server.serverProjectRoot)
3286
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
3258
3287
  return url.toString();
3259
3288
  }
3260
3289
  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,29 @@ 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 checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
185
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
186
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
187
+ if (path)
188
+ writeRepoServerProjectRoot(projectRoot, path);
189
+ return path;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
152
194
  function mergeHeaders(headers, authToken) {
153
195
  const merged = new Headers(headers);
154
196
  if (authToken) {
@@ -173,9 +215,12 @@ function diagnosticMessage(payload) {
173
215
  }
174
216
  async function requestServerJson(context, pathname, init = {}) {
175
217
  const server = await ensureServerForCli(context.projectRoot);
218
+ const headers = mergeHeaders(init.headers, server.authToken);
219
+ if (server.serverProjectRoot)
220
+ headers.set("x-rig-project-root", server.serverProjectRoot);
176
221
  const response = await fetch(`${server.baseUrl}${pathname}`, {
177
222
  ...init,
178
- headers: mergeHeaders(init.headers, server.authToken)
223
+ headers
179
224
  });
180
225
  const text = await response.text();
181
226
  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,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 ? (() => {
@@ -287,6 +332,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
287
332
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
288
333
  if (server.authToken)
289
334
  url.searchParams.set("token", server.authToken);
335
+ if (server.serverProjectRoot)
336
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
290
337
  return url.toString();
291
338
  }
292
339
 
@@ -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,29 @@ 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 checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
190
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
191
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
192
+ if (path)
193
+ writeRepoServerProjectRoot(projectRoot, path);
194
+ return path;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
157
199
  function mergeHeaders(headers, authToken) {
158
200
  const merged = new Headers(headers);
159
201
  if (authToken) {
@@ -178,9 +220,12 @@ function diagnosticMessage(payload) {
178
220
  }
179
221
  async function requestServerJson(context, pathname, init = {}) {
180
222
  const server = await ensureServerForCli(context.projectRoot);
223
+ const headers = mergeHeaders(init.headers, server.authToken);
224
+ if (server.serverProjectRoot)
225
+ headers.set("x-rig-project-root", server.serverProjectRoot);
181
226
  const response = await fetch(`${server.baseUrl}${pathname}`, {
182
227
  ...init,
183
- headers: mergeHeaders(init.headers, server.authToken)
228
+ headers
184
229
  });
185
230
  const text = await response.text();
186
231
  const payload = text.trim().length > 0 ? (() => {
@@ -263,6 +308,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
263
308
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
264
309
  if (server.authToken)
265
310
  url.searchParams.set("token", server.authToken);
311
+ if (server.serverProjectRoot)
312
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
266
313
  return url.toString();
267
314
  }
268
315
 
@@ -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,29 @@ 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 checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
183
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
184
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
185
+ if (path)
186
+ writeRepoServerProjectRoot(projectRoot, path);
187
+ return path;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
150
192
  function mergeHeaders(headers, authToken) {
151
193
  const merged = new Headers(headers);
152
194
  if (authToken) {
@@ -171,9 +213,12 @@ function diagnosticMessage(payload) {
171
213
  }
172
214
  async function requestServerJson(context, pathname, init = {}) {
173
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);
174
219
  const response = await fetch(`${server.baseUrl}${pathname}`, {
175
220
  ...init,
176
- headers: mergeHeaders(init.headers, server.authToken)
221
+ headers
177
222
  });
178
223
  const text = await response.text();
179
224
  const payload = text.trim().length > 0 ? (() => {
@@ -244,6 +289,8 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
244
289
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
245
290
  if (server.authToken)
246
291
  url.searchParams.set("token", server.authToken);
292
+ if (server.serverProjectRoot)
293
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
247
294
  return url.toString();
248
295
  }
249
296