hereya-cli 0.87.0 → 0.89.0

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.
Files changed (54) hide show
  1. package/README.md +111 -163
  2. package/dist/backend/cloud/cloud-backend.d.ts +2 -1
  3. package/dist/backend/cloud/cloud-backend.js +36 -0
  4. package/dist/backend/common.d.ts +17 -0
  5. package/dist/backend/index.js +0 -24
  6. package/dist/commands/app/deploy/index.d.ts +0 -2
  7. package/dist/commands/app/deploy/index.js +0 -17
  8. package/dist/commands/app/deployments/index.d.ts +0 -4
  9. package/dist/commands/app/deployments/index.js +1 -20
  10. package/dist/commands/app/destroy/index.d.ts +0 -2
  11. package/dist/commands/app/destroy/index.js +0 -17
  12. package/dist/commands/app/env/index.d.ts +0 -2
  13. package/dist/commands/app/env/index.js +0 -17
  14. package/dist/commands/app/list/index.d.ts +0 -4
  15. package/dist/commands/app/list/index.js +1 -20
  16. package/dist/commands/app/status/index.d.ts +0 -2
  17. package/dist/commands/app/status/index.js +0 -17
  18. package/dist/commands/clone/index.js +2 -2
  19. package/dist/commands/deploy/index.d.ts +0 -2
  20. package/dist/commands/deploy/index.js +56 -69
  21. package/dist/commands/import-repo/index.d.ts +15 -0
  22. package/dist/commands/import-repo/index.js +111 -0
  23. package/dist/commands/init/index.d.ts +0 -2
  24. package/dist/commands/init/index.js +10 -70
  25. package/dist/commands/login/index.js +0 -5
  26. package/dist/commands/publish/index.d.ts +0 -2
  27. package/dist/commands/publish/index.js +0 -17
  28. package/dist/commands/run/index.d.ts +0 -2
  29. package/dist/commands/run/index.js +0 -17
  30. package/dist/commands/search/index.d.ts +0 -2
  31. package/dist/commands/search/index.js +21 -38
  32. package/dist/commands/undeploy/index.d.ts +0 -2
  33. package/dist/commands/undeploy/index.js +14 -27
  34. package/dist/commands/up/index.d.ts +0 -2
  35. package/dist/commands/up/index.js +1 -17
  36. package/dist/commands/workspace/executor/install/index.d.ts +0 -2
  37. package/dist/commands/workspace/executor/install/index.js +1 -17
  38. package/dist/commands/workspace/executor/token/index.d.ts +0 -2
  39. package/dist/commands/workspace/executor/token/index.js +0 -17
  40. package/dist/commands/workspace/executor/uninstall/index.d.ts +0 -2
  41. package/dist/commands/workspace/executor/uninstall/index.js +1 -17
  42. package/dist/executor/context.js +15 -12
  43. package/dist/lib/clone-and-configure.d.ts +43 -0
  44. package/dist/lib/clone-and-configure.js +77 -0
  45. package/dist/lib/hereya-token.js +11 -10
  46. package/dist/lib/package/index.js +24 -20
  47. package/oclif.manifest.json +145 -254
  48. package/package.json +1 -1
  49. package/dist/commands/git/index.d.ts +0 -12
  50. package/dist/commands/git/index.js +0 -116
  51. package/dist/lib/active-cloud.d.ts +0 -31
  52. package/dist/lib/active-cloud.js +0 -55
  53. package/dist/lib/ephemeral-token.d.ts +0 -45
  54. package/dist/lib/ephemeral-token.js +0 -89
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hereya-cli",
3
3
  "description": "Infrastructure as Package",
4
- "version": "0.87.0",
4
+ "version": "0.89.0",
5
5
  "author": "Hereya Developers",
6
6
  "bin": {
7
7
  "hereya": "./bin/run.js"
@@ -1,12 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class Git extends Command {
3
- static description: string;
4
- static examples: string[];
5
- static flags: {
6
- chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
- token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- };
9
- static strict: boolean;
10
- run(): Promise<void>;
11
- private runInner;
12
- }
@@ -1,116 +0,0 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { withEphemeralToken } from '../../lib/ephemeral-token.js';
5
- import { gitUtils } from '../../lib/git-utils.js';
6
- import { getDefaultLogger } from '../../lib/log.js';
7
- import { shellUtils } from '../../lib/shell.js';
8
- export default class Git extends Command {
9
- static description = 'Run an arbitrary git command with the hereya credential helper available. Wraps `git <args...>`, propagating an ephemeral `--token` to the credential-helper grandchild via `HEREYA_EPHEMERAL_TOKEN`. After a successful `clone`, also wires the cloned repo to use the hereya credential helper for future `push`/`pull`/`fetch` operations. Use `--` to separate hereya flags from git flags so e.g. `--depth` reaches git instead of being parsed by oclif.';
10
- static examples = [
11
- '<%= config.bin %> <%= command.id %> --token <token> -- push',
12
- '<%= config.bin %> <%= command.id %> --token <token> -- pull --rebase',
13
- '<%= config.bin %> <%= command.id %> --token <token> -- clone https://github.com/owner/repo my-dir',
14
- '<%= config.bin %> <%= command.id %> -- log --oneline -10',
15
- ];
16
- static flags = {
17
- chdir: Flags.string({
18
- description: 'directory to run git in (cwd of the spawned git process)',
19
- required: false,
20
- }),
21
- token: Flags.string({
22
- description: 'Ephemeral cloud access token used for this invocation only. Held in memory; never written to the keychain or `~/.hereya/secrets/`. Propagated to the git credential-helper grandchild via the HEREYA_EPHEMERAL_TOKEN env var. Takes precedence over the HEREYA_TOKEN environment variable.',
23
- required: false,
24
- }),
25
- };
26
- static strict = false;
27
- async run() {
28
- const { argv, flags } = await this.parse(Git);
29
- // Precedence: --token flag > HEREYA_TOKEN env var > keychain.
30
- if (flags.token) {
31
- if (process.env.HEREYA_TOKEN) {
32
- getDefaultLogger().debug('[git] --token flag overrides HEREYA_TOKEN environment variable.');
33
- }
34
- return withEphemeralToken(flags.token, () => this.runInner(argv, flags));
35
- }
36
- return this.runInner(argv, flags);
37
- }
38
- async runInner(gitArgs, flags) {
39
- if (gitArgs.length === 0) {
40
- this.error('Missing git arguments. Example: `hereya git push --token <…>`.');
41
- }
42
- const cwd = path.resolve(flags.chdir ?? process.env.HEREYA_PROJECT_ROOT_DIR ?? process.cwd());
43
- const result = await shellUtils.runShell('git', gitArgs, {
44
- directory: cwd,
45
- stdio: 'inherit',
46
- });
47
- if (result.status !== 0) {
48
- this.error(`git ${gitArgs.join(' ')} failed (exit ${result.status})`);
49
- }
50
- // Auto-wire the credential helper after a successful clone so future
51
- // git operations from inside the cloned repo find hereya's helper.
52
- if (gitArgs[0] === 'clone') {
53
- const target = deriveCloneTarget(gitArgs.slice(1), cwd);
54
- if (target && fs.existsSync(path.join(target, '.git'))) {
55
- try {
56
- await gitUtils.setupCredentialHelper({
57
- hereyaBinPath: process.argv[1],
58
- projectDir: target,
59
- });
60
- }
61
- catch (error) {
62
- // Don't fail the clone if the wire-up step fails — surface as warning.
63
- this.warn(`Cloned ${gitArgs.join(' ')} successfully, but failed to wire the hereya credential helper in ${target}: ${error.message}`);
64
- }
65
- }
66
- }
67
- }
68
- }
69
- /**
70
- * Best-effort resolution of the directory `git clone` wrote to. Mirrors git's
71
- * own behavior: if the user passed an explicit target, that's it; otherwise
72
- * derive it from the URL's basename (stripping any trailing `.git`).
73
- *
74
- * Returns an absolute path or `null` if we can't tell.
75
- */
76
- function deriveCloneTarget(cloneArgs, cwd) {
77
- // Walk the args, skipping flag tokens (--xxx, -x) and their values.
78
- // This is heuristic — git's flag schema is large — but it covers the
79
- // common cases (`-c`, `--depth`, `--branch`, `--single-branch`, ...).
80
- // If the heuristic is wrong, the existsSync(.git) check below will skip
81
- // wire-up rather than touching the wrong directory.
82
- const positional = [];
83
- for (let i = 0; i < cloneArgs.length; i++) {
84
- const a = cloneArgs[i];
85
- if (a.startsWith('-')) {
86
- // --flag=value: no separate value arg.
87
- if (a.includes('='))
88
- continue;
89
- // -c key=value, --depth 1, etc.: skip the next arg as a value if it
90
- // doesn't look like another flag.
91
- if (i + 1 < cloneArgs.length && !cloneArgs[i + 1].startsWith('-')) {
92
- i++;
93
- }
94
- continue;
95
- }
96
- positional.push(a);
97
- }
98
- // First positional is the URL; second (optional) is the target.
99
- if (positional.length >= 2) {
100
- return path.resolve(cwd, positional[1]);
101
- }
102
- if (positional.length === 1) {
103
- const url = positional[0];
104
- // Equivalent to Array.findLast(Boolean) but without requiring ES2023 lib.
105
- const parts = url.replace(/\.git$/, '').split(/[/\\:]/);
106
- let basename = '';
107
- for (let i = parts.length - 1; i >= 0; i--) {
108
- if (parts[i]) {
109
- basename = parts[i];
110
- break;
111
- }
112
- }
113
- return basename ? path.resolve(cwd, basename) : null;
114
- }
115
- return null;
116
- }
@@ -1,31 +0,0 @@
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>;
@@ -1,55 +0,0 @@
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,45 +0,0 @@
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
- * Falls back to the `HEREYA_EPHEMERAL_TOKEN` env var when no async-local-
21
- * storage context is set — that's how subprocesses (the credential helper
22
- * grandchild, etc.) inherit the ephemeral context across process
23
- * boundaries. The env var is set automatically by `withEphemeralToken`.
24
- */
25
- export declare function getEphemeralToken(): EphemeralTokenContext | undefined;
26
- /**
27
- * Run `fn` inside an ephemeral-token scope. While `fn` is executing,
28
- * `getEphemeralToken()` returns the supplied token (and optional cloudUrl),
29
- * and `process.env.HEREYA_EPHEMERAL_TOKEN` is set so subprocesses inherit
30
- * the same ephemeral context.
31
- *
32
- * Critical guarantees:
33
- * - The token is held in-memory only. It is NEVER passed to the secret
34
- * manager or written to disk.
35
- * - After `fn` returns or throws, `getEphemeralToken()` returns `undefined`
36
- * again and `HEREYA_EPHEMERAL_TOKEN` is restored to its prior value.
37
- * - The cached backend in `src/backend/index.ts` is cleared before and after
38
- * the scope so that a stale, persisted-credential-backed CloudBackend is
39
- * never used while the scope is active, and the scope's in-memory backend
40
- * does not leak to subsequent code that expected the user's regular
41
- * credentials.
42
- */
43
- export declare function withEphemeralToken<T>(token: string, fn: () => Promise<T>, options?: {
44
- cloudUrl?: string;
45
- }): Promise<T>;
@@ -1,89 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { clearBackend } from '../backend/index.js';
3
- const storage = new AsyncLocalStorage();
4
- /**
5
- * Env-var names used to propagate the ephemeral token to **subprocesses**
6
- * (e.g. the `git` invocation that triggers the `hereya credential-helper`
7
- * grandchild). Async-local-storage doesn't cross process boundaries, so we
8
- * also stash the token in the environment for the duration of the scope.
9
- *
10
- * These are deliberately named differently from `HEREYA_TOKEN` because the
11
- * existing `HEREYA_TOKEN` semantics call `loginWithToken()` (exchange +
12
- * keychain persist), which is exactly what ephemeral mode must NOT do.
13
- */
14
- const EPHEMERAL_TOKEN_ENV = 'HEREYA_EPHEMERAL_TOKEN';
15
- const EPHEMERAL_CLOUD_URL_ENV = 'HEREYA_EPHEMERAL_CLOUD_URL';
16
- /**
17
- * Returns the active ephemeral token context, or `undefined` if no
18
- * `withEphemeralToken` scope is currently in flight.
19
- *
20
- * Falls back to the `HEREYA_EPHEMERAL_TOKEN` env var when no async-local-
21
- * storage context is set — that's how subprocesses (the credential helper
22
- * grandchild, etc.) inherit the ephemeral context across process
23
- * boundaries. The env var is set automatically by `withEphemeralToken`.
24
- */
25
- export function getEphemeralToken() {
26
- const fromAsyncStorage = storage.getStore();
27
- if (fromAsyncStorage) {
28
- return fromAsyncStorage;
29
- }
30
- const fromEnv = process.env[EPHEMERAL_TOKEN_ENV];
31
- if (fromEnv) {
32
- return {
33
- cloudUrl: process.env[EPHEMERAL_CLOUD_URL_ENV] || undefined,
34
- token: fromEnv,
35
- };
36
- }
37
- return undefined;
38
- }
39
- /**
40
- * Run `fn` inside an ephemeral-token scope. While `fn` is executing,
41
- * `getEphemeralToken()` returns the supplied token (and optional cloudUrl),
42
- * and `process.env.HEREYA_EPHEMERAL_TOKEN` is set so subprocesses inherit
43
- * the same ephemeral context.
44
- *
45
- * Critical guarantees:
46
- * - The token is held in-memory only. It is NEVER passed to the secret
47
- * manager or written to disk.
48
- * - After `fn` returns or throws, `getEphemeralToken()` returns `undefined`
49
- * again and `HEREYA_EPHEMERAL_TOKEN` is restored to its prior value.
50
- * - The cached backend in `src/backend/index.ts` is cleared before and after
51
- * the scope so that a stale, persisted-credential-backed CloudBackend is
52
- * never used while the scope is active, and the scope's in-memory backend
53
- * does not leak to subsequent code that expected the user's regular
54
- * credentials.
55
- */
56
- export async function withEphemeralToken(token, fn, options) {
57
- // Drop any cached backend so the next getBackend() call sees the ephemeral context.
58
- clearBackend();
59
- // Snapshot existing env-var values so we can restore them on exit.
60
- const priorTokenEnv = process.env[EPHEMERAL_TOKEN_ENV];
61
- const priorCloudUrlEnv = process.env[EPHEMERAL_CLOUD_URL_ENV];
62
- process.env[EPHEMERAL_TOKEN_ENV] = token;
63
- if (options?.cloudUrl) {
64
- process.env[EPHEMERAL_CLOUD_URL_ENV] = options.cloudUrl;
65
- }
66
- else {
67
- delete process.env[EPHEMERAL_CLOUD_URL_ENV];
68
- }
69
- try {
70
- return await storage.run({ cloudUrl: options?.cloudUrl, token }, fn);
71
- }
72
- finally {
73
- // Restore prior env-var state (no leakage between sibling invocations).
74
- if (priorTokenEnv === undefined) {
75
- delete process.env[EPHEMERAL_TOKEN_ENV];
76
- }
77
- else {
78
- process.env[EPHEMERAL_TOKEN_ENV] = priorTokenEnv;
79
- }
80
- if (priorCloudUrlEnv === undefined) {
81
- delete process.env[EPHEMERAL_CLOUD_URL_ENV];
82
- }
83
- else {
84
- process.env[EPHEMERAL_CLOUD_URL_ENV] = priorCloudUrlEnv;
85
- }
86
- // Drop the in-memory backend bound to the ephemeral token before yielding control.
87
- clearBackend();
88
- }
89
- }