@remixhq/core 0.1.6 → 0.1.7

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/api.d.ts CHANGED
@@ -204,6 +204,9 @@ type ReconcilePreflightResponse = {
204
204
  targetHeadCommitHash: string;
205
205
  warnings: string[];
206
206
  };
207
+ type OrganizationMemberRole = "owner" | "admin" | "member" | "viewer";
208
+ type ProjectMemberRole = "owner" | "maintainer" | "editor" | "viewer";
209
+ type AppMemberRole = "owner" | "maintainer" | "editor" | "viewer";
207
210
  type AppReconcileResponse = {
208
211
  id: string;
209
212
  appId: string;
@@ -436,6 +439,18 @@ type ApiClient = {
436
439
  role?: string;
437
440
  ttlDays?: number;
438
441
  }): Promise<Json>;
442
+ listOrganizationMembers(orgId: string): Promise<Json>;
443
+ updateOrganizationMember(orgId: string, userId: string, payload: {
444
+ role: OrganizationMemberRole;
445
+ }): Promise<Json>;
446
+ listProjectMembers(projectId: string): Promise<Json>;
447
+ updateProjectMember(projectId: string, userId: string, payload: {
448
+ role: ProjectMemberRole;
449
+ }): Promise<Json>;
450
+ listAppMembers(appId: string): Promise<Json>;
451
+ updateAppMember(appId: string, userId: string, payload: {
452
+ role: AppMemberRole;
453
+ }): Promise<Json>;
439
454
  listOrganizationInvites(orgId: string): Promise<Json>;
440
455
  listProjectInvites(projectId: string): Promise<Json>;
441
456
  listAppInvites(appId: string): Promise<Json>;
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-R44EOUS4.js";
3
+ } from "./chunk-4276ARDF.js";
4
4
  import "./chunk-YZ34ICNN.js";
5
5
  export {
6
6
  createApiClient
@@ -0,0 +1,303 @@
1
+ import {
2
+ RemixError
3
+ } from "./chunk-YZ34ICNN.js";
4
+
5
+ // src/api/client.ts
6
+ async function readJsonSafe(res) {
7
+ const ct = res.headers.get("content-type") ?? "";
8
+ if (!ct.toLowerCase().includes("application/json")) return null;
9
+ try {
10
+ return await res.json();
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function createApiClient(config, opts) {
16
+ const apiKey = (opts?.apiKey ?? "").trim();
17
+ const tokenProvider = opts?.tokenProvider;
18
+ const CLIENT_KEY_HEADER = "x-comerge-api-key";
19
+ async function request(path, init) {
20
+ if (!tokenProvider) {
21
+ throw new RemixError("API client is missing a token provider.", {
22
+ exitCode: 1,
23
+ hint: "Configure auth before creating the Remix API client."
24
+ });
25
+ }
26
+ const auth = await tokenProvider();
27
+ const url = new URL(path, config.apiUrl).toString();
28
+ const doFetch = async (bearer) => fetch(url, {
29
+ ...init,
30
+ headers: {
31
+ Accept: "application/json",
32
+ "Content-Type": "application/json",
33
+ ...init?.headers ?? {},
34
+ Authorization: `Bearer ${bearer}`,
35
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
36
+ }
37
+ });
38
+ let res = await doFetch(auth.token);
39
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
40
+ const refreshed = await tokenProvider({ forceRefresh: true });
41
+ res = await doFetch(refreshed.token);
42
+ }
43
+ if (!res.ok) {
44
+ const body = await readJsonSafe(res);
45
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
46
+ throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
47
+ }
48
+ const json = await readJsonSafe(res);
49
+ return json ?? null;
50
+ }
51
+ async function requestBinary(path, init) {
52
+ if (!tokenProvider) {
53
+ throw new RemixError("API client is missing a token provider.", {
54
+ exitCode: 1,
55
+ hint: "Configure auth before creating the Remix API client."
56
+ });
57
+ }
58
+ const auth = await tokenProvider();
59
+ const url = new URL(path, config.apiUrl).toString();
60
+ const doFetch = async (bearer) => fetch(url, {
61
+ ...init,
62
+ headers: {
63
+ Accept: "*/*",
64
+ ...init?.headers ?? {},
65
+ Authorization: `Bearer ${bearer}`,
66
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
67
+ }
68
+ });
69
+ let res = await doFetch(auth.token);
70
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
71
+ const refreshed = await tokenProvider({ forceRefresh: true });
72
+ res = await doFetch(refreshed.token);
73
+ }
74
+ if (!res.ok) {
75
+ const body = await readJsonSafe(res);
76
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
77
+ throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
78
+ }
79
+ const contentDisposition = res.headers.get("content-disposition") ?? "";
80
+ const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
81
+ return {
82
+ data: Buffer.from(await res.arrayBuffer()),
83
+ fileName: fileNameMatch?.[1] ?? null,
84
+ contentType: res.headers.get("content-type")
85
+ };
86
+ }
87
+ return {
88
+ getMe: () => request("/v1/me", { method: "GET" }),
89
+ listOrganizations: () => request("/v1/organizations", { method: "GET" }),
90
+ getOrganization: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}`, { method: "GET" }),
91
+ listProjects: (params) => {
92
+ const qs = new URLSearchParams();
93
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
94
+ if (params?.clientAppId) qs.set("clientAppId", params.clientAppId);
95
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
96
+ return request(`/v1/projects${suffix}`, { method: "GET" });
97
+ },
98
+ getProject: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}`, { method: "GET" }),
99
+ resolveProjectBinding: (params) => {
100
+ const qs = new URLSearchParams();
101
+ if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
102
+ if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
103
+ return request(`/v1/projects/bindings/resolve?${qs.toString()}`, { method: "GET" });
104
+ },
105
+ autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
106
+ listClientApps: (params) => {
107
+ const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
108
+ return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
109
+ },
110
+ createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
111
+ createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
112
+ method: "POST",
113
+ body: JSON.stringify(payload ?? {})
114
+ }),
115
+ listApps: (params) => {
116
+ const qs = new URLSearchParams();
117
+ if (params?.projectId) qs.set("projectId", params.projectId);
118
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
119
+ if (params?.forked) qs.set("forked", params.forked);
120
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
121
+ return request(`/v1/apps${suffix}`, { method: "GET" });
122
+ },
123
+ getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
124
+ getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
125
+ presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
126
+ importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
127
+ presignImportUploadFirstParty: (payload) => request("/v1/apps/import/upload/presign/first-party", { method: "POST", body: JSON.stringify(payload) }),
128
+ importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
129
+ importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
130
+ forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
131
+ downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
132
+ createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
133
+ method: "POST",
134
+ body: JSON.stringify(payload)
135
+ }),
136
+ createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
137
+ method: "POST",
138
+ body: JSON.stringify(payload)
139
+ }),
140
+ listCollabTurns: (appId, params) => {
141
+ const qs = new URLSearchParams();
142
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
143
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
144
+ if (params?.changeStepId) qs.set("changeStepId", params.changeStepId);
145
+ if (params?.threadId) qs.set("threadId", params.threadId);
146
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
147
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
148
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
149
+ return request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns${suffix}`, { method: "GET" });
150
+ },
151
+ getCollabTurn: (appId, collabTurnId) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns/${encodeURIComponent(collabTurnId)}`, {
152
+ method: "GET"
153
+ }),
154
+ getAgentMemorySummary: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/summary`, { method: "GET" }),
155
+ listAgentMemoryTimeline: (appId, params) => {
156
+ const qs = new URLSearchParams();
157
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
158
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
159
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
160
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
161
+ if (params?.kinds?.length) {
162
+ for (const kind of params.kinds) qs.append("kinds", kind);
163
+ }
164
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
165
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/timeline${suffix}`, { method: "GET" });
166
+ },
167
+ searchAgentMemory: (appId, params) => {
168
+ const qs = new URLSearchParams();
169
+ qs.set("q", params.q);
170
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
171
+ if (params.offset !== void 0) qs.set("offset", String(params.offset));
172
+ if (params.createdAfter) qs.set("createdAfter", params.createdAfter);
173
+ if (params.createdBefore) qs.set("createdBefore", params.createdBefore);
174
+ if (params.kinds?.length) {
175
+ for (const kind of params.kinds) qs.append("kinds", kind);
176
+ }
177
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/search?${qs.toString()}`, { method: "GET" });
178
+ },
179
+ getChangeStep: (appId, changeStepId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/${encodeURIComponent(changeStepId)}`, { method: "GET" }),
180
+ getChangeStepDiff: (appId, changeStepId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/${encodeURIComponent(changeStepId)}/diff`, {
181
+ method: "GET"
182
+ }),
183
+ startChangeStepReplay: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays`, {
184
+ method: "POST",
185
+ body: JSON.stringify(payload)
186
+ }),
187
+ getChangeStepReplay: (appId, replayId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays/${encodeURIComponent(replayId)}`, {
188
+ method: "GET"
189
+ }),
190
+ getChangeStepReplayDiff: (appId, replayId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays/${encodeURIComponent(replayId)}/diff`, {
191
+ method: "GET"
192
+ }),
193
+ listMergeRequests: (params) => {
194
+ const qs = new URLSearchParams();
195
+ if (params?.queue) qs.set("queue", params.queue);
196
+ if (params?.appId) qs.set("appId", params.appId);
197
+ if (params?.sourceAppId) qs.set("sourceAppId", params.sourceAppId);
198
+ if (params?.targetAppId) qs.set("targetAppId", params.targetAppId);
199
+ if (Array.isArray(params?.status)) {
200
+ for (const status of params.status) qs.append("status", status);
201
+ } else if (typeof params?.status === "string") {
202
+ qs.set("status", params.status);
203
+ }
204
+ if (params?.kind) qs.set("kind", params.kind);
205
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
206
+ return request(`/v1/merge-requests${suffix}`, { method: "GET" });
207
+ },
208
+ openMergeRequest: (sourceAppId) => request("/v1/merge-requests", { method: "POST", body: JSON.stringify({ sourceAppId }) }),
209
+ getMergeRequestReview: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}/review`, { method: "GET" }),
210
+ updateMergeRequest: (mrId, payload) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "PATCH", body: JSON.stringify(payload) }),
211
+ createOrganizationInvite: (orgId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations`, {
212
+ method: "POST",
213
+ body: JSON.stringify(payload)
214
+ }),
215
+ createProjectInvite: (projectId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations`, {
216
+ method: "POST",
217
+ body: JSON.stringify(payload)
218
+ }),
219
+ createAppInvite: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations`, {
220
+ method: "POST",
221
+ body: JSON.stringify(payload)
222
+ }),
223
+ listOrganizationMembers: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/members`, { method: "GET" }),
224
+ updateOrganizationMember: (orgId, userId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`, {
225
+ method: "PATCH",
226
+ body: JSON.stringify(payload)
227
+ }),
228
+ listProjectMembers: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}/members`, { method: "GET" }),
229
+ updateProjectMember: (projectId, userId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
230
+ method: "PATCH",
231
+ body: JSON.stringify(payload)
232
+ }),
233
+ listAppMembers: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/members`, { method: "GET" }),
234
+ updateAppMember: (appId, userId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/members/${encodeURIComponent(userId)}`, {
235
+ method: "PATCH",
236
+ body: JSON.stringify(payload)
237
+ }),
238
+ listOrganizationInvites: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations`, { method: "GET" }),
239
+ listProjectInvites: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations`, { method: "GET" }),
240
+ listAppInvites: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations`, { method: "GET" }),
241
+ resendOrganizationInvite: (orgId, inviteId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
242
+ method: "POST",
243
+ body: JSON.stringify(payload ?? {})
244
+ }),
245
+ resendProjectInvite: (projectId, inviteId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
246
+ method: "POST",
247
+ body: JSON.stringify(payload ?? {})
248
+ }),
249
+ resendAppInvite: (appId, inviteId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
250
+ method: "POST",
251
+ body: JSON.stringify(payload ?? {})
252
+ }),
253
+ revokeOrganizationInvite: (orgId, inviteId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}`, {
254
+ method: "DELETE"
255
+ }),
256
+ revokeProjectInvite: (projectId, inviteId) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations/${encodeURIComponent(inviteId)}`, {
257
+ method: "DELETE"
258
+ }),
259
+ revokeAppInvite: (appId, inviteId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}`, {
260
+ method: "DELETE"
261
+ }),
262
+ syncUpstreamApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-upstream`, {
263
+ method: "POST",
264
+ body: JSON.stringify({})
265
+ }),
266
+ preflightAppReconcile: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/preflight`, {
267
+ method: "POST",
268
+ body: JSON.stringify(payload)
269
+ }),
270
+ startAppReconcile: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/start`, {
271
+ method: "POST",
272
+ body: JSON.stringify(payload)
273
+ }),
274
+ getAppReconcile: (appId, reconcileId) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/${encodeURIComponent(reconcileId)}`, { method: "GET" }),
275
+ downloadAppReconcileBundle: (appId, reconcileId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/reconcile/${encodeURIComponent(reconcileId)}/download.bundle`, {
276
+ method: "GET"
277
+ }),
278
+ syncLocalApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-local`, {
279
+ method: "POST",
280
+ body: JSON.stringify(payload)
281
+ }),
282
+ initiateBundle: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles`, { method: "POST", body: JSON.stringify(payload) }),
283
+ getBundle: (appId, bundleId) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`, { method: "GET" }),
284
+ getBundleDownloadUrl: (appId, bundleId, options) => request(
285
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/download?redirect=${options?.redirect ?? false}`,
286
+ { method: "GET" }
287
+ ),
288
+ getBundleAssetsDownloadUrl: (appId, bundleId, options) => {
289
+ const qs = new URLSearchParams({
290
+ redirect: String(options?.redirect ?? false),
291
+ kind: options?.kind ?? "metro-assets"
292
+ });
293
+ return request(
294
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download?${qs.toString()}`,
295
+ { method: "GET" }
296
+ );
297
+ }
298
+ };
299
+ }
300
+
301
+ export {
302
+ createApiClient
303
+ };
package/dist/collab.d.ts CHANGED
@@ -242,6 +242,38 @@ type CollabTurn = {
242
242
  assistantResponse: string | null;
243
243
  };
244
244
  type InvitationScopeType = "organization" | "project" | "app";
245
+ type MembershipScopeType = InvitationScopeType;
246
+ type OrganizationMemberRole = "owner" | "admin" | "member" | "viewer";
247
+ type ProjectMemberRole = "owner" | "maintainer" | "editor" | "viewer";
248
+ type AppMemberRole = "owner" | "maintainer" | "editor" | "viewer";
249
+ type OrganizationMember = {
250
+ id: string;
251
+ userId: string;
252
+ organizationId: string;
253
+ role: OrganizationMemberRole;
254
+ invitedBy: string | null;
255
+ createdAt: string;
256
+ updatedAt: string;
257
+ };
258
+ type ProjectMember = {
259
+ id: string;
260
+ userId: string;
261
+ projectId: string;
262
+ role: ProjectMemberRole;
263
+ invitedBy: string | null;
264
+ createdAt: string;
265
+ updatedAt: string;
266
+ };
267
+ type AppMember = {
268
+ id: string;
269
+ userId: string;
270
+ appId: string;
271
+ role: AppMemberRole;
272
+ invitedBy: string | null;
273
+ createdAt: string;
274
+ updatedAt: string;
275
+ };
276
+ type CollabMember = OrganizationMember | ProjectMember | AppMember;
245
277
  type CollabApiClient = {
246
278
  resolveProjectBinding(params: {
247
279
  repoFingerprint?: string;
@@ -367,6 +399,18 @@ type CollabApiClient = {
367
399
  role?: string;
368
400
  ttlDays?: number;
369
401
  }): Promise<unknown>;
402
+ listOrganizationMembers(orgId: string): Promise<unknown>;
403
+ updateOrganizationMember(orgId: string, userId: string, payload: {
404
+ role: OrganizationMemberRole;
405
+ }): Promise<unknown>;
406
+ listProjectMembers(projectId: string): Promise<unknown>;
407
+ updateProjectMember(projectId: string, userId: string, payload: {
408
+ role: ProjectMemberRole;
409
+ }): Promise<unknown>;
410
+ listAppMembers(appId: string): Promise<unknown>;
411
+ updateAppMember(appId: string, userId: string, payload: {
412
+ role: AppMemberRole;
413
+ }): Promise<unknown>;
370
414
  listOrganizationInvites(orgId: string): Promise<unknown>;
371
415
  listProjectInvites(projectId: string): Promise<unknown>;
372
416
  listAppInvites(appId: string): Promise<unknown>;
@@ -459,6 +503,20 @@ declare function collabApprove(params: {
459
503
  allowBranchMismatch?: boolean;
460
504
  }): Promise<CollabApproveResult>;
461
505
 
506
+ declare function collabCheckout(params: {
507
+ api: CollabApiClient;
508
+ cwd: string;
509
+ appId?: string | null;
510
+ outputDir?: string | null;
511
+ }): Promise<{
512
+ appId: string;
513
+ dashboardUrl: string;
514
+ projectId: string;
515
+ upstreamAppId: string;
516
+ bindingPath: string;
517
+ repoRoot: string;
518
+ }>;
519
+
462
520
  declare function collabListMergeRequests(params: {
463
521
  api: CollabApiClient;
464
522
  queue: MergeRequestQueue;
@@ -472,6 +530,31 @@ declare function collabListMergeRequests(params: {
472
530
  mergeRequests: MergeRequest[];
473
531
  }>;
474
532
 
533
+ declare function getMemberRolesForScope(scope: InvitationScopeType): readonly string[];
534
+ declare function validateMemberRole(scope: InvitationScopeType, role: string): string;
535
+ declare function collabListMembers(params: {
536
+ api: CollabApiClient;
537
+ cwd: string;
538
+ scope: InvitationScopeType;
539
+ targetId?: string | null;
540
+ }): Promise<{
541
+ scopeType: InvitationScopeType;
542
+ targetId: string;
543
+ members: CollabMember[];
544
+ }>;
545
+ declare function collabUpdateMemberRole(params: {
546
+ api: CollabApiClient;
547
+ cwd: string;
548
+ scope: InvitationScopeType;
549
+ targetId?: string | null;
550
+ userId: string;
551
+ role: string;
552
+ }): Promise<{
553
+ scopeType: InvitationScopeType;
554
+ targetId: string;
555
+ member: CollabMember;
556
+ }>;
557
+
475
558
  declare function collabInit(params: {
476
559
  api: CollabApiClient;
477
560
  cwd: string;
@@ -483,6 +566,7 @@ declare function collabInit(params: {
483
566
  reused: boolean;
484
567
  projectId: string;
485
568
  appId: string;
569
+ dashboardUrl: string;
486
570
  upstreamAppId: string;
487
571
  bindingPath: string;
488
572
  repoRoot: string;
@@ -491,6 +575,7 @@ declare function collabInit(params: {
491
575
  reused: boolean;
492
576
  projectId: string;
493
577
  appId: string;
578
+ dashboardUrl: string;
494
579
  upstreamAppId: string;
495
580
  bindingPath: string;
496
581
  repoRoot: string;
@@ -596,6 +681,7 @@ declare function collabRemix(params: {
596
681
  outputDir?: string | null;
597
682
  }): Promise<{
598
683
  appId: string;
684
+ dashboardUrl: string;
599
685
  projectId: string;
600
686
  upstreamAppId: string;
601
687
  bindingPath: string;
@@ -711,4 +797,4 @@ declare function collabView(params: {
711
797
  mrId: string;
712
798
  }): Promise<MergeRequestReview>;
713
799
 
714
- export { type AppReconcileResponse, type CollabApiClient, type CollabApproveMode, type CollabApproveResult, type CollabRecordingPreflight, type CollabRecordingPreflightStatus, type CollabStatus, type CollabStatusBlockedReason, type CollabStatusRecommendedAction, type CollabTurn, type InvitationScopeType, type JsonObject, type MergeRequest, type MergeRequestQueue, type MergeRequestReview, type MergeRequestStatus, type ReconcilePreflightResponse, type SyncLocalResponse, type SyncUpstreamResponse, collabAdd, collabApprove, collabInit, collabInvite, collabList, collabListMergeRequests, collabReconcile, collabRecordTurn, collabRecordingPreflight, collabReject, collabRemix, collabRequestMerge, collabStatus, collabSync, collabSyncUpstream, collabView };
800
+ export { type AppMember, type AppMemberRole, type AppReconcileResponse, type CollabApiClient, type CollabApproveMode, type CollabApproveResult, type CollabMember, type CollabRecordingPreflight, type CollabRecordingPreflightStatus, type CollabStatus, type CollabStatusBlockedReason, type CollabStatusRecommendedAction, type CollabTurn, type InvitationScopeType, type JsonObject, type MembershipScopeType, type MergeRequest, type MergeRequestQueue, type MergeRequestReview, type MergeRequestStatus, type OrganizationMember, type OrganizationMemberRole, type ProjectMember, type ProjectMemberRole, type ReconcilePreflightResponse, type SyncLocalResponse, type SyncUpstreamResponse, collabAdd, collabApprove, collabCheckout, collabInit, collabInvite, collabList, collabListMembers, collabListMergeRequests, collabReconcile, collabRecordTurn, collabRecordingPreflight, collabReject, collabRemix, collabRequestMerge, collabStatus, collabSync, collabSyncUpstream, collabUpdateMemberRole, collabView, getMemberRolesForScope, validateMemberRole };
package/dist/collab.js CHANGED
@@ -116,6 +116,9 @@ function sanitizeCheckoutDirName(value) {
116
116
  const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
117
117
  return sanitized || "remix-remix";
118
118
  }
119
+ function buildDashboardAppUrl(appId) {
120
+ return `https://dashboard.remix.one/apps/${encodeURIComponent(appId)}`;
121
+ }
119
122
  async function pollAppReady(api, appId) {
120
123
  const started = Date.now();
121
124
  let delay = 2e3;
@@ -1277,6 +1280,163 @@ async function collabApprove(params) {
1277
1280
  };
1278
1281
  }
1279
1282
 
1283
+ // src/application/collab/checkoutWorkspace.ts
1284
+ import fs4 from "fs/promises";
1285
+ import os3 from "os";
1286
+ import path4 from "path";
1287
+ async function pathExists(targetPath) {
1288
+ try {
1289
+ await fs4.access(targetPath);
1290
+ return true;
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ }
1295
+ async function statIsDirectory(targetPath) {
1296
+ const stats = await fs4.stat(targetPath).catch(() => null);
1297
+ return Boolean(stats?.isDirectory());
1298
+ }
1299
+ async function findContainingGitRoot(startPath) {
1300
+ let current = path4.resolve(startPath);
1301
+ while (true) {
1302
+ if (await pathExists(path4.join(current, ".git"))) return current;
1303
+ const parent = path4.dirname(current);
1304
+ if (parent === current) return null;
1305
+ current = parent;
1306
+ }
1307
+ }
1308
+ function isSubpath(parentPath, candidatePath) {
1309
+ const relative = path4.relative(parentPath, candidatePath);
1310
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
1311
+ }
1312
+ function buildPreferredCheckoutBranch(appId) {
1313
+ const normalized = appId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1314
+ return `remix/remix/${normalized || "app"}`;
1315
+ }
1316
+ async function resolveCheckoutDestination(params) {
1317
+ if (params.outputDir?.trim()) {
1318
+ const preferredRepoRoot = path4.resolve(params.outputDir.trim());
1319
+ const parentDir2 = path4.dirname(preferredRepoRoot);
1320
+ if (!await statIsDirectory(parentDir2)) {
1321
+ throw new RemixError("Remix output parent directory does not exist.", {
1322
+ exitCode: 2,
1323
+ hint: `Create the directory first: ${parentDir2}`
1324
+ });
1325
+ }
1326
+ return {
1327
+ preferredRepoRoot,
1328
+ parentDir: parentDir2,
1329
+ explicitOutputDir: true
1330
+ };
1331
+ }
1332
+ const parentDir = path4.resolve(params.cwd);
1333
+ if (!await statIsDirectory(parentDir)) {
1334
+ throw new RemixError("Remix output parent directory does not exist.", {
1335
+ exitCode: 2,
1336
+ hint: `Create the directory first: ${parentDir}`
1337
+ });
1338
+ }
1339
+ return {
1340
+ preferredRepoRoot: path4.join(parentDir, params.defaultDirName),
1341
+ parentDir,
1342
+ explicitOutputDir: false
1343
+ };
1344
+ }
1345
+ async function assertSafeCheckoutDestination(params) {
1346
+ const callerGitRoot = await findContainingGitRoot(params.cwd);
1347
+ if (callerGitRoot && isSubpath(callerGitRoot, params.repoRoot)) {
1348
+ throw new RemixError("Refusing to create a remix checkout inside an existing git repository.", {
1349
+ exitCode: 2,
1350
+ hint: params.explicitOutputDir ? `Choose a destination outside ${callerGitRoot}.` : `Pass --output-dir outside ${callerGitRoot}.`
1351
+ });
1352
+ }
1353
+ const parentGitRoot = await findContainingGitRoot(params.parentDir);
1354
+ if (parentGitRoot && isSubpath(parentGitRoot, params.repoRoot)) {
1355
+ throw new RemixError("Refusing to create a remix checkout inside an existing git repository.", {
1356
+ exitCode: 2,
1357
+ hint: params.explicitOutputDir ? `Choose a destination outside ${parentGitRoot}.` : `Pass --output-dir outside ${parentGitRoot}.`
1358
+ });
1359
+ }
1360
+ }
1361
+ async function materializeAppCheckout(params) {
1362
+ const destination = await resolveCheckoutDestination({
1363
+ cwd: params.cwd,
1364
+ outputDir: params.outputDir ?? null,
1365
+ defaultDirName: params.defaultDirName
1366
+ });
1367
+ await assertSafeCheckoutDestination({
1368
+ cwd: params.cwd,
1369
+ repoRoot: destination.preferredRepoRoot,
1370
+ parentDir: destination.parentDir,
1371
+ explicitOutputDir: destination.explicitOutputDir
1372
+ });
1373
+ const repoRoot = destination.explicitOutputDir ? await reserveDirectory(destination.preferredRepoRoot) : await reserveAvailableDirPath(destination.preferredRepoRoot);
1374
+ const bundleTempDir = await fs4.mkdtemp(path4.join(os3.tmpdir(), "remix-checkout-"));
1375
+ const bundlePath = path4.join(bundleTempDir, "repository.bundle");
1376
+ try {
1377
+ const bundle = await params.api.downloadAppBundle(params.appId);
1378
+ await fs4.writeFile(bundlePath, bundle.data);
1379
+ await cloneGitBundleToDirectory(bundlePath, repoRoot);
1380
+ await checkoutLocalBranch(repoRoot, buildPreferredCheckoutBranch(params.appId));
1381
+ await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
1382
+ } catch (err) {
1383
+ await fs4.rm(repoRoot, { recursive: true, force: true }).catch(() => {
1384
+ });
1385
+ throw err;
1386
+ } finally {
1387
+ await fs4.rm(bundleTempDir, { recursive: true, force: true });
1388
+ }
1389
+ const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
1390
+ const defaultBranch = await getDefaultBranch(repoRoot) ?? await getCurrentBranch(repoRoot) ?? null;
1391
+ const preferredBranch = await getCurrentBranch(repoRoot) ?? buildPreferredCheckoutBranch(params.appId);
1392
+ const repoFingerprint = remoteUrl ? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch }) : null;
1393
+ return {
1394
+ repoRoot,
1395
+ remoteUrl,
1396
+ defaultBranch,
1397
+ preferredBranch,
1398
+ repoFingerprint
1399
+ };
1400
+ }
1401
+
1402
+ // src/application/collab/collabCheckout.ts
1403
+ async function collabCheckout(params) {
1404
+ const appId = params.appId?.trim() || null;
1405
+ if (!appId) {
1406
+ throw new RemixError("No app selected.", {
1407
+ exitCode: 2,
1408
+ hint: "Pass the app id to checkout."
1409
+ });
1410
+ }
1411
+ const app = await pollAppReady(params.api, appId);
1412
+ const checkout = await materializeAppCheckout({
1413
+ api: params.api,
1414
+ cwd: params.cwd,
1415
+ appId: String(app.id),
1416
+ outputDir: params.outputDir ?? null,
1417
+ defaultDirName: sanitizeCheckoutDirName(String(app.name || app.id))
1418
+ });
1419
+ const upstreamAppId = String(app.forkedFromAppId ?? app.id);
1420
+ const bindingPath = await writeCollabBinding(checkout.repoRoot, {
1421
+ projectId: String(app.projectId),
1422
+ currentAppId: String(app.id),
1423
+ upstreamAppId,
1424
+ threadId: app.threadId ? String(app.threadId) : null,
1425
+ repoFingerprint: checkout.repoFingerprint,
1426
+ remoteUrl: checkout.remoteUrl,
1427
+ defaultBranch: checkout.defaultBranch,
1428
+ preferredBranch: checkout.preferredBranch
1429
+ });
1430
+ return {
1431
+ appId: String(app.id),
1432
+ dashboardUrl: buildDashboardAppUrl(String(app.id)),
1433
+ projectId: String(app.projectId),
1434
+ upstreamAppId,
1435
+ bindingPath,
1436
+ repoRoot: checkout.repoRoot
1437
+ };
1438
+ }
1439
+
1280
1440
  // src/application/collab/collabListMergeRequests.ts
1281
1441
  var APP_SCOPED_QUEUES = /* @__PURE__ */ new Set(["app_reviewable", "app_outgoing", "app_related_visible"]);
1282
1442
  async function resolveQueueAppId(params) {
@@ -1322,17 +1482,81 @@ async function collabListMergeRequests(params) {
1322
1482
  };
1323
1483
  }
1324
1484
 
1485
+ // src/application/collab/resolveScopeTarget.ts
1486
+ async function resolveScopeTarget(params) {
1487
+ if (params.targetId?.trim()) return params.targetId.trim();
1488
+ const repoRoot = await findGitRoot(params.cwd);
1489
+ const binding = await readCollabBinding(repoRoot);
1490
+ if (!binding) {
1491
+ throw new RemixError("Repository is not bound to Remix and no explicit target id was provided.", { exitCode: 2 });
1492
+ }
1493
+ if (params.scope === "project") return binding.projectId;
1494
+ if (params.scope === "app") return binding.currentAppId;
1495
+ const project = unwrapResponseObject(await params.api.getProject(binding.projectId), "project");
1496
+ const organizationId = typeof project.organizationId === "string" ? project.organizationId : null;
1497
+ if (!organizationId) {
1498
+ throw new RemixError("Could not resolve the organization for the current repository binding.", { exitCode: 2 });
1499
+ }
1500
+ return organizationId;
1501
+ }
1502
+
1503
+ // src/application/collab/collabMembers.ts
1504
+ var ORGANIZATION_MEMBER_ROLES = ["owner", "admin", "member", "viewer"];
1505
+ var WORKFLOW_MEMBER_ROLES = ["owner", "maintainer", "editor", "viewer"];
1506
+ function getMemberRolesForScope(scope) {
1507
+ return scope === "organization" ? ORGANIZATION_MEMBER_ROLES : WORKFLOW_MEMBER_ROLES;
1508
+ }
1509
+ function validateMemberRole(scope, role) {
1510
+ const normalized = role.trim().toLowerCase();
1511
+ if (getMemberRolesForScope(scope).includes(normalized)) return normalized;
1512
+ throw new RemixError(`Invalid ${scope} member role.`, {
1513
+ exitCode: 2,
1514
+ hint: `Allowed roles: ${getMemberRolesForScope(scope).join(", ")}`
1515
+ });
1516
+ }
1517
+ async function collabListMembers(params) {
1518
+ const targetId = await resolveScopeTarget({
1519
+ api: params.api,
1520
+ cwd: params.cwd,
1521
+ scope: params.scope,
1522
+ targetId: params.targetId
1523
+ });
1524
+ const resp = params.scope === "organization" ? await params.api.listOrganizationMembers(targetId) : params.scope === "project" ? await params.api.listProjectMembers(targetId) : await params.api.listAppMembers(targetId);
1525
+ const members = params.scope === "organization" ? unwrapResponseObject(resp, "members") : params.scope === "project" ? unwrapResponseObject(resp, "members") : unwrapResponseObject(resp, "members");
1526
+ return {
1527
+ scopeType: params.scope,
1528
+ targetId,
1529
+ members
1530
+ };
1531
+ }
1532
+ async function collabUpdateMemberRole(params) {
1533
+ const targetId = await resolveScopeTarget({
1534
+ api: params.api,
1535
+ cwd: params.cwd,
1536
+ scope: params.scope,
1537
+ targetId: params.targetId
1538
+ });
1539
+ const role = validateMemberRole(params.scope, params.role);
1540
+ const resp = params.scope === "organization" ? await params.api.updateOrganizationMember(targetId, params.userId, { role }) : params.scope === "project" ? await params.api.updateProjectMember(targetId, params.userId, { role }) : await params.api.updateAppMember(targetId, params.userId, { role });
1541
+ const member = params.scope === "organization" ? unwrapResponseObject(resp, "member") : params.scope === "project" ? unwrapResponseObject(resp, "member") : unwrapResponseObject(resp, "member");
1542
+ return {
1543
+ scopeType: params.scope,
1544
+ targetId,
1545
+ member
1546
+ };
1547
+ }
1548
+
1325
1549
  // src/application/collab/collabInit.ts
1326
- import fs6 from "fs/promises";
1327
- import path4 from "path";
1550
+ import fs7 from "fs/promises";
1551
+ import path5 from "path";
1328
1552
 
1329
1553
  // src/shared/hash.ts
1330
1554
  import crypto from "crypto";
1331
- import fs4 from "fs";
1555
+ import fs5 from "fs";
1332
1556
  async function sha256FileHex(filePath) {
1333
1557
  const hash = crypto.createHash("sha256");
1334
1558
  await new Promise((resolve, reject) => {
1335
- const stream = fs4.createReadStream(filePath);
1559
+ const stream = fs5.createReadStream(filePath);
1336
1560
  stream.on("data", (chunk) => hash.update(chunk));
1337
1561
  stream.on("error", reject);
1338
1562
  stream.on("end", () => resolve());
@@ -1341,15 +1565,15 @@ async function sha256FileHex(filePath) {
1341
1565
  }
1342
1566
 
1343
1567
  // src/shared/upload.ts
1344
- import fs5 from "fs";
1568
+ import fs6 from "fs";
1345
1569
  import { PassThrough } from "stream";
1346
1570
  async function uploadPresigned(params) {
1347
- const stats = await fs5.promises.stat(params.filePath).catch(() => null);
1571
+ const stats = await fs6.promises.stat(params.filePath).catch(() => null);
1348
1572
  if (!stats || !stats.isFile()) {
1349
1573
  throw new RemixError("Upload file not found.", { exitCode: 2 });
1350
1574
  }
1351
1575
  const totalBytes = stats.size;
1352
- const fileStream = fs5.createReadStream(params.filePath);
1576
+ const fileStream = fs6.createReadStream(params.filePath);
1353
1577
  const pass = new PassThrough();
1354
1578
  let sentBytes = 0;
1355
1579
  fileStream.on("data", (chunk) => {
@@ -1421,6 +1645,7 @@ async function collabInit(params) {
1421
1645
  reused: true,
1422
1646
  projectId: String(existing.projectId),
1423
1647
  appId: String(existing.appId),
1648
+ dashboardUrl: buildDashboardAppUrl(String(existing.appId)),
1424
1649
  upstreamAppId: String(existing.upstreamAppId ?? existing.appId),
1425
1650
  bindingPath: bindingPath2,
1426
1651
  repoRoot,
@@ -1430,7 +1655,7 @@ async function collabInit(params) {
1430
1655
  }
1431
1656
  const { bundlePath, headCommitHash } = await createGitBundle(repoRoot, "repository.bundle");
1432
1657
  const bundleSha = await sha256FileHex(bundlePath);
1433
- const bundleSize = (await fs6.stat(bundlePath)).size;
1658
+ const bundleSize = (await fs7.stat(bundlePath)).size;
1434
1659
  const presignResp = await params.api.presignImportUploadFirstParty({
1435
1660
  file: {
1436
1661
  name: "repository.bundle",
@@ -1447,7 +1672,7 @@ async function collabInit(params) {
1447
1672
  });
1448
1673
  const importResp = await params.api.importFromUploadFirstParty({
1449
1674
  uploadId: String(presign.uploadId),
1450
- appName: params.appName?.trim() || path4.basename(repoRoot),
1675
+ appName: params.appName?.trim() || path5.basename(repoRoot),
1451
1676
  path: params.path?.trim() || void 0,
1452
1677
  platform: "generic",
1453
1678
  isPublic: false,
@@ -1476,6 +1701,7 @@ async function collabInit(params) {
1476
1701
  reused: false,
1477
1702
  projectId: String(app.projectId),
1478
1703
  appId: String(app.id),
1704
+ dashboardUrl: buildDashboardAppUrl(String(app.id)),
1479
1705
  upstreamAppId: String(app.id),
1480
1706
  bindingPath,
1481
1707
  repoRoot,
@@ -1489,22 +1715,6 @@ async function collabInit(params) {
1489
1715
  }
1490
1716
 
1491
1717
  // src/application/collab/collabInvite.ts
1492
- async function resolveScopeTarget(params) {
1493
- if (params.targetId?.trim()) return params.targetId.trim();
1494
- const repoRoot = await findGitRoot(params.cwd);
1495
- const binding = await readCollabBinding(repoRoot);
1496
- if (!binding) {
1497
- throw new RemixError("Repository is not bound to Remix and no explicit target id was provided.", { exitCode: 2 });
1498
- }
1499
- if (params.scope === "project") return binding.projectId;
1500
- if (params.scope === "app") return binding.currentAppId;
1501
- const project = unwrapResponseObject(await params.api.getProject(binding.projectId), "project");
1502
- const organizationId = typeof project.organizationId === "string" ? project.organizationId : null;
1503
- if (!organizationId) {
1504
- throw new RemixError("Could not resolve the organization for the current repository binding.", { exitCode: 2 });
1505
- }
1506
- return organizationId;
1507
- }
1508
1718
  async function collabInvite(params) {
1509
1719
  const scope = params.scope ?? "project";
1510
1720
  const targetId = await resolveScopeTarget({
@@ -1535,9 +1745,9 @@ async function collabList(params) {
1535
1745
  }
1536
1746
 
1537
1747
  // src/application/collab/collabReconcile.ts
1538
- import fs7 from "fs/promises";
1539
- import os3 from "os";
1540
- import path5 from "path";
1748
+ import fs8 from "fs/promises";
1749
+ import os4 from "os";
1750
+ import path6 from "path";
1541
1751
  async function collabReconcile(params) {
1542
1752
  const repoRoot = await findGitRoot(params.cwd);
1543
1753
  const binding = await readCollabBinding(repoRoot);
@@ -1608,13 +1818,13 @@ async function collabReconcile(params) {
1608
1818
  return previewResult;
1609
1819
  }
1610
1820
  const { bundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(repoRoot, "reconcile-local.bundle");
1611
- const bundleTempDir = path5.dirname(bundlePath);
1821
+ const bundleTempDir = path6.dirname(bundlePath);
1612
1822
  try {
1613
- const bundleStat = await fs7.stat(bundlePath);
1823
+ const bundleStat = await fs8.stat(bundlePath);
1614
1824
  const checksumSha256 = await sha256FileHex(bundlePath);
1615
1825
  const presignResp = await params.api.presignImportUploadFirstParty({
1616
1826
  file: {
1617
- name: path5.basename(bundlePath),
1827
+ name: path6.basename(bundlePath),
1618
1828
  mimeType: "application/x-git-bundle",
1619
1829
  size: bundleStat.size,
1620
1830
  checksumSha256
@@ -1673,10 +1883,10 @@ async function collabReconcile(params) {
1673
1883
  });
1674
1884
  await hardResetToCommit(lockedRepoRoot, mergeBaseCommitHash, "`remix collab reconcile`");
1675
1885
  const bundleResp = await params.api.downloadAppReconcileBundle(binding.currentAppId, reconcile.id);
1676
- const resultTempDir = await fs7.mkdtemp(path5.join(os3.tmpdir(), "remix-reconcile-"));
1677
- const resultBundlePath = path5.join(resultTempDir, bundleResp.fileName ?? "reconcile-result.bundle");
1886
+ const resultTempDir = await fs8.mkdtemp(path6.join(os4.tmpdir(), "remix-reconcile-"));
1887
+ const resultBundlePath = path6.join(resultTempDir, bundleResp.fileName ?? "reconcile-result.bundle");
1678
1888
  try {
1679
- await fs7.writeFile(resultBundlePath, bundleResp.data);
1889
+ await fs8.writeFile(resultBundlePath, bundleResp.data);
1680
1890
  await importGitBundle(lockedRepoRoot, resultBundlePath, resultBundleRef);
1681
1891
  await ensureCommitExists(lockedRepoRoot, reconciledHeadCommitHash);
1682
1892
  const localCommitHash = await fastForwardToCommit(lockedRepoRoot, reconciledHeadCommitHash);
@@ -1697,12 +1907,12 @@ async function collabReconcile(params) {
1697
1907
  warnings: Array.from(/* @__PURE__ */ new Set([...previewResult.warnings, ...warnings]))
1698
1908
  };
1699
1909
  } finally {
1700
- await fs7.rm(resultTempDir, { recursive: true, force: true });
1910
+ await fs8.rm(resultTempDir, { recursive: true, force: true });
1701
1911
  }
1702
1912
  }
1703
1913
  );
1704
1914
  } finally {
1705
- await fs7.rm(bundleTempDir, { recursive: true, force: true });
1915
+ await fs8.rm(bundleTempDir, { recursive: true, force: true });
1706
1916
  }
1707
1917
  }
1708
1918
 
@@ -1713,83 +1923,6 @@ async function collabReject(params) {
1713
1923
  }
1714
1924
 
1715
1925
  // src/application/collab/collabRemix.ts
1716
- import fs8 from "fs/promises";
1717
- import os4 from "os";
1718
- import path6 from "path";
1719
- async function pathExists(targetPath) {
1720
- try {
1721
- await fs8.access(targetPath);
1722
- return true;
1723
- } catch {
1724
- return false;
1725
- }
1726
- }
1727
- async function statIsDirectory(targetPath) {
1728
- const stats = await fs8.stat(targetPath).catch(() => null);
1729
- return Boolean(stats?.isDirectory());
1730
- }
1731
- async function findContainingGitRoot(startPath) {
1732
- let current = path6.resolve(startPath);
1733
- while (true) {
1734
- if (await pathExists(path6.join(current, ".git"))) return current;
1735
- const parent = path6.dirname(current);
1736
- if (parent === current) return null;
1737
- current = parent;
1738
- }
1739
- }
1740
- function isSubpath(parentPath, candidatePath) {
1741
- const relative = path6.relative(parentPath, candidatePath);
1742
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
1743
- }
1744
- function buildPreferredRemixBranch(appId) {
1745
- const normalized = appId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1746
- return `remix/remix/${normalized || "app"}`;
1747
- }
1748
- async function resolveRemixDestination(params) {
1749
- if (params.outputDir?.trim()) {
1750
- const preferredRepoRoot = path6.resolve(params.outputDir.trim());
1751
- const parentDir2 = path6.dirname(preferredRepoRoot);
1752
- if (!await statIsDirectory(parentDir2)) {
1753
- throw new RemixError("Remix output parent directory does not exist.", {
1754
- exitCode: 2,
1755
- hint: `Create the directory first: ${parentDir2}`
1756
- });
1757
- }
1758
- return {
1759
- preferredRepoRoot,
1760
- parentDir: parentDir2,
1761
- explicitOutputDir: true
1762
- };
1763
- }
1764
- const parentDir = path6.resolve(params.cwd);
1765
- if (!await statIsDirectory(parentDir)) {
1766
- throw new RemixError("Remix output parent directory does not exist.", {
1767
- exitCode: 2,
1768
- hint: `Create the directory first: ${parentDir}`
1769
- });
1770
- }
1771
- return {
1772
- preferredRepoRoot: path6.join(parentDir, params.defaultDirName),
1773
- parentDir,
1774
- explicitOutputDir: false
1775
- };
1776
- }
1777
- async function assertSafeRemixDestination(params) {
1778
- const callerGitRoot = await findContainingGitRoot(params.cwd);
1779
- if (callerGitRoot && isSubpath(callerGitRoot, params.repoRoot)) {
1780
- throw new RemixError("Refusing to create a remix checkout inside an existing git repository.", {
1781
- exitCode: 2,
1782
- hint: params.explicitOutputDir ? `Choose a destination outside ${callerGitRoot}.` : `Pass --output-dir outside ${callerGitRoot}.`
1783
- });
1784
- }
1785
- const parentGitRoot = await findContainingGitRoot(params.parentDir);
1786
- if (parentGitRoot && isSubpath(parentGitRoot, params.repoRoot)) {
1787
- throw new RemixError("Refusing to create a remix checkout inside an existing git repository.", {
1788
- exitCode: 2,
1789
- hint: params.explicitOutputDir ? `Choose a destination outside ${parentGitRoot}.` : `Pass --output-dir outside ${parentGitRoot}.`
1790
- });
1791
- }
1792
- }
1793
1926
  async function collabRemix(params) {
1794
1927
  const sourceAppId = params.appId?.trim() || null;
1795
1928
  if (!sourceAppId) {
@@ -1801,54 +1934,30 @@ async function collabRemix(params) {
1801
1934
  const forkResp = await params.api.forkApp(sourceAppId, { name: params.name?.trim() || void 0, platform: "generic" });
1802
1935
  const forked = unwrapResponseObject(forkResp, "fork");
1803
1936
  const app = await pollAppReady(params.api, String(forked.id));
1804
- const baseDirName = sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id));
1805
- const destination = await resolveRemixDestination({
1937
+ const checkout = await materializeAppCheckout({
1938
+ api: params.api,
1806
1939
  cwd: params.cwd,
1940
+ appId: String(app.id),
1807
1941
  outputDir: params.outputDir ?? null,
1808
- defaultDirName: baseDirName
1809
- });
1810
- await assertSafeRemixDestination({
1811
- cwd: params.cwd,
1812
- repoRoot: destination.preferredRepoRoot,
1813
- parentDir: destination.parentDir,
1814
- explicitOutputDir: destination.explicitOutputDir
1942
+ defaultDirName: sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id))
1815
1943
  });
1816
- const repoRoot = destination.explicitOutputDir ? await reserveDirectory(destination.preferredRepoRoot) : await reserveAvailableDirPath(destination.preferredRepoRoot);
1817
- const bundleTempDir = await fs8.mkdtemp(path6.join(os4.tmpdir(), "remix-remix-"));
1818
- const bundlePath = path6.join(bundleTempDir, "repository.bundle");
1819
- try {
1820
- const bundle = await params.api.downloadAppBundle(String(app.id));
1821
- await fs8.writeFile(bundlePath, bundle.data);
1822
- await cloneGitBundleToDirectory(bundlePath, repoRoot);
1823
- await checkoutLocalBranch(repoRoot, buildPreferredRemixBranch(String(app.id)));
1824
- await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
1825
- } catch (err) {
1826
- await fs8.rm(repoRoot, { recursive: true, force: true }).catch(() => {
1827
- });
1828
- throw err;
1829
- } finally {
1830
- await fs8.rm(bundleTempDir, { recursive: true, force: true });
1831
- }
1832
- const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
1833
- const defaultBranch = await getDefaultBranch(repoRoot) ?? await getCurrentBranch(repoRoot) ?? null;
1834
- const preferredBranch = await getCurrentBranch(repoRoot) ?? buildPreferredRemixBranch(String(app.id));
1835
- const repoFingerprint = remoteUrl ? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch }) : null;
1836
- const bindingPath = await writeCollabBinding(repoRoot, {
1944
+ const bindingPath = await writeCollabBinding(checkout.repoRoot, {
1837
1945
  projectId: String(app.projectId),
1838
1946
  currentAppId: String(app.id),
1839
1947
  upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
1840
1948
  threadId: app.threadId ? String(app.threadId) : null,
1841
- repoFingerprint,
1842
- remoteUrl,
1843
- defaultBranch,
1844
- preferredBranch
1949
+ repoFingerprint: checkout.repoFingerprint,
1950
+ remoteUrl: checkout.remoteUrl,
1951
+ defaultBranch: checkout.defaultBranch,
1952
+ preferredBranch: checkout.preferredBranch
1845
1953
  });
1846
1954
  return {
1847
1955
  appId: String(app.id),
1956
+ dashboardUrl: buildDashboardAppUrl(String(app.id)),
1848
1957
  projectId: String(app.projectId),
1849
1958
  upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
1850
1959
  bindingPath,
1851
- repoRoot
1960
+ repoRoot: checkout.repoRoot
1852
1961
  };
1853
1962
  }
1854
1963
 
@@ -2205,9 +2314,11 @@ async function collabView(params) {
2205
2314
  export {
2206
2315
  collabAdd,
2207
2316
  collabApprove,
2317
+ collabCheckout,
2208
2318
  collabInit,
2209
2319
  collabInvite,
2210
2320
  collabList,
2321
+ collabListMembers,
2211
2322
  collabListMergeRequests,
2212
2323
  collabReconcile,
2213
2324
  collabRecordTurn,
@@ -2218,5 +2329,8 @@ export {
2218
2329
  collabStatus,
2219
2330
  collabSync,
2220
2331
  collabSyncUpstream,
2221
- collabView
2332
+ collabUpdateMemberRole,
2333
+ collabView,
2334
+ getMemberRolesForScope,
2335
+ validateMemberRole
2222
2336
  };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-R44EOUS4.js";
3
+ } from "./chunk-4276ARDF.js";
4
4
  import {
5
5
  createLocalSessionStore,
6
6
  createStoredSessionTokenProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixhq/core",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Remix core library",
5
5
  "homepage": "https://github.com/RemixDotOne/remix-core",
6
6
  "license": "MIT",