atris 3.72.4 → 3.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/atris/skills/commerce/SKILL.md +24 -3
- package/bin/atris.js +10 -0
- package/commands/gm.js +17 -6
- package/commands/member.js +59 -9
- package/commands/pay.js +329 -0
- package/commands/task.js +55 -18
- package/commands/trust.js +117 -0
- package/commands/zen.js +17 -0
- package/lib/gm-bet.js +1443 -162
- package/lib/gm-phone-page.js +460 -357
- package/lib/gm-phone.js +74 -13
- package/lib/known-commands.js +1 -1
- package/lib/site.js +9 -1
- package/lib/try-repo.js +442 -0
- package/lib/zen-apply.js +28 -4
- package/lib/zen-fleet.js +100 -0
- package/lib/zen.js +41 -6
- package/package.json +1 -1
- package/scripts/det/xsearch-bench.js +1 -0
package/lib/zen-apply.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
checkZen,
|
|
16
16
|
defaultBranch,
|
|
17
17
|
dirtyEntries,
|
|
18
|
+
dirtyFiles,
|
|
18
19
|
FRESH_MINUTES,
|
|
19
20
|
gitOut,
|
|
20
21
|
headMovedSince,
|
|
@@ -188,8 +189,9 @@ function saveStrayEdits(root, teamState, tag, cutoff) {
|
|
|
188
189
|
for (const entry of dirtyEntries(root)) {
|
|
189
190
|
if (isTeamState(entry.path, teamState)) continue;
|
|
190
191
|
const paths = entry.xy === '??' && entry.path.endsWith('/')
|
|
191
|
-
? untrackedFiles(root, entry.path)
|
|
192
|
+
? untrackedFiles(root, entry.path).filter((p) => !isTeamState(p, teamState))
|
|
192
193
|
: [entry.path, entry.orig].filter(Boolean);
|
|
194
|
+
if (!paths.length) continue;
|
|
193
195
|
const isFresh = paths.some((p) => mtime(path.join(root, p)) > cutoff);
|
|
194
196
|
for (const p of paths) (isFresh ? fresh : files).add(p);
|
|
195
197
|
}
|
|
@@ -365,7 +367,15 @@ function moveKeepingNotes(root, args, target, tag, from = 'HEAD') {
|
|
|
365
367
|
.flatMap((e) => (e.path.endsWith('/') ? untrackedFiles(root, e.path) : [e.path]))
|
|
366
368
|
.filter((p) => changed.has(p))
|
|
367
369
|
.concat(added);
|
|
368
|
-
|
|
370
|
+
// A note you moved with git mv whose old path changed upstream: the old
|
|
371
|
+
// path goes back to HEAD so the move can run, and the new edits are merged
|
|
372
|
+
// into your moved copy afterwards.
|
|
373
|
+
const renamed = dirty.filter((e) => e.xy[0] === 'R' && e.orig && changed.has(e.orig) && !changed.has(e.path));
|
|
374
|
+
for (const e of renamed) {
|
|
375
|
+
runGit(['reset', '-q', '--', e.orig, e.path], root);
|
|
376
|
+
runGit(['checkout', '-q', 'HEAD', '--', e.orig], root);
|
|
377
|
+
}
|
|
378
|
+
if (!tracked.length && !untracked.length && !renamed.length) return { ok: false, error: firstLine(first.stderr) };
|
|
369
379
|
|
|
370
380
|
const dir = path.join(commonDir(root), 'atris-zen', tag);
|
|
371
381
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -423,6 +433,16 @@ function moveKeepingNotes(root, args, target, tag, from = 'HEAD') {
|
|
|
423
433
|
fs.copyFileSync(saved, path.join(root, p));
|
|
424
434
|
}
|
|
425
435
|
}
|
|
436
|
+
for (const e of renamed) {
|
|
437
|
+
const old = path.join(root, e.orig);
|
|
438
|
+
if (second.status !== 0 || !fs.existsSync(old)) continue;
|
|
439
|
+
if (fromCommit && followMove(root, fromCommit, e.orig, e.path, old)) {
|
|
440
|
+
fs.rmSync(old, { force: true });
|
|
441
|
+
notes.push(`${e.orig} changed upstream after you moved it to ${e.path}; the new edits went with it`);
|
|
442
|
+
} else {
|
|
443
|
+
notes.push(`${e.orig} changed upstream after you moved it to ${e.path}; the new version is back at ${e.orig}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
426
446
|
if (second.status !== 0) return { ok: false, error: firstLine(second.stderr), notes, tracked, landed };
|
|
427
447
|
return { ok: true, notes, tracked, landed };
|
|
428
448
|
}
|
|
@@ -522,7 +542,7 @@ function applyZen({ cwd = process.cwd(), fetch = true, freshMinutes, abandonedDa
|
|
|
522
542
|
|
|
523
543
|
let branch = gitOut(['symbolic-ref', '--quiet', '--short', 'HEAD'], root);
|
|
524
544
|
if (main && branch !== main) {
|
|
525
|
-
const stillStray =
|
|
545
|
+
const stillStray = dirtyFiles(root).some((p) => !isTeamState(p, teamState));
|
|
526
546
|
if (stillStray || homeInUse) {
|
|
527
547
|
left.push(`stayed on ${branch || 'a detached commit'}, because it was used in ${window}`);
|
|
528
548
|
} else {
|
|
@@ -536,8 +556,12 @@ function applyZen({ cwd = process.cwd(), fetch = true, freshMinutes, abandonedDa
|
|
|
536
556
|
const base = gitOut(['merge-base', 'HEAD', main], root);
|
|
537
557
|
const moved = moveKeepingNotes(root, ['checkout', '-q', main], main, tag, base || 'HEAD');
|
|
538
558
|
notes.push(...(moved.notes || []));
|
|
559
|
+
// A branch with nothing beyond origin is cleared as finished below, so
|
|
560
|
+
// only a branch with its own commits is called kept.
|
|
561
|
+
const own = branch && before.upstream ? gitOut(['rev-list', '--count', `${before.upstream}..${branch}`], root) : null;
|
|
539
562
|
if (moved.ok) {
|
|
540
|
-
|
|
563
|
+
const why = !branch ? '' : own === '0' ? ` (${branch} had nothing new)` : ` (${branch} is kept)`;
|
|
564
|
+
done.push(`moved back to ${main}${why}`);
|
|
541
565
|
branch = main;
|
|
542
566
|
} else {
|
|
543
567
|
left.push(`could not move to ${main}: ${moved.error}`);
|
package/lib/zen-fleet.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Zen for every repo you keep, in one run. The list comes from the command
|
|
4
|
+
// line or ~/.atris/zen.json ({"repos": ["~/code/app", ...]}). A repo whose
|
|
5
|
+
// unsaved edits touch live app data or secrets is skipped whole, because a
|
|
6
|
+
// person should look at it first. Each repo gets one line, then one shared
|
|
7
|
+
// list of what only a person can do.
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { DEFAULT_TEAM_STATE, dirtyEntries, untrackedFiles } = require('./zen');
|
|
13
|
+
const { applyZen } = require('./zen-apply');
|
|
14
|
+
|
|
15
|
+
const LIVE_DATA = DEFAULT_TEAM_STATE.filter((p) => p.startsWith('*.')).map((p) => p.slice(1));
|
|
16
|
+
const SECRET = /(^|\/)(\.env(\..+)?|id_rsa[^/]*|[^/]*\.(pem|key|p12)|[^/]*(secret|credential)[^/]*)$/i;
|
|
17
|
+
|
|
18
|
+
function fleetFile() {
|
|
19
|
+
return process.env.ATRIS_ZEN_FLEET_FILE || path.join(os.homedir(), '.atris', 'zen.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function expand(p) {
|
|
23
|
+
return path.resolve(p.replace(/^~(?=$|\/)/, os.homedir()));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fleetRepos(paths = []) {
|
|
27
|
+
if (paths.length) return paths.map(expand);
|
|
28
|
+
try {
|
|
29
|
+
const listed = JSON.parse(fs.readFileSync(fleetFile(), 'utf8')).repos;
|
|
30
|
+
return Array.isArray(listed) ? listed.filter((p) => typeof p === 'string').map(expand) : [];
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Unsaved edits that look like live app data or secrets.
|
|
37
|
+
function riskyEdits(root) {
|
|
38
|
+
return dirtyEntries(root)
|
|
39
|
+
.flatMap((e) => (e.xy === '??' && e.path.endsWith('/') ? untrackedFiles(root, e.path) : [e.path]))
|
|
40
|
+
.filter((p) => LIVE_DATA.some((ext) => p.endsWith(ext)) || SECRET.test(p));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function home(p) {
|
|
44
|
+
const dir = os.homedir();
|
|
45
|
+
return p === dir || p.startsWith(dir + path.sep) ? `~${p.slice(dir.length)}` : p;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function zenOne(repo, fetch) {
|
|
49
|
+
const name = path.basename(repo);
|
|
50
|
+
if (!fs.existsSync(repo)) return { repo, name, state: 'missing', text: `not found at ${home(repo)}`, yours: [] };
|
|
51
|
+
const risky = riskyEdits(repo);
|
|
52
|
+
if (risky.length) {
|
|
53
|
+
const shown = risky.slice(0, 3).join(', ') + (risky.length > 3 ? ` and ${risky.length - 3} more` : '');
|
|
54
|
+
return { repo, name, state: 'skipped', text: `skipped, unsaved live data or secrets: ${shown}`, yours: [] };
|
|
55
|
+
}
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = applyZen({ cwd: repo, fetch });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { repo, name, state: 'failed', text: `zen stopped: ${String(err.message || err).split('\n')[0]}`, yours: [] };
|
|
61
|
+
}
|
|
62
|
+
if (!result.ok) return { repo, name, state: 'failed', text: `can't start, ${result.error}`, yours: [] };
|
|
63
|
+
const did = [...result.done, ...(result.workspace?.done || [])];
|
|
64
|
+
let text = result.already ? 'already zen' : did.join(', ') || 'zen';
|
|
65
|
+
if (!result.zen) text += `; still out of place: ${result.remaining.join(', ')}`;
|
|
66
|
+
// Where your copy went and what was left alone matter as much as what was
|
|
67
|
+
// done, so they ride along under the repo line. Side copy paths stay out to
|
|
68
|
+
// keep the pane small; plain atris zen in that repo lists them.
|
|
69
|
+
const more = [...result.notes, ...result.left.filter((line) => !/^\s/.test(line))];
|
|
70
|
+
return { repo, name, state: result.zen ? 'zen' : 'almost', text, more, yours: result.workspace?.yours || [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fleetZen({ repos, fetch = true } = {}) {
|
|
74
|
+
return repos.map((repo) => zenOne(repo, fetch));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printFleet(results) {
|
|
78
|
+
const width = Math.max(...results.map((r) => r.name.length));
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(' atris zen, every repo');
|
|
81
|
+
console.log('');
|
|
82
|
+
for (const r of results) {
|
|
83
|
+
console.log(` ${r.name.padEnd(width)} ${r.text}`);
|
|
84
|
+
for (const line of r.more || []) console.log(` ${''.padEnd(width)} ${line}`);
|
|
85
|
+
}
|
|
86
|
+
const yours = results.flatMap((r) => r.yours.map((item) => ({ text: `${r.name}: ${item.text}`, cmd: item.cmd })));
|
|
87
|
+
if (yours.length) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(' for you');
|
|
90
|
+
const w = Math.max(...yours.map((item) => item.text.length));
|
|
91
|
+
for (const item of yours) console.log(` ${item.text.padEnd(w)} ${item.cmd}`);
|
|
92
|
+
}
|
|
93
|
+
const calm = results.filter((r) => r.state === 'zen').length;
|
|
94
|
+
const skipped = results.filter((r) => r.state === 'skipped').length;
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(` ${calm} of ${results.length} repos zen${skipped ? `, ${skipped} skipped for you to look at` : ''}.`);
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { fleetFile, fleetRepos, fleetZen, printFleet, riskyEdits };
|
package/lib/zen.js
CHANGED
|
@@ -91,6 +91,14 @@ function dirtyPaths(root) {
|
|
|
91
91
|
return dirtyEntries(root).map((entry) => entry.path);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Changed files with untracked folders opened up, so a live database inside
|
|
95
|
+
// a new folder is seen as live data, not as part of a stray folder.
|
|
96
|
+
function dirtyFiles(root) {
|
|
97
|
+
return dirtyEntries(root).flatMap((entry) => (entry.xy === '??' && entry.path.endsWith('/')
|
|
98
|
+
? untrackedFiles(root, entry.path)
|
|
99
|
+
: [entry.path]));
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
function listWorktrees(root) {
|
|
95
103
|
const out = gitOut(['worktree', 'list', '--porcelain'], root) || '';
|
|
96
104
|
const worktrees = [];
|
|
@@ -111,14 +119,41 @@ function listWorktrees(root) {
|
|
|
111
119
|
return worktrees;
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
function expandHome(text) {
|
|
123
|
+
const home = os.homedir();
|
|
124
|
+
return text.replace(/\$\{?HOME\}?/g, home).replace(/(^|[\s"'=:])~\//g, `$1${home}/`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A job often runs a script that names the side copy, not the copy itself,
|
|
128
|
+
// so the scripts a schedule runs are read too, one level down. Logs are left
|
|
129
|
+
// out, and so is anything large or binary.
|
|
130
|
+
function withScripts(text) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const parts = [text];
|
|
133
|
+
for (const [file] of expandHome(text).matchAll(/\/[^\s"'<>;|&()]+/g)) {
|
|
134
|
+
if (seen.has(file) || file.endsWith('.log')) continue;
|
|
135
|
+
seen.add(file);
|
|
136
|
+
try {
|
|
137
|
+
const stat = fs.statSync(file);
|
|
138
|
+
if (!stat.isFile() || stat.size > 256 * 1024) continue;
|
|
139
|
+
const body = fs.readFileSync(file, 'utf8');
|
|
140
|
+
if (!body.includes('\0')) parts.push(body);
|
|
141
|
+
} catch {
|
|
142
|
+
// Not a readable file.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return parts.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
// Text of the jobs this machine runs on a schedule: the user crontab and
|
|
115
|
-
// launchd agents. A side copy named there is in
|
|
149
|
+
// launchd agents, plus the scripts they run. A side copy named there is in
|
|
150
|
+
// use even when it looks idle.
|
|
116
151
|
// ATRIS_ZEN_SCHEDULE_FILE points at a file to read instead (tests).
|
|
117
152
|
function scheduleText() {
|
|
118
153
|
const override = process.env.ATRIS_ZEN_SCHEDULE_FILE;
|
|
119
154
|
if (override) {
|
|
120
155
|
try {
|
|
121
|
-
return fs.readFileSync(override, 'utf8');
|
|
156
|
+
return withScripts(fs.readFileSync(override, 'utf8'));
|
|
122
157
|
} catch {
|
|
123
158
|
return '';
|
|
124
159
|
}
|
|
@@ -134,13 +169,12 @@ function scheduleText() {
|
|
|
134
169
|
} catch {
|
|
135
170
|
// No launchd agents.
|
|
136
171
|
}
|
|
137
|
-
return parts.join('\n');
|
|
172
|
+
return withScripts(parts.join('\n'));
|
|
138
173
|
}
|
|
139
174
|
|
|
140
175
|
function scheduledCopies(worktrees, text = scheduleText()) {
|
|
141
176
|
if (!text) return new Set();
|
|
142
|
-
const
|
|
143
|
-
const expanded = text.replace(/\$\{?HOME\}?/g, home).replace(/(^|[\s"'=:])~\//g, `$1${home}/`);
|
|
177
|
+
const expanded = expandHome(text);
|
|
144
178
|
const used = new Set();
|
|
145
179
|
for (const wt of worktrees) {
|
|
146
180
|
const at = path.resolve(wt.path);
|
|
@@ -279,7 +313,7 @@ function checkZen({ cwd = process.cwd(), fetch = true, freshMinutes = freshMinut
|
|
|
279
313
|
}
|
|
280
314
|
|
|
281
315
|
const teamState = readTeamState(root);
|
|
282
|
-
const dirty =
|
|
316
|
+
const dirty = dirtyFiles(root);
|
|
283
317
|
const stray = dirty.filter((file) => !isTeamState(file, teamState));
|
|
284
318
|
const teamFiles = dirty.length - stray.length;
|
|
285
319
|
if (stray.length) {
|
|
@@ -403,6 +437,7 @@ module.exports = {
|
|
|
403
437
|
dirtyEntries,
|
|
404
438
|
gitOut,
|
|
405
439
|
isTeamState,
|
|
440
|
+
dirtyFiles,
|
|
406
441
|
listWorktrees,
|
|
407
442
|
readTeamState,
|
|
408
443
|
scheduledCopies,
|
package/package.json
CHANGED