@prisma/cli 3.0.0-beta.11 → 3.0.0-beta.13

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.
@@ -4,6 +4,18 @@ import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { CredentialsStore } from "@prisma/credentials-store";
6
6
  //#region src/adapters/token-storage.ts
7
+ const REFRESH_LOCK_RETRY_MS = 100;
8
+ const REFRESH_LOCK_STALE_MS = 3e4;
9
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS = 25e3;
10
+ const EMPTY_AUTH_CONTEXT = {
11
+ activeWorkspaceId: null,
12
+ workspaces: {}
13
+ };
14
+ function getAuthContextFilePath(authFilePath) {
15
+ const extension = path.extname(authFilePath);
16
+ if (!extension) return `${authFilePath}.context.json`;
17
+ return `${authFilePath.slice(0, -extension.length)}.context${extension}`;
18
+ }
7
19
  function findLatestValidTokens(allCredentials) {
8
20
  for (let i = allCredentials.length - 1; i >= 0; i -= 1) {
9
21
  const credential = allCredentials[i];
@@ -17,6 +29,18 @@ function findLatestValidTokens(allCredentials) {
17
29
  }
18
30
  return null;
19
31
  }
32
+ function storedCredentialToTokens(credential) {
33
+ if (!credential) return null;
34
+ if (typeof credential.workspaceId !== "string" || credential.workspaceId.length === 0 || typeof credential.token !== "string" || credential.token.length === 0 || typeof credential.refreshToken !== "string" || credential.refreshToken.length === 0) return null;
35
+ return {
36
+ workspaceId: credential.workspaceId,
37
+ accessToken: credential.token,
38
+ refreshToken: credential.refreshToken
39
+ };
40
+ }
41
+ function findTokensForWorkspace(allCredentials, workspaceId) {
42
+ return storedCredentialToTokens(allCredentials.find((credential) => credential?.workspaceId === workspaceId)) ?? null;
43
+ }
20
44
  function tokensEqual(a, b) {
21
45
  return a?.workspaceId === b?.workspaceId && a?.accessToken === b?.accessToken && a?.refreshToken === b?.refreshToken;
22
46
  }
@@ -36,25 +60,44 @@ function sleep(ms, signal) {
36
60
  }
37
61
  var FileTokenStorage = class {
38
62
  credentialsStore;
63
+ authFilePath;
64
+ contextFilePath;
39
65
  lockFilePath;
40
- constructor(env = process.env, signal) {
66
+ constructor(env = process.env, signal, options = {}) {
41
67
  this.signal = signal;
68
+ this.options = options;
42
69
  const authFilePath = getAuthFilePath(env);
70
+ this.authFilePath = authFilePath;
71
+ this.contextFilePath = getAuthContextFilePath(authFilePath);
43
72
  this.credentialsStore = new CredentialsStore(authFilePath);
44
73
  this.lockFilePath = `${authFilePath}.lock`;
45
74
  }
46
75
  async getTokens() {
47
76
  this.signal?.throwIfAborted();
48
77
  try {
49
- const all = await this.credentialsStore.getCredentials();
50
- this.signal?.throwIfAborted();
51
- return findLatestValidTokens(all);
78
+ const credentials = await this.readCredentialsFromDisk();
79
+ const context = await this.readAuthContext();
80
+ if (context.state.activeWorkspaceId) return findTokensForWorkspace(credentials, context.state.activeWorkspaceId);
81
+ const latest = findLatestValidTokens(credentials);
82
+ if (!latest) return null;
83
+ if (!context.exists) {
84
+ await this.setActiveWorkspaceId(latest.workspaceId);
85
+ return latest;
86
+ }
87
+ return null;
52
88
  } catch (_error) {
53
89
  if (this.signal?.aborted) throw this.signal.reason;
54
90
  return null;
55
91
  }
56
92
  }
57
93
  async setTokens(tokens) {
94
+ if (this.options.lockSetTokens === false) {
95
+ await this.setTokensUnlocked(tokens);
96
+ return;
97
+ }
98
+ await this.withRefreshLock(() => this.setTokensUnlocked(tokens));
99
+ }
100
+ async setTokensUnlocked(tokens) {
58
101
  this.signal?.throwIfAborted();
59
102
  await this.credentialsStore.storeCredentials({
60
103
  workspaceId: tokens.workspaceId,
@@ -62,20 +105,115 @@ var FileTokenStorage = class {
62
105
  refreshToken: tokens.refreshToken
63
106
  });
64
107
  this.signal?.throwIfAborted();
108
+ await this.rememberWorkspaceUnlocked(tokens.workspaceId, {
109
+ id: tokens.workspaceId,
110
+ name: tokens.workspaceId
111
+ });
112
+ await this.maybeActivateWorkspaceId(tokens.workspaceId);
65
113
  }
66
114
  async clearTokens() {
115
+ await this.withRefreshLock(() => this.clearTokensUnlocked());
116
+ }
117
+ async clearTokensUnlocked() {
67
118
  this.signal?.throwIfAborted();
68
- const all = await this.credentialsStore.getCredentials();
119
+ await fs.mkdir(path.dirname(this.authFilePath), { recursive: true });
69
120
  this.signal?.throwIfAborted();
70
- const tokens = findLatestValidTokens(all);
71
- if (!tokens) return;
72
- await this.credentialsStore.deleteCredentials(tokens.workspaceId);
121
+ await fs.writeFile(this.authFilePath, `${JSON.stringify({ tokens: [] }, null, 2)}\n`, {
122
+ encoding: "utf8",
123
+ signal: this.signal
124
+ });
125
+ await this.clearAuthContext();
126
+ await this.credentialsStore.reloadCredentialsFromDisk();
73
127
  this.signal?.throwIfAborted();
74
128
  }
75
129
  async clearTokensIfCurrent(tokens) {
76
130
  this.signal?.throwIfAborted();
77
131
  if (!tokensEqual(await this.getTokens(), tokens)) return;
78
- await this.clearTokens();
132
+ await this.credentialsStore.deleteCredentials(tokens.workspaceId);
133
+ await this.removeRememberedWorkspace(tokens.workspaceId, { preserveActivePointer: true });
134
+ this.signal?.throwIfAborted();
135
+ }
136
+ async listWorkspaces() {
137
+ this.signal?.throwIfAborted();
138
+ const credentials = await this.readCredentialsFromDisk();
139
+ const context = await this.ensureMigratedAuthContext(credentials);
140
+ return credentials.map((credential) => storedCredentialToTokens(credential)).filter((tokens) => tokens !== null).map((tokens) => {
141
+ const cached = context.state.workspaces[tokens.workspaceId];
142
+ const id = typeof cached?.id === "string" && cached.id.trim().length > 0 ? cached.id.trim() : tokens.workspaceId;
143
+ const name = typeof cached?.name === "string" && cached.name.trim().length > 0 ? cached.name.trim() : id;
144
+ const lastSeenAt = typeof cached?.lastSeenAt === "string" && cached.lastSeenAt.trim().length > 0 ? cached.lastSeenAt.trim() : null;
145
+ return {
146
+ id,
147
+ name,
148
+ credentialWorkspaceId: tokens.workspaceId,
149
+ active: context.state.activeWorkspaceId === tokens.workspaceId,
150
+ lastSeenAt
151
+ };
152
+ });
153
+ }
154
+ async useWorkspace(workspaceRef) {
155
+ return this.withRefreshLock(() => this.useWorkspaceUnlocked(workspaceRef));
156
+ }
157
+ async useWorkspaceUnlocked(workspaceRef) {
158
+ const ref = workspaceRef.trim();
159
+ if (!ref) throw new WorkspaceSelectionError("missing");
160
+ const workspaces = await this.listWorkspaces();
161
+ const context = await this.readAuthContext();
162
+ const previous = workspaces.find((workspace) => workspace.credentialWorkspaceId === context.state.activeWorkspaceId) ?? null;
163
+ const matches = workspaces.filter((workspace) => workspaceMatchesRef(workspace, ref));
164
+ if (matches.length === 0) throw new WorkspaceSelectionError("not-found", ref);
165
+ if (matches.length > 1) throw new WorkspaceSelectionError("ambiguous", ref, matches);
166
+ const selected = matches[0];
167
+ await this.setActiveWorkspaceId(selected.credentialWorkspaceId);
168
+ return {
169
+ previous,
170
+ selected
171
+ };
172
+ }
173
+ async logoutWorkspace(workspaceRef) {
174
+ return this.withRefreshLock(() => this.logoutWorkspaceUnlocked(workspaceRef));
175
+ }
176
+ async logoutWorkspaceUnlocked(workspaceRef) {
177
+ const ref = workspaceRef.trim();
178
+ if (!ref) throw new WorkspaceSelectionError("missing");
179
+ const workspaces = await this.listWorkspaces();
180
+ const context = await this.readAuthContext();
181
+ const matches = workspaces.filter((workspace) => workspaceMatchesRef(workspace, ref));
182
+ if (matches.length === 0) throw new WorkspaceSelectionError("not-found", ref);
183
+ if (matches.length > 1) throw new WorkspaceSelectionError("ambiguous", ref, matches);
184
+ const workspace = matches[0];
185
+ const wasActive = context.state.activeWorkspaceId === workspace.credentialWorkspaceId;
186
+ await this.credentialsStore.deleteCredentials(workspace.credentialWorkspaceId);
187
+ this.signal?.throwIfAborted();
188
+ const remainingWorkspaces = workspaces.filter((candidate) => candidate.credentialWorkspaceId !== workspace.credentialWorkspaceId);
189
+ if (remainingWorkspaces.length === 0) {
190
+ await this.clearAuthContext();
191
+ return {
192
+ workspace,
193
+ wasActive,
194
+ activeWorkspace: null
195
+ };
196
+ }
197
+ delete context.state.workspaces[workspace.credentialWorkspaceId];
198
+ if (wasActive) context.state.activeWorkspaceId = null;
199
+ await this.writeAuthContext(context.state);
200
+ return {
201
+ workspace,
202
+ wasActive,
203
+ activeWorkspace: context.state.activeWorkspaceId === null ? null : remainingWorkspaces.find((candidate) => candidate.credentialWorkspaceId === context.state.activeWorkspaceId) ?? null
204
+ };
205
+ }
206
+ async rememberWorkspace(credentialWorkspaceId, workspace) {
207
+ await this.withRefreshLock(() => this.rememberWorkspaceUnlocked(credentialWorkspaceId, workspace));
208
+ }
209
+ async rememberWorkspaceUnlocked(credentialWorkspaceId, workspace) {
210
+ const context = await this.readAuthContext();
211
+ context.state.workspaces[credentialWorkspaceId] = {
212
+ id: workspace.id,
213
+ name: workspace.name,
214
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
215
+ };
216
+ await this.writeAuthContext(context.state);
79
217
  }
80
218
  async withRefreshLock(fn) {
81
219
  const lockId = await this.acquireRefreshLock();
@@ -87,34 +225,48 @@ var FileTokenStorage = class {
87
225
  }
88
226
  async acquireRefreshLock() {
89
227
  const lockId = randomUUID();
228
+ const startedAt = Date.now();
229
+ const retryMs = this.options.lockRetryMs ?? REFRESH_LOCK_RETRY_MS;
230
+ const waitTimeoutMs = this.options.lockWaitTimeoutMs ?? REFRESH_LOCK_WAIT_TIMEOUT_MS;
90
231
  this.signal?.throwIfAborted();
91
232
  await fs.mkdir(path.dirname(this.lockFilePath), { recursive: true });
92
233
  while (true) {
93
234
  this.signal?.throwIfAborted();
94
- let lockFileCreated = false;
235
+ if (await this.tryCreateRefreshLock(lockId)) return lockId;
236
+ if (await this.releaseStaleRefreshLock()) continue;
237
+ this.throwIfRefreshLockWaitTimedOut(startedAt, waitTimeoutMs);
238
+ await sleep(retryMs, this.signal);
239
+ }
240
+ }
241
+ async tryCreateRefreshLock(lockId) {
242
+ let lockFileCreated = false;
243
+ try {
244
+ const handle = await fs.open(this.lockFilePath, "wx");
245
+ lockFileCreated = true;
95
246
  try {
96
- const handle = await fs.open(this.lockFilePath, "wx");
97
- lockFileCreated = true;
98
- try {
99
- this.signal?.throwIfAborted();
100
- await handle.writeFile(lockId, { encoding: "utf8" });
101
- this.signal?.throwIfAborted();
102
- } finally {
103
- await handle.close();
104
- }
105
- return lockId;
106
- } catch (error) {
107
- if (lockFileCreated) await fs.unlink(this.lockFilePath).catch(() => void 0);
108
- if (error.code !== "EEXIST") throw error;
109
- const staleLockId = await this.getStaleRefreshLockId();
110
- if (staleLockId) {
111
- await this.releaseRefreshLock(staleLockId);
112
- continue;
113
- }
114
- await sleep(100, this.signal);
247
+ this.signal?.throwIfAborted();
248
+ await handle.writeFile(lockId, { encoding: "utf8" });
249
+ this.signal?.throwIfAborted();
250
+ } finally {
251
+ await handle.close();
115
252
  }
253
+ return true;
254
+ } catch (error) {
255
+ if (lockFileCreated) await fs.unlink(this.lockFilePath).catch(() => void 0);
256
+ if (error.code === "EEXIST") return false;
257
+ throw error;
116
258
  }
117
259
  }
260
+ async releaseStaleRefreshLock() {
261
+ const staleLockId = await this.getStaleRefreshLockId();
262
+ if (!staleLockId) return false;
263
+ await this.releaseRefreshLock(staleLockId);
264
+ return true;
265
+ }
266
+ throwIfRefreshLockWaitTimedOut(startedAt, waitTimeoutMs) {
267
+ if (Date.now() - startedAt < waitTimeoutMs) return;
268
+ throw new RefreshLockTimeoutError(this.lockFilePath, waitTimeoutMs);
269
+ }
118
270
  async getStaleRefreshLockId() {
119
271
  this.signal?.throwIfAborted();
120
272
  const lockId = await fs.readFile(this.lockFilePath, {
@@ -129,12 +281,130 @@ var FileTokenStorage = class {
129
281
  const stats = await fs.stat(this.lockFilePath).catch(() => null);
130
282
  this.signal?.throwIfAborted();
131
283
  if (!stats) return null;
132
- return Date.now() - stats.mtimeMs > 3e4 ? lockId : null;
284
+ const staleMs = this.options.lockStaleMs ?? REFRESH_LOCK_STALE_MS;
285
+ return Date.now() - stats.mtimeMs > staleMs ? lockId : null;
133
286
  }
134
287
  async releaseRefreshLock(lockId) {
135
288
  if (await fs.readFile(this.lockFilePath, { encoding: "utf8" }).catch(() => null) !== lockId) return;
136
289
  await fs.unlink(this.lockFilePath).catch(() => {});
137
290
  }
291
+ async readCredentialsFromDisk() {
292
+ this.signal?.throwIfAborted();
293
+ await this.credentialsStore.reloadCredentialsFromDisk();
294
+ this.signal?.throwIfAborted();
295
+ return await this.credentialsStore.getCredentials();
296
+ }
297
+ async ensureMigratedAuthContext(credentials) {
298
+ const context = await this.readAuthContext();
299
+ if (context.state.activeWorkspaceId || context.exists) return context;
300
+ const latest = findLatestValidTokens(credentials);
301
+ if (!latest) return context;
302
+ context.state.activeWorkspaceId = latest.workspaceId;
303
+ context.state.workspaces[latest.workspaceId] ??= {
304
+ id: latest.workspaceId,
305
+ name: latest.workspaceId,
306
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
307
+ };
308
+ await this.writeAuthContext(context.state);
309
+ return {
310
+ exists: true,
311
+ state: context.state
312
+ };
313
+ }
314
+ async setActiveWorkspaceId(workspaceId) {
315
+ const context = await this.readAuthContext();
316
+ context.state.activeWorkspaceId = workspaceId;
317
+ context.state.workspaces[workspaceId] ??= {
318
+ id: workspaceId,
319
+ name: workspaceId,
320
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
321
+ };
322
+ await this.writeAuthContext(context.state);
323
+ }
324
+ async maybeActivateWorkspaceId(workspaceId) {
325
+ const context = await this.readAuthContext();
326
+ if (this.options.activateOnSetTokens === false && context.exists && context.state.activeWorkspaceId && context.state.activeWorkspaceId !== workspaceId) return;
327
+ context.state.activeWorkspaceId = workspaceId;
328
+ context.state.workspaces[workspaceId] ??= {
329
+ id: workspaceId,
330
+ name: workspaceId,
331
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
332
+ };
333
+ await this.writeAuthContext(context.state);
334
+ }
335
+ async removeRememberedWorkspace(workspaceId, options) {
336
+ const context = await this.readAuthContext();
337
+ delete context.state.workspaces[workspaceId];
338
+ if (!options.preserveActivePointer && context.state.activeWorkspaceId === workspaceId) context.state.activeWorkspaceId = null;
339
+ await this.writeAuthContext(context.state);
340
+ }
341
+ async readAuthContext() {
342
+ this.signal?.throwIfAborted();
343
+ const raw = await fs.readFile(this.contextFilePath, {
344
+ encoding: "utf8",
345
+ signal: this.signal
346
+ }).catch((error) => {
347
+ if (this.signal?.aborted) throw error;
348
+ if (error.code === "ENOENT") return null;
349
+ throw error;
350
+ });
351
+ if (raw === null) return {
352
+ exists: false,
353
+ state: structuredClone(EMPTY_AUTH_CONTEXT)
354
+ };
355
+ try {
356
+ const parsed = JSON.parse(raw);
357
+ return {
358
+ exists: true,
359
+ state: {
360
+ activeWorkspaceId: typeof parsed.activeWorkspaceId === "string" && parsed.activeWorkspaceId.trim().length > 0 ? parsed.activeWorkspaceId.trim() : null,
361
+ workspaces: parsed.workspaces && typeof parsed.workspaces === "object" && !Array.isArray(parsed.workspaces) ? parsed.workspaces : {}
362
+ }
363
+ };
364
+ } catch {
365
+ return {
366
+ exists: false,
367
+ state: structuredClone(EMPTY_AUTH_CONTEXT)
368
+ };
369
+ }
370
+ }
371
+ async writeAuthContext(state) {
372
+ this.signal?.throwIfAborted();
373
+ await fs.mkdir(path.dirname(this.contextFilePath), { recursive: true });
374
+ this.signal?.throwIfAborted();
375
+ await fs.writeFile(this.contextFilePath, `${JSON.stringify(state, null, 2)}\n`, {
376
+ encoding: "utf8",
377
+ signal: this.signal
378
+ });
379
+ this.signal?.throwIfAborted();
380
+ }
381
+ async clearAuthContext() {
382
+ await fs.unlink(this.contextFilePath).catch((error) => {
383
+ if (error.code === "ENOENT") return;
384
+ throw error;
385
+ });
386
+ }
387
+ };
388
+ var WorkspaceSelectionError = class extends Error {
389
+ constructor(reason, workspaceRef, matches = []) {
390
+ super(reason);
391
+ this.reason = reason;
392
+ this.workspaceRef = workspaceRef;
393
+ this.matches = matches;
394
+ this.name = "WorkspaceSelectionError";
395
+ }
138
396
  };
397
+ var RefreshLockTimeoutError = class extends Error {
398
+ constructor(lockFilePath, waitTimeoutMs) {
399
+ super(`Timed out waiting ${waitTimeoutMs}ms for auth refresh lock at ${lockFilePath}`);
400
+ this.name = "RefreshLockTimeoutError";
401
+ }
402
+ };
403
+ function workspaceMatchesRef(workspace, ref) {
404
+ return workspace.credentialWorkspaceId === ref || workspace.id === ref || stripWorkspacePrefix(workspace.credentialWorkspaceId) === stripWorkspacePrefix(ref) || stripWorkspacePrefix(workspace.id) === stripWorkspacePrefix(ref) || workspace.name.toLowerCase() === ref.toLowerCase();
405
+ }
406
+ function stripWorkspacePrefix(value) {
407
+ return value.startsWith("wksp_") ? value.slice(5) : value;
408
+ }
139
409
  //#endregion
140
- export { FileTokenStorage };
410
+ export { FileTokenStorage, WorkspaceSelectionError };
@@ -1,8 +1,8 @@
1
1
  import { usageError } from "../../shell/errors.js";
2
+ import { APP_BUILD_TYPES } from "../../lib/app/build.js";
2
3
  import { attachCommandDescriptor } from "../../shell/command-meta.js";
3
4
  import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
4
5
  import { configureRuntimeCommand } from "../../shell/runtime.js";
5
- import { PREVIEW_BUILD_TYPES } from "../../lib/app/preview-build.js";
6
6
  import { runAppBuild, runAppDeploy, runAppDomainAdd, runAppDomainRemove, runAppDomainRetry, runAppDomainShow, runAppDomainWait, runAppListDeploys, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy } from "../../controllers/app.js";
7
7
  import { isAppDeployAllResult, renderAppBuild, renderAppDeploy, renderAppDeployAll, renderAppDomainAdd, renderAppDomainRemove, renderAppDomainRetry, renderAppDomainShow, renderAppListDeploys, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, serializeAppBuild, serializeAppDeploy, serializeAppDeployAll, serializeAppDomainAdd, serializeAppDomainRemove, serializeAppDomainRetry, serializeAppDomainShow, serializeAppListDeploys, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy } from "../../presenters/app.js";
8
8
  import { runCommand, runStreamingCommand } from "../../shell/command-runner.js";
@@ -28,7 +28,7 @@ function createAppCommand(runtime) {
28
28
  }
29
29
  function createBuildCommand(runtime) {
30
30
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("build"), runtime), "app.build");
31
- command.argument("[app]", "App target from prisma.compute.ts when the config defines multiple apps").addOption(new Option("--entry <path>", "Entrypoint path for Bun or auto builds")).addOption(new Option("--build-type <type>", "Local build type").choices([...PREVIEW_BUILD_TYPES]).default("auto"));
31
+ command.argument("[app]", "App target from prisma.compute.ts when the config defines multiple apps").addOption(new Option("--entry <path>", "Entrypoint path for Bun or auto builds")).addOption(new Option("--build-type <type>", "Local build type").choices([...APP_BUILD_TYPES]).default("auto"));
32
32
  addGlobalFlags(command);
33
33
  command.action(async (configTarget, options) => {
34
34
  const entry = options.entry;
@@ -1,9 +1,9 @@
1
1
  import { attachCommandDescriptor } from "../../shell/command-meta.js";
2
2
  import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
3
3
  import { configureRuntimeCommand } from "../../shell/runtime.js";
4
- import { runAuthLogin, runAuthLogout, runAuthWhoAmI } from "../../controllers/auth.js";
4
+ import { runAuthLogin, runAuthLogout, runAuthWhoAmI, runAuthWorkspaceList, runAuthWorkspaceLogout, runAuthWorkspaceSelect, runAuthWorkspaceUse } from "../../controllers/auth.js";
5
5
  import { runCommand } from "../../shell/command-runner.js";
6
- import { renderAuthSuccess } from "../../presenters/auth.js";
6
+ import { renderAuthSuccess, renderAuthWorkspaceList, renderAuthWorkspaceLogout, renderAuthWorkspaceUse, serializeAuthWorkspaceList, serializeAuthWorkspaceLogout, serializeAuthWorkspaceUse } from "../../presenters/auth.js";
7
7
  import { Command, Option } from "commander";
8
8
  //#region src/commands/auth/index.ts
9
9
  function createAuthCommand(runtime) {
@@ -12,6 +12,7 @@ function createAuthCommand(runtime) {
12
12
  auth.addCommand(createAuthLoginCommand(runtime));
13
13
  auth.addCommand(createAuthLogoutCommand(runtime));
14
14
  auth.addCommand(createAuthWhoAmICommand(runtime));
15
+ auth.addCommand(createAuthWorkspaceCommand(runtime));
15
16
  return auth;
16
17
  }
17
18
  function createAuthLoginCommand(runtime) {
@@ -25,8 +26,17 @@ function createAuthLoginCommand(runtime) {
25
26
  }
26
27
  function createAuthLogoutCommand(runtime) {
27
28
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("logout"), runtime), "auth.logout");
29
+ command.addOption(new Option("--workspace <id-or-name>", "Remove one stored OAuth workspace session"));
28
30
  addGlobalFlags(command);
29
31
  command.action(async (options) => {
32
+ const logoutOptions = options;
33
+ if (logoutOptions.workspace?.trim()) {
34
+ await runCommand(runtime, "auth.workspace.logout", options, (context) => runAuthWorkspaceLogout(context, logoutOptions.workspace), {
35
+ renderHuman: (context, descriptor, result) => renderAuthWorkspaceLogout(context, descriptor, result),
36
+ renderJson: (result) => serializeAuthWorkspaceLogout(result)
37
+ });
38
+ return;
39
+ }
30
40
  await runCommand(runtime, "auth.logout", options, (context) => runAuthLogout(context), { renderHuman: (context, descriptor, result) => renderAuthSuccess(context, descriptor, "auth.logout", result) });
31
41
  });
32
42
  return command;
@@ -39,5 +49,60 @@ function createAuthWhoAmICommand(runtime) {
39
49
  });
40
50
  return command;
41
51
  }
52
+ function createAuthWorkspaceCommand(runtime) {
53
+ const workspace = attachCommandDescriptor(configureRuntimeCommand(new Command("workspace"), runtime), "auth.workspace");
54
+ addCompactGlobalFlags(workspace);
55
+ workspace.addCommand(createAuthWorkspaceListCommand(runtime));
56
+ workspace.addCommand(createAuthWorkspaceUseCommand(runtime));
57
+ workspace.addCommand(createAuthWorkspaceSelectCommand(runtime));
58
+ workspace.addCommand(createAuthWorkspaceLogoutCommand(runtime));
59
+ return workspace;
60
+ }
61
+ function createAuthWorkspaceListCommand(runtime) {
62
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "auth.workspace.list");
63
+ addGlobalFlags(command);
64
+ command.action(async (options) => {
65
+ await runCommand(runtime, "auth.workspace.list", options, (context) => runAuthWorkspaceList(context), {
66
+ renderHuman: (context, descriptor, result) => renderAuthWorkspaceList(context, descriptor, result),
67
+ renderJson: (result) => serializeAuthWorkspaceList(result)
68
+ });
69
+ });
70
+ return command;
71
+ }
72
+ function createAuthWorkspaceSelectCommand(runtime) {
73
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("select"), runtime), "auth.workspace.select");
74
+ addGlobalFlags(command);
75
+ command.action(async (options) => {
76
+ await runCommand(runtime, "auth.workspace.select", options, (context) => runAuthWorkspaceSelect(context), {
77
+ renderHuman: (context, descriptor, result) => renderAuthWorkspaceUse(context, descriptor, result),
78
+ renderJson: (result) => serializeAuthWorkspaceUse(result)
79
+ });
80
+ });
81
+ return command;
82
+ }
83
+ function createAuthWorkspaceLogoutCommand(runtime) {
84
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("logout"), runtime), "auth.workspace.logout");
85
+ command.argument("<id-or-name>", "Workspace id or exact name");
86
+ addGlobalFlags(command);
87
+ command.action(async (workspaceRef, options) => {
88
+ await runCommand(runtime, "auth.workspace.logout", options, (context) => runAuthWorkspaceLogout(context, typeof workspaceRef === "string" ? workspaceRef : void 0), {
89
+ renderHuman: (context, descriptor, result) => renderAuthWorkspaceLogout(context, descriptor, result),
90
+ renderJson: (result) => serializeAuthWorkspaceLogout(result)
91
+ });
92
+ });
93
+ return command;
94
+ }
95
+ function createAuthWorkspaceUseCommand(runtime) {
96
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("use"), runtime), "auth.workspace.use");
97
+ command.argument("<id-or-name>", "Workspace id or exact name");
98
+ addGlobalFlags(command);
99
+ command.action(async (workspaceRef, options) => {
100
+ await runCommand(runtime, "auth.workspace.use", options, (context) => runAuthWorkspaceUse(context, typeof workspaceRef === "string" ? workspaceRef : void 0), {
101
+ renderHuman: (context, descriptor, result) => renderAuthWorkspaceUse(context, descriptor, result),
102
+ renderJson: (result) => serializeAuthWorkspaceUse(result)
103
+ });
104
+ });
105
+ return command;
106
+ }
42
107
  //#endregion
43
108
  export { createAuthCommand };
@@ -1,7 +1,7 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
- import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
3
2
  import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
4
3
  import { readEnvFileAssignments } from "../lib/app/env-file.js";
4
+ import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
5
5
  import { requireComputeAuth } from "../lib/auth/guard.js";
6
6
  import { readLocalGitBranch } from "../lib/git/local-branch.js";
7
7
  import { requireAuthenticatedAuthState } from "./auth.js";