atris 3.55.0 → 3.56.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.
@@ -3277,11 +3277,14 @@ async function deployBusiness(slug) {
3277
3277
  // Upload workspace files
3278
3278
  const workspaceDir = path.join(bizDir, 'workspace');
3279
3279
  let uploadCount = 0;
3280
+ let uploadFailureCount = 0;
3281
+ let uploadTotal = 0;
3280
3282
  if (fs.existsSync(workspaceDir)) {
3281
3283
  const files = walkDir(workspaceDir);
3282
3284
  for (const filePath of files) {
3283
3285
  const relativePath = path.relative(workspaceDir, filePath);
3284
3286
  if (relativePath.startsWith('.')) continue;
3287
+ uploadTotal++;
3285
3288
  try {
3286
3289
  const content = fs.readFileSync(filePath, 'utf8');
3287
3290
  const uploadResult = await apiRequestJson(
@@ -3291,9 +3294,14 @@ async function deployBusiness(slug) {
3291
3294
  if (uploadResult.ok) {
3292
3295
  uploadCount++;
3293
3296
  process.stdout.write(` Uploaded: ${relativePath}\n`);
3297
+ } else {
3298
+ uploadFailureCount++;
3299
+ const detail = uploadResult.errorMessage || uploadResult.error || uploadResult.status || 'unknown error';
3300
+ console.log(` Failed: ${relativePath} (${detail})`);
3294
3301
  }
3295
3302
  } catch (e) {
3296
- // Skip binary files or errors
3303
+ uploadFailureCount++;
3304
+ console.log(` Failed: ${relativePath} (${e.message || String(e)})`);
3297
3305
  }
3298
3306
  }
3299
3307
  }
@@ -3314,6 +3322,10 @@ async function deployBusiness(slug) {
3314
3322
 
3315
3323
  console.log(`\n Deployed ${uploadCount} files to ${bizConfig.name}`);
3316
3324
  console.log(` Dashboard: https://atris.ai/dashboard/gm/${bizConfig.business_id}`);
3325
+ if (uploadFailureCount > 0) {
3326
+ console.log(` ${uploadFailureCount} of ${uploadTotal} files failed to upload`);
3327
+ process.exitCode = 1;
3328
+ }
3317
3329
  console.log('');
3318
3330
  }
3319
3331
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  const { loadCredentials } = require('../utils/auth');
17
17
  const { apiRequestJson } = require('../utils/api');
18
- const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
18
+ const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
19
19
  const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
20
20
 
21
21
  function sleep(ms) {
@@ -181,7 +181,7 @@ async function fleetReport() {
181
181
  const wake = gate.args.includes('--wake');
182
182
  const dryRun = gate.args.includes('--dry-run');
183
183
  const board = gate.args.includes('--leaderboard');
184
- const slug = gate.args.find((a) => !a.startsWith('--'));
184
+ const slug = gate.args.find((a) => looksLikeBusinessSlug(a));
185
185
 
186
186
  if (!allAlive && !slug && !board) {
187
187
  console.log(usage);
@@ -14,7 +14,7 @@ const {
14
14
  const { apiRequestJson } = require('../utils/api');
15
15
  const { decodeJwtClaims, loadCredentials } = require('../utils/auth');
16
16
  const { isHelpToken } = require('../lib/noninteractive');
17
- const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
17
+ const { isFreshWorkspace, speakFirstMinute, speakNothingRunning } = require('../lib/first-minute');
18
18
 
19
19
  const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
20
20
  const HUMAN_STATES = Object.freeze({
@@ -168,7 +168,8 @@ function parseMissionId(args = []) {
168
168
 
169
169
  /**
170
170
  * Scratch / unbound folders must not drive account-current cloud work.
171
- * Require an explicit mission/task id: atris stop --mission <id>
171
+ * Empty-folder stop talks first-talk instead. After a room exists,
172
+ * require an explicit mission/task id: atris stop --mission <id>
172
173
  */
173
174
  function refuseUnboundCloudComputer(args = [], options = {}, commandName = 'stop') {
174
175
  const root = options.root || process.cwd();
@@ -634,6 +635,15 @@ async function currentMissionCommand(args = [], options = {}) {
634
635
  outputCard(card, asJson, options.log || console.log);
635
636
  return 0;
636
637
  } catch (caught) {
638
+ if (options.fallbackOnMissing) {
639
+ const code = speakFirstMinute({
640
+ root: options.root || process.cwd(),
641
+ asJson,
642
+ log: options.log || console.log,
643
+ });
644
+ if (options.setProcessExitCode !== false) process.exitCode = code;
645
+ return code;
646
+ }
637
647
  return reportError(caught, 'show the current mission', asJson, options);
638
648
  }
639
649
  }
@@ -734,6 +744,20 @@ async function stopCommand(args, options = {}) {
734
744
  (options.log || console.log)('Usage: atris stop [--mission <id>] [--json]');
735
745
  return 0;
736
746
  }
747
+ const root = options.root || process.cwd();
748
+ const asJson = args.includes('--json');
749
+ // Fresh folder: empty talks first-talk. A file already here
750
+ // names that file, same as bare atris. Do not mint a room.
751
+ if (isFreshWorkspace(root) && !parseMissionId(args)) {
752
+ const unbound = refuseUnboundCloudComputer(args, options, 'stop');
753
+ if (unbound) {
754
+ return speakNothingRunning({
755
+ root,
756
+ asJson,
757
+ log: options.log || console.log,
758
+ });
759
+ }
760
+ }
737
761
  return changeCurrentMission('stop', {}, args, options);
738
762
  }
739
763
 
package/commands/init.js CHANGED
@@ -468,41 +468,8 @@ function initAtris() {
468
468
  }
469
469
  }
470
470
 
471
- // Create INTUITION.md - captures learnings, preferences, dead ends
472
- const intuitionFile = path.join(targetDir, 'INTUITION.md');
473
- if (!fs.existsSync(intuitionFile)) {
474
- fs.writeFileSync(intuitionFile, `# INTUITION.md
475
-
476
- > Accumulated learnings. Read before major decisions. Update after discoveries.
477
-
478
- ---
479
-
480
- ## Tripwires
481
-
482
- *Things that seem obvious but break unexpectedly. Check these first when debugging.*
483
-
484
- - (none yet — add when you hit surprising failures)
485
-
486
- ---
487
-
488
- ## Preferences
489
-
490
- *Patterns this codebase prefers. Follow these over generic best practices.*
491
-
492
- - (none yet — add as you discover the codebase style)
493
-
494
- ---
495
-
496
- ## Dead Ends
497
-
498
- *Approaches tried and abandoned. Don't retry without new information.*
499
-
500
- - (none yet — log failed approaches so future agents skip them)
501
-
502
- ---
503
- `);
504
- markReady('workspace', 'INTUITION.md', '✓ Created INTUITION.md');
505
- }
471
+ // INTUITION.md was retired into lessons.md. Do not write a parallel lesson file.
472
+ // Leave a user's existing INTUITION.md alone if one is already on disk.
506
473
 
507
474
  // Create lessons.md (feedback loop for learning across features)
508
475
  const lessonsFile = path.join(targetDir, 'lessons.md');
@@ -5,6 +5,9 @@
5
5
  * atris gmail inbox [--account <id>] - List recent emails for a mailbox
6
6
  * atris gmail read <id> [--account <id>] - Read specific email
7
7
  * atris gmail archive <id> [...] [--account <id>] - Archive messages
8
+ * atris gmail verdicts [--account <id>] [--limit N] - List recent archive verdicts
9
+ * atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
10
+ * atris gmail voice [account] [--clear] - Edit an account's writing voice
8
11
  * atris gmail connect [name] - Connect or reconnect a Gmail account
9
12
  * atris gmail accounts - List connected Gmail accounts and the active account
10
13
  * atris gmail use [<account_id|name>] - Choose or set the active Gmail account
@@ -35,6 +38,79 @@ const { spawnSync } = require('child_process');
35
38
  const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
36
39
  const GMAIL_CONNECT_POLL_MS = 3000;
37
40
  const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
41
+ const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
42
+ const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
43
+ const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--account <id>] [--limit N]';
44
+
45
+ function gmailVerdictsPath(root = process.cwd()) {
46
+ return path.join(root, '.atris', 'state', 'gmail-verdicts.jsonl');
47
+ }
48
+
49
+ function appendGmailVerdicts(verdicts, options = {}) {
50
+ const rows = Array.isArray(verdicts) ? verdicts : [];
51
+ if (!rows.length) return;
52
+ const filePath = options.filePath || gmailVerdictsPath(options.root);
53
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
54
+ const lines = rows.map((row) => JSON.stringify({
55
+ ts: row.ts || new Date().toISOString(),
56
+ account: String(row.account || ''),
57
+ verdict: 'archive',
58
+ message_id: String(row.message_id || ''),
59
+ ...(row.from ? { from: row.from } : {}),
60
+ ...(row.subject ? { subject: row.subject } : {}),
61
+ }));
62
+ fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
63
+ }
64
+
65
+ function readGmailVerdicts(options = {}) {
66
+ const filePath = options.filePath || gmailVerdictsPath(options.root);
67
+ let lines;
68
+ try {
69
+ lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
70
+ } catch (error) {
71
+ if (error.code === 'ENOENT') return [];
72
+ throw error;
73
+ }
74
+ const account = String(options.account || '').trim();
75
+ const limit = Number.isInteger(options.limit) ? options.limit : 20;
76
+ return lines.reduce((rows, line) => {
77
+ try {
78
+ const row = JSON.parse(line);
79
+ if (!account || row.account === account) rows.push(row);
80
+ } catch {}
81
+ return rows;
82
+ }, []).reverse().slice(0, limit);
83
+ }
84
+
85
+ function printGmailVerdicts(options = {}) {
86
+ const rows = readGmailVerdicts(options);
87
+ if (!rows.length) {
88
+ console.log('no gmail verdicts found.');
89
+ return rows;
90
+ }
91
+ for (const row of rows) {
92
+ const details = [row.from && `from ${row.from}`, row.subject && `subject ${row.subject}`].filter(Boolean);
93
+ console.log(`${row.ts} archive ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
94
+ }
95
+ return rows;
96
+ }
97
+
98
+ function parseGmailVerdictsArgs(args = []) {
99
+ let account = null;
100
+ let limit = 20;
101
+ for (let i = 0; i < args.length; i += 1) {
102
+ const arg = args[i];
103
+ const value = String(args[i + 1] || '').trim();
104
+ if (arg === '--account' && value && !value.startsWith('--')) account = value;
105
+ else if (arg === '--limit' && /^\d+$/.test(value) && Number(value) > 0) limit = Number(value);
106
+ else {
107
+ console.error(GMAIL_VERDICTS_USAGE);
108
+ process.exit(1);
109
+ }
110
+ i += 1;
111
+ }
112
+ return { account, limit };
113
+ }
38
114
 
39
115
  function gmailAccountStatePath() {
40
116
  return process.env.ATRIS_GMAIL_ACCOUNT_FILE
@@ -308,9 +384,176 @@ async function gmailArchive(messageIds, options = {}) {
308
384
  }
309
385
 
310
386
  const archived = result.data?.archived ?? result.data?.count ?? ids.length;
387
+ const responseMessages = [
388
+ result.data?.messages,
389
+ result.data?.archived_messages,
390
+ result.data?.results,
391
+ ].find(Array.isArray) || [];
392
+ const metadata = new Map(responseMessages.map((message) => [
393
+ String(message.id || message.message_id || ''),
394
+ message,
395
+ ]));
396
+ appendGmailVerdicts(ids.map((messageId) => {
397
+ const message = metadata.get(messageId) || {};
398
+ return {
399
+ account: accountId,
400
+ message_id: messageId,
401
+ from: message.from || message.sender || undefined,
402
+ subject: message.subject || undefined,
403
+ };
404
+ }));
311
405
  console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
312
406
  }
313
407
 
408
+ function parseGmailSendArgs(args = []) {
409
+ let accountId = null;
410
+ let bodyFile = null;
411
+ const positional = [];
412
+
413
+ for (let i = 0; i < args.length; i += 1) {
414
+ const arg = args[i];
415
+ if (arg === '--account' || arg === '--body-file') {
416
+ const value = String(args[i + 1] || '').trim();
417
+ if (!value || value.startsWith('--')) {
418
+ console.error(GMAIL_SEND_USAGE);
419
+ process.exit(2);
420
+ }
421
+ if (arg === '--account') accountId = value;
422
+ else bodyFile = value;
423
+ i += 1;
424
+ continue;
425
+ }
426
+ positional.push(arg);
427
+ }
428
+
429
+ const [to, subject, ...bodyParts] = positional;
430
+ if (!String(to || '').trim()
431
+ || !String(subject || '').trim()
432
+ || (bodyFile ? bodyParts.length > 0 : bodyParts.length === 0)) {
433
+ console.error(GMAIL_SEND_USAGE);
434
+ process.exit(2);
435
+ }
436
+
437
+ let body = bodyParts.join(' ');
438
+ if (bodyFile) {
439
+ try {
440
+ body = fs.readFileSync(bodyFile, 'utf8');
441
+ } catch (error) {
442
+ console.error(`could not read body file "${bodyFile}": ${error.message}`);
443
+ process.exit(1);
444
+ }
445
+ }
446
+
447
+ return { to, subject, body, accountId };
448
+ }
449
+
450
+ async function gmailSend(to, subject, body, options = {}) {
451
+ const token = await getAuthToken();
452
+ const accountId = resolveGmailAccountId(options.accountId);
453
+ const accounts = await fetchGmailAccounts(token);
454
+ const account = findGmailAccount(accounts, accountId);
455
+
456
+ if (!account) {
457
+ console.error(`gmail account "${accountId}" is not connected.`);
458
+ process.exit(1);
459
+ }
460
+
461
+ const result = await apiRequestJson('/integrations/gmail/send', {
462
+ method: 'POST',
463
+ token,
464
+ body: { to, subject, body, account_id: accountId },
465
+ });
466
+
467
+ if (!result.ok) {
468
+ console.error(`could not send gmail message: ${result.error || 'request failed'}`);
469
+ process.exit(1);
470
+ }
471
+
472
+ const { email, id } = gmailAccountIdentity(account);
473
+ console.log(`sent to ${to} from ${email || id}`);
474
+ }
475
+
476
+ function parseGmailVoiceArgs(args = []) {
477
+ let clear = false;
478
+ const positional = [];
479
+ for (const arg of args) {
480
+ if (arg === '--clear') clear = true;
481
+ else positional.push(arg);
482
+ }
483
+ if (positional.length > 1 || positional.some((arg) => String(arg).startsWith('--'))) {
484
+ console.error(GMAIL_VOICE_USAGE);
485
+ process.exit(2);
486
+ }
487
+ return { accountId: positional[0] || null, clear };
488
+ }
489
+
490
+ function editGmailVoice(currentVoice, deps = {}) {
491
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-gmail-voice-'));
492
+ const voicePath = path.join(tempDir, 'voice.md');
493
+ let editedVoice;
494
+ let editorError = null;
495
+
496
+ try {
497
+ fs.writeFileSync(voicePath, currentVoice, 'utf8');
498
+ const editor = deps.editor || process.env.EDITOR || 'vi';
499
+ const shell = deps.shell || process.env.SHELL || '/bin/sh';
500
+ const run = deps.spawnSync || spawnSync;
501
+ const result = run(shell, ['-c', 'exec $EDITOR "$1"', 'atris-gmail-voice', voicePath], {
502
+ stdio: 'inherit',
503
+ env: { ...process.env, EDITOR: editor },
504
+ });
505
+ if (result.error || result.status !== 0) {
506
+ editorError = result.error?.message || `editor exited with status ${result.status}`;
507
+ } else {
508
+ editedVoice = fs.readFileSync(voicePath, 'utf8');
509
+ }
510
+ } finally {
511
+ fs.rmSync(tempDir, { recursive: true, force: true });
512
+ }
513
+
514
+ if (editorError) {
515
+ console.error(`could not edit gmail voice: ${editorError}`);
516
+ process.exit(1);
517
+ }
518
+ return editedVoice;
519
+ }
520
+
521
+ async function gmailVoice(accountId, options = {}) {
522
+ const token = await getAuthToken();
523
+ const resolvedAccountId = resolveGmailAccountId(accountId);
524
+ const accounts = await fetchGmailAccounts(token);
525
+ const account = findGmailAccount(accounts, resolvedAccountId);
526
+
527
+ if (!account) {
528
+ console.error(`gmail account "${resolvedAccountId}" is not connected.`);
529
+ process.exit(1);
530
+ }
531
+
532
+ let voiceMd = '';
533
+ if (!options.clear) {
534
+ const stdinIsTTY = options.stdinIsTTY === undefined
535
+ ? Boolean(process.stdin.isTTY)
536
+ : options.stdinIsTTY;
537
+ voiceMd = stdinIsTTY
538
+ ? editGmailVoice(String(account.voice_md || ''), options)
539
+ : String((options.readStdin || (() => fs.readFileSync(0, 'utf8')))());
540
+ }
541
+
542
+ const { id, name } = gmailAccountIdentity(account);
543
+ const result = await apiRequestJson(`/integrations/gmail/accounts/${encodeURIComponent(id)}`, {
544
+ method: 'PATCH',
545
+ token,
546
+ body: { voice_md: voiceMd },
547
+ });
548
+
549
+ if (!result.ok) {
550
+ console.error(`could not ${options.clear ? 'clear' : 'save'} gmail voice: ${result.error || 'request failed'}`);
551
+ process.exit(1);
552
+ }
553
+
554
+ console.log(`voice ${options.clear ? 'cleared' : 'saved'} for ${name}`);
555
+ }
556
+
314
557
  function openGmailAuthUrl(authUrl, deps = {}) {
315
558
  const run = deps.spawnSync || spawnSync;
316
559
  const platform = deps.platform || process.platform;
@@ -457,6 +700,21 @@ async function gmailCommand(subcommand, ...args) {
457
700
  await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
458
701
  break;
459
702
  }
703
+ case 'verdicts': {
704
+ const parsed = parseGmailVerdictsArgs(args);
705
+ printGmailVerdicts(parsed);
706
+ break;
707
+ }
708
+ case 'send': {
709
+ const parsed = parseGmailSendArgs(args);
710
+ await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined });
711
+ break;
712
+ }
713
+ case 'voice': {
714
+ const parsed = parseGmailVoiceArgs(args);
715
+ await gmailVoice(parsed.accountId, { clear: parsed.clear });
716
+ break;
717
+ }
460
718
  case 'connect':
461
719
  await gmailConnect(args[0]);
462
720
  break;
@@ -471,6 +729,9 @@ async function gmailCommand(subcommand, ...args) {
471
729
  console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
472
730
  console.log(' atris gmail read <id> [--account <id>] - read specific email');
473
731
  console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
732
+ console.log(' atris gmail verdicts [--account <id>] [--limit N] - list recent archive verdicts');
733
+ console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
734
+ console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
474
735
  console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
475
736
  console.log(' atris gmail accounts - list connected gmail accounts and the active account');
476
737
  console.log(' atris gmail use [<account_id|name>] - choose or set the active gmail account');
@@ -1983,10 +2244,15 @@ async function integrationsStatus(args = []) {
1983
2244
  module.exports = {
1984
2245
  gmailCommand,
1985
2246
  gmailConnect,
2247
+ gmailVoice,
1986
2248
  gmailUse,
1987
2249
  extractGmailMailboxAccount,
1988
2250
  parseGmailArgs,
1989
2251
  gmailAccountStatePath,
2252
+ gmailVerdictsPath,
2253
+ appendGmailVerdicts,
2254
+ readGmailVerdicts,
2255
+ printGmailVerdicts,
1990
2256
  calendarCommand,
1991
2257
  twitterCommand,
1992
2258
  slackCommand,
package/commands/land.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { runGit } = require('../lib/git-spawn');
4
+ const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
4
5
  const { listWorktrees, statusCounts } = require('./worktree');
5
6
 
6
7
  const DEFAULT_TTL_DAYS = 7;
@@ -236,14 +237,18 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
236
237
  // merged branch residue.
237
238
  function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
238
239
  const root = repoRoot(cwd);
239
- if (!root) return null;
240
- const board = collectBoard(root, { ttlDays, light: true });
241
- return {
242
- branches: board.summary.active + board.summary.due,
243
- due: board.summary.due,
244
- stale: board.summary.stale,
245
- ttlDays,
246
- };
240
+ if (!root || !hasCommits(root)) return null;
241
+ try {
242
+ const board = collectBoard(root, { ttlDays, light: true });
243
+ return {
244
+ branches: board.summary.active + board.summary.due,
245
+ due: board.summary.due,
246
+ stale: board.summary.stale,
247
+ ttlDays,
248
+ };
249
+ } catch {
250
+ return null;
251
+ }
247
252
  }
248
253
 
249
254
  function salvageDir(root) {
@@ -636,6 +641,40 @@ function readFollowingFlag(args, name, fallback = '') {
636
641
  return args[idx + 1] || fallback;
637
642
  }
638
643
 
644
+ const EMPTY_LAND = {
645
+ not_a_git_repo: 'this folder is not a git repo yet, so there is nothing to land.',
646
+ no_commits: 'nothing is in the air yet because this folder has no commits.',
647
+ no_base: 'no master or main branch yet, so there is nothing to land.',
648
+ };
649
+
650
+ function emptyLandKindFromError(err) {
651
+ if (err && err.code === 'LAND_NO_BASE') return 'no_base';
652
+ const msg = String(err && err.message || err || '');
653
+ if (/not a git/i.test(msg)) return 'not_a_git_repo';
654
+ if (/no commits/i.test(msg)) return 'no_commits';
655
+ if (/no master\/main/i.test(msg)) return 'no_base';
656
+ return '';
657
+ }
658
+
659
+ function printEmptyLand(kind, json) {
660
+ const reason = EMPTY_LAND[kind] ? kind : 'no_base';
661
+ const detail = EMPTY_LAND[reason];
662
+ if (json) {
663
+ const payload = compactErrorPayload({ reason, detail });
664
+ printCliJson(payload, payload, ['--json']);
665
+ } else {
666
+ console.log(detail);
667
+ }
668
+ return 2;
669
+ }
670
+
671
+ function finishLandError(err, json) {
672
+ const kind = emptyLandKindFromError(err);
673
+ if (kind) return printEmptyLand(kind, json);
674
+ console.error(String(err && err.message || err).split('\n')[0]);
675
+ return 2;
676
+ }
677
+
639
678
  function showHelp() {
640
679
  console.log('');
641
680
  console.log('atris land: the landing, what is actually done vs still in the air');
@@ -666,20 +705,14 @@ function landCommand(args = []) {
666
705
  showHelp();
667
706
  return 0;
668
707
  }
708
+ const json = args.includes('--json');
669
709
  const root = repoRoot();
670
- if (!root) {
671
- console.error('not a git repository. fix: git init -b main');
672
- return 1;
673
- }
674
- if (!hasCommits(root)) {
675
- console.error('no commits yet. fix: git commit --allow-empty -m init');
676
- return 1;
677
- }
710
+ if (!root) return printEmptyLand('not_a_git_repo', json);
711
+ if (!hasCommits(root)) return printEmptyLand('no_commits', json);
678
712
  const ttlRaw = readFollowingFlag(args, '--ttl', '');
679
713
  const ttlParsed = Number(ttlRaw);
680
714
  const ttlDays = ttlRaw !== '' && Number.isFinite(ttlParsed) && ttlParsed >= 0 ? ttlParsed : DEFAULT_TTL_DAYS;
681
715
  const base = readFollowingFlag(args, '--base', '');
682
- const json = args.includes('--json');
683
716
 
684
717
  if (args.includes('--reap')) {
685
718
  try {
@@ -688,8 +721,7 @@ function landCommand(args = []) {
688
721
  else printReceipt(receipt);
689
722
  return 0;
690
723
  } catch (err) {
691
- console.error(String(err && err.message || err));
692
- return 1;
724
+ return finishLandError(err, json);
693
725
  }
694
726
  }
695
727
 
@@ -706,8 +738,7 @@ function landCommand(args = []) {
706
738
  else printStory(story);
707
739
  return 0;
708
740
  } catch (err) {
709
- console.error(String(err && err.message || err));
710
- return 1;
741
+ return finishLandError(err, json);
711
742
  }
712
743
  }
713
744
 
@@ -717,8 +748,7 @@ function landCommand(args = []) {
717
748
  else printBoard(board);
718
749
  return 0;
719
750
  } catch (err) {
720
- console.error(String(err && err.message || err));
721
- return 1;
751
+ return finishLandError(err, json);
722
752
  }
723
753
  }
724
754
 
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const { argsWantHelp } = require('../lib/noninteractive');
4
+ const {
5
+ laterNotePath,
6
+ listUserVisibleWork,
7
+ personName,
8
+ renderLaterRemember,
9
+ spokenLaterNote,
10
+ writeLaterNote,
11
+ } = require('../lib/first-minute');
12
+
13
+ function laterUsage() {
14
+ return 'Usage: atris later "<sentence>"';
15
+ }
16
+
17
+ function laterSentence(args = []) {
18
+ return spokenLaterNote((Array.isArray(args) ? args : [])
19
+ .filter((arg) => !String(arg).startsWith('-'))
20
+ .join(' '));
21
+ }
22
+
23
+ function laterAtris(args = process.argv.slice(3), {
24
+ root = process.cwd(),
25
+ log = console.log,
26
+ } = {}) {
27
+ const list = Array.isArray(args) ? args : [];
28
+ if (argsWantHelp(list)) {
29
+ log(laterUsage());
30
+ return 0;
31
+ }
32
+ const sentence = laterSentence(list);
33
+ if (!sentence) {
34
+ log(laterUsage());
35
+ return 2;
36
+ }
37
+ writeLaterNote(root, sentence);
38
+ const files = listUserVisibleWork(root);
39
+ log('');
40
+ log(renderLaterRemember({
41
+ person: personName(),
42
+ sentence,
43
+ files,
44
+ root,
45
+ }));
46
+ return 0;
47
+ }
48
+
49
+ module.exports = {
50
+ laterAtris,
51
+ laterNotePath,
52
+ };