@autohq/cli 0.1.86 → 0.1.88

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.
@@ -21330,6 +21330,21 @@ var SessionApplyTriggersSchema = external_exports.array(
21330
21330
  ).transform(expandSessionApplyTriggerDefinitions).superRefine(validateAttributedRunsTriggerPairing);
21331
21331
  var AVATAR_ASSET_EXTENSIONS = [".png", ".jpg", ".jpeg"];
21332
21332
  var MAX_AVATAR_ASSET_BYTES = 2 * 1024 * 1024;
21333
+ var SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH = 140;
21334
+ function sessionIdentityDescriptionLength(value2) {
21335
+ let length = 0;
21336
+ for (const char of value2) {
21337
+ const codePoint = char.codePointAt(0) ?? 0;
21338
+ if (codePoint < 32 || codePoint > 126) {
21339
+ length += 6 * char.length;
21340
+ } else if (char === '"' || char === "\\" || char === "/") {
21341
+ length += 2;
21342
+ } else {
21343
+ length += 1;
21344
+ }
21345
+ }
21346
+ return length;
21347
+ }
21333
21348
  var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
21334
21349
  function isAvatarAssetPathShapeValid(asset) {
21335
21350
  const normalized = asset.trim().replaceAll("\\", "/");
@@ -21353,7 +21368,12 @@ var SessionIdentitySchema = external_exports.object({
21353
21368
  // overwritten, so it is never trusted input.
21354
21369
  sha256: external_exports.string().regex(SHA256_HEX_PATTERN).optional()
21355
21370
  }).strict().optional(),
21356
- description: external_exports.string().trim().min(1).max(256).optional()
21371
+ description: external_exports.string().trim().min(1).refine(
21372
+ (value2) => sessionIdentityDescriptionLength(value2) <= SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH,
21373
+ {
21374
+ message: `description must be at most ${SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH} characters as Slack counts them (a non-ASCII character like "\u2014" counts as 6)`
21375
+ }
21376
+ ).optional()
21357
21377
  }).strict().refine((identity) => Object.keys(identity).length > 0, {
21358
21378
  message: "Session identity requires at least one field"
21359
21379
  });
package/dist/index.js CHANGED
@@ -16989,6 +16989,20 @@ var init_trigger_router = __esm({
16989
16989
  });
16990
16990
 
16991
16991
  // ../../packages/schemas/src/sessions.ts
16992
+ function sessionIdentityDescriptionLength(value) {
16993
+ let length = 0;
16994
+ for (const char of value) {
16995
+ const codePoint = char.codePointAt(0) ?? 0;
16996
+ if (codePoint < 32 || codePoint > 126) {
16997
+ length += 6 * char.length;
16998
+ } else if (char === '"' || char === "\\" || char === "/") {
16999
+ length += 2;
17000
+ } else {
17001
+ length += 1;
17002
+ }
17003
+ }
17004
+ return length;
17005
+ }
16992
17006
  function isAvatarAssetPathShapeValid(asset) {
16993
17007
  const normalized = asset.trim().replaceAll("\\", "/");
16994
17008
  if (normalized.length === 0) return false;
@@ -17122,7 +17136,7 @@ function isChatMessageEvent(trigger) {
17122
17136
  function hasFilterValue(trigger, path, expected) {
17123
17137
  return trigger.where?.[path] === expected;
17124
17138
  }
17125
- var RESOURCE_KIND_SESSION, TriggerFilterScalarSchema, TriggerFilterPathSchema, TriggerFilterClauseSchema, TriggerFilterSchema, SessionTriggerCheckTimeoutSchema, TriggerEventSchema, TriggerEventsSchema, SessionTriggerSharedFields, SessionTriggerEventSourceFields, SessionTriggerBaseSchema, SessionTriggerDefinitionBaseSchema, SessionTriggerSchema, SessionApplyTriggerSchema, SessionHeartbeatTriggerBaseSchema, SessionHeartbeatTriggerSchema, SessionApplyHeartbeatTriggerSchema, SessionTriggerDefinitionSchema, SessionApplyTriggerDefinitionSchema, SessionTriggersSchema, SessionApplyTriggersSchema, AVATAR_ASSET_EXTENSIONS, MAX_AVATAR_ASSET_BYTES, SHA256_HEX_PATTERN, SessionIdentitySchema, SessionSpecSchema, SessionApplySpecSchema, SessionStatusSchema, SessionResourceSchema, SessionApplyRequestSchema, SessionApplyTriggerReceiptSchema, SessionApplyResponseSchema, SESSION_TELEGRAM_IDENTITY_STATUSES, SessionTelegramIdentityStatusSchema, SessionPresenceIdentitySchema, SessionPresenceResponseSchema, SessionPresenceConnectRequestSchema, SessionPresenceConnectPendingSchema, SessionPresenceConnectResponseSchema, SessionPresenceIconRequestSchema, SessionPresenceIconResponseSchema, SessionPresenceCompleteResponseSchema;
17139
+ var RESOURCE_KIND_SESSION, TriggerFilterScalarSchema, TriggerFilterPathSchema, TriggerFilterClauseSchema, TriggerFilterSchema, SessionTriggerCheckTimeoutSchema, TriggerEventSchema, TriggerEventsSchema, SessionTriggerSharedFields, SessionTriggerEventSourceFields, SessionTriggerBaseSchema, SessionTriggerDefinitionBaseSchema, SessionTriggerSchema, SessionApplyTriggerSchema, SessionHeartbeatTriggerBaseSchema, SessionHeartbeatTriggerSchema, SessionApplyHeartbeatTriggerSchema, SessionTriggerDefinitionSchema, SessionApplyTriggerDefinitionSchema, SessionTriggersSchema, SessionApplyTriggersSchema, AVATAR_ASSET_EXTENSIONS, MAX_AVATAR_ASSET_BYTES, SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH, SHA256_HEX_PATTERN, SessionIdentitySchema, SessionSpecSchema, SessionApplySpecSchema, SessionStatusSchema, SessionResourceSchema, SessionApplyRequestSchema, SessionApplyTriggerReceiptSchema, SessionApplyResponseSchema, SESSION_TELEGRAM_IDENTITY_STATUSES, SessionTelegramIdentityStatusSchema, SessionPresenceIdentitySchema, SessionPresenceResponseSchema, SessionPresenceConnectRequestSchema, SessionPresenceConnectPendingSchema, SessionPresenceConnectResponseSchema, SessionPresenceIconRequestSchema, SessionPresenceIconResponseSchema, SessionPresenceCompleteResponseSchema;
17126
17140
  var init_sessions = __esm({
17127
17141
  "../../packages/schemas/src/sessions.ts"() {
17128
17142
  "use strict";
@@ -17299,6 +17313,7 @@ var init_sessions = __esm({
17299
17313
  ).transform(expandSessionApplyTriggerDefinitions).superRefine(validateAttributedRunsTriggerPairing);
17300
17314
  AVATAR_ASSET_EXTENSIONS = [".png", ".jpg", ".jpeg"];
17301
17315
  MAX_AVATAR_ASSET_BYTES = 2 * 1024 * 1024;
17316
+ SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH = 140;
17302
17317
  SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
17303
17318
  SessionIdentitySchema = external_exports.object({
17304
17319
  displayName: external_exports.string().trim().min(1).max(80).optional(),
@@ -17312,7 +17327,12 @@ var init_sessions = __esm({
17312
17327
  // overwritten, so it is never trusted input.
17313
17328
  sha256: external_exports.string().regex(SHA256_HEX_PATTERN).optional()
17314
17329
  }).strict().optional(),
17315
- description: external_exports.string().trim().min(1).max(256).optional()
17330
+ description: external_exports.string().trim().min(1).refine(
17331
+ (value) => sessionIdentityDescriptionLength(value) <= SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH,
17332
+ {
17333
+ message: `description must be at most ${SESSION_IDENTITY_DESCRIPTION_MAX_LENGTH} characters as Slack counts them (a non-ASCII character like "\u2014" counts as 6)`
17334
+ }
17335
+ ).optional()
17316
17336
  }).strict().refine((identity) => Object.keys(identity).length > 0, {
17317
17337
  message: "Session identity requires at least one field"
17318
17338
  });
@@ -18138,20 +18158,42 @@ var init_active_project = __esm({
18138
18158
  });
18139
18159
 
18140
18160
  // src/lib/config/path.ts
18161
+ import { createHash } from "crypto";
18141
18162
  import { homedir } from "os";
18142
- import { join } from "path";
18163
+ import { join, normalize } from "path";
18143
18164
  function defaultConfigPath() {
18144
18165
  return process.env.AUTO_CLI_CONFIG ?? join(homedir(), ".auto", "config.yaml");
18145
18166
  }
18167
+ function profilesDir(configPath = defaultConfigPath()) {
18168
+ return normalize(join(configPath, "..", PROFILES_DIR_NAME));
18169
+ }
18170
+ function profileFileName(input) {
18171
+ const key = `${input.userEmail.toLowerCase()}
18172
+ ${serverHost(input.serverUrl)}`;
18173
+ const hash2 = createHash("sha256").update(key).digest("hex").slice(0, 8);
18174
+ return `${slug(input.userEmail)}--${slug(serverHost(input.serverUrl))}-${hash2}.yaml`;
18175
+ }
18176
+ function serverHost(serverUrl) {
18177
+ try {
18178
+ return new URL(serverUrl).host;
18179
+ } catch {
18180
+ return serverUrl;
18181
+ }
18182
+ }
18183
+ function slug(value) {
18184
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "_");
18185
+ }
18186
+ var PROFILES_DIR_NAME;
18146
18187
  var init_path = __esm({
18147
18188
  "src/lib/config/path.ts"() {
18148
18189
  "use strict";
18190
+ PROFILES_DIR_NAME = "profiles";
18149
18191
  }
18150
18192
  });
18151
18193
 
18152
18194
  // src/lib/config/file.ts
18153
18195
  import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
18154
- import { dirname as dirname2 } from "path";
18196
+ import { basename, dirname as dirname2, join as join2 } from "path";
18155
18197
  function readConfig(path = defaultConfigPath()) {
18156
18198
  try {
18157
18199
  const text = readFileSync2(path, "utf8");
@@ -18159,21 +18201,8 @@ function readConfig(path = defaultConfigPath()) {
18159
18201
  for (const line of text.split(/\r?\n/)) {
18160
18202
  const match = /^([A-Za-z0-9_]+):\s*(.*)$/.exec(line.trim());
18161
18203
  if (!match) continue;
18162
- const value = match[2] ?? "";
18163
- if (match[1] === "serverUrl") config2.serverUrl = value;
18164
- if (match[1] === "organizationId") config2.organizationId = value;
18165
- if (match[1] === "projectId") config2.projectId = value;
18166
- if (match[1] === "refreshToken") config2.refreshToken = value;
18167
- if (match[1] === "accessToken") config2.accessToken = value;
18168
- if (match[1] === "accessTokenExpiresAt") {
18169
- config2.accessTokenExpiresAt = value;
18170
- }
18171
- if (match[1] === "accessTokenOrganizationId") {
18172
- config2.accessTokenOrganizationId = value;
18173
- }
18174
- if (match[1] === "accessTokenProjectId") {
18175
- config2.accessTokenProjectId = value;
18176
- }
18204
+ const key = CONFIG_KEYS.find((candidate) => candidate === match[1]);
18205
+ if (key) config2[key] = match[2] ?? "";
18177
18206
  }
18178
18207
  return config2;
18179
18208
  } catch (err) {
@@ -18182,17 +18211,26 @@ function readConfig(path = defaultConfigPath()) {
18182
18211
  }
18183
18212
  }
18184
18213
  function writeConfig(config2, path = defaultConfigPath()) {
18214
+ writeConfigFile(config2, path);
18215
+ if (config2.userEmail && config2.serverUrl && basename(dirname2(path)) !== PROFILES_DIR_NAME) {
18216
+ writeConfigFile(
18217
+ config2,
18218
+ join2(
18219
+ dirname2(path),
18220
+ PROFILES_DIR_NAME,
18221
+ profileFileName({
18222
+ userEmail: config2.userEmail,
18223
+ serverUrl: config2.serverUrl
18224
+ })
18225
+ )
18226
+ );
18227
+ }
18228
+ }
18229
+ function writeConfigFile(config2, path) {
18185
18230
  mkdirSync2(dirname2(path), { recursive: true });
18186
- const lines = [
18187
- ["serverUrl", config2.serverUrl],
18188
- ["organizationId", config2.organizationId],
18189
- ["projectId", config2.projectId],
18190
- ["refreshToken", config2.refreshToken],
18191
- ["accessToken", config2.accessToken],
18192
- ["accessTokenExpiresAt", config2.accessTokenExpiresAt],
18193
- ["accessTokenOrganizationId", config2.accessTokenOrganizationId],
18194
- ["accessTokenProjectId", config2.accessTokenProjectId]
18195
- ].filter(([, value]) => value).map(([key, value]) => `${key}: ${value}`);
18231
+ const lines = CONFIG_KEYS.filter((key) => config2[key]).map(
18232
+ (key) => `${key}: ${config2[key]}`
18233
+ );
18196
18234
  writeFileSync2(path, `${lines.join("\n")}
18197
18235
  `, {
18198
18236
  encoding: "utf8",
@@ -18200,10 +18238,23 @@ function writeConfig(config2, path = defaultConfigPath()) {
18200
18238
  });
18201
18239
  chmodSync(path, 384);
18202
18240
  }
18241
+ var CONFIG_KEYS;
18203
18242
  var init_file = __esm({
18204
18243
  "src/lib/config/file.ts"() {
18205
18244
  "use strict";
18206
18245
  init_path();
18246
+ CONFIG_KEYS = [
18247
+ "serverUrl",
18248
+ "userId",
18249
+ "userEmail",
18250
+ "organizationId",
18251
+ "projectId",
18252
+ "refreshToken",
18253
+ "accessToken",
18254
+ "accessTokenExpiresAt",
18255
+ "accessTokenOrganizationId",
18256
+ "accessTokenProjectId"
18257
+ ];
18207
18258
  }
18208
18259
  });
18209
18260
 
@@ -20193,7 +20244,7 @@ var init_connect = __esm({
20193
20244
  });
20194
20245
 
20195
20246
  // src/commands/apply/files.ts
20196
- import { createHash } from "crypto";
20247
+ import { createHash as createHash2 } from "crypto";
20197
20248
  import {
20198
20249
  readFileSync as readFileSync3,
20199
20250
  readdirSync,
@@ -20201,11 +20252,11 @@ import {
20201
20252
  statSync
20202
20253
  } from "fs";
20203
20254
  import {
20204
- basename,
20255
+ basename as basename2,
20205
20256
  dirname as dirname3,
20206
20257
  extname,
20207
20258
  isAbsolute,
20208
- join as join2,
20259
+ join as join3,
20209
20260
  resolve
20210
20261
  } from "path";
20211
20262
  import { parseAllDocuments as parseYamlDocuments } from "yaml";
@@ -20221,7 +20272,7 @@ function readProjectApplyRequest(options) {
20221
20272
  );
20222
20273
  return { ...request, assets: assets2 };
20223
20274
  }
20224
- const directory = options.directory ?? join2(process.cwd(), ".auto");
20275
+ const directory = options.directory ?? join3(process.cwd(), ".auto");
20225
20276
  const files = applyFiles(directory);
20226
20277
  if (files.length === 0) {
20227
20278
  throw new Error(`No resource files found in ${directory}`);
@@ -20284,7 +20335,7 @@ function applyFiles(root) {
20284
20335
  const files = [];
20285
20336
  for (const kind of APPLY_RESOURCE_ORDER) {
20286
20337
  const directory = APPLY_DIRECTORIES[kind];
20287
- const path = join2(root, directory);
20338
+ const path = join3(root, directory);
20288
20339
  let entries;
20289
20340
  try {
20290
20341
  entries = readdirSync(path, { withFileTypes: true });
@@ -20358,7 +20409,7 @@ function readApplyAssets(resources, projectRoot) {
20358
20409
  });
20359
20410
  const bytes = readFileSync3(resolvedPath);
20360
20411
  assets[avatarAsset] = {
20361
- sha256: createHash("sha256").update(bytes).digest("hex"),
20412
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
20362
20413
  contentType: extname(resolvedPath).toLowerCase() === ".png" ? "image/png" : "image/jpeg",
20363
20414
  dataBase64: bytes.toString("base64")
20364
20415
  };
@@ -20422,12 +20473,12 @@ function validateSessionAvatarAsset(input) {
20422
20473
  }
20423
20474
  function applyProjectRoot(directory) {
20424
20475
  const resolved = resolve(directory);
20425
- return basename(resolved) === ".auto" ? dirname3(resolved) : resolved;
20476
+ return basename2(resolved) === ".auto" ? dirname3(resolved) : resolved;
20426
20477
  }
20427
20478
  function applyFileProjectRoot(file2) {
20428
20479
  let dir = dirname3(resolve(file2));
20429
20480
  while (true) {
20430
- if (basename(dir) === ".auto") {
20481
+ if (basename2(dir) === ".auto") {
20431
20482
  return dirname3(dir);
20432
20483
  }
20433
20484
  const parent = dirname3(dir);
@@ -20464,7 +20515,7 @@ function isRecord(value) {
20464
20515
  function resourceApplyFiles(directory, entries) {
20465
20516
  const files = [];
20466
20517
  for (const entry of entries) {
20467
- const path = join2(directory, entry.name);
20518
+ const path = join3(directory, entry.name);
20468
20519
  if (entry.isDirectory()) {
20469
20520
  files.push(
20470
20521
  ...resourceApplyFiles(path, readdirSync(path, { withFileTypes: true }))
@@ -20613,13 +20664,49 @@ var init_actions = __esm({
20613
20664
  }
20614
20665
  });
20615
20666
 
20667
+ // src/lib/config/profiles.ts
20668
+ import { readdirSync as readdirSync2 } from "fs";
20669
+ import { join as join4 } from "path";
20670
+ function listProfiles(configPath = defaultConfigPath()) {
20671
+ const dir = profilesDir(configPath);
20672
+ let entries;
20673
+ try {
20674
+ entries = readdirSync2(dir);
20675
+ } catch (err) {
20676
+ if (err.code === "ENOENT") return [];
20677
+ throw err;
20678
+ }
20679
+ return entries.filter((entry) => entry.endsWith(".yaml")).sort().map((entry) => {
20680
+ const path = join4(dir, entry);
20681
+ return { path, config: readConfig(path) };
20682
+ }).filter((profile) => profile.config.userEmail);
20683
+ }
20684
+ function findAccountProfile(input) {
20685
+ const candidates = listProfiles(input.configPath).map((profile) => profile.config).filter((config2) => config2.serverUrl === input.serverUrl);
20686
+ return candidates.find(
20687
+ (config2) => input.userId && config2.userId === input.userId
20688
+ ) ?? // Email is only a fallback for when a user id is missing on either side;
20689
+ // it must never override a known user-id mismatch (emails can be
20690
+ // reassigned to a different user).
20691
+ candidates.find(
20692
+ (config2) => input.userEmail && config2.userEmail?.toLowerCase() === input.userEmail.toLowerCase() && !(input.userId && config2.userId && config2.userId !== input.userId)
20693
+ );
20694
+ }
20695
+ var init_profiles2 = __esm({
20696
+ "src/lib/config/profiles.ts"() {
20697
+ "use strict";
20698
+ init_file();
20699
+ init_path();
20700
+ }
20701
+ });
20702
+
20616
20703
  // src/commands/auth/pkce.ts
20617
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
20704
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
20618
20705
  function pkceVerifier() {
20619
20706
  return randomBytes2(32).toString("base64url");
20620
20707
  }
20621
20708
  function pkceChallenge(verifier) {
20622
- return createHash2("sha256").update(verifier).digest("base64url");
20709
+ return createHash3("sha256").update(verifier).digest("base64url");
20623
20710
  }
20624
20711
  var init_pkce = __esm({
20625
20712
  "src/commands/auth/pkce.ts"() {
@@ -20630,7 +20717,6 @@ var init_pkce = __esm({
20630
20717
  // src/commands/auth/login.ts
20631
20718
  async function login(input) {
20632
20719
  const serverUrl = resolveApiBaseUrl({ explicit: input.options.apiUrl });
20633
- const previous = readConfig(input.configPath);
20634
20720
  if (input.options.device) {
20635
20721
  const device = await postJson(
20636
20722
  input.fetch,
@@ -20648,194 +20734,158 @@ async function login(input) {
20648
20734
  await sleep(Math.max(0, device.interval) * 1e3);
20649
20735
  }
20650
20736
  firstAttempt = false;
20737
+ let token3;
20651
20738
  try {
20652
- const token2 = await postJson(
20739
+ token3 = await postJson(
20653
20740
  input.fetch,
20654
20741
  `${serverUrl}/api/v1/auth/cli/token`,
20655
20742
  {
20656
20743
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
20657
- device_code: device.device_code,
20658
- ...previous.organizationId && previous.projectId ? {
20659
- organization_id: previous.organizationId,
20660
- project_id: previous.projectId
20661
- } : {}
20744
+ device_code: device.device_code
20662
20745
  }
20663
20746
  );
20664
- writeConfig(
20665
- {
20666
- ...previous,
20667
- serverUrl,
20668
- refreshToken: token2.refresh_token,
20669
- accessToken: token2.access_token,
20670
- accessTokenExpiresAt: token2.access_token ? accessTokenExpiresAt(token2) : void 0,
20671
- accessTokenOrganizationId: token2.access_token ? previous.organizationId : void 0,
20672
- accessTokenProjectId: token2.access_token ? previous.projectId : void 0
20673
- },
20674
- input.configPath
20675
- );
20676
- input.writeOutput("Logged in.");
20677
- return;
20678
20747
  } catch (error51) {
20679
20748
  if (error51 instanceof Error && error51.message === "authorization_pending") {
20680
20749
  continue;
20681
20750
  }
20682
20751
  throw error51;
20683
20752
  }
20753
+ await finishLogin({
20754
+ token: token3,
20755
+ serverUrl,
20756
+ fetch: input.fetch,
20757
+ configPath: input.configPath,
20758
+ writeOutput: input.writeOutput
20759
+ });
20760
+ return;
20684
20761
  }
20685
20762
  throw new Error("Device authorization expired before approval.");
20686
20763
  }
20687
20764
  const verifier = input.options.verifier ?? pkceVerifier();
20688
20765
  if (!input.options.code) {
20689
- const selectionDetails = await resolveLoginSelectionDetails({
20690
- configPath: input.configPath,
20691
- env: input.env ?? process.env,
20692
- fetch: input.fetch,
20693
- serverUrl,
20694
- previous,
20695
- writeError: input.writeError ?? (() => {
20696
- })
20697
- });
20698
20766
  const callback = await createOAuthLoopbackCallback({
20699
- successHtml: (result) => renderOAuthLoopbackPage({
20767
+ successHtml: () => renderOAuthLoopbackPage({
20700
20768
  status: "success",
20701
20769
  eyebrow: "Auto CLI",
20702
20770
  title: "Login authorized",
20703
20771
  message: "Auto received the browser authorization. The CLI will finish signing you in from your terminal.",
20704
- details: [
20705
- { label: "Server", value: serverUrl },
20706
- {
20707
- label: "Organization",
20708
- value: result.organizationName ?? selectionDetails.organization
20709
- },
20710
- {
20711
- label: "Project",
20712
- value: result.projectName ?? selectionDetails.project
20713
- }
20714
- ]
20772
+ details: [{ label: "Server", value: serverUrl }]
20715
20773
  }),
20716
20774
  failureHtml: () => renderOAuthLoopbackPage({
20717
20775
  status: "failure",
20718
20776
  eyebrow: "Auto CLI",
20719
20777
  title: "Login failed",
20720
20778
  message: "The browser authorization did not complete. Return to your terminal to retry or inspect the error.",
20721
- details: [
20722
- { label: "Server", value: serverUrl },
20723
- { label: "Organization", value: selectionDetails.organization },
20724
- { label: "Project", value: selectionDetails.project }
20725
- ]
20779
+ details: [{ label: "Server", value: serverUrl }]
20726
20780
  })
20727
20781
  });
20728
20782
  try {
20729
20783
  const authorizeUrl = new URL("/auth/cli", serverUrl);
20730
20784
  authorizeUrl.searchParams.set("pkce_challenge", pkceChallenge(verifier));
20731
20785
  authorizeUrl.searchParams.set("redirect_uri", callback.redirectUri);
20732
- if (previous.organizationId && previous.projectId) {
20733
- authorizeUrl.searchParams.set(
20734
- "organization_id",
20735
- previous.organizationId
20736
- );
20737
- authorizeUrl.searchParams.set("project_id", previous.projectId);
20738
- }
20739
20786
  input.writeOutput(`Opening ${authorizeUrl.toString()}`);
20740
20787
  input.writeOutput("Waiting for browser authorization...");
20741
20788
  openBrowser(authorizeUrl.toString());
20742
20789
  const { code } = await callback.result;
20743
- await exchangeCode({
20790
+ const token3 = await exchangeAuthorizationCode({
20744
20791
  code,
20745
- configPath: input.configPath,
20746
20792
  fetch: input.fetch,
20747
- previous,
20748
20793
  redirectUri: callback.redirectUri,
20749
20794
  serverUrl,
20750
20795
  verifier
20751
20796
  });
20752
- input.writeOutput("Logged in.");
20797
+ await finishLogin({
20798
+ token: token3,
20799
+ serverUrl,
20800
+ fetch: input.fetch,
20801
+ configPath: input.configPath,
20802
+ writeOutput: input.writeOutput
20803
+ });
20753
20804
  return;
20754
20805
  } finally {
20755
20806
  callback.close();
20756
20807
  }
20757
20808
  }
20758
- await exchangeCode({
20809
+ const token2 = await exchangeAuthorizationCode({
20759
20810
  code: input.options.code,
20760
- configPath: input.configPath,
20761
20811
  fetch: input.fetch,
20762
- previous,
20763
20812
  redirectUri: "http://127.0.0.1/callback",
20764
20813
  serverUrl,
20765
20814
  verifier
20766
20815
  });
20767
- input.writeOutput("Logged in.");
20816
+ await finishLogin({
20817
+ token: token2,
20818
+ serverUrl,
20819
+ fetch: input.fetch,
20820
+ configPath: input.configPath,
20821
+ writeOutput: input.writeOutput
20822
+ });
20768
20823
  }
20769
- async function exchangeCode(input) {
20770
- const token2 = await postJson(
20824
+ async function exchangeAuthorizationCode(input) {
20825
+ return postJson(
20771
20826
  input.fetch,
20772
20827
  `${input.serverUrl}/api/v1/auth/cli/token`,
20773
20828
  {
20774
20829
  grant_type: "authorization_code",
20775
20830
  code: input.code,
20776
20831
  code_verifier: input.verifier,
20777
- redirect_uri: input.redirectUri,
20778
- ...input.previous.organizationId && input.previous.projectId ? {
20779
- organization_id: input.previous.organizationId,
20780
- project_id: input.previous.projectId
20781
- } : {}
20832
+ redirect_uri: input.redirectUri
20782
20833
  }
20783
20834
  );
20784
- writeConfig(
20785
- {
20786
- ...input.previous,
20787
- serverUrl: input.serverUrl,
20788
- refreshToken: token2.refresh_token,
20789
- accessToken: token2.access_token,
20790
- accessTokenExpiresAt: token2.access_token ? accessTokenExpiresAt(token2) : void 0,
20791
- accessTokenOrganizationId: token2.access_token ? input.previous.organizationId : void 0,
20792
- accessTokenProjectId: token2.access_token ? input.previous.projectId : void 0
20793
- },
20794
- input.configPath
20795
- );
20796
20835
  }
20797
- async function resolveLoginSelectionDetails(input) {
20798
- const fallback = {
20799
- organization: input.previous.organizationId,
20800
- project: input.previous.projectId
20801
- };
20802
- if (!input.previous.organizationId) {
20803
- return fallback;
20804
- }
20805
- const client = createApiClient({
20836
+ async function finishLogin(input) {
20837
+ const { token: token2, serverUrl } = input;
20838
+ const previous = readConfig(input.configPath);
20839
+ const profile = token2.user ? findAccountProfile({
20806
20840
  configPath: input.configPath,
20807
- env: input.env,
20808
- fetch: input.fetch,
20809
- writeError: input.writeError
20810
- });
20811
- try {
20812
- if (input.previous.projectId) {
20813
- const projects = await client.listProjects({
20814
- apiBaseUrl: input.serverUrl
20815
- });
20816
- const project = projects.projects.find(
20817
- (candidate) => candidate.organizationId === input.previous.organizationId && candidate.projectId === input.previous.projectId
20841
+ serverUrl,
20842
+ userId: token2.user.id,
20843
+ userEmail: token2.user.email
20844
+ }) : void 0;
20845
+ const selectionSource = profile ?? previous;
20846
+ const selection = selectionSource.organizationId && selectionSource.projectId ? {
20847
+ organizationId: selectionSource.organizationId,
20848
+ projectId: selectionSource.projectId
20849
+ } : void 0;
20850
+ let config2 = {
20851
+ serverUrl,
20852
+ userId: token2.user?.id,
20853
+ userEmail: token2.user?.email,
20854
+ refreshToken: token2.refresh_token,
20855
+ accessToken: token2.access_token,
20856
+ accessTokenExpiresAt: token2.access_token ? accessTokenExpiresAt(token2) : void 0
20857
+ };
20858
+ if (selection) {
20859
+ try {
20860
+ const scoped = await postJson(
20861
+ input.fetch,
20862
+ `${serverUrl}/api/v1/auth/cli/token`,
20863
+ {
20864
+ grant_type: "refresh_token",
20865
+ refresh_token: token2.refresh_token,
20866
+ organization_id: selection.organizationId,
20867
+ project_id: selection.projectId
20868
+ }
20869
+ );
20870
+ config2 = {
20871
+ ...config2,
20872
+ ...selection,
20873
+ refreshToken: scoped.refresh_token,
20874
+ accessToken: scoped.access_token,
20875
+ accessTokenExpiresAt: scoped.access_token ? accessTokenExpiresAt(scoped) : void 0,
20876
+ accessTokenOrganizationId: scoped.access_token ? selection.organizationId : void 0,
20877
+ accessTokenProjectId: scoped.access_token ? selection.projectId : void 0
20878
+ };
20879
+ } catch {
20880
+ input.writeOutput(
20881
+ "The saved organization/project selection is not available for this account; run `auto orgs list` to pick a new one."
20818
20882
  );
20819
- if (project) {
20820
- return {
20821
- organization: project.organizationName,
20822
- project: project.projectName
20823
- };
20824
- }
20825
20883
  }
20826
- const organizations = await client.listOrganizations({
20827
- apiBaseUrl: input.serverUrl
20828
- });
20829
- const organization = organizations.organizations.find(
20830
- (candidate) => candidate.organizationId === input.previous.organizationId
20831
- );
20832
- return {
20833
- organization: organization?.organizationName ?? fallback.organization,
20834
- project: fallback.project
20835
- };
20836
- } catch {
20837
- return fallback;
20838
20884
  }
20885
+ writeConfig(config2, input.configPath);
20886
+ input.writeOutput(
20887
+ token2.user ? `Logged in as ${token2.user.email}.` : "Logged in."
20888
+ );
20839
20889
  }
20840
20890
  async function sleep(ms) {
20841
20891
  await new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -20844,11 +20894,11 @@ var init_login = __esm({
20844
20894
  "src/commands/auth/login.ts"() {
20845
20895
  "use strict";
20846
20896
  init_base_url();
20847
- init_client();
20848
20897
  init_http();
20849
20898
  init_tokens();
20850
20899
  init_browser();
20851
20900
  init_file();
20901
+ init_profiles2();
20852
20902
  init_loopback();
20853
20903
  init_pkce();
20854
20904
  }
@@ -20858,7 +20908,7 @@ var init_login = __esm({
20858
20908
  import { spawn as spawn2 } from "child_process";
20859
20909
  import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
20860
20910
  import { tmpdir } from "os";
20861
- import { join as join3 } from "path";
20911
+ import { join as join5 } from "path";
20862
20912
  import { parseAllDocuments as parseYamlDocuments2, stringify as stringify2 } from "yaml";
20863
20913
  async function editResource(input) {
20864
20914
  const reference = parseEditableResource(input.resource);
@@ -20873,8 +20923,8 @@ async function editResource(input) {
20873
20923
  const document = editableResourceDocument(reference.kind, current);
20874
20924
  const source = `${stringify2(document).trimEnd()}
20875
20925
  `;
20876
- const tempRoot = mkdtempSync(join3(tmpdir(), "auto-edit-"));
20877
- const filePath = join3(tempRoot, `${reference.kind}-${reference.name}.yaml`);
20926
+ const tempRoot = mkdtempSync(join5(tmpdir(), "auto-edit-"));
20927
+ const filePath = join5(tempRoot, `${reference.kind}-${reference.name}.yaml`);
20878
20928
  writeFileSync3(filePath, source, "utf8");
20879
20929
  let removeTempFile = false;
20880
20930
  try {
@@ -21133,7 +21183,7 @@ var init_package = __esm({
21133
21183
  "package.json"() {
21134
21184
  package_default = {
21135
21185
  name: "@autohq/cli",
21136
- version: "0.1.86",
21186
+ version: "0.1.88",
21137
21187
  license: "SEE LICENSE IN README.md",
21138
21188
  publishConfig: {
21139
21189
  access: "public"
@@ -27986,6 +28036,7 @@ init_login();
27986
28036
 
27987
28037
  // src/commands/auth/profile.ts
27988
28038
  init_file();
28039
+ init_profiles2();
27989
28040
  async function handleAuthStatus(context) {
27990
28041
  const result = await fetchAuthStatus(context);
27991
28042
  context.io.writeResult(result, formatAuthStatusText);
@@ -28010,12 +28061,58 @@ function logout(context) {
28010
28061
  );
28011
28062
  context.writeOutput("Logged out.");
28012
28063
  }
28064
+ function switchAccount(context, accountEmail, options = {}) {
28065
+ const profiles = listProfiles(context.configPath);
28066
+ if (profiles.length === 0) {
28067
+ throw new Error("No stored accounts. Run `auto auth login` first.");
28068
+ }
28069
+ const active = readConfig(context.configPath);
28070
+ if (!accountEmail) {
28071
+ for (const profile of profiles) {
28072
+ context.writeOutput(accountLine(profile.config, active));
28073
+ }
28074
+ return;
28075
+ }
28076
+ const matches = profiles.map((profile) => profile.config).filter(
28077
+ (config2) => config2.userEmail?.toLowerCase() === accountEmail.toLowerCase() && (!options.server || config2.serverUrl === options.server)
28078
+ );
28079
+ if (matches.length === 0) {
28080
+ throw new Error(
28081
+ `No stored account for ${accountEmail}. Run \`auto auth login\` to add it.`
28082
+ );
28083
+ }
28084
+ const match = matches.length === 1 ? matches[0] : matches.find((config2) => config2.serverUrl === active.serverUrl);
28085
+ if (!match) {
28086
+ throw new Error(
28087
+ `Multiple servers have a stored account for ${accountEmail}: ${matches.map((config2) => config2.serverUrl).join(
28088
+ ", "
28089
+ )}. Pick one with \`auto auth switch ${accountEmail} --server <url>\`.`
28090
+ );
28091
+ }
28092
+ writeConfig(match, context.configPath);
28093
+ context.writeOutput(`Switched to ${match.userEmail} (${match.serverUrl}).`);
28094
+ if (!match.refreshToken) {
28095
+ context.writeOutput(
28096
+ "This account has no stored credentials; run `auto auth login`."
28097
+ );
28098
+ }
28099
+ }
28100
+ function accountLine(config2, active) {
28101
+ const isActive = Boolean(config2.userEmail) && config2.userEmail === active.userEmail && config2.serverUrl === active.serverUrl;
28102
+ return [
28103
+ config2.userEmail,
28104
+ `server=${config2.serverUrl ?? "(unset)"}`,
28105
+ config2.refreshToken ? void 0 : "logged_out",
28106
+ isActive ? "(active)" : void 0
28107
+ ].filter(Boolean).join(" ");
28108
+ }
28013
28109
  async function fetchAuthStatus(context) {
28014
28110
  const config2 = readConfig(context.configPath);
28015
28111
  const client = createContextApiClient(context);
28016
28112
  const operator = client.getOperatorInfo();
28017
28113
  const base = {
28018
28114
  serverUrl: config2.serverUrl ?? context.env.AUTO_API_BASE_URL ?? null,
28115
+ account: config2.userEmail ?? null,
28019
28116
  organizationId: config2.organizationId ?? null,
28020
28117
  projectId: config2.projectId ?? null,
28021
28118
  authSource: operator.authType
@@ -28057,6 +28154,9 @@ async function fetchAuthStatus(context) {
28057
28154
  }
28058
28155
  function formatAuthStatusText(result, writeLine) {
28059
28156
  writeLine(`server: ${result.serverUrl ?? "(unset)"}`);
28157
+ if (result.account) {
28158
+ writeLine(`account: ${result.account}`);
28159
+ }
28060
28160
  writeLine(`organization: ${result.organizationId ?? "(unset)"}`);
28061
28161
  writeLine(`project: ${result.projectId ?? "(unset)"}`);
28062
28162
  writeLine(
@@ -28169,6 +28269,11 @@ function registerAuthCommands(program, context) {
28169
28269
  await handleWhoami(context);
28170
28270
  });
28171
28271
  auth.command("logout").description("Remove the local user refresh token.").action(() => logout(context));
28272
+ auth.command("switch").description(
28273
+ "Switch the active account to a stored profile, or list stored accounts."
28274
+ ).argument("[email]", "Email of a stored account").option("--server <url>", "Auto web server URL of the stored account").action(
28275
+ (email3, options) => switchAccount(context, email3, options)
28276
+ );
28172
28277
  }
28173
28278
 
28174
28279
  // src/lib/stdio/readline.ts
@@ -30725,7 +30830,7 @@ init_resources2();
30725
30830
  init_browser();
30726
30831
  import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync4 } from "fs";
30727
30832
  import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
30728
- import { join as join4 } from "path";
30833
+ import { join as join6 } from "path";
30729
30834
  var POLL_INTERVAL_MS = 2e3;
30730
30835
  var POLL_TIMEOUT_MS = 5 * 6e4;
30731
30836
  var SLACK_APPS_URL = "https://api.slack.com/apps";
@@ -30886,7 +30991,7 @@ async function promptForIconUploads(input, options) {
30886
30991
  }
30887
30992
  }
30888
30993
  function stagedLocationLabel(stagedPath) {
30889
- return stagedPath.startsWith(`${join4(homedir2(), "Downloads")}/`) ? "Downloads" : "the printed path";
30994
+ return stagedPath.startsWith(`${join6(homedir2(), "Downloads")}/`) ? "Downloads" : "the printed path";
30890
30995
  }
30891
30996
  async function stageAvatarImage(input) {
30892
30997
  try {
@@ -30898,9 +31003,9 @@ async function stageAvatarImage(input) {
30898
31003
  }
30899
31004
  const contentType = response.headers.get("content-type") ?? "";
30900
31005
  const extension = contentType.includes("jpeg") ? ".jpg" : ".png";
30901
- const downloads = join4(homedir2(), "Downloads");
30902
- const directory = existsSync2(downloads) ? downloads : mkdtempSync2(join4(tmpdir2(), "auto-avatar-"));
30903
- const path = join4(directory, `${input.session}-avatar${extension}`);
31006
+ const downloads = join6(homedir2(), "Downloads");
31007
+ const directory = existsSync2(downloads) ? downloads : mkdtempSync2(join6(tmpdir2(), "auto-avatar-"));
31008
+ const path = join6(directory, `${input.session}-avatar${extension}`);
30904
31009
  writeFileSync4(path, Buffer.from(await response.arrayBuffer()));
30905
31010
  return path;
30906
31011
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autohq/cli",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "license": "SEE LICENSE IN README.md",
5
5
  "publishConfig": {
6
6
  "access": "public"