atris 3.24.0 → 3.25.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 +6 -6
- package/atris/atrisDev.md +717 -0
- package/atris/policies/outbound-artifact-gate.md +48 -0
- package/atris/skills/atris-feedback/SKILL.md +2 -3
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +6 -9
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +4 -5
- package/atris.md +19 -43
- package/ax +1695 -101
- package/bin/atris.js +2 -38
- package/commands/aeo.js +5 -5
- package/commands/computer.js +0 -1
- package/commands/mission.js +2 -1
- package/commands/recap.js +0 -16
- package/commands/sync.js +2 -0
- package/commands/workflow.js +1 -2
- package/commands/youtube.js +183 -0
- package/lib/ax-chat-input.js +164 -0
- package/lib/ax-goal.js +307 -0
- package/lib/ax-prefs.js +70 -0
- package/lib/ax-shimmer.js +63 -0
- package/lib/context-gatherer.js +8 -26
- package/package.json +2 -1
- package/commands/card.js +0 -121
- package/commands/deck.js +0 -184
- package/commands/reel.js +0 -128
- package/commands/site.js +0 -48
- package/commands/slop.js +0 -307
- package/commands/theme.js +0 -217
- package/lib/card.js +0 -120
- package/lib/deck-from-md.js +0 -110
- package/lib/html-render.js +0 -257
- package/lib/memory-view.js +0 -95
- package/lib/reel.js +0 -52
- package/lib/site.js +0 -114
- package/lib/slides-deck.js +0 -237
- package/lib/theme.js +0 -264
package/commands/deck.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// atris deck — generate a premium, on-brand Google Slides deck from a plain
|
|
2
|
-
// content spec, using the Atris deck engine (lib/slides-deck.js). The pitch:
|
|
3
|
-
// describe the deck, get the design system for free. No Arial-on-white slop.
|
|
4
|
-
//
|
|
5
|
-
// Usage:
|
|
6
|
-
// atris deck themes list design themes
|
|
7
|
-
// atris deck build <spec.json> [--title T] [--theme terminal|paper] [--update ID]
|
|
8
|
-
// atris deck sample [--theme paper] print a starter spec to stdout
|
|
9
|
-
//
|
|
10
|
-
// A spec is JSON: { theme, brand:{name,accent}, slides:[ {type,...} ] }.
|
|
11
|
-
// Slide types: title, statement, columns, panel, chips, bignumber, close.
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const https = require('https');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
const { buildDeck, THEMES } = require('../lib/slides-deck');
|
|
17
|
-
const { parseMarkdownToSpec } = require('../lib/deck-from-md');
|
|
18
|
-
const { mergedThemes } = require('../lib/theme');
|
|
19
|
-
|
|
20
|
-
const BASE = 'api.atris.ai';
|
|
21
|
-
const PFX = '/api/integrations/google-slides';
|
|
22
|
-
|
|
23
|
-
function token() {
|
|
24
|
-
try { return require(os.homedir() + '/.atris/credentials.json').token; }
|
|
25
|
-
catch { return null; }
|
|
26
|
-
}
|
|
27
|
-
function api(method, path, body, tok) {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const data = body ? JSON.stringify(body) : null;
|
|
30
|
-
const req = https.request({ host: BASE, path: PFX + path, method,
|
|
31
|
-
headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json',
|
|
32
|
-
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
|
|
33
|
-
(res) => { let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => {
|
|
34
|
-
let j; try { j = JSON.parse(b); } catch { j = b; }
|
|
35
|
-
if (res.statusCode >= 300) reject(new Error('HTTP ' + res.statusCode + ': ' + (typeof j === 'string' ? j : JSON.stringify(j)).slice(0, 600)));
|
|
36
|
-
else resolve(j); }); });
|
|
37
|
-
req.on('error', reject); if (data) req.write(data); req.end();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const SAMPLE = {
|
|
42
|
-
theme: 'terminal',
|
|
43
|
-
brand: { name: 'Sentinel', accent: '.' },
|
|
44
|
-
slides: [
|
|
45
|
-
{ type: 'title', headline: 'Read your incidents in the **dark.**',
|
|
46
|
-
sub: "On-call shouldn't mean panic. Every alert ranked by blast radius, in one calm view.",
|
|
47
|
-
panel: { header: { title: 'Active incidents', meta: 'updated 12s ago' },
|
|
48
|
-
rows: [
|
|
49
|
-
{ title: 'Checkout latency spike', sub: 'api-gateway · us-east-1', value: '42%', valueSub: 'of traffic', sev: 0, active: true },
|
|
50
|
-
{ title: 'Stale read replica', sub: 'orders-db · eu-west-2', value: '8%', valueSub: 'of traffic', sev: 1 },
|
|
51
|
-
{ title: 'Elevated 4xx on search', sub: 'search-svc · global', value: '1.2%', valueSub: 'of traffic', sev: 2 },
|
|
52
|
-
], footer: { left: '3 active, 1 worth a page', right: 'View all' } } },
|
|
53
|
-
{ type: 'statement', text: "On-call shouldn't mean **panic.**",
|
|
54
|
-
sub: 'So the console is calm by default. One screen, ranked by real impact.' },
|
|
55
|
-
{ type: 'columns', heading: 'What makes it calm', columns: [
|
|
56
|
-
{ h: 'Ranked by impact', b: 'Severity comes from real blast radius, so the top of the list is the thing to fix.' },
|
|
57
|
-
{ h: 'Quiet by default', b: 'One page-worthy signal per incident. The rest stays in the log until you ask.' },
|
|
58
|
-
{ h: 'Built for 3am', b: 'High contrast, keyboard-first, and readable before you are fully awake.' } ] },
|
|
59
|
-
{ type: 'bignumber', number: '11 min', label: 'median time to first action', sub: 'down from 47 minutes before Sentinel.' },
|
|
60
|
-
{ type: 'close', tagline: 'Read your incidents in the dark.',
|
|
61
|
-
buttons: [{ label: 'Open the console', primary: true }, { label: 'Read the docs' }], footer: 'sentinel.sh · 2026' },
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// shared: spec -> live deck. Returns the URL.
|
|
66
|
-
async function publishDeck(spec, { title, updateId, tok }) {
|
|
67
|
-
const { requests } = buildDeck(spec, { themes: mergedThemes(THEMES) });
|
|
68
|
-
let id, firstSlide;
|
|
69
|
-
if (updateId) {
|
|
70
|
-
id = updateId;
|
|
71
|
-
const got = await api('GET', `/presentations/${id}`, null, tok);
|
|
72
|
-
const slides = got.slides || (got.presentation && got.presentation.slides) || [];
|
|
73
|
-
firstSlide = slides[0] && slides[0].objectId;
|
|
74
|
-
} else {
|
|
75
|
-
const pres = await api('POST', '/presentations', { title }, tok);
|
|
76
|
-
id = pres.presentationId || pres.id || (pres.presentation && pres.presentation.presentationId);
|
|
77
|
-
const slides = pres.slides || (pres.presentation && pres.presentation.slides) || [];
|
|
78
|
-
firstSlide = slides[0] && slides[0].objectId;
|
|
79
|
-
}
|
|
80
|
-
const reqs = firstSlide ? [...requests, { deleteObject: { objectId: firstSlide } }] : requests;
|
|
81
|
-
console.log(` building ${spec.slides.length} slides (${spec.theme}) · ${reqs.length} ops...`);
|
|
82
|
-
await api('POST', `/presentations/${id}/batch-update`, { requests: reqs }, tok);
|
|
83
|
-
return `https://docs.google.com/presentation/d/${id}/edit`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// beautiful HTML output (page or AppBlock JSON) from a content spec
|
|
87
|
-
function outputHtml(spec, argv, srcLabel) {
|
|
88
|
-
const { renderHtml, renderBlock, THEMES: HTML_THEMES } = require('../lib/html-render');
|
|
89
|
-
const themes = mergedThemes(HTML_THEMES);
|
|
90
|
-
if (!themes[spec.theme]) spec.theme = 'atris';
|
|
91
|
-
const title = flag(argv, '--title');
|
|
92
|
-
if (hasFlag(argv, '--block')) {
|
|
93
|
-
console.log(JSON.stringify(renderBlock(spec, { title, themes }), null, 2));
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
const html = renderHtml(spec, { title, themes });
|
|
97
|
-
const out = flag(argv, '--out');
|
|
98
|
-
if (out) { fs.writeFileSync(out, html); console.log(`\n ✓ html written: ${out}${srcLabel ? ` (from ${srcLabel})` : ''}\n`); }
|
|
99
|
-
else process.stdout.write(html + '\n');
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function run(argv) {
|
|
104
|
-
const sub = argv[0];
|
|
105
|
-
|
|
106
|
-
if (sub === 'from') {
|
|
107
|
-
const docPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
108
|
-
if (!docPath) { console.error(' usage: atris deck from <doc.md> [--theme x] [--brand Name] [--build] [--title T]'); return 2; }
|
|
109
|
-
let md;
|
|
110
|
-
try { md = fs.readFileSync(docPath, 'utf8'); }
|
|
111
|
-
catch (e) { console.error(` cannot read doc: ${e.message}`); return 2; }
|
|
112
|
-
const spec = parseMarkdownToSpec(md, { theme: flag(argv, '--theme'), brandName: flag(argv, '--brand') });
|
|
113
|
-
if (hasFlag(argv, '--html') || hasFlag(argv, '--block')) return outputHtml(spec, argv, docPath);
|
|
114
|
-
{ const dt = mergedThemes(THEMES); if (!dt[spec.theme]) { console.error(` unknown theme "${spec.theme}". try: ${Object.keys(dt).join(', ')}`); return 2; } }
|
|
115
|
-
if (!hasFlag(argv, '--build')) {
|
|
116
|
-
// default: print the spec so the PM can tweak before building
|
|
117
|
-
console.log(JSON.stringify(spec, null, 2));
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
const tok = token();
|
|
121
|
-
if (!tok) { console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.'); return 1; }
|
|
122
|
-
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
123
|
-
const url = await publishDeck(spec, { title, updateId: flag(argv, '--update'), tok });
|
|
124
|
-
console.log(`\n ✓ deck from ${docPath} ready: ${url}\n`);
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (sub === 'themes') {
|
|
129
|
-
console.log('\n atris deck themes:\n');
|
|
130
|
-
for (const [name, t] of Object.entries(THEMES)) {
|
|
131
|
-
console.log(` ${name.padEnd(10)} ${t.fonts.display} + ${t.fonts.body} · accent ${t.color.accent} bg ${t.color.bg}`);
|
|
132
|
-
}
|
|
133
|
-
console.log('\n slide types: title, statement, columns, panel, chips, bignumber, close\n');
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (sub === 'sample') {
|
|
138
|
-
const theme = flag(argv, '--theme') || 'terminal';
|
|
139
|
-
console.log(JSON.stringify({ ...SAMPLE, theme }, null, 2));
|
|
140
|
-
return 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (sub === 'build') {
|
|
144
|
-
const specPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
145
|
-
if (!specPath) { console.error(' usage: atris deck build <spec.json> [--title T] [--theme x] [--update ID]'); return 2; }
|
|
146
|
-
let spec;
|
|
147
|
-
try { spec = JSON.parse(fs.readFileSync(specPath, 'utf8')); }
|
|
148
|
-
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
149
|
-
const themeOverride = flag(argv, '--theme'); if (themeOverride) spec.theme = themeOverride;
|
|
150
|
-
if (hasFlag(argv, '--html') || hasFlag(argv, '--block')) return outputHtml(spec, argv, specPath);
|
|
151
|
-
{ const dt = mergedThemes(THEMES); if (!dt[spec.theme]) { console.error(` unknown theme "${spec.theme}". try: ${Object.keys(dt).join(', ')}`); return 2; } }
|
|
152
|
-
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
153
|
-
|
|
154
|
-
const tok = token();
|
|
155
|
-
if (!tok) { console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.'); return 1; }
|
|
156
|
-
|
|
157
|
-
const url = await publishDeck(spec, { title, updateId: flag(argv, '--update'), tok });
|
|
158
|
-
console.log(`\n ✓ deck ready: ${url}\n`);
|
|
159
|
-
return 0;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
console.log(`
|
|
163
|
-
atris deck — premium Google Slides from a plain content spec or a markdown doc
|
|
164
|
-
|
|
165
|
-
atris deck from doc.md [--build] [--title T] turn a markdown doc into a deck
|
|
166
|
-
atris deck from doc.md --html --out page.html beautiful HTML page (theme: atris|terminal|paper)
|
|
167
|
-
atris deck from doc.md --block emit the AppBlock JSON for a web app
|
|
168
|
-
atris deck sample [--theme paper] > my.json start from a sample spec
|
|
169
|
-
atris deck build my.json [--title "Q3 review"] create the deck, print the URL
|
|
170
|
-
atris deck build my.json --html --out p.html render the spec as HTML instead of slides
|
|
171
|
-
atris deck themes list design themes
|
|
172
|
-
|
|
173
|
-
'from' maps headings to slides (## with bullets -> columns, "**X** label" -> a
|
|
174
|
-
big number, Close -> a closing slide). Without --build it prints the spec to tweak.
|
|
175
|
-
Design system is baked in: distinctive fonts, one accent, real data panels, and
|
|
176
|
-
no AI tells (em dashes sanitized, sentence-case labels, no gradient text).
|
|
177
|
-
`);
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
|
|
182
|
-
function hasFlag(argv, name) { return argv.includes(name); }
|
|
183
|
-
|
|
184
|
-
module.exports = { run, SAMPLE, publishDeck };
|
package/commands/reel.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// atris reel — one line of text into a short, on-brand video (an animated card).
|
|
2
|
-
//
|
|
3
|
-
// atris reel "Ship faster" --theme brand --size square
|
|
4
|
-
// atris reel "It just works." --kind quote --by "a founder" --seconds 3
|
|
5
|
-
//
|
|
6
|
-
// Renders frames with headless Chrome (the same one `atris card` uses) and encodes
|
|
7
|
-
// with ffmpeg. No new dependency. Falls back to a card image if either is missing.
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const { execFile, spawnSync } = require('child_process');
|
|
13
|
-
const { promisify } = require('util');
|
|
14
|
-
const pexec = promisify(execFile);
|
|
15
|
-
const { buildReelFrame, reelFrames } = require('../lib/reel');
|
|
16
|
-
const { SIZES, KINDS } = require('../lib/card');
|
|
17
|
-
|
|
18
|
-
function parseFlags(argv) {
|
|
19
|
-
const flags = {}; const pos = [];
|
|
20
|
-
for (let i = 0; i < argv.length; i++) {
|
|
21
|
-
const a = argv[i];
|
|
22
|
-
if (a.startsWith('--')) {
|
|
23
|
-
const key = a.slice(2); const next = argv[i + 1];
|
|
24
|
-
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
pos.push(a);
|
|
28
|
-
}
|
|
29
|
-
return { flags, pos };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function findBin(macApps, names) {
|
|
33
|
-
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
36
|
-
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
const findChrome = () => (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH))
|
|
41
|
-
? process.env.CHROME_PATH
|
|
42
|
-
: findBin([
|
|
43
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
44
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
45
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
46
|
-
], ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']);
|
|
47
|
-
const findFfmpeg = () => findBin(['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg'], ['ffmpeg']);
|
|
48
|
-
|
|
49
|
-
async function renderFrames(chrome, ts, spec, dir, w, h, onDone) {
|
|
50
|
-
const pad = (i) => String(i).padStart(4, '0');
|
|
51
|
-
let idx = 0; let done = 0;
|
|
52
|
-
async function worker() {
|
|
53
|
-
while (idx < ts.length) {
|
|
54
|
-
const i = idx++;
|
|
55
|
-
const { html } = buildReelFrame(spec, ts[i]);
|
|
56
|
-
const hf = path.join(dir, `f-${pad(i)}.html`);
|
|
57
|
-
const pf = path.join(dir, `f-${pad(i)}.png`);
|
|
58
|
-
fs.writeFileSync(hf, html);
|
|
59
|
-
await pexec(chrome, [
|
|
60
|
-
'--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
|
|
61
|
-
`--window-size=${w},${h}`, '--virtual-time-budget=2200',
|
|
62
|
-
`--user-data-dir=${path.join(dir, 'ud-' + i)}`,
|
|
63
|
-
`--screenshot=${pf}`, `file://${hf}`,
|
|
64
|
-
], { timeout: 60000 });
|
|
65
|
-
onDone && onDone(++done, ts.length);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const conc = Math.min(5, ts.length);
|
|
69
|
-
await Promise.all(Array.from({ length: conc }, worker));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function run(argv) {
|
|
73
|
-
const { flags, pos } = parseFlags(argv);
|
|
74
|
-
if (pos[0] === 'help' || flags.help) {
|
|
75
|
-
console.log(`\n atris reel — one line of text into a short on-brand video\n
|
|
76
|
-
atris reel "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size square|og|wide|story] [--seconds 2.6]
|
|
77
|
-
flags: --sub --kicker --by --number --label --brand --version --out <file.mp4>\n`);
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const text = pos.join(' ').trim();
|
|
82
|
-
const kind = flags.kind || 'statement';
|
|
83
|
-
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
84
|
-
const size = flags.size || 'square';
|
|
85
|
-
if (!SIZES[size]) { console.error(` unknown size "${size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
86
|
-
if (kind === 'stat' && !flags.number && !text) { console.error(' stat reels need --number'); return 2; }
|
|
87
|
-
if (kind !== 'stat' && !text) { console.error(' give the reel some text: atris reel "Your headline"'); return 2; }
|
|
88
|
-
|
|
89
|
-
const seconds = Math.max(1, Math.min(8, parseFloat(flags.seconds) || 2.6));
|
|
90
|
-
const fps = 20;
|
|
91
|
-
const spec = {
|
|
92
|
-
kind, text, headline: text, theme: flags.theme, size,
|
|
93
|
-
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
94
|
-
number: flags.number, label: flags.label, brand: flags.brand, version: flags.version,
|
|
95
|
-
};
|
|
96
|
-
const { w, h } = SIZES[size];
|
|
97
|
-
const base = flags.out ? String(flags.out).replace(/\.mp4$/i, '') : `reel-${kind}-${flags.theme || 'atris'}-${size}`;
|
|
98
|
-
const outMp4 = path.resolve(`${base}.mp4`);
|
|
99
|
-
|
|
100
|
-
const chrome = findChrome();
|
|
101
|
-
if (!chrome) { console.error('\n no Chrome found to render frames. install Chrome or set CHROME_PATH.\n'); return 1; }
|
|
102
|
-
const ffmpeg = findFfmpeg();
|
|
103
|
-
if (!ffmpeg) { console.error('\n no ffmpeg found to encode the video. install ffmpeg (brew install ffmpeg).\n'); return 1; }
|
|
104
|
-
|
|
105
|
-
const ts = reelFrames(seconds, fps);
|
|
106
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-reel-'));
|
|
107
|
-
console.log(`\n rendering ${ts.length} frames (${seconds}s, ${size})...`);
|
|
108
|
-
try {
|
|
109
|
-
await renderFrames(chrome, ts, spec, dir, w, h, (d, n) => {
|
|
110
|
-
if (d === n || d % 10 === 0) process.stdout.write(`\r frames ${d}/${n} `);
|
|
111
|
-
});
|
|
112
|
-
process.stdout.write('\n encoding...\n');
|
|
113
|
-
await pexec(ffmpeg, [
|
|
114
|
-
'-y', '-framerate', String(fps), '-i', path.join(dir, 'f-%04d.png'),
|
|
115
|
-
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', outMp4,
|
|
116
|
-
]);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
console.error(`\n render failed: ${String(e.message).split('\n')[0]}`); return 1;
|
|
119
|
-
} finally {
|
|
120
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!fs.existsSync(outMp4)) { console.error(' no video produced'); return 1; }
|
|
124
|
-
console.log(`\n ✓ ${kind} reel, ${w}x${h}, ${seconds}s, theme ${flags.theme || 'atris'}\n ${outMp4}\n`);
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = { run, findChrome, findFfmpeg };
|
package/commands/site.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// atris site — turn a folder of markdown (docs, your wiki, memory) into a
|
|
2
|
-
// beautiful, navigable static site in the design system. Built on lib/site.js.
|
|
3
|
-
//
|
|
4
|
-
// atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T] [--serve]
|
|
5
|
-
|
|
6
|
-
const { buildSite, serveSite } = require('../lib/site');
|
|
7
|
-
|
|
8
|
-
function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
|
|
9
|
-
function hasFlag(argv, name) { return argv.includes(name); }
|
|
10
|
-
|
|
11
|
-
async function run(argv) {
|
|
12
|
-
const input = argv.find((a) => !a.startsWith('-'));
|
|
13
|
-
if (!input || input === 'help' || hasFlag(argv, '--help')) {
|
|
14
|
-
console.log(`
|
|
15
|
-
atris site — a beautiful static site from a folder of markdown
|
|
16
|
-
|
|
17
|
-
atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T]
|
|
18
|
-
atris site atris/wiki --title "Atris Wiki" --serve
|
|
19
|
-
|
|
20
|
-
Each .md becomes a page; an index links them all. Same anti-slop design system,
|
|
21
|
-
semantic data-atris-block sections, ready for the web app. --serve previews it.
|
|
22
|
-
`);
|
|
23
|
-
return input === 'help' || hasFlag(argv, '--help') ? 0 : 2;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let res;
|
|
27
|
-
try {
|
|
28
|
-
res = buildSite(input, {
|
|
29
|
-
out: flag(argv, '--out') || 'dist',
|
|
30
|
-
theme: flag(argv, '--theme'),
|
|
31
|
-
title: flag(argv, '--title'),
|
|
32
|
-
brand: flag(argv, '--brand'),
|
|
33
|
-
});
|
|
34
|
-
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
35
|
-
|
|
36
|
-
console.log(`\n ✓ site built: ${res.pages.length} page${res.pages.length === 1 ? '' : 's'} + index -> ${res.outDir}/`);
|
|
37
|
-
console.log(` open ${res.indexPath}`);
|
|
38
|
-
|
|
39
|
-
if (hasFlag(argv, '--serve')) {
|
|
40
|
-
const port = Number(flag(argv, '--port')) || 4321;
|
|
41
|
-
const { url } = await serveSite(res.outDir, port);
|
|
42
|
-
console.log(`\n serving at ${url} (ctrl-c to stop)\n`);
|
|
43
|
-
await new Promise(() => {}); // keep alive until killed
|
|
44
|
-
}
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
module.exports = { run };
|
package/commands/slop.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
// atris slop — deterministic frontend-slop detector (no LLM).
|
|
2
|
-
//
|
|
3
|
-
// Steal-from-Impeccable, the Atris way: makes "looks AI-generated" concrete and
|
|
4
|
-
// CHECKABLE. A failure is a fact (file:line + rule), not a taste opinion — so it
|
|
5
|
-
// drops straight into the autopilot/review verification gate and CI. Each finding
|
|
6
|
-
// is the seed of a typed lesson; the ruleset is meant to GROW from lessons.md
|
|
7
|
-
// rather than be hand-curated forever.
|
|
8
|
-
//
|
|
9
|
-
// Zero external deps (Node built-ins only) — repo contract.
|
|
10
|
-
//
|
|
11
|
-
// Usage:
|
|
12
|
-
// atris slop detect [path] # scan a file or dir (default: .)
|
|
13
|
-
// atris slop detect src/ --json # machine output for CI / the loop
|
|
14
|
-
// atris slop detect src/ --quiet # only print the summary line
|
|
15
|
-
//
|
|
16
|
-
// Exit code: 0 = clean, 1 = slop found, 2 = bad usage. CI/PR gates read this.
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { execFileSync } = require('child_process');
|
|
21
|
-
|
|
22
|
-
const SCAN_EXTS = new Set(['.css', '.scss', '.sass', '.less', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte', '.astro',
|
|
23
|
-
'.md', '.mdx', '.txt']); // prose too: the voice doctrine (em-dash, hype-copy) is enforceable, not just advice
|
|
24
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.astro', 'coverage', '.cache', 'out', 'vendor']);
|
|
25
|
-
|
|
26
|
-
// Each rule is deterministic: a regex + a one-line why. severity drives the icon.
|
|
27
|
-
// Kept high-precision on purpose — a noisy gate gets muted, and a muted gate is dead.
|
|
28
|
-
const RULES = [
|
|
29
|
-
{ id: 'ai-gradient-text', sev: 'error',
|
|
30
|
-
re: /(text-transparent[^"'`]{0,40}bg-clip-text|bg-clip-text[^"'`]{0,40}text-transparent|-webkit-text-fill-color:\s*transparent|background-clip:\s*text)/i,
|
|
31
|
-
why: 'gradient-filled text headline: the #1 generated-look tell' },
|
|
32
|
-
{ id: 'ai-purple-gradient', sev: 'error',
|
|
33
|
-
re: /((from|via|to)-(purple|violet|indigo|fuchsia)-\d{2,3}\b|linear-gradient\([^)]*(#6366f1|#8b5cf6|#a855f7|#7c3aed|#4f46e5|\bpurple\b|\bviolet\b|\bindigo\b))/i,
|
|
34
|
-
why: 'purple/indigo gradient: default "AI startup" palette' },
|
|
35
|
-
{ id: 'ai-indigo-brand', sev: 'warn',
|
|
36
|
-
re: /(#6366f1|#4f46e5|#4338ca|(?:bg|text|border|from|to|ring)-indigo-(?:500|600|700)\b)/i,
|
|
37
|
-
why: 'canonical AI indigo used as brand color' },
|
|
38
|
-
{ id: 'glassmorphism', sev: 'warn',
|
|
39
|
-
re: /(backdrop-blur(?:-\w+)?\b|backdrop-filter:\s*blur)/i,
|
|
40
|
-
why: 'glassmorphism (frosted blur): overused default' },
|
|
41
|
-
{ id: 'over-rounding', sev: 'warn',
|
|
42
|
-
re: /(rounded-(?:3xl|\[(?:[2-9]\d?|1\d\d)(?:px|rem)\])|border-radius:\s*(?:2[4-9]|[3-9]\d|\d{3})px|border-radius:\s*(?:[2-9](?:\.\d+)?)rem)/i,
|
|
43
|
-
why: 'over-rounded corners (>=24px / rounded-3xl)' },
|
|
44
|
-
{ id: 'mega-shadow', sev: 'warn',
|
|
45
|
-
re: /(shadow-2xl\b|box-shadow:\s*0\s+\d{2,}px)/i,
|
|
46
|
-
why: 'oversized generic drop shadow (depth-by-blur)' },
|
|
47
|
-
{ id: 'side-stripe-card', sev: 'warn',
|
|
48
|
-
re: /border-(?:left|l)-(?:4|8|\[\d+px\])\b|border-left:\s*[3-9]px\s+solid/i,
|
|
49
|
-
why: 'accent side-stripe on a card: generated layout reflex' },
|
|
50
|
-
{ id: 'transition-all', sev: 'warn',
|
|
51
|
-
re: /\btransition-all\b|transition:\s*all\b/i,
|
|
52
|
-
why: 'transition-all: animate-everything laziness, not intent' },
|
|
53
|
-
{ id: 'pulse-animation', sev: 'warn',
|
|
54
|
-
re: /animation:[^;]*\binfinite\b|@keyframes\s+(pulse|ping|blink|glow|throb)\b|\banimate-(pulse|ping|bounce)\b/i,
|
|
55
|
-
why: 'looping pulse/ping/glow animation: distracting live-status reflex' },
|
|
56
|
-
{ id: 'eyebrow-caps', sev: 'warn',
|
|
57
|
-
re: /text-transform:\s*uppercase\b|\buppercase\b[^"'`]{0,30}tracking-|tracking-[^"'`]{0,30}\buppercase\b/i,
|
|
58
|
-
why: 'tracked all-caps eyebrow/label: dated reflex; use sentence case' },
|
|
59
|
-
{ id: 'decorative-emoji', sev: 'warn',
|
|
60
|
-
re: /[✨\u{1F680}\u{1F4A1}\u{1F525}\u{1F389}⚡\u{1F31F}\u{1FA84}\u{1F4AB}\u{1F44B}]/u,
|
|
61
|
-
why: 'decorative emoji in UI copy' },
|
|
62
|
-
{ id: 'em-dash', sev: 'warn',
|
|
63
|
-
re: /—/,
|
|
64
|
-
fix: (s) => s.replace(/\s*—\s*/g, ', '), // safe deterministic repair (prose)
|
|
65
|
-
why: 'em dash: a top AI-writing tell; use a comma, colon, or period' },
|
|
66
|
-
{ id: 'hype-copy', sev: 'error',
|
|
67
|
-
re: /\b(boost your productivity|supercharge|unleash|game[- ]?chang(?:er|ing)|seamlessly|effortlessly|revolutioniz(?:e|ing)|take your .{1,30} to the next level|elevate your|cutting[- ]edge|powered by ai|next[- ]generation)\b/i,
|
|
68
|
-
why: 'hype/marketing slop phrase: say the specific thing instead' },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const ICON = { error: '✗', warn: '⚠' }; // ✗ ⚠
|
|
72
|
-
|
|
73
|
-
const PROJECT_RULES_FILE = path.join('.atris', 'slop.rules.json');
|
|
74
|
-
|
|
75
|
-
// Compounding: projects grow their own anti-slop ruleset in .atris/slop.rules.json.
|
|
76
|
-
// Each entry: { id, pattern, flags?, why?, sev? }. Loaded on top of the built-ins.
|
|
77
|
-
function loadProjectRules(root = process.cwd()) {
|
|
78
|
-
try {
|
|
79
|
-
const raw = JSON.parse(fs.readFileSync(path.join(root, PROJECT_RULES_FILE), 'utf8'));
|
|
80
|
-
const arr = Array.isArray(raw) ? raw : (raw.rules || []);
|
|
81
|
-
return arr.map((r) => {
|
|
82
|
-
if (!r || !r.id || !r.pattern) return null;
|
|
83
|
-
let re; try { re = new RegExp(r.pattern, r.flags || 'i'); } catch { return null; }
|
|
84
|
-
return { id: r.id, sev: r.sev === 'error' ? 'error' : 'warn', re, why: r.why || r.id, project: true };
|
|
85
|
-
}).filter(Boolean);
|
|
86
|
-
} catch { return []; }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function addProjectRule(rule, root = process.cwd()) {
|
|
90
|
-
const file = path.join(root, PROJECT_RULES_FILE);
|
|
91
|
-
let arr = [];
|
|
92
|
-
try { const raw = JSON.parse(fs.readFileSync(file, 'utf8')); arr = Array.isArray(raw) ? raw : (raw.rules || []); } catch {}
|
|
93
|
-
arr = arr.filter((r) => r.id !== rule.id);
|
|
94
|
-
arr.push(rule);
|
|
95
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
96
|
-
fs.writeFileSync(file, JSON.stringify(arr, null, 2) + '\n');
|
|
97
|
-
return file;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Map<absFile, Set<changedLineNumber>> from the working-tree (or --cached) git diff.
|
|
101
|
-
function gitChangedLines(staged, cwd = process.cwd()) {
|
|
102
|
-
const map = new Map();
|
|
103
|
-
let out;
|
|
104
|
-
try { out = execFileSync('git', ['diff', '--unified=0', ...(staged ? ['--cached'] : [])], { encoding: 'utf8', cwd }); }
|
|
105
|
-
catch { return map; }
|
|
106
|
-
let cur = null;
|
|
107
|
-
for (const line of out.split('\n')) {
|
|
108
|
-
const f = line.match(/^\+\+\+ b\/(.+)$/);
|
|
109
|
-
if (f) { cur = path.resolve(cwd, f[1]); map.set(cur, map.get(cur) || new Set()); continue; }
|
|
110
|
-
const h = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
111
|
-
if (h && cur) { const start = +h[1], count = h[2] != null ? +h[2] : 1; for (let i = 0; i < count; i++) map.get(cur).add(start + i); }
|
|
112
|
-
}
|
|
113
|
-
return map;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Install a pre-commit hook that gates staged changes through `atris slop detect --staged`.
|
|
117
|
-
// Idempotent and non-destructive: appends a marked block, skips gracefully if atris is absent.
|
|
118
|
-
function installHook(root = process.cwd()) {
|
|
119
|
-
if (!fs.existsSync(path.join(root, '.git'))) throw new Error('not a git repo (no .git here)');
|
|
120
|
-
const hookDir = path.join(root, '.git', 'hooks');
|
|
121
|
-
fs.mkdirSync(hookDir, { recursive: true });
|
|
122
|
-
const hookPath = path.join(hookDir, 'pre-commit');
|
|
123
|
-
const marker = '# atris slop gate';
|
|
124
|
-
let content = '';
|
|
125
|
-
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
126
|
-
if (content.includes(marker)) return { hookPath, already: true };
|
|
127
|
-
if (!content) content = '#!/bin/sh\n';
|
|
128
|
-
if (!content.endsWith('\n')) content += '\n';
|
|
129
|
-
content += `\n${marker}\nif command -v atris >/dev/null 2>&1; then atris slop detect --staged --quiet || exit 1; fi\n`;
|
|
130
|
-
fs.writeFileSync(hookPath, content);
|
|
131
|
-
fs.chmodSync(hookPath, 0o755);
|
|
132
|
-
return { hookPath, already: false };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Apply every fixable rule's safe transform in place. Returns { fixedCount, fixedFiles }.
|
|
136
|
-
function applyFixes(files, rules) {
|
|
137
|
-
const fixable = rules.filter((r) => typeof r.fix === 'function');
|
|
138
|
-
let fixedCount = 0; const fixedFiles = [];
|
|
139
|
-
for (const file of files) {
|
|
140
|
-
let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
141
|
-
const lines = text.split('\n');
|
|
142
|
-
let touched = false;
|
|
143
|
-
for (let i = 0; i < lines.length; i++) {
|
|
144
|
-
let line = lines[i];
|
|
145
|
-
for (const r of fixable) { if (r.re.test(line)) { const nl = r.fix(line); if (nl !== line) { line = nl; fixedCount++; } } }
|
|
146
|
-
if (line !== lines[i]) { lines[i] = line; touched = true; }
|
|
147
|
-
}
|
|
148
|
-
if (touched) { fs.writeFileSync(file, lines.join('\n')); fixedFiles.push(file); }
|
|
149
|
-
}
|
|
150
|
-
return { fixedCount, fixedFiles };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function walk(target, out) {
|
|
154
|
-
let stat;
|
|
155
|
-
try { stat = fs.statSync(target); } catch { return out; }
|
|
156
|
-
if (stat.isFile()) {
|
|
157
|
-
if (SCAN_EXTS.has(path.extname(target))) out.push(target);
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
if (stat.isDirectory()) {
|
|
161
|
-
if (SKIP_DIRS.has(path.basename(target))) return out;
|
|
162
|
-
for (const name of fs.readdirSync(target)) {
|
|
163
|
-
if (name.startsWith('.') && name !== '.') continue;
|
|
164
|
-
walk(path.join(target, name), out);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return out;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function scanFile(file, rules = RULES) {
|
|
171
|
-
const findings = [];
|
|
172
|
-
let text;
|
|
173
|
-
try { text = fs.readFileSync(file, 'utf8'); } catch { return findings; }
|
|
174
|
-
const lines = text.split('\n');
|
|
175
|
-
for (let i = 0; i < lines.length; i++) {
|
|
176
|
-
const line = lines[i];
|
|
177
|
-
for (const rule of rules) {
|
|
178
|
-
const m = rule.re.exec(line);
|
|
179
|
-
if (m) {
|
|
180
|
-
findings.push({
|
|
181
|
-
file, line: i + 1, rule: rule.id, sev: rule.sev, why: rule.why,
|
|
182
|
-
snippet: m[0].trim().slice(0, 48),
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return findings;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function detect(argv) {
|
|
191
|
-
const json = argv.includes('--json');
|
|
192
|
-
const quiet = argv.includes('--quiet');
|
|
193
|
-
const doFix = argv.includes('--fix');
|
|
194
|
-
const staged = argv.includes('--staged');
|
|
195
|
-
const diffMode = staged || argv.includes('--diff');
|
|
196
|
-
const rules = RULES.concat(loadProjectRules());
|
|
197
|
-
|
|
198
|
-
// pick the file set: a git diff (changed files) or a path walk
|
|
199
|
-
let files, changed = null;
|
|
200
|
-
if (diffMode) {
|
|
201
|
-
changed = gitChangedLines(staged);
|
|
202
|
-
files = [...changed.keys()].filter((f) => SCAN_EXTS.has(path.extname(f)) && fs.existsSync(f));
|
|
203
|
-
} else {
|
|
204
|
-
const target = argv.find((a) => !a.startsWith('-')) || '.';
|
|
205
|
-
files = walk(path.resolve(target), []);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let fixed = null;
|
|
209
|
-
if (doFix) {
|
|
210
|
-
fixed = applyFixes(files, rules);
|
|
211
|
-
if (!json && fixed.fixedCount) {
|
|
212
|
-
console.log(`\n ✎ fixed ${fixed.fixedCount} tell${fixed.fixedCount === 1 ? '' : 's'} in ${fixed.fixedFiles.length} file${fixed.fixedFiles.length === 1 ? '' : 's'}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
let findings = files.flatMap((f) => scanFile(f, rules));
|
|
217
|
-
if (diffMode && changed) findings = findings.filter((f) => changed.get(f.file) && changed.get(f.file).has(f.line));
|
|
218
|
-
const errors = findings.filter((f) => f.sev === 'error').length;
|
|
219
|
-
|
|
220
|
-
if (json) {
|
|
221
|
-
console.log(JSON.stringify({
|
|
222
|
-
ok: findings.length === 0, scanned: files.length,
|
|
223
|
-
mode: diffMode ? (staged ? 'staged' : 'diff') : 'path',
|
|
224
|
-
fixed: fixed ? fixed.fixedCount : 0,
|
|
225
|
-
slop: findings.length, errors,
|
|
226
|
-
findings: findings.map((f) => ({ ...f, file: path.relative(process.cwd(), f.file) })),
|
|
227
|
-
}, null, 2));
|
|
228
|
-
return findings.length ? 1 : 0;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const rel = (f) => path.relative(process.cwd(), f);
|
|
232
|
-
if (!quiet) {
|
|
233
|
-
if (!findings.length) {
|
|
234
|
-
console.log(`\n ✓ clean — no slop tells in ${files.length} file${files.length === 1 ? '' : 's'}`);
|
|
235
|
-
} else {
|
|
236
|
-
console.log('');
|
|
237
|
-
const w = Math.max(...findings.map((f) => `${rel(f.file)}:${f.line}`.length));
|
|
238
|
-
for (const f of findings) {
|
|
239
|
-
const loc = `${rel(f.file)}:${f.line}`.padEnd(w);
|
|
240
|
-
console.log(` ${ICON[f.sev]} ${loc} ${f.rule.padEnd(20)} ${f.why}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (findings.length) {
|
|
245
|
-
console.log(`\n ${findings.length} slop tell${findings.length === 1 ? '' : 's'} (${errors} error) · exit 1\n`);
|
|
246
|
-
} else if (quiet) {
|
|
247
|
-
console.log(` ✓ clean · exit 0`);
|
|
248
|
-
}
|
|
249
|
-
return findings.length ? 1 : 0;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function slopCommand(argv) {
|
|
253
|
-
const sub = argv[0];
|
|
254
|
-
if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help', 'hook', 'install-hook'].includes(sub)) {
|
|
255
|
-
// default + `detect`: scan. Bare `atris slop` scans cwd too.
|
|
256
|
-
const rest = sub === 'detect' ? argv.slice(1) : argv;
|
|
257
|
-
return detect(rest);
|
|
258
|
-
}
|
|
259
|
-
if (sub === 'hook' || sub === 'install-hook') {
|
|
260
|
-
try {
|
|
261
|
-
const { hookPath, already } = installHook();
|
|
262
|
-
console.log(already
|
|
263
|
-
? `\n already installed: ${path.relative(process.cwd(), hookPath)}\n`
|
|
264
|
-
: `\n ✓ slop pre-commit gate installed: ${path.relative(process.cwd(), hookPath)}\n every commit now runs: atris slop detect --staged\n`);
|
|
265
|
-
return 0;
|
|
266
|
-
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (sub === 'rules') {
|
|
270
|
-
if (argv.includes('--add')) {
|
|
271
|
-
const rest = argv.slice(argv.indexOf('--add') + 1).filter((a) => !a.startsWith('-'));
|
|
272
|
-
const [id, pattern, ...whyParts] = rest;
|
|
273
|
-
if (!id || !pattern) { console.error(' usage: atris slop rules --add <id> <regex-pattern> <why...> [--sev error|warn]'); return 2; }
|
|
274
|
-
let valid = true; try { new RegExp(pattern, 'i'); } catch { valid = false; }
|
|
275
|
-
if (!valid) { console.error(` invalid regex: ${pattern}`); return 2; }
|
|
276
|
-
const sev = (argv[argv.indexOf('--sev') + 1] === 'error') ? 'error' : 'warn';
|
|
277
|
-
const file = addProjectRule({ id, pattern, why: whyParts.join(' ') || id, sev });
|
|
278
|
-
console.log(` ✓ added project rule "${id}" to ${path.relative(process.cwd(), file)}`);
|
|
279
|
-
return 0;
|
|
280
|
-
}
|
|
281
|
-
const project = loadProjectRules();
|
|
282
|
-
console.log('\n atris slop — deterministic rules:\n');
|
|
283
|
-
for (const r of RULES) console.log(` ${ICON[r.sev]} ${r.id.padEnd(20)} ${r.why}`);
|
|
284
|
-
for (const r of project) console.log(` ${ICON[r.sev]} ${r.id.padEnd(20)} ${r.why} (project)`);
|
|
285
|
-
console.log(`\n ${RULES.length} built-in${project.length ? ` + ${project.length} project` : ''} rule${RULES.length + project.length === 1 ? '' : 's'}\n`);
|
|
286
|
-
return 0;
|
|
287
|
-
}
|
|
288
|
-
// help
|
|
289
|
-
console.log(`
|
|
290
|
-
atris slop — deterministic slop detector + repairer (no LLM)
|
|
291
|
-
|
|
292
|
-
atris slop detect [path] scan a file or dir (default: .)
|
|
293
|
-
atris slop detect --diff scan only changed lines (commit/PR gate)
|
|
294
|
-
atris slop detect --staged scan only staged changes (pre-commit hook)
|
|
295
|
-
atris slop detect --fix auto-repair the safe tells (em dashes), report the rest
|
|
296
|
-
atris slop detect [path] --json machine output for CI / the loop
|
|
297
|
-
atris slop rules list active rules (built-in + project)
|
|
298
|
-
atris slop rules --add <id> <pattern> <why> grow the project ruleset
|
|
299
|
-
atris slop hook install a pre-commit gate (runs --staged)
|
|
300
|
-
|
|
301
|
-
Project rules live in .atris/slop.rules.json and compound over time.
|
|
302
|
-
exit 0 = clean, 1 = slop found. Wire into PR checks and the autopilot gate.
|
|
303
|
-
`);
|
|
304
|
-
return 0;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
module.exports = { slopCommand, detect, scanFile, RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
|