@eve-horizon/cli 0.2.9 → 0.2.10

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 CHANGED
@@ -6,18 +6,38 @@ Published CLI (npx) for interacting with the Eve Horizon API. Dev/ops tooling li
6
6
  npx @eve/cli --help
7
7
  ```
8
8
 
9
+ ## Local Development & Global Parity
10
+
11
+ When testing CLI changes locally, you have two options:
12
+
13
+ ```bash
14
+ # Option 1: run the repo-local build directly
15
+ pnpm -C packages/cli build
16
+ node packages/cli/bin/eve.js --help
17
+
18
+ # Option 2: link the CLI so the global `eve` binary matches your local build
19
+ pnpm -C packages/cli build
20
+ cd packages/cli && npm link
21
+ eve --help
22
+ ```
23
+
24
+ To update global installs for others, publish a new CLI version via the `cli-v*` tag flow
25
+ (see root AGENTS.md for release instructions).
26
+
9
27
  ## Philosophy
10
28
 
11
29
  The CLI is the **primary interface** for Eve Horizon, designed for humans and AI agents. The REST API is the substrate: every operation is exposed via HTTP, and the CLI is a thin wrapper that handles argument parsing and output formatting. It does not bypass the API or contain business logic.
12
30
 
13
31
  See [API Philosophy](../../docs/system/api-philosophy.md) and [OpenAPI](../../docs/system/openapi.md).
14
32
 
15
- ## Profiles (defaults + credentials)
33
+ ## Profiles (repo-local defaults)
16
34
 
17
35
  Profiles store API URL, default org/project IDs, default harness, and Supabase auth config.
36
+ Profiles are **repo-local** and live in `.eve/profile.yaml` so each project keeps its own
37
+ defaults without impacting other checkouts.
18
38
 
19
39
  ```bash
20
- # Create or update a profile
40
+ # Create or update a profile (repo-local)
21
41
  eve profile set local \
22
42
  --api-url http://localhost:4801 \
23
43
  --org org_defaulttestorg \
@@ -27,7 +47,7 @@ eve profile set local \
27
47
  eve profile set --harness mclaude
28
48
  eve profile set --harness mclaude:fast # harness with variant
29
49
 
30
- # Set active profile
50
+ # Set active profile (repo-local)
31
51
  eve profile use local
32
52
 
33
53
  # Show active profile
@@ -54,12 +74,16 @@ eve profile set prod \
54
74
  Auth is **required** for cloud stacks. The default flow uses GitHub SSH keys; Supabase remains an
55
75
  optional adapter for legacy deployments.
56
76
 
57
- The CLI will refresh Supabase access tokens automatically if a refresh token is available.
77
+ Credentials are stored globally in `~/.eve/credentials.json`, keyed by API URL. The CLI will
78
+ refresh Supabase access tokens automatically if a refresh token is available.
58
79
 
59
80
  ```bash
60
81
  # SSH login (default)
61
82
  eve auth login --email you@example.com --ssh-key ~/.ssh/id_ed25519
62
83
 
84
+ # SSH login with custom token TTL (1-90 days)
85
+ eve auth login --email you@example.com --ttl 30
86
+
63
87
  # Supabase login (optional)
64
88
  eve auth login --email you@example.com --password '...' \
65
89
  --supabase-url https://your-project.supabase.co \
@@ -59,6 +59,11 @@ async function handleAuth(subcommand, flags, context, credentials) {
59
59
  const email = (0, args_1.getStringFlag)(flags, ['email']) ?? process.env.EVE_AUTH_EMAIL ?? context.profile.default_email;
60
60
  const userId = (0, args_1.getStringFlag)(flags, ['user-id']);
61
61
  const password = (0, args_1.getStringFlag)(flags, ['password']) ?? process.env.EVE_AUTH_PASSWORD;
62
+ const ttlStr = (0, args_1.getStringFlag)(flags, ['ttl']);
63
+ const ttlDays = ttlStr ? parseInt(ttlStr, 10) : undefined;
64
+ if (ttlDays !== undefined && (isNaN(ttlDays) || ttlDays < 1 || ttlDays > 90)) {
65
+ throw new Error('--ttl must be between 1 and 90 days');
66
+ }
62
67
  const supabaseUrl = (0, args_1.getStringFlag)(flags, ['supabase-url']) ||
63
68
  process.env.EVE_SUPABASE_URL ||
64
69
  context.profile.supabase_url;
@@ -102,7 +107,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
102
107
  const expiresAt = payload.expires_in
103
108
  ? Math.floor(Date.now() / 1000) + payload.expires_in
104
109
  : undefined;
105
- credentials.profiles[context.profileName] = {
110
+ credentials.tokens[context.authKey] = {
106
111
  access_token: payload.access_token,
107
112
  refresh_token: payload.refresh_token,
108
113
  expires_at: expiresAt,
@@ -116,7 +121,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
116
121
  throw new Error('Usage: eve auth login --email <email> or --user-id <id>');
117
122
  }
118
123
  // Attempt SSH key login with GitHub key auto-discovery on failure
119
- const loginResult = await attemptSshLogin(context, credentials, flags, email, userId);
124
+ const loginResult = await attemptSshLogin(context, credentials, flags, email, userId, ttlDays);
120
125
  if (loginResult.success) {
121
126
  (0, output_1.outputJson)({ profile: context.profileName, token_type: loginResult.tokenType }, json, '✓ Logged in');
122
127
  return;
@@ -136,7 +141,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
136
141
  }
137
142
  // Retry login after key registration
138
143
  console.log('\nRetrying login with registered keys...');
139
- const retryResult = await attemptSshLogin(context, credentials, flags, email, userId);
144
+ const retryResult = await attemptSshLogin(context, credentials, flags, email, userId, ttlDays);
140
145
  if (retryResult.success) {
141
146
  (0, output_1.outputJson)({ profile: context.profileName, token_type: retryResult.tokenType }, json, '✓ Logged in');
142
147
  return;
@@ -144,8 +149,14 @@ async function handleAuth(subcommand, flags, context, credentials) {
144
149
  throw new Error(retryResult.error ?? 'Auth verify failed after key registration');
145
150
  }
146
151
  case 'logout': {
147
- if (credentials.profiles[context.profileName]) {
152
+ const hadToken = Boolean(credentials.tokens[context.authKey] || credentials.profiles?.[context.profileName]);
153
+ if (credentials.tokens[context.authKey]) {
154
+ delete credentials.tokens[context.authKey];
155
+ }
156
+ if (credentials.profiles?.[context.profileName]) {
148
157
  delete credentials.profiles[context.profileName];
158
+ }
159
+ if (hadToken) {
149
160
  (0, config_1.saveCredentials)(credentials);
150
161
  }
151
162
  (0, output_1.outputJson)({ profile: context.profileName }, json, '✓ Logged out');
@@ -251,7 +262,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
251
262
  if (!payload.access_token) {
252
263
  throw new Error('Bootstrap response missing access_token');
253
264
  }
254
- credentials.profiles[context.profileName] = {
265
+ credentials.tokens[context.authKey] = {
255
266
  access_token: payload.access_token,
256
267
  expires_at: payload.expires_at,
257
268
  token_type: payload.token_type,
@@ -262,7 +273,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
262
273
  }
263
274
  case 'token': {
264
275
  // Print the current access token to stdout for use in scripts
265
- const tokenEntry = credentials.profiles[context.profileName];
276
+ const tokenEntry = credentials.tokens[context.authKey] || credentials.profiles?.[context.profileName];
266
277
  if (!tokenEntry || !tokenEntry.access_token) {
267
278
  console.error('No valid token found. Please login first with: eve auth login');
268
279
  process.exit(1);
@@ -691,7 +702,7 @@ function signNonceWithSsh(keyPath, nonce) {
691
702
  (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
692
703
  }
693
704
  }
694
- async function attemptSshLogin(context, credentials, flags, email, userId) {
705
+ async function attemptSshLogin(context, credentials, flags, email, userId, ttlDays) {
695
706
  const challengeResponse = await (0, client_1.requestRaw)(context, '/auth/challenge', {
696
707
  method: 'POST',
697
708
  body: {
@@ -723,7 +734,7 @@ async function attemptSshLogin(context, credentials, flags, email, userId) {
723
734
  }
724
735
  const verifyResponse = await (0, client_1.requestRaw)(context, '/auth/verify', {
725
736
  method: 'POST',
726
- body: { challenge_id: challenge.challenge_id, signature },
737
+ body: { challenge_id: challenge.challenge_id, signature, ...(ttlDays !== undefined && { ttl_days: ttlDays }) },
727
738
  });
728
739
  if (!verifyResponse.ok) {
729
740
  const message = typeof verifyResponse.data === 'string'
@@ -735,7 +746,7 @@ async function attemptSshLogin(context, credentials, flags, email, userId) {
735
746
  if (!payload.access_token) {
736
747
  return { success: false, error: 'Auth verify response missing access_token' };
737
748
  }
738
- credentials.profiles[context.profileName] = {
749
+ credentials.tokens[context.authKey] = {
739
750
  access_token: payload.access_token,
740
751
  expires_at: payload.expires_at,
741
752
  token_type: payload.token_type,
@@ -4,7 +4,7 @@ exports.handleBuild = handleBuild;
4
4
  const args_1 = require("../lib/args");
5
5
  const client_1 = require("../lib/client");
6
6
  const output_1 = require("../lib/output");
7
- const node_child_process_1 = require("node:child_process");
7
+ const git_js_1 = require("../lib/git.js");
8
8
  const ERROR_CODES = {
9
9
  auth_error: { code: 'auth_error', label: 'Authentication Error', hint: "Check GITHUB_TOKEN via 'eve secrets set'" },
10
10
  clone_error: { code: 'clone_error', label: 'Git Clone Error', hint: "Verify repo URL and access. Check 'eve secrets list'" },
@@ -63,23 +63,14 @@ async function handleCreate(flags, context, json) {
63
63
  const ref = (0, args_1.getStringFlag)(flags, ['ref']);
64
64
  const manifestHash = (0, args_1.getStringFlag)(flags, ['manifest-hash', 'manifest']);
65
65
  const services = (0, args_1.getStringFlag)(flags, ['services']);
66
+ const repoDir = (0, args_1.getStringFlag)(flags, ['repo-dir', 'repo_dir', 'dir']);
66
67
  if (!projectId || !ref || !manifestHash) {
67
- throw new Error('Usage: eve build create --project <id> --ref <sha> --manifest-hash <hash> [--services <s1,s2>]');
68
+ throw new Error('Usage: eve build create --project <id> --ref <sha> --manifest-hash <hash> [--services <s1,s2>] [--repo-dir <path>]');
68
69
  }
69
70
  // Resolve git ref to actual 40-char SHA
70
- let gitSha;
71
- try {
72
- gitSha = (0, node_child_process_1.execSync)(`git rev-parse ${ref}`, {
73
- encoding: 'utf-8',
74
- stdio: ['pipe', 'pipe', 'pipe'],
75
- }).trim();
76
- if (!json) {
77
- console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
78
- }
79
- }
80
- catch (error) {
81
- throw new Error(`Failed to resolve git ref '${ref}': ${error instanceof Error ? error.message : String(error)}\n` +
82
- 'Make sure you are in a git repository and the ref exists.');
71
+ const gitSha = await (0, git_js_1.resolveGitRef)(context, projectId, ref, repoDir);
72
+ if (!json && ref !== gitSha) {
73
+ console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
83
74
  }
84
75
  const body = { git_sha: gitSha, manifest_hash: manifestHash };
85
76
  if (services) {
@@ -37,7 +37,7 @@ exports.handleEnv = handleEnv;
37
37
  const args_1 = require("../lib/args");
38
38
  const client_1 = require("../lib/client");
39
39
  const output_1 = require("../lib/output");
40
- const child_process_1 = require("child_process");
40
+ const git_js_1 = require("../lib/git.js");
41
41
  const readline = __importStar(require("node:readline/promises"));
42
42
  // ============================================================================
43
43
  // Main Handler
@@ -64,7 +64,7 @@ async function handleEnv(subcommand, positionals, flags, context) {
64
64
  ' list [project] - list environments for a project\n' +
65
65
  ' show <project> <name> - show details of an environment\n' +
66
66
  ' create <name> --type=<type> [options] - create an environment\n' +
67
- ' deploy <env> --ref <sha> [--direct] [--inputs <json>] - deploy to an environment\n' +
67
+ ' deploy <env> --ref <sha> [--direct] [--inputs <json>] [--repo-dir <path>] - deploy to an environment\n' +
68
68
  ' logs <project> <env> <service> [--since <seconds>] [--tail <n>] [--grep <text>] - get service logs\n' +
69
69
  ' diagnose <project> <env> - diagnose deployment health and events\n' +
70
70
  ' delete <name> [--project=<id>] [--force] - delete an environment');
@@ -194,27 +194,18 @@ async function handleDeploy(positionals, flags, context, json) {
194
194
  envName = flagName;
195
195
  }
196
196
  if (!projectId || !envName) {
197
- throw new Error('Usage: eve env deploy <env> --ref <sha> [--direct] [--inputs <json>] [--image-tag <tag>] [--project=<id>]');
197
+ throw new Error('Usage: eve env deploy <env> --ref <sha> [--direct] [--inputs <json>] [--image-tag <tag>] [--repo-dir <path>] [--project=<id>]');
198
198
  }
199
199
  // --ref flag is now required
200
200
  const ref = (0, args_1.getStringFlag)(flags, ['ref']);
201
201
  if (!ref) {
202
202
  throw new Error('Usage: eve env deploy <env> --ref <sha> [options]\n\nThe --ref flag is required (git SHA or commit reference)');
203
203
  }
204
+ const repoDir = (0, args_1.getStringFlag)(flags, ['repo-dir', 'repo_dir', 'dir']);
204
205
  // Resolve git ref to actual 40-char SHA
205
- let gitSha;
206
- try {
207
- gitSha = (0, child_process_1.execSync)(`git rev-parse ${ref}`, {
208
- encoding: 'utf-8',
209
- stdio: ['pipe', 'pipe', 'pipe'],
210
- }).trim();
211
- if (!json) {
212
- console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
213
- }
214
- }
215
- catch (error) {
216
- throw new Error(`Failed to resolve git ref '${ref}': ${error instanceof Error ? error.message : String(error)}\n` +
217
- 'Make sure you are in a git repository and the ref exists.');
206
+ const gitSha = await (0, git_js_1.resolveGitRef)(context, projectId, ref, repoDir);
207
+ if (!json && ref !== gitSha) {
208
+ console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
218
209
  }
219
210
  if (!json) {
220
211
  console.log(`Deploying commit ${gitSha.substring(0, 8)} to ${envName}...`);
@@ -434,21 +425,6 @@ function buildQuery(params) {
434
425
  const query = search.toString();
435
426
  return query ? `?${query}` : '';
436
427
  }
437
- /**
438
- * Get current git SHA from working directory
439
- */
440
- function getGitSha() {
441
- try {
442
- const sha = (0, child_process_1.execSync)('git rev-parse HEAD', {
443
- encoding: 'utf8',
444
- stdio: ['pipe', 'pipe', 'pipe'],
445
- }).trim();
446
- return sha;
447
- }
448
- catch {
449
- return null;
450
- }
451
- }
452
428
  /**
453
429
  * Format environments as a human-readable table
454
430
  */
@@ -4,7 +4,7 @@ exports.handlePipeline = handlePipeline;
4
4
  const args_1 = require("../lib/args");
5
5
  const client_1 = require("../lib/client");
6
6
  const output_1 = require("../lib/output");
7
- const node_child_process_1 = require("node:child_process");
7
+ const git_js_1 = require("../lib/git.js");
8
8
  async function handlePipeline(subcommand, positionals, flags, context) {
9
9
  const json = Boolean(flags.json);
10
10
  switch (subcommand) {
@@ -182,23 +182,14 @@ async function handleRun(positionals, flags, context, json) {
182
182
  const wait = Boolean(flags.wait);
183
183
  const timeout = (0, args_1.getStringFlag)(flags, ['timeout']);
184
184
  const inputsRaw = (0, args_1.getStringFlag)(flags, ['inputs']);
185
+ const repoDir = (0, args_1.getStringFlag)(flags, ['repo-dir', 'repo_dir', 'dir']);
185
186
  if (!pipelineName || !projectId || !ref) {
186
- throw new Error('Usage: eve pipeline run <name> --ref <sha> [--env <env>] [--project <id>] [--wait] [--inputs <json>] [--only <step>]');
187
+ throw new Error('Usage: eve pipeline run <name> --ref <sha> [--env <env>] [--project <id>] [--repo-dir <path>] [--wait] [--inputs <json>] [--only <step>]');
187
188
  }
188
189
  // Resolve git ref to actual 40-char SHA
189
- let gitSha;
190
- try {
191
- gitSha = (0, node_child_process_1.execSync)(`git rev-parse ${ref}`, {
192
- encoding: 'utf-8',
193
- stdio: ['pipe', 'pipe', 'pipe'],
194
- }).trim();
195
- if (!json) {
196
- console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
197
- }
198
- }
199
- catch (error) {
200
- throw new Error(`Failed to resolve git ref '${ref}': ${error instanceof Error ? error.message : String(error)}\n` +
201
- 'Make sure you are in a git repository and the ref exists.');
190
+ const gitSha = await (0, git_js_1.resolveGitRef)(context, projectId, ref, repoDir);
191
+ if (!json && ref !== gitSha) {
192
+ console.log(`Resolved ref '${ref}' → ${gitSha.substring(0, 8)}...`);
202
193
  }
203
194
  let inputs;
204
195
  if (inputsRaw) {
@@ -1,76 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleProfile = handleProfile;
4
- const config_1 = require("../lib/config");
5
4
  const context_1 = require("../lib/context");
6
5
  const output_1 = require("../lib/output");
7
- function handleProfile(subcommand, positionals, flags, config) {
6
+ function handleProfile(subcommand, positionals, flags) {
8
7
  const json = Boolean(flags.json);
8
+ if (flags.global) {
9
+ throw new Error('Global profiles have been removed. Profiles are repo-local; remove --global.');
10
+ }
9
11
  switch (subcommand) {
10
12
  case 'list': {
11
- const repoProfile = (0, context_1.loadRepoProfile)();
12
- const localProfileName = repoProfile?.profile;
13
- const profiles = Object.entries(config.profiles).map(([name, profile]) => ({
13
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
14
+ const profiles = Object.entries(repoProfiles.profiles).map(([name, profile]) => ({
14
15
  name,
15
- active: name === config.active_profile,
16
- local: name === localProfileName,
16
+ active: name === repoProfiles.activeProfile,
17
17
  ...profile,
18
18
  }));
19
- (0, output_1.outputJson)({
20
- active_profile: config.active_profile,
21
- local_profile: localProfileName ?? null,
22
- profiles,
23
- }, json);
19
+ (0, output_1.outputJson)({ active_profile: repoProfiles.activeProfile ?? null, profiles }, json);
24
20
  return;
25
21
  }
26
22
  case 'show': {
27
- // Check for local profile first
28
- const repoProfile = (0, context_1.loadRepoProfile)();
29
- const localProfileName = repoProfile?.profile;
30
- // Determine which profile to show
31
23
  const requestedName = positionals[0];
32
- let name;
33
- let source;
34
- if (requestedName) {
35
- // Explicit name requested
36
- name = requestedName;
37
- source = localProfileName === requestedName ? 'local' : 'global';
38
- }
39
- else if (localProfileName) {
40
- // No name specified, use local profile if exists
41
- name = localProfileName;
42
- source = 'local';
43
- }
44
- else {
45
- // Fall back to global active profile
46
- name = config.active_profile;
47
- source = 'global';
24
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
25
+ const name = resolveProfileName(repoProfiles, requestedName);
26
+ if (!name) {
27
+ throw new Error('No local profiles configured. Use `eve profile set` to create one.');
48
28
  }
49
- const profile = config.profiles[name];
29
+ const profile = repoProfiles.profiles[name];
50
30
  if (!profile) {
51
31
  throw new Error(`Profile ${name} not found`);
52
32
  }
53
- // Merge local overrides if showing local profile
54
- const effectiveProfile = source === 'local' && repoProfile
55
- ? {
56
- ...profile,
57
- ...(repoProfile.api_url && { api_url: repoProfile.api_url }),
58
- ...(repoProfile.org_id && { org_id: repoProfile.org_id }),
59
- ...(repoProfile.project_id && { project_id: repoProfile.project_id }),
60
- }
61
- : profile;
62
- const sourceLabel = source === 'local' ? ` (local: .eve/profile.yaml)` : ' (global)';
63
- (0, output_1.outputJson)({ name, source, ...effectiveProfile }, json, `${name}${sourceLabel}`);
33
+ const active = repoProfiles.activeProfile === name;
34
+ (0, output_1.outputJson)({ name, active, ...profile }, json, `${name} (local)`);
64
35
  return;
65
36
  }
66
37
  case 'use': {
67
- const isGlobal = Boolean(flags.global);
68
38
  const isClear = Boolean(flags.clear);
69
39
  // Handle --clear: remove local profile
70
40
  if (isClear) {
71
41
  const removed = (0, context_1.removeRepoProfile)();
72
42
  if (removed) {
73
- (0, output_1.outputJson)({ cleared: true, path: (0, context_1.getRepoProfilePath)() }, json, `✓ Removed local profile`);
43
+ (0, output_1.outputJson)({ cleared: true, path: (0, context_1.getRepoProfilePath)() }, json, `✓ Removed local profile store (${(0, context_1.getRepoProfilePath)()})`);
74
44
  }
75
45
  else {
76
46
  (0, output_1.outputJson)({ cleared: false }, json, `No local profile to remove`);
@@ -79,32 +49,16 @@ function handleProfile(subcommand, positionals, flags, config) {
79
49
  }
80
50
  const name = positionals[0];
81
51
  if (!name) {
82
- throw new Error('Usage: eve profile use <name> [--global]');
83
- }
84
- // Validate profile exists in global config (auto-create if needed)
85
- if (!config.profiles[name]) {
86
- config.profiles[name] = {};
87
- (0, config_1.saveConfig)(config);
52
+ throw new Error('Usage: eve profile use <name>');
88
53
  }
89
- if (isGlobal) {
90
- // Explicit: set global active profile in ~/.eve/config.json
91
- config.active_profile = name;
92
- (0, config_1.saveConfig)(config);
93
- (0, output_1.outputJson)({ active_profile: name }, json, `✓ Active profile: ${name} (global)`);
94
- }
95
- else {
96
- // Default: write to .eve/profile.yaml (local project mode)
97
- const repoProfile = { profile: name };
98
- // Apply any override flags
99
- if (typeof flags['api-url'] === 'string')
100
- repoProfile.api_url = flags['api-url'];
101
- if (typeof flags.org === 'string')
102
- repoProfile.org_id = flags.org;
103
- if (typeof flags.project === 'string')
104
- repoProfile.project_id = flags.project;
105
- (0, context_1.saveRepoProfile)(repoProfile);
106
- (0, output_1.outputJson)({ profile: name, local: true, path: (0, context_1.getRepoProfilePath)(), ...repoProfile }, json, `✓ Local profile set: ${name} (${(0, context_1.getRepoProfilePath)()})`);
54
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
55
+ if (!repoProfiles.profiles[name]) {
56
+ repoProfiles.profiles[name] = {};
107
57
  }
58
+ repoProfiles.profiles[name] = applyProfileFlags(repoProfiles.profiles[name], flags);
59
+ repoProfiles.activeProfile = name;
60
+ (0, context_1.saveRepoProfiles)(repoProfiles);
61
+ (0, output_1.outputJson)({ active_profile: name, path: (0, context_1.getRepoProfilePath)(), ...repoProfiles.profiles[name] }, json, `✓ Active profile: ${name} (${(0, context_1.getRepoProfilePath)()})`);
108
62
  return;
109
63
  }
110
64
  case 'create': {
@@ -112,51 +66,28 @@ function handleProfile(subcommand, positionals, flags, config) {
112
66
  if (!name) {
113
67
  throw new Error('Usage: eve profile create <name> [--api-url <url> ...]');
114
68
  }
115
- if (config.profiles[name]) {
69
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
70
+ if (repoProfiles.profiles[name]) {
116
71
  throw new Error(`Profile ${name} already exists`);
117
72
  }
118
- config.profiles[name] = applyProfileFlags({}, flags);
119
- (0, config_1.saveConfig)(config);
120
- (0, output_1.outputJson)({ name, ...config.profiles[name] }, json, `✓ Profile created: ${name}`);
73
+ repoProfiles.profiles[name] = applyProfileFlags({}, flags);
74
+ if (!repoProfiles.activeProfile) {
75
+ repoProfiles.activeProfile = name;
76
+ }
77
+ (0, context_1.saveRepoProfiles)(repoProfiles);
78
+ (0, output_1.outputJson)({ name, ...repoProfiles.profiles[name] }, json, `✓ Profile created: ${name}`);
121
79
  return;
122
80
  }
123
81
  case 'set': {
124
- const isGlobal = Boolean(flags.global);
125
- if (isGlobal) {
126
- // Explicit: write to global ~/.eve/config.json
127
- const name = positionals[0] ?? config.active_profile;
128
- if (!config.profiles[name]) {
129
- config.profiles[name] = {};
130
- }
131
- config.profiles[name] = applyProfileFlags(config.profiles[name], flags);
132
- config.active_profile = name;
133
- (0, config_1.saveConfig)(config);
134
- (0, output_1.outputJson)({ active_profile: name, ...config.profiles[name] }, json, `✓ Profile set: ${name} (global)`);
135
- }
136
- else {
137
- // Default: write to local .eve/profile.yaml
138
- const globalOnlyFields = ['supabase-url', 'supabase-anon-key', 'default-email', 'default-ssh-key'];
139
- const usedGlobalFields = globalOnlyFields.filter(f => typeof flags[f] === 'string');
140
- if (usedGlobalFields.length > 0) {
141
- throw new Error(`${usedGlobalFields.map(f => `--${f}`).join(', ')} are user-level settings.\n` +
142
- `Use --global to write them to ~/.eve/config.json`);
143
- }
144
- const existing = (0, context_1.loadRepoProfile)() ?? {};
145
- const repoProfile = { ...existing };
146
- const name = positionals[0];
147
- if (name)
148
- repoProfile.profile = name;
149
- if (typeof flags['api-url'] === 'string')
150
- repoProfile.api_url = flags['api-url'];
151
- if (typeof flags.org === 'string')
152
- repoProfile.org_id = flags.org;
153
- if (typeof flags.project === 'string')
154
- repoProfile.project_id = flags.project;
155
- if (typeof flags.harness === 'string')
156
- repoProfile.default_harness = flags.harness;
157
- (0, context_1.saveRepoProfile)(repoProfile);
158
- (0, output_1.outputJson)({ ...repoProfile, path: (0, context_1.getRepoProfilePath)() }, json, `✓ Profile set: ${repoProfile.profile ?? 'local'} (${(0, context_1.getRepoProfilePath)()})`);
159
- }
82
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
83
+ const name = resolveProfileName(repoProfiles, positionals[0]) ?? 'default';
84
+ if (!repoProfiles.profiles[name]) {
85
+ repoProfiles.profiles[name] = {};
86
+ }
87
+ repoProfiles.profiles[name] = applyProfileFlags(repoProfiles.profiles[name], flags);
88
+ repoProfiles.activeProfile = name;
89
+ (0, context_1.saveRepoProfiles)(repoProfiles);
90
+ (0, output_1.outputJson)({ active_profile: name, ...repoProfiles.profiles[name], path: (0, context_1.getRepoProfilePath)() }, json, `✓ Profile set: ${name} (${(0, context_1.getRepoProfilePath)()})`);
160
91
  return;
161
92
  }
162
93
  case 'remove': {
@@ -164,19 +95,22 @@ function handleProfile(subcommand, positionals, flags, config) {
164
95
  if (!name) {
165
96
  throw new Error('Usage: eve profile remove <name>');
166
97
  }
167
- if (!config.profiles[name]) {
98
+ const repoProfiles = (0, context_1.loadRepoProfiles)();
99
+ if (!repoProfiles.profiles[name]) {
168
100
  throw new Error(`Profile ${name} not found`);
169
101
  }
170
- delete config.profiles[name];
171
- if (config.active_profile === name) {
172
- const [first] = Object.keys(config.profiles);
173
- config.active_profile = first ?? 'default';
174
- if (!config.profiles[config.active_profile]) {
175
- config.profiles[config.active_profile] = {};
176
- }
102
+ delete repoProfiles.profiles[name];
103
+ if (repoProfiles.activeProfile === name) {
104
+ const [first] = Object.keys(repoProfiles.profiles);
105
+ repoProfiles.activeProfile = first;
106
+ }
107
+ if (!repoProfiles.activeProfile && Object.keys(repoProfiles.profiles).length === 0) {
108
+ (0, context_1.removeRepoProfile)();
109
+ (0, output_1.outputJson)({ removed: name, cleared: true }, json, `✓ Removed profile ${name} (store cleared)`);
110
+ return;
177
111
  }
178
- (0, config_1.saveConfig)(config);
179
- (0, output_1.outputJson)({ removed: name, active_profile: config.active_profile }, json, `✓ Removed profile ${name}`);
112
+ (0, context_1.saveRepoProfiles)(repoProfiles);
113
+ (0, output_1.outputJson)({ removed: name, active_profile: repoProfiles.activeProfile ?? null }, json, `✓ Removed profile ${name}`);
180
114
  return;
181
115
  }
182
116
  default:
@@ -205,3 +139,17 @@ function applyProfileFlags(profile, flags) {
205
139
  updated.default_ssh_key = flags['default-ssh-key'];
206
140
  return updated;
207
141
  }
142
+ function resolveProfileName(repoProfiles, requestedName) {
143
+ if (requestedName)
144
+ return requestedName;
145
+ if (repoProfiles.activeProfile)
146
+ return repoProfiles.activeProfile;
147
+ const names = Object.keys(repoProfiles.profiles);
148
+ if (names.length === 0)
149
+ return undefined;
150
+ if (names.length === 1)
151
+ return names[0];
152
+ if (names.includes('default'))
153
+ return 'default';
154
+ return names[0];
155
+ }
package/dist/index.js CHANGED
@@ -47,9 +47,8 @@ async function main() {
47
47
  (0, help_1.showSubcommandHelp)(command, subcommand);
48
48
  return;
49
49
  }
50
- const config = (0, config_1.loadConfig)();
51
50
  const credentials = (0, config_1.loadCredentials)();
52
- const context = (0, context_1.resolveContext)(flags, config, credentials);
51
+ const context = (0, context_1.resolveContext)(flags, credentials);
53
52
  switch (command) {
54
53
  case 'org':
55
54
  await (0, org_1.handleOrg)(subcommand, rest, flags, context);
@@ -61,7 +60,7 @@ async function main() {
61
60
  await (0, job_1.handleJob)(subcommand, rest, flags, context);
62
61
  return;
63
62
  case 'profile':
64
- (0, profile_1.handleProfile)(subcommand, rest, flags, config);
63
+ (0, profile_1.handleProfile)(subcommand, rest, flags);
65
64
  return;
66
65
  case 'auth':
67
66
  await (0, auth_1.handleAuth)(subcommand, flags, context, credentials);
@@ -110,7 +110,7 @@ async function attemptRefresh(context) {
110
110
  token_type: payload.token_type,
111
111
  };
112
112
  const credentials = (0, config_1.loadCredentials)();
113
- credentials.profiles[context.profileName] = nextToken;
113
+ credentials.tokens[context.authKey] = nextToken;
114
114
  (0, config_1.saveCredentials)(credentials);
115
115
  return nextToken;
116
116
  }
@@ -1,16 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadConfig = loadConfig;
4
- exports.saveConfig = saveConfig;
5
3
  exports.loadCredentials = loadCredentials;
6
4
  exports.saveCredentials = saveCredentials;
7
- exports.getDefaultProfileName = getDefaultProfileName;
8
5
  const node_fs_1 = require("node:fs");
9
6
  const node_os_1 = require("node:os");
10
7
  const node_path_1 = require("node:path");
11
- const DEFAULT_PROFILE = 'default';
12
8
  const CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.eve');
13
- const CONFIG_PATH = (0, node_path_1.join)(CONFIG_DIR, 'config.json');
14
9
  const CREDENTIALS_PATH = (0, node_path_1.join)(CONFIG_DIR, 'credentials.json');
15
10
  function ensureConfigDir() {
16
11
  if (!(0, node_fs_1.existsSync)(CONFIG_DIR)) {
@@ -40,37 +35,15 @@ function writeJsonFile(path, value) {
40
35
  // Best-effort for platforms that don't support chmod.
41
36
  }
42
37
  }
43
- function loadConfig() {
44
- const fallback = { active_profile: DEFAULT_PROFILE, profiles: { [DEFAULT_PROFILE]: {} } };
45
- const config = readJsonFile(CONFIG_PATH, fallback);
46
- config.profiles = config.profiles ?? {};
47
- if (!config.active_profile) {
48
- const [first] = Object.keys(config.profiles);
49
- config.active_profile = first ?? DEFAULT_PROFILE;
50
- }
51
- if (!config.profiles[config.active_profile]) {
52
- config.profiles[config.active_profile] = {};
53
- }
54
- return config;
55
- }
56
- function saveConfig(config) {
57
- config.profiles = config.profiles ?? {};
58
- if (!config.active_profile) {
59
- const [first] = Object.keys(config.profiles);
60
- config.active_profile = first ?? DEFAULT_PROFILE;
61
- }
62
- writeJsonFile(CONFIG_PATH, config);
63
- }
64
38
  function loadCredentials() {
65
- const fallback = { profiles: {} };
39
+ const fallback = { tokens: {} };
66
40
  const credentials = readJsonFile(CREDENTIALS_PATH, fallback);
41
+ credentials.tokens = credentials.tokens ?? {};
67
42
  credentials.profiles = credentials.profiles ?? {};
68
43
  return credentials;
69
44
  }
70
45
  function saveCredentials(credentials) {
46
+ credentials.tokens = credentials.tokens ?? {};
71
47
  credentials.profiles = credentials.profiles ?? {};
72
48
  writeJsonFile(CREDENTIALS_PATH, credentials);
73
49
  }
74
- function getDefaultProfileName(config) {
75
- return config.active_profile ?? DEFAULT_PROFILE;
76
- }
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getRepoProfilePath = getRepoProfilePath;
4
- exports.loadRepoProfile = loadRepoProfile;
5
- exports.saveRepoProfile = saveRepoProfile;
4
+ exports.loadRepoProfiles = loadRepoProfiles;
5
+ exports.saveRepoProfiles = saveRepoProfiles;
6
6
  exports.removeRepoProfile = removeRepoProfile;
7
7
  exports.resolveContext = resolveContext;
8
8
  exports.parseHarnessSpec = parseHarnessSpec;
@@ -10,6 +10,7 @@ const node_fs_1 = require("node:fs");
10
10
  const node_path_1 = require("node:path");
11
11
  const yaml_1 = require("yaml");
12
12
  const args_1 = require("./args");
13
+ const DEFAULT_PROFILE = 'default';
13
14
  // Default to Ingress URL for k8s stack (works via lvh.me → 127.0.0.1)
14
15
  // Override with EVE_API_URL for other environments (docker compose, local dev)
15
16
  const DEFAULT_API_URL = 'http://api.eve.lvh.me';
@@ -20,34 +21,37 @@ function getRepoProfilePath() {
20
21
  return (0, node_path_1.join)(process.cwd(), '.eve', 'profile.yaml');
21
22
  }
22
23
  /**
23
- * Load repository profile from .eve/profile.yaml if it exists
24
- * Returns null if file not found
24
+ * Load repository profiles from .eve/profile.yaml if it exists
25
25
  */
26
- function loadRepoProfile() {
26
+ function loadRepoProfiles() {
27
27
  const profilePath = getRepoProfilePath();
28
28
  if (!(0, node_fs_1.existsSync)(profilePath)) {
29
- return null;
29
+ return { profiles: {} };
30
30
  }
31
31
  try {
32
32
  const content = (0, node_fs_1.readFileSync)(profilePath, 'utf-8');
33
33
  const parsed = (0, yaml_1.parse)(content);
34
- return parsed || null;
34
+ return normalizeRepoProfiles(parsed);
35
35
  }
36
36
  catch {
37
37
  // Silently ignore parse errors
38
- return null;
38
+ return { profiles: {} };
39
39
  }
40
40
  }
41
41
  /**
42
- * Save repository profile to .eve/profile.yaml
42
+ * Save repository profiles to .eve/profile.yaml
43
43
  */
44
- function saveRepoProfile(repoProfile) {
44
+ function saveRepoProfiles(repoProfiles) {
45
45
  const profilePath = getRepoProfilePath();
46
46
  const dir = (0, node_path_1.dirname)(profilePath);
47
47
  if (!(0, node_fs_1.existsSync)(dir)) {
48
48
  (0, node_fs_1.mkdirSync)(dir, { recursive: true });
49
49
  }
50
- (0, node_fs_1.writeFileSync)(profilePath, (0, yaml_1.stringify)(repoProfile));
50
+ const payload = {
51
+ active_profile: repoProfiles.activeProfile,
52
+ profiles: repoProfiles.profiles,
53
+ };
54
+ (0, node_fs_1.writeFileSync)(profilePath, (0, yaml_1.stringify)(payload));
51
55
  }
52
56
  /**
53
57
  * Remove repository profile file
@@ -60,15 +64,16 @@ function removeRepoProfile() {
60
64
  (0, node_fs_1.unlinkSync)(profilePath);
61
65
  return true;
62
66
  }
63
- function resolveContext(flags, config, credentials) {
64
- const repoProfile = loadRepoProfile();
67
+ function resolveContext(flags, credentials) {
68
+ const repoProfiles = loadRepoProfiles();
69
+ const profileNames = Object.keys(repoProfiles.profiles);
65
70
  // Determine profile name and source
66
- // Priority: flag > env > local (.eve/profile.yaml) > global active
71
+ // Priority: flag > env > local (.eve/profile.yaml) > default
67
72
  let profileName;
68
73
  let profileSource;
69
74
  const flagProfile = (0, args_1.getStringFlag)(flags, ['profile']);
70
75
  const envProfile = process.env.EVE_PROFILE;
71
- const localProfile = repoProfile?.profile;
76
+ const localProfile = repoProfiles.activeProfile;
72
77
  if (flagProfile) {
73
78
  profileName = flagProfile;
74
79
  profileSource = 'flag';
@@ -81,34 +86,43 @@ function resolveContext(flags, config, credentials) {
81
86
  profileName = localProfile;
82
87
  profileSource = 'local';
83
88
  }
89
+ else if (profileNames.length === 1) {
90
+ profileName = profileNames[0];
91
+ profileSource = 'local';
92
+ }
93
+ else if (profileNames.includes(DEFAULT_PROFILE)) {
94
+ profileName = DEFAULT_PROFILE;
95
+ profileSource = 'local';
96
+ }
97
+ else if (profileNames.length > 0) {
98
+ profileName = profileNames[0];
99
+ profileSource = 'local';
100
+ }
84
101
  else {
85
- profileName = config.active_profile || 'default';
86
- profileSource = 'global';
102
+ profileName = DEFAULT_PROFILE;
103
+ profileSource = 'default';
87
104
  }
88
- // Get base profile config from global profiles
89
- const profile = config.profiles[profileName] ?? {};
90
- // Apply overrides from repo profile on top of base profile
105
+ // Get base profile config from local profiles
106
+ const profile = repoProfiles.profiles[profileName] ?? {};
91
107
  const apiUrl = (0, args_1.getStringFlag)(flags, ['api', 'api-url']) ||
92
- repoProfile?.api_url ||
93
108
  process.env.EVE_API_URL ||
94
109
  profile.api_url ||
95
110
  DEFAULT_API_URL;
96
111
  const orgId = (0, args_1.getStringFlag)(flags, ['org']) ||
97
- repoProfile?.org_id ||
98
112
  process.env.EVE_ORG_ID ||
99
113
  profile.org_id;
100
114
  const projectId = (0, args_1.getStringFlag)(flags, ['project']) ||
101
- repoProfile?.project_id ||
102
115
  process.env.EVE_PROJECT_ID ||
103
116
  profile.project_id;
104
- // Credentials are always keyed by profile name (from global credentials file)
105
- const tokenEntry = credentials.profiles[profileName];
117
+ const authKey = toAuthKey(apiUrl);
118
+ const tokenEntry = credentials.tokens[authKey] || credentials.profiles?.[profileName];
106
119
  return {
107
120
  apiUrl,
108
121
  orgId,
109
122
  projectId,
110
123
  profileName,
111
124
  profile,
125
+ authKey,
112
126
  token: tokenEntry?.access_token,
113
127
  refreshToken: tokenEntry?.refresh_token,
114
128
  expiresAt: tokenEntry?.expires_at,
@@ -126,3 +140,48 @@ function parseHarnessSpec(spec) {
126
140
  return [spec, undefined];
127
141
  return [spec.slice(0, colonIdx), spec.slice(colonIdx + 1) || undefined];
128
142
  }
143
+ function normalizeRepoProfiles(parsed) {
144
+ if (!parsed || typeof parsed !== 'object') {
145
+ return { profiles: {} };
146
+ }
147
+ if (parsed.profiles && typeof parsed.profiles === 'object') {
148
+ return {
149
+ activeProfile: parsed.active_profile,
150
+ profiles: parsed.profiles,
151
+ };
152
+ }
153
+ const legacyConfig = extractProfileConfig(parsed);
154
+ const hasLegacyFields = Object.keys(legacyConfig).length > 0;
155
+ const legacyName = parsed.profile ?? parsed.active_profile;
156
+ if (!hasLegacyFields && !legacyName) {
157
+ return { profiles: {} };
158
+ }
159
+ const name = legacyName ?? DEFAULT_PROFILE;
160
+ return {
161
+ activeProfile: name,
162
+ profiles: { [name]: legacyConfig },
163
+ };
164
+ }
165
+ function extractProfileConfig(raw) {
166
+ const config = {};
167
+ if (raw.api_url)
168
+ config.api_url = raw.api_url;
169
+ if (raw.org_id)
170
+ config.org_id = raw.org_id;
171
+ if (raw.project_id)
172
+ config.project_id = raw.project_id;
173
+ if (raw.default_harness)
174
+ config.default_harness = raw.default_harness;
175
+ if (raw.supabase_url)
176
+ config.supabase_url = raw.supabase_url;
177
+ if (raw.supabase_anon_key)
178
+ config.supabase_anon_key = raw.supabase_anon_key;
179
+ if (raw.default_email)
180
+ config.default_email = raw.default_email;
181
+ if (raw.default_ssh_key)
182
+ config.default_ssh_key = raw.default_ssh_key;
183
+ return config;
184
+ }
185
+ function toAuthKey(apiUrl) {
186
+ return apiUrl.trim().replace(/\/+$/, '');
187
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isGitSha = isGitSha;
7
+ exports.resolveGitRef = resolveGitRef;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const client_js_1 = require("./client.js");
11
+ const GIT_SHA_REGEX = /^[0-9a-f]{40}$/;
12
+ function isGitSha(ref) {
13
+ return GIT_SHA_REGEX.test(ref);
14
+ }
15
+ async function resolveGitRef(context, projectId, ref, repoDir) {
16
+ if (isGitSha(ref)) {
17
+ return ref;
18
+ }
19
+ const resolvedRepoDir = repoDir ?? getGitRoot();
20
+ if (!resolvedRepoDir) {
21
+ throw new Error(`Failed to resolve git ref '${ref}': not in a git repository.\n` +
22
+ 'Run the command from the project repository, pass --repo-dir <path>, or use a 40-character SHA.');
23
+ }
24
+ if (projectId) {
25
+ const project = await (0, client_js_1.requestJson)(context, `/projects/${projectId}`);
26
+ const expected = normalizeRepoIdentity(project.repo_url);
27
+ const actual = normalizeRepoIdentity(getGitOriginUrl(resolvedRepoDir));
28
+ const repoDirIdentity = normalizeRepoIdentity(resolvedRepoDir);
29
+ if (expected && actual && expected !== actual) {
30
+ throw new Error(`Failed to resolve git ref '${ref}': current repo does not match project repo.\n` +
31
+ ` Project repo: ${project.repo_url}\n` +
32
+ ` Current repo: ${getGitOriginUrl(resolvedRepoDir)}\n` +
33
+ 'Run the command from the project repository, pass --repo-dir <path>, or use a 40-character SHA.');
34
+ }
35
+ if (expected && !actual && (!repoDirIdentity || repoDirIdentity !== expected)) {
36
+ throw new Error(`Failed to resolve git ref '${ref}': current repo has no origin remote to validate against project repo.\n` +
37
+ ` Project repo: ${project.repo_url}\n` +
38
+ 'Run the command from the project repository, pass --repo-dir <path>, or use a 40-character SHA.');
39
+ }
40
+ }
41
+ try {
42
+ return (0, node_child_process_1.execSync)(`git rev-parse ${ref}`, {
43
+ cwd: resolvedRepoDir,
44
+ encoding: 'utf-8',
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ }).trim();
47
+ }
48
+ catch (error) {
49
+ throw new Error(`Failed to resolve git ref '${ref}': ${error instanceof Error ? error.message : String(error)}\n` +
50
+ 'Make sure the ref exists in the repository, or use a 40-character SHA.');
51
+ }
52
+ }
53
+ function getGitRoot() {
54
+ try {
55
+ return (0, node_child_process_1.execSync)('git rev-parse --show-toplevel', {
56
+ encoding: 'utf-8',
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ }).trim();
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function getGitOriginUrl(repoDir) {
65
+ try {
66
+ const url = (0, node_child_process_1.execSync)('git config --get remote.origin.url', {
67
+ cwd: repoDir,
68
+ encoding: 'utf-8',
69
+ stdio: ['pipe', 'pipe', 'pipe'],
70
+ }).trim();
71
+ return url || null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ function normalizeRepoIdentity(repoUrl) {
78
+ if (!repoUrl) {
79
+ return null;
80
+ }
81
+ if (repoUrl.startsWith('file://')) {
82
+ return stripFilePath(node_path_1.default.resolve(repoUrl.replace('file://', '')));
83
+ }
84
+ if (repoUrl.startsWith('/') || repoUrl.startsWith('./') || repoUrl.startsWith('../')) {
85
+ return stripFilePath(node_path_1.default.resolve(repoUrl));
86
+ }
87
+ if (repoUrl.startsWith('git@')) {
88
+ const match = repoUrl.match(/^git@([^:]+):(.+)$/);
89
+ if (!match) {
90
+ return repoUrl;
91
+ }
92
+ const host = match[1].toLowerCase();
93
+ const pathname = stripGitSuffix(match[2]).toLowerCase();
94
+ return `${host}/${pathname}`;
95
+ }
96
+ try {
97
+ const parsed = new URL(repoUrl);
98
+ const host = parsed.host.toLowerCase();
99
+ const pathname = stripGitSuffix(parsed.pathname.replace(/^\//, '')).toLowerCase();
100
+ return `${host}/${pathname}`;
101
+ }
102
+ catch {
103
+ return repoUrl;
104
+ }
105
+ }
106
+ function stripGitSuffix(pathname) {
107
+ return pathname.replace(/\.git$/i, '');
108
+ }
109
+ function stripFilePath(filePath) {
110
+ return filePath.replace(/\.git$/i, '');
111
+ }
package/dist/lib/help.js CHANGED
@@ -529,12 +529,11 @@ Jobs default to 'ready' phase, making them immediately schedulable.`,
529
529
  examples: ['eve agents config --json'],
530
530
  },
531
531
  profile: {
532
- description: `Manage CLI profiles. Profiles store defaults (API URL, org, project) so you don't
532
+ description: `Manage repo-local CLI profiles. Profiles store defaults (API URL, org, project) so you don't
533
533
  have to specify them on every command.
534
534
 
535
- By default, set and use write to .eve/profile.yaml (project-local). Use --global to
536
- write to ~/.eve/config.json instead. This lets you work on multiple projects without
537
- clobbering your global config.`,
535
+ Profiles live in .eve/profile.yaml inside the repo, so each project keeps its own defaults
536
+ and switching profiles won't affect other checkouts.`,
538
537
  usage: 'eve profile <subcommand> [options]',
539
538
  subcommands: {
540
539
  list: {
@@ -542,28 +541,26 @@ clobbering your global config.`,
542
541
  usage: 'eve profile list',
543
542
  },
544
543
  show: {
545
- description: 'Show profile details (local overrides applied)',
544
+ description: 'Show profile details',
546
545
  usage: 'eve profile show [name]',
547
546
  examples: ['eve profile show', 'eve profile show prod'],
548
547
  },
549
548
  use: {
550
- description: 'Switch active profile (defaults to local .eve/profile.yaml)',
551
- usage: 'eve profile use <name> [--global] [--org <id>] [--project <id>]',
549
+ description: 'Switch active profile (repo-local)',
550
+ usage: 'eve profile use <name> [--org <id>] [--project <id>]',
552
551
  options: [
553
- '--global Write to ~/.eve/config.json instead of local project',
554
552
  '--org <id> Set org override',
555
553
  '--project <id> Set project override',
556
554
  '--api-url <url> Set API URL override',
557
- '--clear Remove local .eve/profile.yaml',
555
+ '--clear Remove local .eve/profile.yaml (clears all profiles)',
558
556
  ],
559
557
  examples: [
560
558
  'eve profile use staging --org org_xxx --project proj_yyy',
561
- 'eve profile use staging --global',
562
559
  'eve profile use --clear',
563
560
  ],
564
561
  },
565
562
  create: {
566
- description: 'Create a new named profile (always global)',
563
+ description: 'Create a new named profile (repo-local)',
567
564
  usage: 'eve profile create <name> [--api-url <url>] [--org <id>] [--project <id>]',
568
565
  options: [
569
566
  '--api-url <url> API base URL',
@@ -581,16 +578,13 @@ clobbering your global config.`,
581
578
  ],
582
579
  },
583
580
  set: {
584
- description: 'Update profile settings (defaults to local .eve/profile.yaml)',
585
- usage: 'eve profile set [name] [--org <id>] [--project <id>] [--global]',
581
+ description: 'Update profile settings (repo-local)',
582
+ usage: 'eve profile set [name] [--org <id>] [--project <id>]',
586
583
  options: [
587
584
  '--org <id> Default organization ID',
588
585
  '--project <id> Default project ID',
589
586
  '--api-url <url> API base URL',
590
587
  '--harness <name> Default harness',
591
- '--global Write to ~/.eve/config.json instead of local project',
592
- '',
593
- 'Global-only (require --global):',
594
588
  '--supabase-url <url> Supabase URL',
595
589
  '--supabase-anon-key <key> Supabase anon key',
596
590
  '--default-email <email> Default email for auth login',
@@ -599,38 +593,40 @@ clobbering your global config.`,
599
593
  examples: [
600
594
  'eve profile set --org org_xxx --project proj_yyy',
601
595
  'eve profile set staging --org org_xxx --project proj_yyy',
602
- 'eve profile set --global --default-email user@example.com',
596
+ 'eve profile set --default-email user@example.com',
603
597
  ],
604
598
  },
605
599
  remove: {
606
- description: 'Remove a named profile (global)',
600
+ description: 'Remove a named profile (repo-local)',
607
601
  usage: 'eve profile remove <name>',
608
602
  },
609
603
  },
610
604
  examples: [
611
605
  'eve profile set --org org_xxx --project proj_yyy # writes .eve/profile.yaml',
612
606
  'eve profile use staging --org org_xxx # writes .eve/profile.yaml',
613
- 'eve profile set --global --default-email me@dev.com # writes ~/.eve/config.json',
607
+ 'eve profile set --default-email me@dev.com # writes .eve/profile.yaml',
614
608
  ],
615
609
  },
616
610
  auth: {
617
611
  description: `Authenticate with Eve Horizon. Auth is optional for local development but required
618
- for cloud deployments. Credentials are stored per-profile.`,
612
+ for cloud deployments. Credentials are stored globally per API URL.`,
619
613
  usage: 'eve auth <login|logout|status|whoami|bootstrap|sync|creds|token|mint>',
620
614
  subcommands: {
621
615
  login: {
622
616
  description: 'Login via GitHub SSH challenge (default) or Supabase (legacy)',
623
- usage: 'eve auth login [--email <email>] [--ssh-key <path>]',
617
+ usage: 'eve auth login [--email <email>] [--ssh-key <path>] [--ttl <days>]',
624
618
  options: [
625
619
  '--email <email> Email address for SSH login (uses profile default_email if not provided)',
626
620
  '--user-id <id> User id for SSH login',
627
621
  '--ssh-key <path> Path to SSH private key (uses profile default_ssh_key, then ~/.ssh/id_ed25519)',
622
+ '--ttl <days> Token TTL in days (1-90, default: server configured)',
628
623
  '--password <pass> Supabase password (triggers Supabase login)',
629
624
  '--supabase-url <url> Supabase URL',
630
625
  '--supabase-anon-key <key> Supabase anon key',
631
626
  ],
632
627
  examples: [
633
628
  'eve auth login --email user@example.com',
629
+ 'eve auth login --email user@example.com --ttl 30',
634
630
  'eve auth login # uses profile defaults if set',
635
631
  'eve auth login --ssh-key ~/.ssh/id_rsa',
636
632
  ],
@@ -771,23 +767,24 @@ for cloud deployments. Credentials are stored per-profile.`,
771
767
  },
772
768
  deploy: {
773
769
  description: 'Deploy to an environment',
774
- usage: 'eve env deploy <env> --ref <sha> [--direct] [--inputs <json>] [--image-tag <tag>] [--project <id>]',
770
+ usage: 'eve env deploy <env> --ref <sha> [--direct] [--inputs <json>] [--image-tag <tag>] [--repo-dir <path>] [--project <id>]',
775
771
  options: [
776
772
  '<env> Environment name (staging, production, test)',
777
- '--ref <sha> Git SHA or commit reference (required)',
773
+ '--ref <sha> Git SHA (required). Non-SHA refs resolve against the repo in --repo-dir or cwd.',
778
774
  '--direct Bypass pipeline and do direct deploy',
779
775
  '--inputs <json> JSON inputs for the deployment (e.g., \'{"release_id":"rel_xxx"}\')',
780
776
  '--image-tag <tag> Use a specific image tag for deploy (direct only)',
777
+ '--repo-dir <path> Resolve --ref against this repo instead of cwd',
781
778
  '--project <id> Project ID or slug (uses profile default if omitted)',
782
779
  '--watch Poll deployment status until ready (default: true)',
783
780
  '--timeout <seconds> Watch timeout in seconds (default: 120)',
784
781
  ],
785
782
  examples: [
786
- 'eve env deploy staging --ref abc123',
787
- 'eve env deploy staging --ref abc123 --direct',
788
- 'eve env deploy staging --ref abc123 --inputs \'{"release_id":"rel_xxx","smoke_test":false}\'',
789
- 'eve env deploy staging --ref abc123 --direct --inputs \'{"release_id":"rel_xxx"}\'',
790
- 'eve env deploy staging --ref abc123 --direct --image-tag demo-abc123',
783
+ 'eve env deploy staging --ref 0123456789abcdef0123456789abcdef01234567',
784
+ 'eve env deploy staging --ref 0123456789abcdef0123456789abcdef01234567 --direct',
785
+ 'eve env deploy staging --ref 0123456789abcdef0123456789abcdef01234567 --inputs \'{"release_id":"rel_xxx","smoke_test":false}\'',
786
+ 'eve env deploy staging --ref 0123456789abcdef0123456789abcdef01234567 --direct --inputs \'{"release_id":"rel_xxx"}\'',
787
+ 'eve env deploy staging --ref main --repo-dir ./my-app',
791
788
  ],
792
789
  },
793
790
  diagnose: {
@@ -952,19 +949,21 @@ for cloud deployments. Credentials are stored per-profile.`,
952
949
  },
953
950
  run: {
954
951
  description: 'Run a pipeline',
955
- usage: 'eve pipeline run <name> --ref <sha> [--env <env>] [--wait] [--only <step>]',
952
+ usage: 'eve pipeline run <name> --ref <sha> [--env <env>] [--repo-dir <path>] [--wait] [--only <step>]',
956
953
  options: [
957
- '--ref <sha> Git SHA or ref (required)',
954
+ '--ref <sha> Git SHA (required). Non-SHA refs resolve against the repo in --repo-dir or cwd.',
958
955
  '--env <env> Target environment',
959
956
  '--project <id> Project ID (uses profile default)',
960
957
  '--wait Wait for completion',
961
958
  '--timeout <n> Max wait time (seconds)',
962
959
  '--inputs <json> JSON inputs for the pipeline',
963
960
  '--only <step> Run a single step (includes dependencies)',
961
+ '--repo-dir <path> Resolve --ref against this repo instead of cwd',
964
962
  ],
965
963
  examples: [
966
- 'eve pipeline run deploy-test --ref abc123 --env test',
967
- 'eve pipeline run deploy-test --ref abc123 --env test --wait --timeout 120',
964
+ 'eve pipeline run deploy-test --ref 0123456789abcdef0123456789abcdef01234567 --env test',
965
+ 'eve pipeline run deploy-test --ref 0123456789abcdef0123456789abcdef01234567 --env test --wait --timeout 120',
966
+ 'eve pipeline run deploy-test --ref main --repo-dir ./my-app --env test',
968
967
  ],
969
968
  },
970
969
  runs: {
@@ -1006,7 +1005,7 @@ for cloud deployments. Credentials are stored per-profile.`,
1006
1005
  ],
1007
1006
  },
1008
1007
  },
1009
- examples: ['eve pipeline list', 'eve pipeline run deploy-test --ref abc123 --env test', 'eve pipeline logs deploy-test prun_xxx --follow'],
1008
+ examples: ['eve pipeline list', 'eve pipeline run deploy-test --ref 0123456789abcdef0123456789abcdef01234567 --env test', 'eve pipeline logs deploy-test prun_xxx --follow'],
1010
1009
  },
1011
1010
  workflow: {
1012
1011
  description: 'Inspect workflows defined in the project manifest (read-only in Phase 1).',
@@ -1148,16 +1147,18 @@ for cloud deployments. Credentials are stored per-profile.`,
1148
1147
  subcommands: {
1149
1148
  create: {
1150
1149
  description: 'Create a new build spec',
1151
- usage: 'eve build create --project <id> --ref <sha> --manifest-hash <hash> [--services <s1,s2>]',
1150
+ usage: 'eve build create --project <id> --ref <sha> --manifest-hash <hash> [--services <s1,s2>] [--repo-dir <path>]',
1152
1151
  options: [
1153
1152
  '--project <id> Project ID (uses profile default)',
1154
- '--ref <sha> Git SHA or commit reference (required)',
1153
+ '--ref <sha> Git SHA (required). Non-SHA refs resolve against the repo in --repo-dir or cwd.',
1155
1154
  '--manifest-hash <h> Manifest hash (required)',
1156
1155
  '--services <list> Comma-separated service names to build',
1156
+ '--repo-dir <path> Resolve --ref against this repo instead of cwd',
1157
1157
  ],
1158
1158
  examples: [
1159
- 'eve build create --ref abc123 --manifest-hash mfst_123',
1160
- 'eve build create --project proj_xxx --ref abc123 --manifest-hash mfst_123 --services api,web',
1159
+ 'eve build create --ref 0123456789abcdef0123456789abcdef01234567 --manifest-hash mfst_123',
1160
+ 'eve build create --project proj_xxx --ref 0123456789abcdef0123456789abcdef01234567 --manifest-hash mfst_123 --services api,web',
1161
+ 'eve build create --project proj_xxx --ref main --repo-dir ./my-app --manifest-hash mfst_123',
1161
1162
  ],
1162
1163
  },
1163
1164
  list: {
@@ -1232,7 +1233,7 @@ for cloud deployments. Credentials are stored per-profile.`,
1232
1233
  },
1233
1234
  },
1234
1235
  examples: [
1235
- 'eve build create --ref abc123 --manifest-hash mfst_123 --services api,web',
1236
+ 'eve build create --ref 0123456789abcdef0123456789abcdef01234567 --manifest-hash mfst_123 --services api,web',
1236
1237
  'eve build list',
1237
1238
  'eve build show build_xxx',
1238
1239
  'eve build run build_xxx',
@@ -1274,7 +1275,7 @@ eve-new-project-setup skill to complete configuration.`,
1274
1275
  ],
1275
1276
  },
1276
1277
  system: {
1277
- description: 'System administration and health checks.',
1278
+ description: 'System administration and health checks (admin scope required for most commands).',
1278
1279
  usage: 'eve system <subcommand> [options]',
1279
1280
  subcommands: {
1280
1281
  health: {
@@ -1283,7 +1284,7 @@ eve-new-project-setup skill to complete configuration.`,
1283
1284
  examples: ['eve system health', 'eve system health --json'],
1284
1285
  },
1285
1286
  status: {
1286
- description: 'Show comprehensive system status (API, orchestrator, worker, queue)',
1287
+ description: 'Show comprehensive system status (admin only)',
1287
1288
  usage: 'eve system status',
1288
1289
  examples: ['eve system status', 'eve system status --json'],
1289
1290
  },
@@ -1318,7 +1319,7 @@ eve-new-project-setup skill to complete configuration.`,
1318
1319
  ],
1319
1320
  },
1320
1321
  logs: {
1321
- description: 'Fetch recent logs for a system service (api, orchestrator, worker, postgres)',
1322
+ description: 'Fetch recent logs for a system service (admin only)',
1322
1323
  usage: 'eve system logs <service> [--tail <n>]',
1323
1324
  options: [
1324
1325
  '--tail <n> Number of log lines (default: 100)',
@@ -1329,12 +1330,12 @@ eve-new-project-setup skill to complete configuration.`,
1329
1330
  ],
1330
1331
  },
1331
1332
  pods: {
1332
- description: 'List pods across the cluster (admin view)',
1333
+ description: 'List pods across the cluster (admin only)',
1333
1334
  usage: 'eve system pods',
1334
1335
  examples: ['eve system pods'],
1335
1336
  },
1336
1337
  events: {
1337
- description: 'List recent cluster events (admin view)',
1338
+ description: 'List recent cluster events (admin only)',
1338
1339
  usage: 'eve system events [--limit <n>]',
1339
1340
  options: [
1340
1341
  '--limit <n> Max number of events (default: 50)',
@@ -1415,7 +1416,7 @@ function showMainHelp() {
1415
1416
  console.log('Global options:');
1416
1417
  console.log(' --help Show help for command');
1417
1418
  console.log(' --api-url <url> Override API URL');
1418
- console.log(' --profile <name> Use named profile');
1419
+ console.log(' --profile <name> Use named repo profile');
1419
1420
  console.log(' --org <id> Override default org');
1420
1421
  console.log(' --project <id> Override default project');
1421
1422
  console.log(' --json Output as JSON');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eve-horizon/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Eve Horizon CLI",
5
5
  "license": "MIT",
6
6
  "repository": {