atris 3.17.0 → 3.23.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/atris.md +3 -3
- package/bin/atris.js +20 -2
- package/commands/card.js +121 -0
- package/commands/deck.js +77 -28
- package/commands/recap.js +16 -0
- package/commands/site.js +48 -0
- package/commands/slop.js +146 -12
- package/commands/theme.js +217 -0
- package/lib/card.js +120 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/html-render.js +257 -0
- package/lib/memory-view.js +95 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +3 -2
- package/lib/theme.js +264 -0
- package/package.json +1 -1
package/atris.md
CHANGED
|
@@ -62,12 +62,12 @@ Labels used below:
|
|
|
62
62
|
|
|
63
63
|
What you ship should not read as generated. The test: if someone said "an AI made this," would they believe it instantly? If yes, that is the bug. The model has no words for restraint and it falls into gravity wells. Beat both.
|
|
64
64
|
|
|
65
|
-
- **Gate it.** `atris slop detect <path>` is deterministic: no model, exit 1 on a tell, built for CI and the review stage. A finding is a fact (file:line + rule), not an opinion. `guarded` once wired into review.
|
|
65
|
+
- **Gate it.** `atris slop detect <path>` is deterministic: no model, exit 1 on a tell, built for CI and the review stage. A finding is a fact (file:line + rule), not an opinion. `--diff`/`--staged` scopes it to changed lines for a commit gate; `--fix` repairs the safe tells. `guarded` once wired into review.
|
|
66
66
|
- **Name the move.** Vague prompts make vague output. Direct with craft words: vertical rhythm, negative space, hierarchy, contrast, bolder here / quieter there, restraint. Precise language is the lever. Own it.
|
|
67
67
|
- **Refuse the wells** (named so you can): purple/indigo gradients, gradient-filled text, glassmorphism, Inter/Roboto defaults, claude-beige, neon-on-dark, hero-metric rows, identical card grids, eyebrow/tracked-caps labels, pulsing live-dots, em dashes.
|
|
68
68
|
- **Commit to constraints.** One distinctive font, one accent hue, a small spacing scale. Taste is subtraction, not addition.
|
|
69
|
-
- **Generate it right.** `atris deck` (slides)
|
|
70
|
-
- **Compound it.** A new tell becomes a
|
|
69
|
+
- **Generate it right.** `atris deck` (slides), `atris deck from <doc.md> --html` (a web page from a plain doc, in the web app's design tokens), `atris site <dir>` (a whole markdown folder into a navigable site), and `atris recap --html` (a memory-updates page) all apply the system by default: own backgrounds and fonts, never the tool's stock template. Output as an AppBlock with `--block` to drop into a web app.
|
|
70
|
+
- **Compound it.** A new tell becomes a project rule in `.atris/slop.rules.json` (`atris slop rules --add`), and a project's brand lives in `.atris/theme.json` (`atris theme create` builds your own by feel, or `atris theme init` scaffolds one) so every deck, page, and site is on-brand by default, and one line of text becomes an on-brand image with `atris card`. The gate and the look grow per project instead of leaning on memory. Taste lives in code, not vibes.
|
|
71
71
|
|
|
72
72
|
## voice
|
|
73
73
|
|
package/bin/atris.js
CHANGED
|
@@ -363,6 +363,9 @@ function showHelp() {
|
|
|
363
363
|
console.log(' review - Validate work (tests, safety checks, docs)');
|
|
364
364
|
console.log(' slop - Detect frontend AI-slop tells (deterministic, exit 1 = found)');
|
|
365
365
|
console.log(' deck - Generate a premium Google Slides deck from a content spec');
|
|
366
|
+
console.log(' site - Build a beautiful static site from a folder of markdown');
|
|
367
|
+
console.log(' theme - Brand themes: "theme create" builds your own by feel (deck/html/site)');
|
|
368
|
+
console.log(' card - One line of text into an on-brand image (uses your theme)');
|
|
366
369
|
console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
|
|
367
370
|
console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
|
|
368
371
|
console.log(' run search - Search phase reasoning across all run logs');
|
|
@@ -376,7 +379,7 @@ function showHelp() {
|
|
|
376
379
|
console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
|
|
377
380
|
console.log(' ctop - Show a process-first live agent CPU/memory view');
|
|
378
381
|
console.log(' status - See local work and completions (`atris status <business>` for remote)');
|
|
379
|
-
console.log(' recap - What your AI team did, in plain English (--share for
|
|
382
|
+
console.log(' recap - What your AI team did, in plain English (--share, or --html for a memory page)');
|
|
380
383
|
console.log(' xp - Show Career XP and contribution graph');
|
|
381
384
|
console.log(' analytics - Show recent productivity from journals');
|
|
382
385
|
console.log(' search - Search journal history (atris search <keyword>)');
|
|
@@ -828,7 +831,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
|
|
|
828
831
|
const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
829
832
|
'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
830
833
|
'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
831
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
834
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
832
835
|
'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
833
836
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
|
|
834
837
|
|
|
@@ -1352,6 +1355,21 @@ if (command === 'init') {
|
|
|
1352
1355
|
Promise.resolve(require('../commands/deck').run(process.argv.slice(3)))
|
|
1353
1356
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1354
1357
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1358
|
+
} else if (command === 'site') {
|
|
1359
|
+
// Site: beautiful static site from a folder of markdown, in the anti-slop design system.
|
|
1360
|
+
Promise.resolve(require('../commands/site').run(process.argv.slice(3)))
|
|
1361
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1362
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1363
|
+
} else if (command === 'theme') {
|
|
1364
|
+
// Theme: brand themes (.atris/theme.json) for the whole design system (deck/html/site).
|
|
1365
|
+
Promise.resolve(require('../commands/theme').run(process.argv.slice(3)))
|
|
1366
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1367
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1368
|
+
} else if (command === 'card') {
|
|
1369
|
+
// Card: one line of text into an on-brand image (uses your theme + the design system).
|
|
1370
|
+
Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
|
|
1371
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1372
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1355
1373
|
} else if (command === 'aeo') {
|
|
1356
1374
|
// AEO: AI Engine Optimization — credit-metered citation drafting against the customer workspace.
|
|
1357
1375
|
Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
|
package/commands/card.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// atris card — one line of text -> a beautiful, on-brand image (your theme).
|
|
2
|
+
//
|
|
3
|
+
// atris card "Ship faster" --kind statement --theme brand --size og
|
|
4
|
+
// atris card "It just works" --kind quote --by "a happy user"
|
|
5
|
+
// atris card --kind stat --number "10x" --label "faster reviews"
|
|
6
|
+
//
|
|
7
|
+
// Writes an .html (always) and a .png (when headless Chrome is available).
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
13
|
+
const { buildCard, SIZES, KINDS } = require('../lib/card');
|
|
14
|
+
|
|
15
|
+
function parseFlags(argv) {
|
|
16
|
+
const flags = {}; const pos = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === '--html-only') { flags.htmlOnly = true; continue; }
|
|
20
|
+
if (a.startsWith('--')) {
|
|
21
|
+
const key = a.slice(2);
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
return { flags, pos };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// find an installed Chrome/Chromium without adding a dependency
|
|
32
|
+
function findChrome() {
|
|
33
|
+
if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH;
|
|
34
|
+
const macApps = [
|
|
35
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
36
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
37
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
38
|
+
];
|
|
39
|
+
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
40
|
+
for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']) {
|
|
41
|
+
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
42
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderPng(chrome, html, width, height, outPng) {
|
|
48
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-card-'));
|
|
49
|
+
const htmlFile = path.join(tmp, 'card.html');
|
|
50
|
+
fs.writeFileSync(htmlFile, html);
|
|
51
|
+
const args = [
|
|
52
|
+
'--headless=new', '--disable-gpu', '--hide-scrollbars',
|
|
53
|
+
'--force-device-scale-factor=2',
|
|
54
|
+
`--window-size=${width},${height}`,
|
|
55
|
+
'--virtual-time-budget=4000',
|
|
56
|
+
`--screenshot=${outPng}`,
|
|
57
|
+
`file://${htmlFile}`,
|
|
58
|
+
];
|
|
59
|
+
execFileSync(chrome, args, { stdio: 'ignore', timeout: 60000 });
|
|
60
|
+
return fs.existsSync(outPng);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function run(argv) {
|
|
64
|
+
const { flags, pos } = parseFlags(argv);
|
|
65
|
+
|
|
66
|
+
if (pos[0] === 'help' || flags.help) {
|
|
67
|
+
console.log(`\n atris card — one line of text into an on-brand image\n
|
|
68
|
+
atris card "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size og|wide|square|story]
|
|
69
|
+
flags: --sub --kicker --by --number --label --brand --version --out <file.png> --html-only\n
|
|
70
|
+
examples:
|
|
71
|
+
atris card "Design that builds itself" --kicker "Atris v3.23.0" --theme brand
|
|
72
|
+
atris card "It just works." --kind quote --by "a founder" --size square
|
|
73
|
+
atris card --kind stat --number "1260" --label "tests, all green"\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = pos.join(' ').trim();
|
|
78
|
+
const kind = flags.kind || 'statement';
|
|
79
|
+
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
80
|
+
if (flags.size && !SIZES[flags.size]) { console.error(` unknown size "${flags.size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
81
|
+
if (kind === 'stat' && !flags.number && !text) { console.error(' stat cards need --number (e.g. --number "10x")'); return 2; }
|
|
82
|
+
if (kind !== 'stat' && !text) { console.error(' give the card some text: atris card "Your headline"'); return 2; }
|
|
83
|
+
|
|
84
|
+
const spec = {
|
|
85
|
+
kind, text, headline: text,
|
|
86
|
+
theme: flags.theme, size: flags.size,
|
|
87
|
+
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
88
|
+
number: flags.number, label: flags.label,
|
|
89
|
+
brand: flags.brand, version: flags.version,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let card;
|
|
93
|
+
try { card = buildCard(spec); }
|
|
94
|
+
catch (e) { console.error(` could not build card: ${e.message}`); return 1; }
|
|
95
|
+
|
|
96
|
+
const base = (flags.out ? String(flags.out).replace(/\.png$/i, '') : `card-${kind}-${card.theme}-${card.size}`);
|
|
97
|
+
const outPng = path.resolve(`${base}.png`);
|
|
98
|
+
const outHtml = path.resolve(`${base}.html`);
|
|
99
|
+
fs.writeFileSync(outHtml, card.html);
|
|
100
|
+
|
|
101
|
+
if (flags.htmlOnly) {
|
|
102
|
+
console.log(`\n ✓ ${card.kind} card (${card.width}x${card.height}, theme ${card.theme})\n html: ${outHtml}\n open it, or render a png with Chrome installed.\n`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chrome = findChrome();
|
|
107
|
+
if (!chrome) {
|
|
108
|
+
console.log(`\n ✓ wrote html: ${outHtml}\n no Chrome found for png. open the html, or set CHROME_PATH and re-run.\n`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
renderPng(chrome, card.html, card.width, card.height, outPng);
|
|
113
|
+
console.log(`\n ✓ ${card.kind} card, ${card.width}x${card.height}, theme ${card.theme}\n ${outPng}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.log(`\n ! png render failed (${e.message.split('\n')[0]})\n html is ready: ${outHtml}\n`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { run };
|
package/commands/deck.js
CHANGED
|
@@ -14,6 +14,8 @@ const fs = require('fs');
|
|
|
14
14
|
const https = require('https');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const { buildDeck, THEMES } = require('../lib/slides-deck');
|
|
17
|
+
const { parseMarkdownToSpec } = require('../lib/deck-from-md');
|
|
18
|
+
const { mergedThemes } = require('../lib/theme');
|
|
17
19
|
|
|
18
20
|
const BASE = 'api.atris.ai';
|
|
19
21
|
const PFX = '/api/integrations/google-slides';
|
|
@@ -60,9 +62,69 @@ const SAMPLE = {
|
|
|
60
62
|
],
|
|
61
63
|
};
|
|
62
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
|
+
|
|
63
103
|
async function run(argv) {
|
|
64
104
|
const sub = argv[0];
|
|
65
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
|
+
|
|
66
128
|
if (sub === 'themes') {
|
|
67
129
|
console.log('\n atris deck themes:\n');
|
|
68
130
|
for (const [name, t] of Object.entries(THEMES)) {
|
|
@@ -85,51 +147,38 @@ async function run(argv) {
|
|
|
85
147
|
try { spec = JSON.parse(fs.readFileSync(specPath, 'utf8')); }
|
|
86
148
|
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
87
149
|
const themeOverride = flag(argv, '--theme'); if (themeOverride) spec.theme = themeOverride;
|
|
88
|
-
if (
|
|
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; } }
|
|
89
152
|
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
90
153
|
|
|
91
154
|
const tok = token();
|
|
92
155
|
if (!tok) { console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.'); return 1; }
|
|
93
156
|
|
|
94
|
-
const {
|
|
95
|
-
|
|
96
|
-
let id, firstSlide;
|
|
97
|
-
const updateId = flag(argv, '--update');
|
|
98
|
-
if (updateId) {
|
|
99
|
-
id = updateId;
|
|
100
|
-
const got = await api('GET', `/presentations/${id}`, null, tok);
|
|
101
|
-
const slides = got.slides || (got.presentation && got.presentation.slides) || [];
|
|
102
|
-
firstSlide = slides[0] && slides[0].objectId;
|
|
103
|
-
} else {
|
|
104
|
-
const pres = await api('POST', '/presentations', { title }, tok);
|
|
105
|
-
id = pres.presentationId || pres.id || (pres.presentation && pres.presentation.presentationId);
|
|
106
|
-
const slides = pres.slides || (pres.presentation && pres.presentation.slides) || [];
|
|
107
|
-
firstSlide = slides[0] && slides[0].objectId;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const reqs = firstSlide ? [...requests, { deleteObject: { objectId: firstSlide } }] : requests;
|
|
111
|
-
console.log(` building ${spec.slides.length} slides (${spec.theme}) · ${reqs.length} ops...`);
|
|
112
|
-
await api('POST', `/presentations/${id}/batch-update`, { requests: reqs }, tok);
|
|
113
|
-
|
|
114
|
-
const url = `https://docs.google.com/presentation/d/${id}/edit`;
|
|
157
|
+
const url = await publishDeck(spec, { title, updateId: flag(argv, '--update'), tok });
|
|
115
158
|
console.log(`\n ✓ deck ready: ${url}\n`);
|
|
116
159
|
return 0;
|
|
117
160
|
}
|
|
118
161
|
|
|
119
162
|
console.log(`
|
|
120
|
-
atris deck — premium Google Slides from a plain content spec
|
|
163
|
+
atris deck — premium Google Slides from a plain content spec or a markdown doc
|
|
121
164
|
|
|
122
|
-
atris deck
|
|
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
|
|
123
169
|
atris deck build my.json [--title "Q3 review"] create the deck, print the URL
|
|
124
|
-
atris deck build my.json --
|
|
170
|
+
atris deck build my.json --html --out p.html render the spec as HTML instead of slides
|
|
125
171
|
atris deck themes list design themes
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
|
|
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).
|
|
129
177
|
`);
|
|
130
178
|
return 0;
|
|
131
179
|
}
|
|
132
180
|
|
|
133
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); }
|
|
134
183
|
|
|
135
|
-
module.exports = { run, SAMPLE };
|
|
184
|
+
module.exports = { run, SAMPLE, publishDeck };
|
package/commands/recap.js
CHANGED
|
@@ -220,6 +220,22 @@ function recapAtris(args = []) {
|
|
|
220
220
|
printRecapHelp();
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
|
+
if (args.includes('--html')) {
|
|
224
|
+
// another way to view memory updates: a beautiful HTML page of what the workspace learned
|
|
225
|
+
const { buildMemorySpec } = require('../lib/memory-view');
|
|
226
|
+
const { renderHtml, renderBlock, THEMES: HTML_THEMES } = require('../lib/html-render');
|
|
227
|
+
const { mergedThemes } = require('../lib/theme');
|
|
228
|
+
const themes = mergedThemes(HTML_THEMES);
|
|
229
|
+
const flagVal = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
|
|
230
|
+
const spec = buildMemorySpec(process.cwd(), { theme: flagVal('--theme'), brand: flagVal('--brand') });
|
|
231
|
+
if (!themes[spec.theme]) spec.theme = 'atris';
|
|
232
|
+
if (args.includes('--block')) { console.log(JSON.stringify(renderBlock(spec, { title: 'Workspace memory', themes }), null, 2)); return; }
|
|
233
|
+
const html = renderHtml(spec, { title: 'Workspace memory', themes });
|
|
234
|
+
const out = flagVal('--out');
|
|
235
|
+
if (out) { fs.writeFileSync(out, html); console.log(`\n ✓ memory view written: ${out}\n`); }
|
|
236
|
+
else process.stdout.write(html + '\n');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
223
239
|
const daysIdx = args.indexOf('--days');
|
|
224
240
|
const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
|
|
225
241
|
const data = buildRecapData(process.cwd(), { days });
|
package/commands/site.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
20
21
|
|
|
21
22
|
const SCAN_EXTS = new Set(['.css', '.scss', '.sass', '.less', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte', '.astro',
|
|
22
23
|
'.md', '.mdx', '.txt']); // prose too: the voice doctrine (em-dash, hype-copy) is enforceable, not just advice
|
|
@@ -60,6 +61,7 @@ const RULES = [
|
|
|
60
61
|
why: 'decorative emoji in UI copy' },
|
|
61
62
|
{ id: 'em-dash', sev: 'warn',
|
|
62
63
|
re: /—/,
|
|
64
|
+
fix: (s) => s.replace(/\s*—\s*/g, ', '), // safe deterministic repair (prose)
|
|
63
65
|
why: 'em dash: a top AI-writing tell; use a comma, colon, or period' },
|
|
64
66
|
{ id: 'hype-copy', sev: 'error',
|
|
65
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,6 +70,86 @@ const RULES = [
|
|
|
68
70
|
|
|
69
71
|
const ICON = { error: '✗', warn: '⚠' }; // ✗ ⚠
|
|
70
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
|
+
|
|
71
153
|
function walk(target, out) {
|
|
72
154
|
let stat;
|
|
73
155
|
try { stat = fs.statSync(target); } catch { return out; }
|
|
@@ -85,14 +167,14 @@ function walk(target, out) {
|
|
|
85
167
|
return out;
|
|
86
168
|
}
|
|
87
169
|
|
|
88
|
-
function scanFile(file) {
|
|
170
|
+
function scanFile(file, rules = RULES) {
|
|
89
171
|
const findings = [];
|
|
90
172
|
let text;
|
|
91
173
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return findings; }
|
|
92
174
|
const lines = text.split('\n');
|
|
93
175
|
for (let i = 0; i < lines.length; i++) {
|
|
94
176
|
const line = lines[i];
|
|
95
|
-
for (const rule of
|
|
177
|
+
for (const rule of rules) {
|
|
96
178
|
const m = rule.re.exec(line);
|
|
97
179
|
if (m) {
|
|
98
180
|
findings.push({
|
|
@@ -108,15 +190,38 @@ function scanFile(file) {
|
|
|
108
190
|
function detect(argv) {
|
|
109
191
|
const json = argv.includes('--json');
|
|
110
192
|
const quiet = argv.includes('--quiet');
|
|
111
|
-
const
|
|
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());
|
|
112
197
|
|
|
113
|
-
|
|
114
|
-
|
|
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));
|
|
115
218
|
const errors = findings.filter((f) => f.sev === 'error').length;
|
|
116
219
|
|
|
117
220
|
if (json) {
|
|
118
221
|
console.log(JSON.stringify({
|
|
119
222
|
ok: findings.length === 0, scanned: files.length,
|
|
223
|
+
mode: diffMode ? (staged ? 'staged' : 'diff') : 'path',
|
|
224
|
+
fixed: fixed ? fixed.fixedCount : 0,
|
|
120
225
|
slop: findings.length, errors,
|
|
121
226
|
findings: findings.map((f) => ({ ...f, file: path.relative(process.cwd(), f.file) })),
|
|
122
227
|
}, null, 2));
|
|
@@ -146,28 +251,57 @@ function detect(argv) {
|
|
|
146
251
|
|
|
147
252
|
function slopCommand(argv) {
|
|
148
253
|
const sub = argv[0];
|
|
149
|
-
if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help'].includes(sub)) {
|
|
254
|
+
if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help', 'hook', 'install-hook'].includes(sub)) {
|
|
150
255
|
// default + `detect`: scan. Bare `atris slop` scans cwd too.
|
|
151
256
|
const rest = sub === 'detect' ? argv.slice(1) : argv;
|
|
152
257
|
return detect(rest);
|
|
153
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
|
+
|
|
154
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();
|
|
155
282
|
console.log('\n atris slop — deterministic rules:\n');
|
|
156
283
|
for (const r of RULES) console.log(` ${ICON[r.sev]} ${r.id.padEnd(20)} ${r.why}`);
|
|
157
|
-
console.log(
|
|
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`);
|
|
158
286
|
return 0;
|
|
159
287
|
}
|
|
160
288
|
// help
|
|
161
289
|
console.log(`
|
|
162
|
-
atris slop — deterministic
|
|
290
|
+
atris slop — deterministic slop detector + repairer (no LLM)
|
|
163
291
|
|
|
164
|
-
atris slop detect [path]
|
|
165
|
-
atris slop detect
|
|
166
|
-
atris slop
|
|
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)
|
|
167
300
|
|
|
301
|
+
Project rules live in .atris/slop.rules.json and compound over time.
|
|
168
302
|
exit 0 = clean, 1 = slop found. Wire into PR checks and the autopilot gate.
|
|
169
303
|
`);
|
|
170
304
|
return 0;
|
|
171
305
|
}
|
|
172
306
|
|
|
173
|
-
module.exports = { slopCommand, detect, scanFile, RULES };
|
|
307
|
+
module.exports = { slopCommand, detect, scanFile, RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
|