hereya-cli 0.86.0 → 0.86.2

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.
@@ -2,12 +2,12 @@ import { Command, Flags } from '@oclif/core';
2
2
  import { getRendererClass, Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
3
  import path from 'node:path';
4
4
  import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
5
- import { getCloudCredentials, loadBackendConfig } from '../../backend/config.js';
6
5
  import { getBackend } from '../../backend/index.js';
7
6
  import { getExecutor } from '../../executor/index.js';
7
+ import { getActiveCloudAuth } from '../../lib/active-cloud.js';
8
8
  import { getConfigManager } from '../../lib/config/index.js';
9
9
  import { getEnvManager } from '../../lib/env/index.js';
10
- import { getEphemeralToken, withEphemeralToken } from '../../lib/ephemeral-token.js';
10
+ import { withEphemeralToken } from '../../lib/ephemeral-token.js';
11
11
  import { gitUtils } from '../../lib/git-utils.js';
12
12
  import { getDefaultLogger, getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
13
13
  import { getParameterManager } from '../../lib/parameter/index.js';
@@ -338,41 +338,19 @@ See ${getLogPath()} for more details`);
338
338
  this.error(cleanCheck.reason);
339
339
  }
340
340
  myLogger.log(ListrLogLevels.STARTED, `Undeploying ${input.workspace} via remote executor (branch: ${input.gitBranch})`);
341
- // 2. Get cloud backend for job submission. If we are inside an ephemeral
342
- // token scope (set by --token), use that token directly without touching
343
- // the keychain or persisted credentials. Otherwise fall back to the
344
- // standard "load credentials from disk + keychain" path.
345
- const ephemeral = getEphemeralToken();
346
- const resolvedWorkspaceName = resolveWorkspaceName(input.workspace, input.project);
347
- let cloudBackend;
348
- if (ephemeral) {
349
- const url = ephemeral.cloudUrl
350
- ?? process.env.HEREYA_CLOUD_URL
351
- ?? (await loadBackendConfig()).cloud?.url
352
- ?? 'https://cloud.hereya.dev';
353
- cloudBackend = new CloudBackend({
354
- accessToken: ephemeral.token,
355
- clientId: '',
356
- refreshToken: '',
357
- url,
358
- });
359
- }
360
- else {
361
- const backendConfig = await loadBackendConfig();
362
- if (!backendConfig.cloud) {
363
- this.error('Remote undeployment requires cloud backend. Run `hereya login` first.');
364
- }
365
- const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
366
- if (!credentials) {
367
- this.error('Cloud credentials not found. Run `hereya login` first.');
368
- }
369
- cloudBackend = new CloudBackend({
370
- accessToken: credentials.accessToken,
371
- clientId: backendConfig.cloud.clientId,
372
- refreshToken: credentials.refreshToken,
373
- url: backendConfig.cloud.url,
374
- });
341
+ // 2. Resolve cloud auth (ephemeral --token first, then persisted creds)
342
+ // and build a CloudBackend for job submission + polling.
343
+ const auth = await getActiveCloudAuth();
344
+ if (!auth) {
345
+ this.error('Remote undeployment requires cloud backend. Run `hereya login` or pass `--token <token>`.');
375
346
  }
347
+ const resolvedWorkspaceName = resolveWorkspaceName(input.workspace, input.project);
348
+ const cloudBackend = new CloudBackend({
349
+ accessToken: auth.accessToken,
350
+ clientId: auth.clientId ?? '',
351
+ refreshToken: auth.refreshToken ?? '',
352
+ url: auth.cloudUrl,
353
+ });
376
354
  // 3. Submit undeploy job
377
355
  const submitResult = await cloudBackend.submitExecutorJob({
378
356
  payload: {
@@ -1,16 +1,17 @@
1
- import { getCloudCredentials, loadBackendConfig } from '../backend/config.js';
2
- import { BackendType, getBackend } from '../backend/index.js';
1
+ import { getBackend } from '../backend/index.js';
2
+ import { getActiveCloudAuth } from '../lib/active-cloud.js';
3
3
  import { resolveWorkspaceName } from '../lib/workspace-utils.js';
4
4
  import { getExecutor } from './index.js';
5
5
  export async function getExecutorForWorkspace(workspaceName, project) {
6
6
  if (!workspaceName) {
7
7
  return getExecutor();
8
8
  }
9
- const backendConfig = await loadBackendConfig();
10
- if (backendConfig.current !== BackendType.Cloud) {
11
- return getExecutor();
12
- }
13
- if (!backendConfig.cloud) {
9
+ // Honors both `hereya login` (persisted on disk) and an in-scope
10
+ // ephemeral `--token`. Without this, `--token` invocations silently
11
+ // fell through to the local executor regardless of whether the
12
+ // workspace actually has a remote executor.
13
+ const auth = await getActiveCloudAuth();
14
+ if (!auth) {
14
15
  return getExecutor();
15
16
  }
16
17
  const resolvedName = resolveWorkspaceName(workspaceName, project);
@@ -23,16 +24,12 @@ export async function getExecutorForWorkspace(workspaceName, project) {
23
24
  if (!workspace.hasExecutor) {
24
25
  return getExecutor();
25
26
  }
26
- const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
27
- if (!credentials) {
28
- return getExecutor();
29
- }
30
27
  return getExecutor({
31
28
  cloudConfig: {
32
- accessToken: credentials.accessToken,
33
- clientId: backendConfig.cloud.clientId,
34
- refreshToken: credentials.refreshToken,
35
- url: backendConfig.cloud.url,
29
+ accessToken: auth.accessToken,
30
+ clientId: auth.clientId ?? '',
31
+ refreshToken: auth.refreshToken ?? '',
32
+ url: auth.cloudUrl,
36
33
  },
37
34
  workspace: resolvedName,
38
35
  });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The active cloud auth credentials for this CLI invocation.
3
+ *
4
+ * Either sourced from an ephemeral `--token` (in which case `clientId` and
5
+ * `refreshToken` are `null` because there is no persisted client identity
6
+ * and the token cannot be refreshed) or from the persisted `backend.yaml`
7
+ * + keychain credentials.
8
+ */
9
+ export interface ActiveCloudAuth {
10
+ accessToken: string;
11
+ clientId: null | string;
12
+ cloudUrl: string;
13
+ refreshToken: null | string;
14
+ }
15
+ /**
16
+ * Returns the active cloud auth, preferring an in-scope ephemeral `--token`
17
+ * over the persisted `backend.yaml` + keychain credentials. Returns `null`
18
+ * when neither is available.
19
+ *
20
+ * **This is the single source of truth for "are we authenticated to
21
+ * hereya-cloud right now?"** — every code path that previously read from
22
+ * `loadBackendConfig()` + `getCloudCredentials()` directly (and therefore
23
+ * silently ignored an ephemeral `--token`) should call this instead.
24
+ */
25
+ export declare function getActiveCloudAuth(): Promise<ActiveCloudAuth | null>;
26
+ /**
27
+ * Whether the cloud backend is "active" — either via `hereya login` or via
28
+ * an in-scope ephemeral `--token`. Used to decide whether to consult the
29
+ * cloud package registry, the remote executor, etc.
30
+ */
31
+ export declare function isCloudBackendActive(): Promise<boolean>;
@@ -0,0 +1,55 @@
1
+ import { getCloudCredentials, loadBackendConfig } from '../backend/config.js';
2
+ import { BackendType } from '../backend/index.js';
3
+ import { getEphemeralToken } from './ephemeral-token.js';
4
+ const DEFAULT_CLOUD_URL = 'https://cloud.hereya.dev';
5
+ /**
6
+ * Returns the active cloud auth, preferring an in-scope ephemeral `--token`
7
+ * over the persisted `backend.yaml` + keychain credentials. Returns `null`
8
+ * when neither is available.
9
+ *
10
+ * **This is the single source of truth for "are we authenticated to
11
+ * hereya-cloud right now?"** — every code path that previously read from
12
+ * `loadBackendConfig()` + `getCloudCredentials()` directly (and therefore
13
+ * silently ignored an ephemeral `--token`) should call this instead.
14
+ */
15
+ export async function getActiveCloudAuth() {
16
+ const ephemeral = getEphemeralToken();
17
+ if (ephemeral) {
18
+ const cloudUrl = ephemeral.cloudUrl
19
+ ?? process.env.HEREYA_CLOUD_URL
20
+ ?? (await loadBackendConfig()).cloud?.url
21
+ ?? DEFAULT_CLOUD_URL;
22
+ return {
23
+ accessToken: ephemeral.token,
24
+ clientId: null,
25
+ cloudUrl,
26
+ refreshToken: null,
27
+ };
28
+ }
29
+ const config = await loadBackendConfig();
30
+ if (config.current !== BackendType.Cloud || !config.cloud) {
31
+ return null;
32
+ }
33
+ const credentials = await getCloudCredentials(config.cloud.clientId);
34
+ if (!credentials) {
35
+ return null;
36
+ }
37
+ return {
38
+ accessToken: credentials.accessToken,
39
+ clientId: config.cloud.clientId,
40
+ cloudUrl: config.cloud.url,
41
+ refreshToken: credentials.refreshToken,
42
+ };
43
+ }
44
+ /**
45
+ * Whether the cloud backend is "active" — either via `hereya login` or via
46
+ * an in-scope ephemeral `--token`. Used to decide whether to consult the
47
+ * cloud package registry, the remote executor, etc.
48
+ */
49
+ export async function isCloudBackendActive() {
50
+ if (getEphemeralToken()) {
51
+ return true;
52
+ }
53
+ const config = await loadBackendConfig();
54
+ return config.current === BackendType.Cloud;
55
+ }
@@ -1,22 +1,21 @@
1
- import { getCloudCredentials, loadBackendConfig } from '../backend/config.js';
1
+ import { getActiveCloudAuth } from './active-cloud.js';
2
2
  export const hereyaTokenUtils = {
3
3
  async generateHereyaToken(description) {
4
- const backendConfig = await loadBackendConfig();
5
- if (!backendConfig.cloud) {
6
- return null;
7
- }
8
- const { clientId, url } = backendConfig.cloud;
9
- const credentials = await getCloudCredentials(clientId);
10
- if (!credentials) {
4
+ // Honors both `hereya login` (persisted) and an in-scope ephemeral
5
+ // `--token`. Note: workspace-scoped CLI tokens (kind:"cli") are
6
+ // intentionally rejected by the cloud's /api/personal-tokens route —
7
+ // only user-level tokens can mint long-lived personal tokens.
8
+ const auth = await getActiveCloudAuth();
9
+ if (!auth) {
11
10
  return null;
12
11
  }
13
12
  const formData = new FormData();
14
13
  formData.append('description', description);
15
14
  formData.append('expiresInDays', '365');
16
- const response = await fetch(`${url}/api/personal-tokens`, {
15
+ const response = await fetch(`${auth.cloudUrl}/api/personal-tokens`, {
17
16
  body: formData,
18
17
  headers: {
19
- Authorization: `Bearer ${credentials.accessToken}`,
18
+ Authorization: `Bearer ${auth.accessToken}`,
20
19
  },
21
20
  method: 'POST',
22
21
  });
@@ -24,6 +23,6 @@ export const hereyaTokenUtils = {
24
23
  return null;
25
24
  }
26
25
  const result = await response.json();
27
- return { cloudUrl: url, token: result.data.token };
26
+ return { cloudUrl: auth.cloudUrl, token: result.data.token };
28
27
  },
29
28
  };
@@ -1,9 +1,9 @@
1
1
  import * as yaml from 'yaml';
2
2
  import { z } from 'zod';
3
- import { getCurrentBackendType } from '../../backend/config.js';
4
- import { BackendType, getBackend } from '../../backend/index.js';
3
+ import { getBackend } from '../../backend/index.js';
5
4
  import { IacType } from '../../iac/common.js';
6
5
  import { InfrastructureType } from '../../infrastructure/common.js';
6
+ import { isCloudBackendActive } from '../active-cloud.js';
7
7
  import { CloudPackageManager } from './cloud.js';
8
8
  import { GitHubPackageManager } from './github.js';
9
9
  import { LocalPackageManager } from './local.js';
@@ -31,23 +31,21 @@ export async function resolvePackage(input) {
31
31
  return { found: false, reason: 'Invalid package format. Package name cannot contain dots (.) nor double dashes (--)' };
32
32
  }
33
33
  const isLocal = packageName.startsWith('local/');
34
- // Try cloud first if not local and backend is cloud
35
- if (!isLocal) {
36
- const backendType = await getCurrentBackendType();
37
- if (backendType === BackendType.Cloud) {
38
- const result = await tryCloudResolution(input);
39
- if (result.found) {
40
- return result; // Success
41
- }
42
- // Failed - check if we should fallback to GitHub or return the error
43
- if (!packageName.includes('/')) {
44
- // Simple package names can't fallback to GitHub, return the detailed error
45
- return result;
46
- }
47
- // For owner/repo format, we can fallback to GitHub
48
- input.logger?.debug(`Package ${packageName} not found in registry, trying GitHub...`);
49
- input.logger?.debug(`Registry error: ${result.reason}`);
34
+ // Try cloud first if not local and backend is cloud (or an ephemeral
35
+ // --token is in scope, which acts as Cloud regardless of on-disk config).
36
+ if (!isLocal && await isCloudBackendActive()) {
37
+ const result = await tryCloudResolution(input);
38
+ if (result.found) {
39
+ return result; // Success
50
40
  }
41
+ // Failed - check if we should fallback to GitHub or return the error
42
+ if (!packageName.includes('/')) {
43
+ // Simple package names can't fallback to GitHub, return the detailed error
44
+ return result;
45
+ }
46
+ // For owner/repo format, we can fallback to GitHub
47
+ input.logger?.debug(`Package ${packageName} not found in registry, trying GitHub...`);
48
+ input.logger?.debug(`Registry error: ${result.reason}`);
51
49
  }
52
50
  // Determine protocol for standard resolution
53
51
  const protocol = isLocal ? 'local' : '';
@@ -63,12 +61,10 @@ export async function downloadPackage(pkgUrl, destPath) {
63
61
  if (pkgUrl.startsWith('local/')) {
64
62
  protocol = 'local';
65
63
  }
66
- else if (pkgUrl.includes('#')) {
67
- // Registry package URLs contain commit SHA
68
- const backendType = await getCurrentBackendType();
69
- if (backendType === BackendType.Cloud) {
70
- protocol = 'cloud';
71
- }
64
+ else if (pkgUrl.includes('#') && await isCloudBackendActive()) {
65
+ // Registry package URLs contain commit SHA. Cloud branch fires when
66
+ // backend.yaml says Cloud OR an ephemeral --token is in scope.
67
+ protocol = 'cloud';
72
68
  }
73
69
  // Otherwise defaults to GitHub
74
70
  const packageManager = await getPackageManager(protocol);