atris 3.72.5 → 3.73.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.
- package/README.md +2 -0
- package/atris/skills/commerce/SKILL.md +24 -3
- package/bin/atris.js +10 -0
- package/commands/gm.js +16 -5
- package/commands/member.js +59 -9
- package/commands/pay.js +329 -0
- 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 +58 -7
- package/lib/known-commands.js +1 -1
- package/lib/try-repo.js +442 -0
- package/lib/zen-apply.js +23 -3
- package/lib/zen-fleet.js +100 -0
- package/lib/zen-workspace.js +6 -1
- package/lib/zen.js +50 -6
- package/package.json +2 -1
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, LIVE_LOGS, dirtyEntries, untrackedFiles } = require('./zen');
|
|
13
|
+
const { applyZen } = require('./zen-apply');
|
|
14
|
+
|
|
15
|
+
const LIVE_DATA = DEFAULT_TEAM_STATE.filter((p) => p.startsWith('*.') && !LIVE_LOGS.includes(p)).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-workspace.js
CHANGED
|
@@ -73,10 +73,15 @@ function workspaceZen({ root, dryRun = false } = {}) {
|
|
|
73
73
|
|
|
74
74
|
const unhealable = r.map_refs?.unhealable_count || 0;
|
|
75
75
|
if (unhealable) out.yours.push({ text: `${plural(unhealable, 'map link points', 'map links point')} nowhere`, cmd: 'atris clean' });
|
|
76
|
+
// These are notes (often feature docs, not wiki pages) written about code
|
|
77
|
+
// that has changed since. Rewriting one takes judgment, so it is yours; name
|
|
78
|
+
// the note when there is only one so the line says exactly what to open.
|
|
76
79
|
const pages = r.stale_pages?.items || [];
|
|
77
80
|
if (pages.length) {
|
|
78
81
|
out.yours.push({
|
|
79
|
-
text:
|
|
82
|
+
text: pages.length === 1
|
|
83
|
+
? `${pages[0].page} is older than the code it describes`
|
|
84
|
+
: `${pages.length} notes are older than the code they describe`,
|
|
80
85
|
cmd: 'atris clean --dry-run',
|
|
81
86
|
});
|
|
82
87
|
}
|
package/lib/zen.js
CHANGED
|
@@ -28,8 +28,16 @@ const DEFAULT_TEAM_STATE = [
|
|
|
28
28
|
'*.sqlite',
|
|
29
29
|
'*.sqlite3',
|
|
30
30
|
'*.duckdb',
|
|
31
|
+
// Append-only logs and ledgers a running app keeps writing. Left in place
|
|
32
|
+
// like a database, but a repo is not skipped for one (see LIVE_LOGS).
|
|
33
|
+
'*.jsonl',
|
|
34
|
+
'*.ndjson',
|
|
31
35
|
];
|
|
32
36
|
|
|
37
|
+
// Growing logs are everywhere in team state, so zen --all does not hold a
|
|
38
|
+
// whole repo back for them the way it does for an unsaved database.
|
|
39
|
+
const LIVE_LOGS = ['*.jsonl', '*.ndjson'];
|
|
40
|
+
|
|
33
41
|
function gitOut(args, cwd) {
|
|
34
42
|
const result = runGit(args, cwd);
|
|
35
43
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
@@ -91,6 +99,14 @@ function dirtyPaths(root) {
|
|
|
91
99
|
return dirtyEntries(root).map((entry) => entry.path);
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
// Changed files with untracked folders opened up, so a live database inside
|
|
103
|
+
// a new folder is seen as live data, not as part of a stray folder.
|
|
104
|
+
function dirtyFiles(root) {
|
|
105
|
+
return dirtyEntries(root).flatMap((entry) => (entry.xy === '??' && entry.path.endsWith('/')
|
|
106
|
+
? untrackedFiles(root, entry.path)
|
|
107
|
+
: [entry.path]));
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
function listWorktrees(root) {
|
|
95
111
|
const out = gitOut(['worktree', 'list', '--porcelain'], root) || '';
|
|
96
112
|
const worktrees = [];
|
|
@@ -111,14 +127,41 @@ function listWorktrees(root) {
|
|
|
111
127
|
return worktrees;
|
|
112
128
|
}
|
|
113
129
|
|
|
130
|
+
function expandHome(text) {
|
|
131
|
+
const home = os.homedir();
|
|
132
|
+
return text.replace(/\$\{?HOME\}?/g, home).replace(/(^|[\s"'=:])~\//g, `$1${home}/`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A job often runs a script that names the side copy, not the copy itself,
|
|
136
|
+
// so the scripts a schedule runs are read too, one level down. Logs are left
|
|
137
|
+
// out, and so is anything large or binary.
|
|
138
|
+
function withScripts(text) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const parts = [text];
|
|
141
|
+
for (const [file] of expandHome(text).matchAll(/\/[^\s"'<>;|&()]+/g)) {
|
|
142
|
+
if (seen.has(file) || file.endsWith('.log')) continue;
|
|
143
|
+
seen.add(file);
|
|
144
|
+
try {
|
|
145
|
+
const stat = fs.statSync(file);
|
|
146
|
+
if (!stat.isFile() || stat.size > 256 * 1024) continue;
|
|
147
|
+
const body = fs.readFileSync(file, 'utf8');
|
|
148
|
+
if (!body.includes('\0')) parts.push(body);
|
|
149
|
+
} catch {
|
|
150
|
+
// Not a readable file.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return parts.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
114
156
|
// Text of the jobs this machine runs on a schedule: the user crontab and
|
|
115
|
-
// launchd agents. A side copy named there is in
|
|
157
|
+
// launchd agents, plus the scripts they run. A side copy named there is in
|
|
158
|
+
// use even when it looks idle.
|
|
116
159
|
// ATRIS_ZEN_SCHEDULE_FILE points at a file to read instead (tests).
|
|
117
160
|
function scheduleText() {
|
|
118
161
|
const override = process.env.ATRIS_ZEN_SCHEDULE_FILE;
|
|
119
162
|
if (override) {
|
|
120
163
|
try {
|
|
121
|
-
return fs.readFileSync(override, 'utf8');
|
|
164
|
+
return withScripts(fs.readFileSync(override, 'utf8'));
|
|
122
165
|
} catch {
|
|
123
166
|
return '';
|
|
124
167
|
}
|
|
@@ -134,13 +177,12 @@ function scheduleText() {
|
|
|
134
177
|
} catch {
|
|
135
178
|
// No launchd agents.
|
|
136
179
|
}
|
|
137
|
-
return parts.join('\n');
|
|
180
|
+
return withScripts(parts.join('\n'));
|
|
138
181
|
}
|
|
139
182
|
|
|
140
183
|
function scheduledCopies(worktrees, text = scheduleText()) {
|
|
141
184
|
if (!text) return new Set();
|
|
142
|
-
const
|
|
143
|
-
const expanded = text.replace(/\$\{?HOME\}?/g, home).replace(/(^|[\s"'=:])~\//g, `$1${home}/`);
|
|
185
|
+
const expanded = expandHome(text);
|
|
144
186
|
const used = new Set();
|
|
145
187
|
for (const wt of worktrees) {
|
|
146
188
|
const at = path.resolve(wt.path);
|
|
@@ -279,7 +321,7 @@ function checkZen({ cwd = process.cwd(), fetch = true, freshMinutes = freshMinut
|
|
|
279
321
|
}
|
|
280
322
|
|
|
281
323
|
const teamState = readTeamState(root);
|
|
282
|
-
const dirty =
|
|
324
|
+
const dirty = dirtyFiles(root);
|
|
283
325
|
const stray = dirty.filter((file) => !isTeamState(file, teamState));
|
|
284
326
|
const teamFiles = dirty.length - stray.length;
|
|
285
327
|
if (stray.length) {
|
|
@@ -403,6 +445,8 @@ module.exports = {
|
|
|
403
445
|
dirtyEntries,
|
|
404
446
|
gitOut,
|
|
405
447
|
isTeamState,
|
|
448
|
+
LIVE_LOGS,
|
|
449
|
+
dirtyFiles,
|
|
406
450
|
listWorktrees,
|
|
407
451
|
readTeamState,
|
|
408
452
|
scheduledCopies,
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.73.1",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
|
+
"mcpName": "io.github.atrislabs/atris",
|
|
5
6
|
"main": "bin/atris.js",
|
|
6
7
|
"bin": {
|
|
7
8
|
"atris": "bin/atris",
|