atris 2.5.5 β†’ 2.6.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.
Files changed (44) hide show
  1. package/GETTING_STARTED.md +2 -2
  2. package/atris/GETTING_STARTED.md +2 -2
  3. package/atris/policies/atris-design.md +200 -15
  4. package/atris/skills/design/SKILL.md +32 -7
  5. package/bin/atris.js +34 -10
  6. package/commands/auth.js +317 -67
  7. package/commands/context-sync.js +226 -0
  8. package/commands/pull.js +118 -40
  9. package/commands/push.js +150 -61
  10. package/lib/manifest.js +222 -0
  11. package/package.json +9 -4
  12. package/utils/auth.js +127 -0
  13. package/AGENT.md +0 -35
  14. package/atris/experiments/README.md +0 -118
  15. package/atris/experiments/_examples/smoke-keep-revert/README.md +0 -45
  16. package/atris/experiments/_examples/smoke-keep-revert/candidate.py +0 -8
  17. package/atris/experiments/_examples/smoke-keep-revert/loop.py +0 -129
  18. package/atris/experiments/_examples/smoke-keep-revert/measure.py +0 -47
  19. package/atris/experiments/_examples/smoke-keep-revert/program.md +0 -3
  20. package/atris/experiments/_examples/smoke-keep-revert/proposals/bad_patch.py +0 -19
  21. package/atris/experiments/_examples/smoke-keep-revert/proposals/fix_patch.py +0 -22
  22. package/atris/experiments/_examples/smoke-keep-revert/reset.py +0 -21
  23. package/atris/experiments/_examples/smoke-keep-revert/results.tsv +0 -5
  24. package/atris/experiments/_examples/smoke-keep-revert/visual.svg +0 -52
  25. package/atris/experiments/_fixtures/invalid/BadName/loop.py +0 -1
  26. package/atris/experiments/_fixtures/invalid/BadName/program.md +0 -3
  27. package/atris/experiments/_fixtures/invalid/BadName/results.tsv +0 -1
  28. package/atris/experiments/_fixtures/invalid/bloated-context/loop.py +0 -1
  29. package/atris/experiments/_fixtures/invalid/bloated-context/measure.py +0 -1
  30. package/atris/experiments/_fixtures/invalid/bloated-context/program.md +0 -6
  31. package/atris/experiments/_fixtures/invalid/bloated-context/results.tsv +0 -1
  32. package/atris/experiments/_fixtures/valid/good-experiment/loop.py +0 -1
  33. package/atris/experiments/_fixtures/valid/good-experiment/measure.py +0 -1
  34. package/atris/experiments/_fixtures/valid/good-experiment/program.md +0 -3
  35. package/atris/experiments/_fixtures/valid/good-experiment/results.tsv +0 -1
  36. package/atris/experiments/_template/pack/loop.py +0 -3
  37. package/atris/experiments/_template/pack/measure.py +0 -13
  38. package/atris/experiments/_template/pack/program.md +0 -3
  39. package/atris/experiments/_template/pack/reset.py +0 -3
  40. package/atris/experiments/_template/pack/results.tsv +0 -1
  41. package/atris/experiments/benchmark_runtime.py +0 -81
  42. package/atris/experiments/benchmark_validate.py +0 -70
  43. package/atris/experiments/validate.py +0 -92
  44. package/atris/team/navigator/journal/2026-02-23.md +0 -6
package/commands/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, loadProfile, listProfiles, profileNameFromEmail } = require('../utils/auth');
1
+ const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions } = require('../utils/auth');
2
2
  const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
@@ -11,8 +11,6 @@ async function loginAtris(options = {}) {
11
11
  const directToken = tokenIndex !== -1 ? args[tokenIndex + 1] : options.token;
12
12
 
13
13
  try {
14
- console.log('πŸ” Login to AtrisOS\n');
15
-
16
14
  const existing = loadCredentials();
17
15
 
18
16
  // Direct token mode (non-interactive)
@@ -30,13 +28,24 @@ async function loginAtris(options = {}) {
30
28
  }
31
29
 
32
30
  if (existing && !forceFlag) {
33
- const label = existing.email || existing.user_id || 'unknown user';
34
- console.log(`Already logged in as: ${label}`);
35
- const confirm = await promptUser('Do you want to login again? (y/N): ');
36
- if (confirm.toLowerCase() !== 'y') {
37
- console.log('Login cancelled.');
31
+ const label = existing.email || existing.user_id || 'unknown';
32
+ const profiles = listProfiles();
33
+ console.log(`Currently signed in as: ${label}`);
34
+ if (profiles.length > 1) {
35
+ console.log(`${profiles.length} accounts saved. Use "atris switch" to change.\n`);
36
+ }
37
+ console.log(' 1. Add another account');
38
+ console.log(' 2. Re-login to current account');
39
+ console.log(' 3. Cancel\n');
40
+
41
+ const choice = await promptUser('Choice (1-3): ');
42
+ if (choice === '3' || (!choice)) {
43
+ console.log('Cancelled.');
38
44
  process.exit(0);
39
45
  }
46
+ // Both 1 and 2 proceed to OAuth β€” the difference is just the prompt
47
+ } else if (!existing) {
48
+ console.log('Welcome to Atris! Let\'s get you signed in.\n');
40
49
  }
41
50
 
42
51
  console.log('Choose login method:');
@@ -44,21 +53,20 @@ async function loginAtris(options = {}) {
44
53
  console.log(' 2. Paste existing API token');
45
54
  console.log(' 3. Cancel');
46
55
 
47
- const choice = await promptUser('\nEnter choice (1-3): ');
56
+ const methodChoice = await promptUser('\nEnter choice (1-3): ');
48
57
 
49
- if (choice === '1') {
58
+ if (methodChoice === '1') {
50
59
  const loginUrl = `${getAppBaseUrl()}/auth/cli`;
51
- console.log('\n🌐 Opening browser for OAuth login…');
52
- console.log('If it does not open automatically, visit:');
53
- console.log(loginUrl);
54
- console.log('\nAfter signing in, copy the CLI code shown in the browser and paste it below.');
55
- console.log('Codes expire after five minutes.\n');
60
+ console.log('\nOpening browser…');
61
+ console.log('If it doesn\'t open, visit:');
62
+ console.log(` ${loginUrl}\n`);
63
+ console.log('After signing in, paste the CLI code shown in the browser.\n');
56
64
 
57
65
  openBrowser(loginUrl);
58
66
 
59
- const code = await promptUser('Paste the CLI code here: ');
67
+ const code = await promptUser('CLI code: ');
60
68
  if (!code) {
61
- console.error('βœ— Error: Code is required');
69
+ console.error('Error: Code is required.');
62
70
  process.exit(1);
63
71
  }
64
72
 
@@ -68,7 +76,7 @@ async function loginAtris(options = {}) {
68
76
  });
69
77
 
70
78
  if (!exchange.ok || !exchange.data) {
71
- console.error(`βœ— Error: ${exchange.error || 'Invalid or expired code'}`);
79
+ console.error(`Error: ${exchange.error || 'Invalid or expired code'}`);
72
80
  process.exit(1);
73
81
  }
74
82
 
@@ -77,49 +85,47 @@ async function loginAtris(options = {}) {
77
85
  const refreshToken = payload.refresh_token;
78
86
 
79
87
  if (!token || !refreshToken) {
80
- console.error('βœ— Error: Backend did not return tokens. Please try again.');
88
+ console.error('Error: Backend did not return tokens. Try again.');
81
89
  process.exit(1);
82
90
  }
83
91
 
84
- const email = payload.email || existing?.email || null;
85
- const userId = payload.user_id || existing?.user_id || null;
92
+ const email = payload.email || null;
93
+ const userId = payload.user_id || null;
86
94
  const provider = payload.provider || 'atris';
87
95
 
88
96
  saveCredentials(token, refreshToken, email, userId, provider);
89
- console.log('\nβœ“ Successfully logged in!');
97
+ const name = profileNameFromEmail(email);
98
+ console.log(`\nβœ“ Signed in as ${email || 'unknown'}${name ? ` (profile: ${name})` : ''}`);
90
99
  await displayAccountSummary(apiRequestJson);
91
- console.log('\nYou can now use cloud features with atris commands.');
92
100
  process.exit(0);
93
- } else if (choice === '2') {
94
- console.log('\nπŸ“‹ Manual Token Entry');
95
- console.log('Get your token from: https://atris.ai/auth/cli\n');
101
+ } else if (methodChoice === '2') {
102
+ console.log('\nGet your token from: https://atris.ai/auth/cli\n');
96
103
 
97
- const tokenInput = await promptUser('Paste your API token: ');
104
+ const tokenInput = await promptUser('API token: ');
98
105
 
99
106
  if (!tokenInput) {
100
- console.error('βœ— Error: Token is required');
107
+ console.error('Error: Token is required.');
101
108
  process.exit(1);
102
109
  }
103
110
 
104
111
  const trimmed = tokenInput.trim();
105
112
  saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
106
- console.log('\nAttempting to validate token…\n');
113
+ console.log('\nValidating…\n');
107
114
 
108
115
  const summary = await displayAccountSummary(apiRequestJson);
109
116
  if (summary.error) {
110
- console.log('\n⚠️ Token saved, but validation failed. You may need to relogin.');
117
+ console.log('\n⚠️ Token saved, but validation failed.');
111
118
  } else {
112
- console.log('\nβœ“ Token validated successfully.');
119
+ console.log('\nβœ“ Token validated.');
113
120
  }
114
121
 
115
- console.log('\nYou can now use cloud features with atris commands.');
116
122
  process.exit(0);
117
123
  } else {
118
- console.log('Login cancelled.');
124
+ console.log('Cancelled.');
119
125
  process.exit(0);
120
126
  }
121
127
  } catch (error) {
122
- console.error(`\nβœ— Login failed: ${error.message || error}`);
128
+ console.error(`\nLogin failed: ${error.message || error}`);
123
129
  process.exit(1);
124
130
  }
125
131
  }
@@ -128,39 +134,52 @@ function logoutAtris() {
128
134
  const credentials = loadCredentials();
129
135
 
130
136
  if (!credentials) {
131
- console.log('Not currently logged in.');
137
+ console.log('Not signed in.');
132
138
  process.exit(0);
133
139
  }
134
140
 
141
+ const profiles = listProfiles();
142
+ const currentName = profileNameFromEmail(credentials?.email);
143
+
135
144
  deleteCredentials();
136
- console.log('βœ“ Successfully logged out');
137
- console.log(`βœ“ Removed credentials from ${getCredentialsPath()}`);
145
+ console.log(`βœ“ Signed out from ${credentials.email || 'current account'}`);
146
+
147
+ // Remind about other profiles
148
+ const remaining = profiles.filter(p => p !== currentName);
149
+ if (remaining.length > 0) {
150
+ console.log(`\n${remaining.length} other account${remaining.length > 1 ? 's' : ''} saved.`);
151
+ console.log(`Switch to one: atris switch ${remaining[0]}`);
152
+ console.log('Or remove all: atris accounts remove --all');
153
+ }
138
154
  }
139
155
 
140
156
  async function whoamiAtris() {
141
157
  const { apiRequestJson } = require('../utils/api');
142
-
158
+
143
159
  try {
144
160
  const summary = await displayAccountSummary(apiRequestJson);
145
161
  if (summary.error) {
146
- console.log('\nRun "atris login" to authenticate with AtrisOS.');
162
+ console.log('\nRun "atris login" to sign in.');
147
163
  process.exit(1);
148
164
  }
149
165
  process.exit(0);
150
166
  } catch (error) {
151
- console.error(`βœ— Failed to fetch account details: ${error.message || error}`);
167
+ console.error(`Failed to fetch account: ${error.message || error}`);
152
168
  process.exit(1);
153
169
  }
154
170
  }
155
171
 
156
172
  async function switchAccount() {
157
173
  const args = process.argv.slice(3);
174
+ const globalFlag = args.includes('--global') || args.includes('-g');
158
175
  const targetName = args.filter(a => !a.startsWith('-'))[0];
159
176
 
177
+ // Clean up stale session files in the background
178
+ cleanStaleSessions();
179
+
160
180
  const profiles = listProfiles();
161
181
  if (profiles.length === 0) {
162
- console.log('No saved profiles. Log in with different accounts to create profiles.');
163
- console.log('Profiles are auto-saved on login.');
182
+ console.log('No saved accounts. Run "atris login" to add one.');
164
183
  process.exit(1);
165
184
  }
166
185
 
@@ -173,39 +192,50 @@ async function switchAccount() {
173
192
  profiles.forEach((name, i) => {
174
193
  const profile = loadProfile(name);
175
194
  const email = profile?.email || 'unknown';
176
- const marker = name === currentName ? ' (active)' : '';
195
+ const marker = name === currentName ? ' ← active' : '';
177
196
  console.log(` ${i + 1}. ${name} β€” ${email}${marker}`);
178
197
  });
179
- console.log(` ${profiles.length + 1}. Cancel`);
198
+ console.log(` ${profiles.length + 1}. Add new account`);
199
+ console.log(` ${profiles.length + 2}. Cancel`);
180
200
 
181
- const choice = await promptUser(`\nEnter choice (1-${profiles.length + 1}): `);
201
+ const choice = await promptUser(`\nChoice (1-${profiles.length + 2}): `);
182
202
  const idx = parseInt(choice, 10) - 1;
183
203
 
204
+ if (idx === profiles.length) {
205
+ // Add new account
206
+ return loginAtris({ force: true });
207
+ }
208
+
184
209
  if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
185
210
  console.log('Cancelled.');
186
211
  process.exit(0);
187
212
  }
188
213
 
189
214
  const chosen = profiles[idx];
190
- return activateProfile(chosen, currentName);
215
+ return activateProfile(chosen, currentName, { global: globalFlag });
191
216
  }
192
217
 
193
218
  // Direct: atris switch <name>
194
- // Fuzzy match: allow partial names
195
- const exact = profiles.find(p => p === targetName);
196
- const partial = !exact ? profiles.find(p => p.startsWith(targetName)) : null;
197
- const match = exact || partial;
219
+ // Fuzzy match: exact β†’ startsWith β†’ substring β†’ email substring
220
+ const q = targetName.toLowerCase();
221
+ const match = profiles.find(p => p.toLowerCase() === q)
222
+ || profiles.find(p => p.toLowerCase().startsWith(q))
223
+ || profiles.find(p => p.toLowerCase().includes(q))
224
+ || profiles.find(p => {
225
+ const profile = loadProfile(p);
226
+ return profile?.email?.toLowerCase().includes(q);
227
+ });
198
228
 
199
229
  if (!match) {
200
- console.error(`Profile "${targetName}" not found.`);
230
+ console.error(`No account matching "${targetName}".`);
201
231
  console.log(`Available: ${profiles.join(', ')}`);
202
232
  process.exit(1);
203
233
  }
204
234
 
205
- return activateProfile(match, currentName);
235
+ return activateProfile(match, currentName, { global: globalFlag });
206
236
  }
207
237
 
208
- function activateProfile(name, currentName) {
238
+ function activateProfile(name, currentName, { global = false } = {}) {
209
239
  if (name === currentName) {
210
240
  console.log(`Already on "${name}".`);
211
241
  process.exit(0);
@@ -213,37 +243,257 @@ function activateProfile(name, currentName) {
213
243
 
214
244
  const profile = loadProfile(name);
215
245
  if (!profile || !profile.token) {
216
- console.error(`Profile "${name}" is corrupted. Login again to fix it.`);
246
+ console.error(`Profile "${name}" is corrupted. Run "atris login" to fix.`);
217
247
  process.exit(1);
218
248
  }
219
249
 
220
- // Copy profile to credentials.json
221
- const credentialsPath = getCredentialsPath();
222
- fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
223
- try { fs.chmodSync(credentialsPath, 0o600); } catch {}
250
+ if (global || !getTerminalSessionId()) {
251
+ // Global switch β€” write to credentials.json (affects all terminals)
252
+ const credentialsPath = getCredentialsPath();
253
+ fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
254
+ try { fs.chmodSync(credentialsPath, 0o600); } catch {}
255
+ console.log(`Switched to ${profile.email || name} (global β€” all terminals)`);
256
+ } else {
257
+ // Per-terminal switch β€” write session file (only this terminal)
258
+ setSessionProfile(name);
259
+ console.log(`Switched to ${profile.email || name}`);
260
+ }
261
+ }
262
+
263
+ function useAccount() {
264
+ const args = process.argv.slice(3);
265
+ const targetName = args.filter(a => !a.startsWith('-'))[0];
266
+
267
+ if (!targetName) {
268
+ // Show current per-terminal override or global
269
+ const envProfile = process.env.ATRIS_PROFILE;
270
+ if (envProfile) {
271
+ const profile = loadProfile(envProfile);
272
+ const email = profile?.email || envProfile;
273
+ console.log(`This terminal: ${email} (ATRIS_PROFILE=${envProfile})`);
274
+ } else {
275
+ const current = loadCredentials();
276
+ if (current) {
277
+ console.log(`Global: ${current.email || 'unknown'} (no per-terminal override)`);
278
+ } else {
279
+ console.log('Not signed in.');
280
+ }
281
+ }
282
+ console.log('\nSet per-terminal account:');
283
+ console.log(' eval "$(atris use <name>)"');
284
+ console.log('\nOr manually:');
285
+ console.log(' export ATRIS_PROFILE=<name>');
286
+ process.exit(0);
287
+ }
288
+
289
+ // Fuzzy match the profile
290
+ const profiles = listProfiles();
291
+ const q = targetName.toLowerCase();
292
+ const match = profiles.find(p => p.toLowerCase() === q)
293
+ || profiles.find(p => p.toLowerCase().startsWith(q))
294
+ || profiles.find(p => p.toLowerCase().includes(q))
295
+ || profiles.find(p => {
296
+ const profile = loadProfile(p);
297
+ return profile?.email?.toLowerCase().includes(q);
298
+ });
299
+
300
+ if (!match) {
301
+ console.error(`No account matching "${targetName}".`);
302
+ console.error(`Available: ${profiles.join(', ')}`);
303
+ process.exit(1);
304
+ }
224
305
 
225
- console.log(`Switched to "${name}" (${profile.email || 'unknown'})`);
306
+ const profile = loadProfile(match);
307
+ const email = profile?.email || match;
308
+
309
+ // If stdout is piped (eval mode), output just the export
310
+ if (!process.stdout.isTTY) {
311
+ process.stdout.write(`export ATRIS_PROFILE=${match}\n`);
312
+ } else {
313
+ // Interactive β€” print instructions
314
+ console.log(`export ATRIS_PROFILE=${match}`);
315
+ console.log(`\n# Run this to activate ${email} in this terminal:`);
316
+ console.log(`# eval "$(atris use ${targetName})"`);
317
+ console.log(`# Or just copy the export line above.`);
318
+ }
319
+ }
320
+
321
+ async function accountsCmd() {
322
+ const args = process.argv.slice(3);
323
+ const subCmd = args[0];
324
+
325
+ if (subCmd === 'add' || subCmd === 'login') {
326
+ return loginAtris({ force: true });
327
+ }
328
+
329
+ if (subCmd === 'remove' || subCmd === 'rm') {
330
+ const target = args[1];
331
+ if (target === '--all') {
332
+ const profiles = listProfiles();
333
+ if (profiles.length === 0) {
334
+ console.log('No accounts to remove.');
335
+ process.exit(0);
336
+ }
337
+ const confirm = await promptUser(`Remove all ${profiles.length} accounts? (y/N): `);
338
+ if (confirm.toLowerCase() !== 'y') {
339
+ console.log('Cancelled.');
340
+ process.exit(0);
341
+ }
342
+ profiles.forEach(p => deleteProfile(p));
343
+ deleteCredentials();
344
+ console.log(`βœ“ Removed ${profiles.length} account(s).`);
345
+ process.exit(0);
346
+ }
347
+ if (!target) {
348
+ console.log('Usage: atris accounts remove <name>');
349
+ console.log(' atris accounts remove --all');
350
+ process.exit(1);
351
+ }
352
+ // Fuzzy match
353
+ const profiles = listProfiles();
354
+ const q = target.toLowerCase();
355
+ const match = profiles.find(p => p.toLowerCase() === q)
356
+ || profiles.find(p => p.toLowerCase().startsWith(q))
357
+ || profiles.find(p => p.toLowerCase().includes(q));
358
+ if (!match) {
359
+ console.error(`No account matching "${target}".`);
360
+ console.log(`Available: ${profiles.join(', ')}`);
361
+ process.exit(1);
362
+ }
363
+ const profile = loadProfile(match);
364
+ const email = profile?.email || 'unknown';
365
+ const confirm = await promptUser(`Remove ${match} (${email})? (y/N): `);
366
+ if (confirm.toLowerCase() !== 'y') {
367
+ console.log('Cancelled.');
368
+ process.exit(0);
369
+ }
370
+ deleteProfile(match);
371
+ // If this was the active account, clear credentials
372
+ const current = loadCredentials();
373
+ if (current && profileNameFromEmail(current.email) === match) {
374
+ deleteCredentials();
375
+ const remaining = listProfiles();
376
+ if (remaining.length > 0) {
377
+ console.log(`βœ“ Removed ${email}. No active account.`);
378
+ console.log(`Switch to another: atris switch ${remaining[0]}`);
379
+ } else {
380
+ console.log(`βœ“ Removed ${email}. No accounts remaining.`);
381
+ console.log('Run "atris login" to add one.');
382
+ }
383
+ } else {
384
+ console.log(`βœ“ Removed ${email}.`);
385
+ }
386
+ process.exit(0);
387
+ }
388
+
389
+ // Default: list accounts
390
+ return listAccountsCmd();
226
391
  }
227
392
 
228
393
  function listAccountsCmd() {
229
394
  const profiles = listProfiles();
230
395
  if (profiles.length === 0) {
231
- console.log('No saved profiles. Profiles are auto-saved on login.');
396
+ console.log('No accounts saved. Run "atris login" to add one.');
232
397
  process.exit(0);
233
398
  }
234
399
 
235
400
  const current = loadCredentials();
236
- const currentName = profileNameFromEmail(current?.email);
401
+ const currentUid = current?.user_id;
402
+ // Also check which profile name is active (env var or session)
403
+ const envProfile = process.env.ATRIS_PROFILE;
404
+ const sessionProfile = getSessionProfile();
237
405
 
238
- console.log('Accounts:\n');
406
+ console.log('\n Accounts\n');
239
407
  profiles.forEach(name => {
240
408
  const profile = loadProfile(name);
241
409
  const email = profile?.email || 'unknown';
242
- const marker = name === currentName ? ' *' : '';
243
- console.log(` ${name} β€” ${email}${marker}`);
410
+ const isActive = profile?.user_id === currentUid;
411
+ if (isActive) {
412
+ console.log(` ● ${name} ${email}`);
413
+ } else {
414
+ console.log(` ${name} ${email}`);
415
+ }
244
416
  });
245
- console.log('\n* = active');
246
- console.log('\nSwitch: atris switch <name>');
417
+ console.log(`\n Switch: atris switch <name>`);
418
+ console.log(` Add: atris accounts add`);
419
+ console.log(` Remove: atris accounts remove <name>\n`);
420
+ }
421
+
422
+ function resolveProfile() {
423
+ // Hidden command: atris _resolve <query> β†’ prints resolved profile name
424
+ const query = process.argv[3];
425
+ if (!query) { process.exit(1); }
426
+ const profiles = listProfiles();
427
+ const q = query.toLowerCase();
428
+ const match = profiles.find(p => p.toLowerCase() === q)
429
+ || profiles.find(p => p.toLowerCase().startsWith(q))
430
+ || profiles.find(p => p.toLowerCase().includes(q))
431
+ || profiles.find(p => {
432
+ const profile = loadProfile(p);
433
+ return profile?.email?.toLowerCase().includes(q);
434
+ });
435
+ if (match) {
436
+ process.stdout.write(match);
437
+ process.exit(0);
438
+ }
439
+ process.exit(1);
440
+ }
441
+
442
+ function profileEmail() {
443
+ // Hidden command: atris _profile-email <name> β†’ prints email
444
+ const name = process.argv[3];
445
+ if (!name) { process.exit(1); }
446
+ const profile = loadProfile(name);
447
+ if (profile?.email) {
448
+ process.stdout.write(profile.email);
449
+ process.exit(0);
450
+ }
451
+ process.exit(1);
452
+ }
453
+
454
+ function shellInit() {
455
+ // Output shell function for per-terminal account switching
456
+ // Usage: eval "$(atris shell-init)" (add to ~/.zshrc)
457
+ // Use array join to avoid JS template literal parsing issues with ${}
458
+ const lines = [
459
+ '# Atris per-terminal account switching',
460
+ '# Added by: eval "$(atris shell-init)"',
461
+ 'atris() {',
462
+ ' if [[ "$1" == "switch" && -n "$2" && "$2" != "--"* ]]; then',
463
+ ' local _profile',
464
+ ' _profile=$(command atris _resolve "$2" 2>/dev/null)',
465
+ ' if [[ $? -eq 0 && -n "$_profile" ]]; then',
466
+ ' export ATRIS_PROFILE="$_profile"',
467
+ ' local _email',
468
+ ' _email=$(command atris _profile-email "$_profile" 2>/dev/null)',
469
+ ' echo "Switched to ${_email:-$_profile}"',
470
+ ' else',
471
+ ' echo "No account matching \'$2\'."',
472
+ ' command atris accounts',
473
+ ' fi',
474
+ ' elif [[ "$1" == "switch" && $# -eq 1 ]]; then',
475
+ ' command atris accounts',
476
+ ' echo ""',
477
+ ' printf "Switch to: "',
478
+ ' read _pick',
479
+ ' if [[ -n "$_pick" ]]; then',
480
+ ' local _profile',
481
+ ' _profile=$(command atris _resolve "$_pick" 2>/dev/null)',
482
+ ' if [[ $? -eq 0 && -n "$_profile" ]]; then',
483
+ ' export ATRIS_PROFILE="$_profile"',
484
+ ' local _email',
485
+ ' _email=$(command atris _profile-email "$_profile" 2>/dev/null)',
486
+ ' echo "Switched to ${_email:-$_profile}"',
487
+ ' else',
488
+ ' echo "No account matching \'$_pick\'."',
489
+ ' fi',
490
+ ' fi',
491
+ ' else',
492
+ ' command atris "$@"',
493
+ ' fi',
494
+ '}',
495
+ ];
496
+ console.log(lines.join('\n'));
247
497
  }
248
498
 
249
- module.exports = { loginAtris, logoutAtris, whoamiAtris, switchAccount, listAccountsCmd };
499
+ module.exports = { loginAtris, logoutAtris, whoamiAtris, switchAccount, useAccount, accountsCmd, listAccountsCmd, resolveProfile, profileEmail, shellInit };