hereya-cli 0.85.6 → 0.86.1
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/README.md +130 -80
- package/dist/backend/index.js +24 -0
- package/dist/commands/app/deploy/index.d.ts +2 -0
- package/dist/commands/app/deploy/index.js +17 -0
- package/dist/commands/app/deployments/index.d.ts +4 -0
- package/dist/commands/app/deployments/index.js +20 -1
- package/dist/commands/app/destroy/index.d.ts +2 -0
- package/dist/commands/app/destroy/index.js +17 -0
- package/dist/commands/app/env/index.d.ts +2 -0
- package/dist/commands/app/env/index.js +17 -0
- package/dist/commands/app/list/index.d.ts +4 -0
- package/dist/commands/app/list/index.js +20 -1
- package/dist/commands/app/status/index.d.ts +2 -0
- package/dist/commands/app/status/index.js +17 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +90 -55
- package/dist/commands/init/index.d.ts +2 -0
- package/dist/commands/init/index.js +18 -1
- package/dist/commands/login/index.js +5 -0
- package/dist/commands/publish/index.d.ts +2 -0
- package/dist/commands/publish/index.js +17 -0
- package/dist/commands/run/index.d.ts +2 -0
- package/dist/commands/run/index.js +17 -0
- package/dist/commands/search/index.d.ts +2 -0
- package/dist/commands/search/index.js +38 -21
- package/dist/commands/undeploy/index.d.ts +2 -0
- package/dist/commands/undeploy/index.js +50 -15
- package/dist/commands/up/index.d.ts +2 -0
- package/dist/commands/up/index.js +17 -1
- package/dist/commands/workspace/executor/install/index.d.ts +2 -0
- package/dist/commands/workspace/executor/install/index.js +17 -1
- package/dist/commands/workspace/executor/token/index.d.ts +2 -0
- package/dist/commands/workspace/executor/token/index.js +17 -0
- package/dist/commands/workspace/executor/uninstall/index.d.ts +2 -0
- package/dist/commands/workspace/executor/uninstall/index.js +17 -1
- package/dist/lib/ephemeral-token.d.ts +38 -0
- package/dist/lib/ephemeral-token.js +36 -0
- package/dist/lib/package/index.d.ts +12 -0
- package/dist/lib/package/index.js +36 -22
- package/oclif.manifest.json +133 -3
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@ import { Args, Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { getBackend } from '../../backend/index.js';
|
|
3
3
|
import { getConfigManager } from '../../lib/config/index.js';
|
|
4
4
|
import { getEnvManager } from '../../lib/env/index.js';
|
|
5
|
+
import { withEphemeralToken } from '../../lib/ephemeral-token.js';
|
|
6
|
+
import { getDefaultLogger } from '../../lib/log.js';
|
|
5
7
|
import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
|
|
6
8
|
import { runShell } from '../../lib/shell.js';
|
|
7
9
|
import { validateDevelopmentWorkspace } from '../../lib/workspace-validation.js';
|
|
@@ -19,6 +21,10 @@ export default class Run extends Command {
|
|
|
19
21
|
description: 'directory to run command in',
|
|
20
22
|
required: false,
|
|
21
23
|
}),
|
|
24
|
+
token: Flags.string({
|
|
25
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
22
28
|
workspace: Flags.string({
|
|
23
29
|
char: 'w',
|
|
24
30
|
description: 'name of the workspace to run the command in',
|
|
@@ -27,6 +33,17 @@ export default class Run extends Command {
|
|
|
27
33
|
};
|
|
28
34
|
static strict = false;
|
|
29
35
|
async run() {
|
|
36
|
+
const { flags } = await this.parse(Run);
|
|
37
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
38
|
+
if (flags.token) {
|
|
39
|
+
if (process.env.HEREYA_TOKEN) {
|
|
40
|
+
getDefaultLogger().debug('[run] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
41
|
+
}
|
|
42
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
43
|
+
}
|
|
44
|
+
return this.runInner();
|
|
45
|
+
}
|
|
46
|
+
async runInner() {
|
|
30
47
|
const { args, argv, flags } = await this.parse(Run);
|
|
31
48
|
const projectRootDir = flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR;
|
|
32
49
|
const configManager = getConfigManager();
|
|
@@ -9,7 +9,9 @@ export default class Search extends Command {
|
|
|
9
9
|
chdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
};
|
|
13
14
|
run(): Promise<void>;
|
|
14
15
|
private displayResults;
|
|
16
|
+
private runInner;
|
|
15
17
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { getBackend } from '../../backend/index.js';
|
|
4
|
+
import { withEphemeralToken } from '../../lib/ephemeral-token.js';
|
|
5
|
+
import { getDefaultLogger } from '../../lib/log.js';
|
|
4
6
|
export default class Search extends Command {
|
|
5
7
|
static args = {
|
|
6
8
|
query: Args.string({
|
|
@@ -30,8 +32,44 @@ export default class Search extends Command {
|
|
|
30
32
|
default: 20,
|
|
31
33
|
description: 'Maximum number of results to return',
|
|
32
34
|
}),
|
|
35
|
+
token: Flags.string({
|
|
36
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
33
39
|
};
|
|
34
40
|
async run() {
|
|
41
|
+
const { flags } = await this.parse(Search);
|
|
42
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
43
|
+
if (flags.token) {
|
|
44
|
+
if (process.env.HEREYA_TOKEN) {
|
|
45
|
+
getDefaultLogger().debug('[search] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
46
|
+
}
|
|
47
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
48
|
+
}
|
|
49
|
+
return this.runInner();
|
|
50
|
+
}
|
|
51
|
+
displayResults(packages, hasMore) {
|
|
52
|
+
if (packages.length === 0) {
|
|
53
|
+
this.log(chalk.yellow('No packages found matching your query.'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.log(`Found ${chalk.bold(packages.length)} package(s):`);
|
|
57
|
+
this.log();
|
|
58
|
+
for (const pkg of packages) {
|
|
59
|
+
const visibilityBadge = pkg.visibility === 'PUBLIC'
|
|
60
|
+
? chalk.green('PUBLIC')
|
|
61
|
+
: chalk.yellow('PRIVATE');
|
|
62
|
+
this.log(` ${chalk.cyan.bold(`${pkg.name}@${pkg.version}`)} ${visibilityBadge}`);
|
|
63
|
+
if (pkg.description) {
|
|
64
|
+
this.log(` ${chalk.gray(pkg.description)}`);
|
|
65
|
+
}
|
|
66
|
+
this.log();
|
|
67
|
+
}
|
|
68
|
+
if (hasMore) {
|
|
69
|
+
this.log(chalk.dim('More results available. Use --limit to see more.'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async runInner() {
|
|
35
73
|
const { args, flags } = await this.parse(Search);
|
|
36
74
|
// Change to specified directory
|
|
37
75
|
if (flags.chdir !== '.') {
|
|
@@ -66,25 +104,4 @@ export default class Search extends Command {
|
|
|
66
104
|
this.error(error instanceof Error ? error.message : 'Failed to search packages');
|
|
67
105
|
}
|
|
68
106
|
}
|
|
69
|
-
displayResults(packages, hasMore) {
|
|
70
|
-
if (packages.length === 0) {
|
|
71
|
-
this.log(chalk.yellow('No packages found matching your query.'));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
this.log(`Found ${chalk.bold(packages.length)} package(s):`);
|
|
75
|
-
this.log();
|
|
76
|
-
for (const pkg of packages) {
|
|
77
|
-
const visibilityBadge = pkg.visibility === 'PUBLIC'
|
|
78
|
-
? chalk.green('PUBLIC')
|
|
79
|
-
: chalk.yellow('PRIVATE');
|
|
80
|
-
this.log(` ${chalk.cyan.bold(`${pkg.name}@${pkg.version}`)} ${visibilityBadge}`);
|
|
81
|
-
if (pkg.description) {
|
|
82
|
-
this.log(` ${chalk.gray(pkg.description)}`);
|
|
83
|
-
}
|
|
84
|
-
this.log();
|
|
85
|
-
}
|
|
86
|
-
if (hasMore) {
|
|
87
|
-
this.log(chalk.dim('More results available. Use --limit to see more.'));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
107
|
}
|
|
@@ -6,8 +6,10 @@ export default class Undeploy extends Command {
|
|
|
6
6
|
chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
};
|
|
11
12
|
run(): Promise<void>;
|
|
13
|
+
private runInner;
|
|
12
14
|
private undeployRemotely;
|
|
13
15
|
}
|
|
@@ -7,8 +7,9 @@ import { getBackend } from '../../backend/index.js';
|
|
|
7
7
|
import { getExecutor } from '../../executor/index.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
11
|
import { gitUtils } from '../../lib/git-utils.js';
|
|
11
|
-
import { getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
|
|
12
|
+
import { getDefaultLogger, getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
|
|
12
13
|
import { getParameterManager } from '../../lib/parameter/index.js';
|
|
13
14
|
import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
|
|
14
15
|
import { pollExecutorJob } from '../../lib/remote-job-utils.js';
|
|
@@ -35,6 +36,10 @@ export default class Undeploy extends Command {
|
|
|
35
36
|
default: false,
|
|
36
37
|
description: 'force local execution (skip remote executor)',
|
|
37
38
|
}),
|
|
39
|
+
token: Flags.string({
|
|
40
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
38
43
|
workspace: Flags.string({
|
|
39
44
|
char: 'w',
|
|
40
45
|
description: 'name of the workspace to undeploy the packages for',
|
|
@@ -42,6 +47,17 @@ export default class Undeploy extends Command {
|
|
|
42
47
|
}),
|
|
43
48
|
};
|
|
44
49
|
async run() {
|
|
50
|
+
const { flags } = await this.parse(Undeploy);
|
|
51
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
52
|
+
if (flags.token) {
|
|
53
|
+
if (process.env.HEREYA_TOKEN) {
|
|
54
|
+
getDefaultLogger().debug('[undeploy] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
55
|
+
}
|
|
56
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
57
|
+
}
|
|
58
|
+
return this.runInner();
|
|
59
|
+
}
|
|
60
|
+
async runInner() {
|
|
45
61
|
const { flags } = await this.parse(Undeploy);
|
|
46
62
|
setDebug(flags.debug);
|
|
47
63
|
const projectRootDir = path.resolve(flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR || process.cwd());
|
|
@@ -322,22 +338,41 @@ See ${getLogPath()} for more details`);
|
|
|
322
338
|
this.error(cleanCheck.reason);
|
|
323
339
|
}
|
|
324
340
|
myLogger.log(ListrLogLevels.STARTED, `Undeploying ${input.workspace} via remote executor (branch: ${input.gitBranch})`);
|
|
325
|
-
// 2. Get cloud backend for job submission
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
});
|
|
329
359
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
});
|
|
333
375
|
}
|
|
334
|
-
const resolvedWorkspaceName = resolveWorkspaceName(input.workspace, input.project);
|
|
335
|
-
const cloudBackend = new CloudBackend({
|
|
336
|
-
accessToken: credentials.accessToken,
|
|
337
|
-
clientId: backendConfig.cloud.clientId,
|
|
338
|
-
refreshToken: credentials.refreshToken,
|
|
339
|
-
url: backendConfig.cloud.url,
|
|
340
|
-
});
|
|
341
376
|
// 3. Submit undeploy job
|
|
342
377
|
const submitResult = await cloudBackend.submitExecutorJob({
|
|
343
378
|
payload: {
|
|
@@ -7,7 +7,9 @@ export default class Up extends Command {
|
|
|
7
7
|
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
deploy: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
select: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
};
|
|
12
13
|
run(): Promise<void>;
|
|
14
|
+
private runInner;
|
|
13
15
|
}
|
|
@@ -4,7 +4,8 @@ import { getBackend } from '../../backend/index.js';
|
|
|
4
4
|
import { getExecutorForWorkspace } from '../../executor/context.js';
|
|
5
5
|
import { getConfigManager } from '../../lib/config/index.js';
|
|
6
6
|
import { getEnvManager } from '../../lib/env/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { withEphemeralToken } from '../../lib/ephemeral-token.js';
|
|
8
|
+
import { getDefaultLogger, getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
|
|
8
9
|
import { getParameterManager } from '../../lib/parameter/index.js';
|
|
9
10
|
import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
|
|
10
11
|
import { delay } from '../../lib/shell.js';
|
|
@@ -35,6 +36,10 @@ export default class Up extends Command {
|
|
|
35
36
|
description: 'select the packages to provision',
|
|
36
37
|
multiple: true,
|
|
37
38
|
}),
|
|
39
|
+
token: Flags.string({
|
|
40
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
38
43
|
workspace: Flags.string({
|
|
39
44
|
char: 'w',
|
|
40
45
|
description: 'name of the workspace to install the packages for',
|
|
@@ -42,6 +47,17 @@ export default class Up extends Command {
|
|
|
42
47
|
}),
|
|
43
48
|
};
|
|
44
49
|
async run() {
|
|
50
|
+
const { flags } = await this.parse(Up);
|
|
51
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
52
|
+
if (flags.token) {
|
|
53
|
+
if (process.env.HEREYA_TOKEN) {
|
|
54
|
+
getDefaultLogger().debug('[up] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
55
|
+
}
|
|
56
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
57
|
+
}
|
|
58
|
+
return this.runInner();
|
|
59
|
+
}
|
|
60
|
+
async runInner() {
|
|
45
61
|
const { flags } = await this.parse(Up);
|
|
46
62
|
setDebug(flags.debug);
|
|
47
63
|
const projectRootDir = flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR;
|
|
@@ -4,7 +4,9 @@ export default class WorkspaceExecutorInstall extends Command {
|
|
|
4
4
|
static flags: {
|
|
5
5
|
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
6
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
};
|
|
9
10
|
run(): Promise<void>;
|
|
11
|
+
private runInner;
|
|
10
12
|
}
|
|
@@ -3,7 +3,8 @@ import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
|
|
|
3
3
|
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
4
4
|
import { getBackend } from '../../../../backend/index.js';
|
|
5
5
|
import { getExecutor } from '../../../../executor/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { withEphemeralToken } from '../../../../lib/ephemeral-token.js';
|
|
7
|
+
import { getDefaultLogger, getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
|
|
7
8
|
import { delay } from '../../../../lib/shell.js';
|
|
8
9
|
const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
|
|
9
10
|
export default class WorkspaceExecutorInstall extends Command {
|
|
@@ -11,6 +12,10 @@ export default class WorkspaceExecutorInstall extends Command {
|
|
|
11
12
|
static flags = {
|
|
12
13
|
debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
|
|
13
14
|
force: Flags.boolean({ char: 'f', default: false, description: 'force reinstall even if executor is already installed' }),
|
|
15
|
+
token: Flags.string({
|
|
16
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
17
|
+
required: false,
|
|
18
|
+
}),
|
|
14
19
|
workspace: Flags.string({
|
|
15
20
|
char: 'w',
|
|
16
21
|
description: 'name of the workspace',
|
|
@@ -18,6 +23,17 @@ export default class WorkspaceExecutorInstall extends Command {
|
|
|
18
23
|
}),
|
|
19
24
|
};
|
|
20
25
|
async run() {
|
|
26
|
+
const { flags } = await this.parse(WorkspaceExecutorInstall);
|
|
27
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
28
|
+
if (flags.token) {
|
|
29
|
+
if (process.env.HEREYA_TOKEN) {
|
|
30
|
+
getDefaultLogger().debug('[workspace executor install] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
31
|
+
}
|
|
32
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
33
|
+
}
|
|
34
|
+
return this.runInner();
|
|
35
|
+
}
|
|
36
|
+
async runInner() {
|
|
21
37
|
const { flags } = await this.parse(WorkspaceExecutorInstall);
|
|
22
38
|
setDebug(flags.debug);
|
|
23
39
|
const myLogger = new ListrLogger({ useIcons: false });
|
|
@@ -2,7 +2,9 @@ import { Command } from '@oclif/core';
|
|
|
2
2
|
export default class WorkspaceExecutorToken extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
6
|
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
};
|
|
7
8
|
run(): Promise<void>;
|
|
9
|
+
private runInner;
|
|
8
10
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
3
3
|
import { getBackend } from '../../../../backend/index.js';
|
|
4
|
+
import { withEphemeralToken } from '../../../../lib/ephemeral-token.js';
|
|
5
|
+
import { getDefaultLogger } from '../../../../lib/log.js';
|
|
4
6
|
export default class WorkspaceExecutorToken extends Command {
|
|
5
7
|
static description = 'Generate a workspace-scoped executor token for the remote executor';
|
|
6
8
|
static flags = {
|
|
9
|
+
token: Flags.string({
|
|
10
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
11
|
+
required: false,
|
|
12
|
+
}),
|
|
7
13
|
workspace: Flags.string({
|
|
8
14
|
char: 'w',
|
|
9
15
|
description: 'name of the workspace',
|
|
@@ -11,6 +17,17 @@ export default class WorkspaceExecutorToken extends Command {
|
|
|
11
17
|
}),
|
|
12
18
|
};
|
|
13
19
|
async run() {
|
|
20
|
+
const { flags } = await this.parse(WorkspaceExecutorToken);
|
|
21
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
22
|
+
if (flags.token) {
|
|
23
|
+
if (process.env.HEREYA_TOKEN) {
|
|
24
|
+
getDefaultLogger().debug('[workspace executor token] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
25
|
+
}
|
|
26
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
27
|
+
}
|
|
28
|
+
return this.runInner();
|
|
29
|
+
}
|
|
30
|
+
async runInner() {
|
|
14
31
|
const { flags } = await this.parse(WorkspaceExecutorToken);
|
|
15
32
|
const backend = await getBackend();
|
|
16
33
|
if (!(backend instanceof CloudBackend)) {
|
|
@@ -3,7 +3,9 @@ export default class WorkspaceExecutorUninstall extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
5
|
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
};
|
|
8
9
|
run(): Promise<void>;
|
|
10
|
+
private runInner;
|
|
9
11
|
}
|
|
@@ -3,13 +3,18 @@ import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
|
|
|
3
3
|
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
4
4
|
import { getBackend } from '../../../../backend/index.js';
|
|
5
5
|
import { getExecutor } from '../../../../executor/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { withEphemeralToken } from '../../../../lib/ephemeral-token.js';
|
|
7
|
+
import { getDefaultLogger, getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
|
|
7
8
|
import { delay } from '../../../../lib/shell.js';
|
|
8
9
|
const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
|
|
9
10
|
export default class WorkspaceExecutorUninstall extends Command {
|
|
10
11
|
static description = 'Uninstall the remote executor from a workspace';
|
|
11
12
|
static flags = {
|
|
12
13
|
debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
|
|
14
|
+
token: Flags.string({
|
|
15
|
+
description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Takes precedence over the HEREYA_TOKEN environment variable.',
|
|
16
|
+
required: false,
|
|
17
|
+
}),
|
|
13
18
|
workspace: Flags.string({
|
|
14
19
|
char: 'w',
|
|
15
20
|
description: 'name of the workspace',
|
|
@@ -17,6 +22,17 @@ export default class WorkspaceExecutorUninstall extends Command {
|
|
|
17
22
|
}),
|
|
18
23
|
};
|
|
19
24
|
async run() {
|
|
25
|
+
const { flags } = await this.parse(WorkspaceExecutorUninstall);
|
|
26
|
+
// Precedence: --token flag > HEREYA_TOKEN env var > keychain.
|
|
27
|
+
if (flags.token) {
|
|
28
|
+
if (process.env.HEREYA_TOKEN) {
|
|
29
|
+
getDefaultLogger().debug('[workspace executor uninstall] --token flag overrides HEREYA_TOKEN environment variable.');
|
|
30
|
+
}
|
|
31
|
+
return withEphemeralToken(flags.token, () => this.runInner());
|
|
32
|
+
}
|
|
33
|
+
return this.runInner();
|
|
34
|
+
}
|
|
35
|
+
async runInner() {
|
|
20
36
|
const { flags } = await this.parse(WorkspaceExecutorUninstall);
|
|
21
37
|
setDebug(flags.debug);
|
|
22
38
|
const myLogger = new ListrLogger({ useIcons: false });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory context for an ephemeral, in-memory-only access token.
|
|
3
|
+
*
|
|
4
|
+
* This is used to support per-invocation tokens (e.g. minted by a separate
|
|
5
|
+
* MCP server) without persisting credentials to the user's keychain or
|
|
6
|
+
* `~/.hereya/secrets/`. Code that needs to read the active token (e.g. the
|
|
7
|
+
* cloud backend factory in `src/backend/index.ts`) calls
|
|
8
|
+
* {@link getEphemeralToken} and uses it directly as the bearer for outgoing
|
|
9
|
+
* cloud API calls. No exchange via `loginWithToken` happens — that path is
|
|
10
|
+
* intentionally avoided because it would mint a long-lived refresh token.
|
|
11
|
+
*/
|
|
12
|
+
export interface EphemeralTokenContext {
|
|
13
|
+
cloudUrl?: string;
|
|
14
|
+
token: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the active ephemeral token context, or `undefined` if no
|
|
18
|
+
* `withEphemeralToken` scope is currently in flight.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getEphemeralToken(): EphemeralTokenContext | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Run `fn` inside an ephemeral-token scope. While `fn` is executing,
|
|
23
|
+
* `getEphemeralToken()` returns the supplied token (and optional cloudUrl).
|
|
24
|
+
*
|
|
25
|
+
* Critical guarantees:
|
|
26
|
+
* - The token is held in-memory only. It is NEVER passed to the secret
|
|
27
|
+
* manager or written to disk.
|
|
28
|
+
* - After `fn` returns or throws, `getEphemeralToken()` returns `undefined`
|
|
29
|
+
* again.
|
|
30
|
+
* - The cached backend in `src/backend/index.ts` is cleared before and after
|
|
31
|
+
* the scope so that a stale, persisted-credential-backed CloudBackend is
|
|
32
|
+
* never used while the scope is active, and the scope's in-memory backend
|
|
33
|
+
* does not leak to subsequent code that expected the user's regular
|
|
34
|
+
* credentials.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withEphemeralToken<T>(token: string, fn: () => Promise<T>, options?: {
|
|
37
|
+
cloudUrl?: string;
|
|
38
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { clearBackend } from '../backend/index.js';
|
|
3
|
+
const storage = new AsyncLocalStorage();
|
|
4
|
+
/**
|
|
5
|
+
* Returns the active ephemeral token context, or `undefined` if no
|
|
6
|
+
* `withEphemeralToken` scope is currently in flight.
|
|
7
|
+
*/
|
|
8
|
+
export function getEphemeralToken() {
|
|
9
|
+
return storage.getStore();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` inside an ephemeral-token scope. While `fn` is executing,
|
|
13
|
+
* `getEphemeralToken()` returns the supplied token (and optional cloudUrl).
|
|
14
|
+
*
|
|
15
|
+
* Critical guarantees:
|
|
16
|
+
* - The token is held in-memory only. It is NEVER passed to the secret
|
|
17
|
+
* manager or written to disk.
|
|
18
|
+
* - After `fn` returns or throws, `getEphemeralToken()` returns `undefined`
|
|
19
|
+
* again.
|
|
20
|
+
* - The cached backend in `src/backend/index.ts` is cleared before and after
|
|
21
|
+
* the scope so that a stale, persisted-credential-backed CloudBackend is
|
|
22
|
+
* never used while the scope is active, and the scope's in-memory backend
|
|
23
|
+
* does not leak to subsequent code that expected the user's regular
|
|
24
|
+
* credentials.
|
|
25
|
+
*/
|
|
26
|
+
export async function withEphemeralToken(token, fn, options) {
|
|
27
|
+
// Drop any cached backend so the next getBackend() call sees the ephemeral context.
|
|
28
|
+
clearBackend();
|
|
29
|
+
try {
|
|
30
|
+
return await storage.run({ cloudUrl: options?.cloudUrl, token }, fn);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
// Drop the in-memory backend bound to the ephemeral token before yielding control.
|
|
34
|
+
clearBackend();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -8,6 +8,18 @@ import { LocalPackageManager } from './local.js';
|
|
|
8
8
|
export declare const githubPackageManager: PackageManager;
|
|
9
9
|
export declare const localPackageManager: LocalPackageManager;
|
|
10
10
|
export declare function getPackageManager(protocol: string, logger?: Logger): Promise<PackageManager>;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the cloud package registry should be consulted on this invocation.
|
|
13
|
+
*
|
|
14
|
+
* Returns true when either:
|
|
15
|
+
* - the persisted backend type (from `~/.hereya/backend.yaml`) is `Cloud`, or
|
|
16
|
+
* - an ephemeral `--token` is in scope (set via `withEphemeralToken`).
|
|
17
|
+
*
|
|
18
|
+
* The ephemeral-token branch is what makes `--token` work for fresh users who
|
|
19
|
+
* have never run `hereya login` — without it, the on-disk backend type stays
|
|
20
|
+
* `Local` and cloud-only packages fail to resolve.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isCloudBackendActive(): Promise<boolean>;
|
|
11
23
|
export declare function resolvePackage(input: ResolvePackageInput): Promise<ResolvePackageOutput>;
|
|
12
24
|
export declare function getPackageCanonicalName(packageName: string): string;
|
|
13
25
|
export declare function downloadPackage(pkgUrl: string, destPath: string): Promise<string>;
|
|
@@ -4,6 +4,7 @@ import { getCurrentBackendType } from '../../backend/config.js';
|
|
|
4
4
|
import { BackendType, getBackend } from '../../backend/index.js';
|
|
5
5
|
import { IacType } from '../../iac/common.js';
|
|
6
6
|
import { InfrastructureType } from '../../infrastructure/common.js';
|
|
7
|
+
import { getEphemeralToken } from '../ephemeral-token.js';
|
|
7
8
|
import { CloudPackageManager } from './cloud.js';
|
|
8
9
|
import { GitHubPackageManager } from './github.js';
|
|
9
10
|
import { LocalPackageManager } from './local.js';
|
|
@@ -20,6 +21,23 @@ export async function getPackageManager(protocol, logger) {
|
|
|
20
21
|
// Default to GitHub
|
|
21
22
|
return githubPackageManager;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether the cloud package registry should be consulted on this invocation.
|
|
26
|
+
*
|
|
27
|
+
* Returns true when either:
|
|
28
|
+
* - the persisted backend type (from `~/.hereya/backend.yaml`) is `Cloud`, or
|
|
29
|
+
* - an ephemeral `--token` is in scope (set via `withEphemeralToken`).
|
|
30
|
+
*
|
|
31
|
+
* The ephemeral-token branch is what makes `--token` work for fresh users who
|
|
32
|
+
* have never run `hereya login` — without it, the on-disk backend type stays
|
|
33
|
+
* `Local` and cloud-only packages fail to resolve.
|
|
34
|
+
*/
|
|
35
|
+
export async function isCloudBackendActive() {
|
|
36
|
+
if (getEphemeralToken()) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return (await getCurrentBackendType()) === BackendType.Cloud;
|
|
40
|
+
}
|
|
23
41
|
export async function resolvePackage(input) {
|
|
24
42
|
// Parse package spec to extract name and version
|
|
25
43
|
const { packageName, version } = parsePackageSpec(input.package);
|
|
@@ -31,23 +49,21 @@ export async function resolvePackage(input) {
|
|
|
31
49
|
return { found: false, reason: 'Invalid package format. Package name cannot contain dots (.) nor double dashes (--)' };
|
|
32
50
|
}
|
|
33
51
|
const isLocal = packageName.startsWith('local/');
|
|
34
|
-
// Try cloud first if not local and backend is cloud
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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}`);
|
|
52
|
+
// Try cloud first if not local and backend is cloud (or an ephemeral
|
|
53
|
+
// --token is in scope, which acts as Cloud regardless of on-disk config).
|
|
54
|
+
if (!isLocal && await isCloudBackendActive()) {
|
|
55
|
+
const result = await tryCloudResolution(input);
|
|
56
|
+
if (result.found) {
|
|
57
|
+
return result; // Success
|
|
50
58
|
}
|
|
59
|
+
// Failed - check if we should fallback to GitHub or return the error
|
|
60
|
+
if (!packageName.includes('/')) {
|
|
61
|
+
// Simple package names can't fallback to GitHub, return the detailed error
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
// For owner/repo format, we can fallback to GitHub
|
|
65
|
+
input.logger?.debug(`Package ${packageName} not found in registry, trying GitHub...`);
|
|
66
|
+
input.logger?.debug(`Registry error: ${result.reason}`);
|
|
51
67
|
}
|
|
52
68
|
// Determine protocol for standard resolution
|
|
53
69
|
const protocol = isLocal ? 'local' : '';
|
|
@@ -63,12 +79,10 @@ export async function downloadPackage(pkgUrl, destPath) {
|
|
|
63
79
|
if (pkgUrl.startsWith('local/')) {
|
|
64
80
|
protocol = 'local';
|
|
65
81
|
}
|
|
66
|
-
else if (pkgUrl.includes('#')) {
|
|
67
|
-
// Registry package URLs contain commit SHA
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
protocol = 'cloud';
|
|
71
|
-
}
|
|
82
|
+
else if (pkgUrl.includes('#') && await isCloudBackendActive()) {
|
|
83
|
+
// Registry package URLs contain commit SHA. Cloud branch fires when
|
|
84
|
+
// backend.yaml says Cloud OR an ephemeral --token is in scope.
|
|
85
|
+
protocol = 'cloud';
|
|
72
86
|
}
|
|
73
87
|
// Otherwise defaults to GitHub
|
|
74
88
|
const packageManager = await getPackageManager(protocol);
|