@albsugy/aos 0.5.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/CHANGELOG.md +80 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/assets/skills/aos-ask/SKILL.md +16 -0
- package/assets/skills/aos-learn/SKILL.md +21 -0
- package/assets/skills/aos-ticket/SKILL.md +61 -0
- package/assets/skills/aos-verify/SKILL.md +20 -0
- package/assets/templates/decisions.md +10 -0
- package/assets/templates/learnings.md +4 -0
- package/assets/templates/pack.md +25 -0
- package/assets/templates/policy.yaml +35 -0
- package/dist/aos.mjs +200 -0
- package/dist/ui.html +224 -0
- package/install.sh +181 -0
- package/package.json +39 -0
package/dist/ui.html
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>AOS Console</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0e1116; --panel: #161b22; --border: #262d38;
|
|
10
|
+
--text: #d5dbe3; --muted: #8b949e;
|
|
11
|
+
--amber: #d29922; --red: #f85149; --green: #3fb950; --blue: #58a6ff;
|
|
12
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; margin: 0; }
|
|
15
|
+
body { background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
16
|
+
header { display: flex; align-items: baseline; gap: 16px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
|
|
17
|
+
header h1 { font-size: 15px; font-weight: 600; letter-spacing: .04em; }
|
|
18
|
+
header .kpis { display: flex; gap: 18px; margin-left: auto; color: var(--muted); font-size: 13px; }
|
|
19
|
+
header .kpis b { color: var(--text); font-family: var(--mono); font-weight: 600; }
|
|
20
|
+
main { max-width: 1100px; margin: 0 auto; padding: 20px; }
|
|
21
|
+
section { margin-bottom: 28px; }
|
|
22
|
+
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 10px; }
|
|
23
|
+
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
24
|
+
.project-head { display: flex; align-items: baseline; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
25
|
+
.project-head .name { font-weight: 600; }
|
|
26
|
+
.project-head .meta { color: var(--muted); font-size: 12.5px; }
|
|
27
|
+
.project-head .tokens { margin-left: auto; font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
|
|
28
|
+
table { width: 100%; border-collapse: collapse; }
|
|
29
|
+
th { text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 8px 16px; border-bottom: 1px solid var(--border); font-weight: 500; }
|
|
30
|
+
td { padding: 9px 16px; border-bottom: 1px solid var(--border); }
|
|
31
|
+
tr:last-child td { border-bottom: none; }
|
|
32
|
+
tbody tr { cursor: pointer; }
|
|
33
|
+
tbody tr:hover { background: #1b2230; }
|
|
34
|
+
td.num, th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
35
|
+
td.mono { font-family: var(--mono); font-size: 12.5px; }
|
|
36
|
+
.dim td { opacity: .55; } /* shipped/done rows recede; live work stands forward */
|
|
37
|
+
.chip { display: inline-block; padding: 1px 9px; border-radius: 20px; font-size: 12px; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }
|
|
38
|
+
.chip.awaiting-review { color: var(--amber); border-color: color-mix(in srgb, var(--amber) 45%, transparent); background: color-mix(in srgb, var(--amber) 10%, transparent); }
|
|
39
|
+
.chip.blocked, .chip.fail { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, transparent); background: color-mix(in srgb, var(--red) 10%, transparent); }
|
|
40
|
+
.chip.pass, .chip.done, .chip.shipped { color: var(--green); border-color: color-mix(in srgb, var(--green) 40%, transparent); }
|
|
41
|
+
.chip.in-progress { color: var(--blue); border-color: color-mix(in srgb, var(--blue) 45%, transparent); }
|
|
42
|
+
.queue-row { display: flex; align-items: center; gap: 14px; padding: 11px 16px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
43
|
+
.queue-row:hover { background: #1b2230; }
|
|
44
|
+
.queue-row:last-child { border-bottom: none; }
|
|
45
|
+
.queue-row .ticket { font-family: var(--mono); font-size: 13px; }
|
|
46
|
+
.queue-row .proj { color: var(--muted); font-size: 12.5px; }
|
|
47
|
+
.queue-row .age { margin-left: auto; color: var(--muted); font-size: 12px; font-family: var(--mono); }
|
|
48
|
+
.empty { padding: 28px 16px; text-align: center; color: var(--muted); }
|
|
49
|
+
.empty code { font-family: var(--mono); background: var(--panel); border: 1px solid var(--border); padding: 1px 6px; border-radius: 4px; }
|
|
50
|
+
#error { display: none; padding: 10px 16px; background: color-mix(in srgb, var(--red) 12%, transparent); border: 1px solid var(--red); border-radius: 8px; margin-bottom: 16px; color: var(--red); font-size: 13px; }
|
|
51
|
+
/* run detail drawer */
|
|
52
|
+
#drawer { position: fixed; top: 0; right: -560px; width: 560px; max-width: 92vw; height: 100vh; background: var(--panel); border-left: 1px solid var(--border); transition: right .18s ease; overflow-y: auto; z-index: 10; }
|
|
53
|
+
#drawer.open { right: 0; }
|
|
54
|
+
#drawer .head { position: sticky; top: 0; background: var(--panel); display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border); }
|
|
55
|
+
#drawer .head .close { margin-left: auto; cursor: pointer; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 2px 9px; background: none; font-size: 13px; }
|
|
56
|
+
#drawer .head .close:hover { color: var(--text); }
|
|
57
|
+
#drawer .body { padding: 16px 18px 40px; }
|
|
58
|
+
#drawer h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); margin: 18px 0 8px; }
|
|
59
|
+
#drawer pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; font-size: 12px; overflow-x: auto; white-space: pre-wrap; font-family: var(--mono); }
|
|
60
|
+
.timeline { border-left: 2px solid var(--border); margin-left: 6px; padding-left: 16px; }
|
|
61
|
+
.tl-item { position: relative; padding: 3px 0 10px; font-size: 12.5px; }
|
|
62
|
+
.tl-item::before { content: ""; position: absolute; left: -21.5px; top: 9px; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
|
|
63
|
+
.tl-item.gate::before { background: var(--amber); }
|
|
64
|
+
.tl-item.verify-pass::before { background: var(--green); }
|
|
65
|
+
.tl-item.verify-fail::before { background: var(--red); }
|
|
66
|
+
.tl-item .t { color: var(--muted); font-family: var(--mono); font-size: 11px; margin-right: 8px; }
|
|
67
|
+
.tl-item .s { color: var(--muted); font-family: var(--mono); font-size: 11.5px; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
68
|
+
#overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 9; }
|
|
69
|
+
#overlay.open { display: block; }
|
|
70
|
+
.loading { color: var(--muted); padding: 24px; text-align: center; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<header>
|
|
75
|
+
<h1>AOS CONSOLE</h1>
|
|
76
|
+
<div class="kpis" id="kpis"></div>
|
|
77
|
+
</header>
|
|
78
|
+
<main>
|
|
79
|
+
<div id="error"></div>
|
|
80
|
+
<div id="app"><div class="loading">Loading…</div></div>
|
|
81
|
+
</main>
|
|
82
|
+
<div id="overlay" onclick="closeDrawer()"></div>
|
|
83
|
+
<aside id="drawer" aria-label="Run detail"></aside>
|
|
84
|
+
<script>
|
|
85
|
+
const $ = (s) => document.querySelector(s);
|
|
86
|
+
let state = null;
|
|
87
|
+
|
|
88
|
+
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
89
|
+
function fmtTok(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? Math.round(n/1e3)+'k' : String(n); }
|
|
90
|
+
function rel(ts) {
|
|
91
|
+
const s = (Date.now() - new Date(ts).getTime()) / 1000;
|
|
92
|
+
if (isNaN(s)) return '';
|
|
93
|
+
if (s < 90) return 'now';
|
|
94
|
+
if (s < 3600) return Math.round(s/60) + 'm';
|
|
95
|
+
if (s < 86400) return Math.round(s/3600) + 'h';
|
|
96
|
+
return Math.round(s/86400) + 'd';
|
|
97
|
+
}
|
|
98
|
+
const STATE_TIP = {
|
|
99
|
+
'in-progress': 'Agent is working', 'blocked': 'Agent stopped — needs your input',
|
|
100
|
+
'awaiting-review': 'Done and verified — waiting for your review', 'done': 'Reviewed', 'shipped': 'Shipped'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function render() {
|
|
104
|
+
const projects = state.projects || [];
|
|
105
|
+
const queue = projects.flatMap(p => p.runs.filter(r => r.state === 'awaiting-review' || r.state === 'blocked').map(r => ({...r, project: p})));
|
|
106
|
+
const totals = {
|
|
107
|
+
projects: projects.length,
|
|
108
|
+
queue: queue.length,
|
|
109
|
+
tokens: projects.reduce((a,p) => a + p.tokens.input + p.tokens.output, 0),
|
|
110
|
+
};
|
|
111
|
+
$('#kpis').innerHTML =
|
|
112
|
+
`<span><b>${totals.projects}</b> projects</span>` +
|
|
113
|
+
`<span title="Runs blocked or awaiting your review"><b>${totals.queue}</b> need you</span>` +
|
|
114
|
+
`<span title="Total tokens across all sessions"><b>${fmtTok(totals.tokens)}</b> tokens</span>`;
|
|
115
|
+
|
|
116
|
+
if (!projects.length) {
|
|
117
|
+
$('#app').innerHTML = `<div class="card"><div class="empty">No projects yet.<br>Run <code>aos init</code> inside a repo to register one.</div></div>`;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let html = '';
|
|
122
|
+
html += `<section><h2 title="Everything that is blocked on a human decision">Decision queue</h2><div class="card">`;
|
|
123
|
+
if (!queue.length) {
|
|
124
|
+
html += `<div class="empty">Nothing needs you. ✨</div>`;
|
|
125
|
+
} else {
|
|
126
|
+
for (const r of queue) {
|
|
127
|
+
html += `<div class="queue-row" onclick="openRun('${esc(r.project.id)}','${esc(r.run)}')">
|
|
128
|
+
<span class="chip ${esc(r.state)}" title="${esc(STATE_TIP[r.state]||'')}">${esc(r.state)}</span>
|
|
129
|
+
<span class="ticket">${esc(r.ticket || r.run)}</span>
|
|
130
|
+
<span class="proj">${esc(r.project.name)}</span>
|
|
131
|
+
<span class="age" title="Last update">${rel(r.updated)}</span>
|
|
132
|
+
</div>`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
html += `</div></section>`;
|
|
136
|
+
|
|
137
|
+
for (const p of projects) {
|
|
138
|
+
const lev = p.leverage === null ? '—' : p.leverage + '%';
|
|
139
|
+
html += `<section><div class="card">
|
|
140
|
+
<div class="project-head">
|
|
141
|
+
<span class="name">${esc(p.name)}</span>
|
|
142
|
+
<span class="meta" title="Share of finished runs that passed verification on the first attempt">leverage ${lev}</span>
|
|
143
|
+
${p.activeRun ? `<span class="meta" style="color:var(--blue)">● active: ${esc(p.activeRun)}</span>` : ''}
|
|
144
|
+
<span class="tokens" title="Session tokens in/out">${fmtTok(p.tokens.input)} in · ${fmtTok(p.tokens.output)} out</span>
|
|
145
|
+
</div>`;
|
|
146
|
+
if (!p.runs.length) {
|
|
147
|
+
html += `<div class="empty">No runs yet — start one with <code>/aos-ticket</code> in Claude Code.</div>`;
|
|
148
|
+
} else {
|
|
149
|
+
html += `<table><thead><tr>
|
|
150
|
+
<th>Run</th><th>Ticket</th><th>State</th><th>Verify</th>
|
|
151
|
+
<th class="num" title="Verification attempts before pass">Attempts</th>
|
|
152
|
+
<th class="num">Tokens</th><th class="num">Updated</th>
|
|
153
|
+
</tr></thead><tbody>`;
|
|
154
|
+
for (const r of p.runs) {
|
|
155
|
+
const dim = ['done','shipped'].includes(r.state) ? ' class="dim"' : '';
|
|
156
|
+
html += `<tr${dim} onclick="openRun('${esc(p.id)}','${esc(r.run)}')">
|
|
157
|
+
<td class="mono">${esc(r.run)}</td>
|
|
158
|
+
<td>${esc(r.title || r.ticket || '')}</td>
|
|
159
|
+
<td><span class="chip ${esc(r.state)}" title="${esc(STATE_TIP[r.state]||'')}">${esc(r.state)}</span></td>
|
|
160
|
+
<td><span class="chip ${esc(r.verification)}">${esc(r.verification)}</span></td>
|
|
161
|
+
<td class="num">${r.verification_attempts || 0}</td>
|
|
162
|
+
<td class="num">${fmtTok((r.tokens?.input||0)+(r.tokens?.output||0))}</td>
|
|
163
|
+
<td class="num" title="${esc(r.updated)}">${rel(r.updated)}</td>
|
|
164
|
+
</tr>`;
|
|
165
|
+
}
|
|
166
|
+
html += `</tbody></table>`;
|
|
167
|
+
}
|
|
168
|
+
html += `</div></section>`;
|
|
169
|
+
}
|
|
170
|
+
$('#app').innerHTML = html;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function refresh() {
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch('/api/state');
|
|
176
|
+
if (!res.ok) throw new Error('API ' + res.status);
|
|
177
|
+
state = await res.json();
|
|
178
|
+
$('#error').style.display = 'none';
|
|
179
|
+
render();
|
|
180
|
+
} catch (e) {
|
|
181
|
+
$('#error').textContent = 'Console cannot reach the AOS API: ' + e.message;
|
|
182
|
+
$('#error').style.display = 'block';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function openRun(projectId, runId) {
|
|
187
|
+
$('#drawer').innerHTML = `<div class="head"><b>${esc(runId)}</b><button class="close" onclick="closeDrawer()" title="Close (Esc)">✕</button></div><div class="loading">Loading…</div>`;
|
|
188
|
+
$('#drawer').classList.add('open');
|
|
189
|
+
$('#overlay').classList.add('open');
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(`/api/run?project=${encodeURIComponent(projectId)}&run=${encodeURIComponent(runId)}`);
|
|
192
|
+
const d = await res.json();
|
|
193
|
+
let body = `<div class="body">`;
|
|
194
|
+
body += `<span class="chip ${esc(d.meta.state)}">${esc(d.meta.state)}</span> <span class="chip ${esc(d.meta.verification)}">verify: ${esc(d.meta.verification)}</span>`;
|
|
195
|
+
if (d.outcome) body += `<h3>Outcome</h3><pre>${esc(d.outcome)}</pre>`;
|
|
196
|
+
if (d.verification) body += `<h3>Verification</h3><pre>${esc(d.verification)}</pre>`;
|
|
197
|
+
if (d.audit?.length) {
|
|
198
|
+
body += `<h3>Audit timeline</h3><div class="timeline">`;
|
|
199
|
+
for (const a of [...d.audit].reverse()) {
|
|
200
|
+
const cls = a.event === 'gate' ? 'gate' : a.event === 'verify' ? (a.verdict === 'pass' ? 'verify-pass' : 'verify-fail') : '';
|
|
201
|
+
const label = a.event === 'tool' ? esc(a.tool) : esc(a.event) + (a.decision ? ` → ${esc(a.decision)}` : '') + (a.verdict ? ` → ${esc(a.verdict)}` : '');
|
|
202
|
+
body += `<div class="tl-item ${cls}"><span class="t">${esc((a.ts||'').slice(11,19))}</span>${label}${a.summary || a.command ? `<span class="s" title="${esc(a.summary || a.command)}">${esc(a.summary || a.command)}</span>` : ''}</div>`;
|
|
203
|
+
}
|
|
204
|
+
body += `</div>`;
|
|
205
|
+
}
|
|
206
|
+
if (d.ticket) body += `<h3>Ticket</h3><pre>${esc(d.ticket)}</pre>`;
|
|
207
|
+
if (d.plan) body += `<h3>Plan</h3><pre>${esc(d.plan)}</pre>`;
|
|
208
|
+
body += `</div>`;
|
|
209
|
+
$('#drawer').innerHTML = `<div class="head"><b>${esc(runId)}</b><button class="close" onclick="closeDrawer()" title="Close (Esc)">✕</button></div>` + body;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
$('#drawer').innerHTML += `<div class="empty">Failed to load run: ${esc(e.message)}</div>`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function closeDrawer() {
|
|
215
|
+
$('#drawer').classList.remove('open');
|
|
216
|
+
$('#overlay').classList.remove('open');
|
|
217
|
+
}
|
|
218
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeDrawer(); });
|
|
219
|
+
|
|
220
|
+
refresh();
|
|
221
|
+
setInterval(refresh, 5000);
|
|
222
|
+
</script>
|
|
223
|
+
</body>
|
|
224
|
+
</html>
|
package/install.sh
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# AOS installer
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://cdn.jsdelivr.net/npm/@albsugy/aos/install.sh | bash
|
|
5
|
+
# # or: npm i -g @albsugy/aos
|
|
6
|
+
#
|
|
7
|
+
# Installs the compiled bundle published on the npm registry: resolve version →
|
|
8
|
+
# download tarball → verify registry integrity hash → unpack → symlink.
|
|
9
|
+
# No git, no source code, no npm client needed.
|
|
10
|
+
#
|
|
11
|
+
# Overrides:
|
|
12
|
+
# AOS_VERSION version to install, e.g. 0.5.0 or v0.5.0 (default: latest)
|
|
13
|
+
# AOS_INSTALL_DIR where the app lives (default: ~/.local/share/aos)
|
|
14
|
+
# AOS_BIN_DIR where the symlink goes (default: ~/.local/bin)
|
|
15
|
+
# AOS_NPM_PKG package name (default: @albsugy/aos)
|
|
16
|
+
# AOS_NPM_REGISTRY registry base URL (default: https://registry.npmjs.org)
|
|
17
|
+
# AOS_TARBALL_URL direct tarball URL (mirrors / testing); checksum fetched from <url>.sha256
|
|
18
|
+
# AOS_FROM_SOURCE =1 to clone and build from source (requires repo access, git, npm)
|
|
19
|
+
# AOS_REPO_URL source-mode repo URL (default: git@github.com:albsugy/aos.git)
|
|
20
|
+
# AOS_REF source-mode branch/tag (default: main)
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
PKG="${AOS_NPM_PKG:-@albsugy/aos}"
|
|
24
|
+
REG="${AOS_NPM_REGISTRY:-https://registry.npmjs.org}"
|
|
25
|
+
REPO_URL="${AOS_REPO_URL:-git@github.com:albsugy/aos.git}"
|
|
26
|
+
INSTALL_DIR="${AOS_INSTALL_DIR:-$HOME/.local/share/aos}"
|
|
27
|
+
BIN_DIR="${AOS_BIN_DIR:-$HOME/.local/bin}"
|
|
28
|
+
VERSION="${AOS_VERSION:-latest}"
|
|
29
|
+
VERSION="${VERSION#v}"
|
|
30
|
+
REF="${AOS_REF:-main}"
|
|
31
|
+
FROM_SOURCE="${AOS_FROM_SOURCE:-0}"
|
|
32
|
+
|
|
33
|
+
info() { printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
|
34
|
+
ok() { printf '\033[1;32m✔\033[0m %s\n' "$*"; }
|
|
35
|
+
fail() { printf '\033[1;31m✖\033[0m %s\n' "$*" >&2; exit 1; }
|
|
36
|
+
|
|
37
|
+
# --- prerequisites -----------------------------------------------------------
|
|
38
|
+
command -v curl >/dev/null 2>&1 || fail "curl is required."
|
|
39
|
+
command -v tar >/dev/null 2>&1 || fail "tar is required."
|
|
40
|
+
command -v node >/dev/null 2>&1 || fail "Node.js >= 22 is required. Install from https://nodejs.org and re-run."
|
|
41
|
+
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
42
|
+
[ "$NODE_MAJOR" -ge 22 ] || fail "Node.js >= 22 required (found $(node -v))."
|
|
43
|
+
ok "Prerequisites: node $(node -v)"
|
|
44
|
+
|
|
45
|
+
# A git checkout at the install path is a dev install — don't clobber it.
|
|
46
|
+
if [ -d "$INSTALL_DIR/.git" ] && [ "$FROM_SOURCE" != "1" ]; then
|
|
47
|
+
fail "Found a source checkout at $INSTALL_DIR — update it with 'git pull', or remove it to switch to release installs."
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
unpack_tarball() {
|
|
51
|
+
# npm tarballs nest everything under package/ — strip it; plain tarballs pass through.
|
|
52
|
+
local tarball="$1" dest="$2" first
|
|
53
|
+
mkdir -p "$dest"
|
|
54
|
+
first="$(tar -tzf "$tarball" | head -1)"
|
|
55
|
+
case "$first" in
|
|
56
|
+
package/*) tar -xzf "$tarball" -C "$dest" --strip-components=1 ;;
|
|
57
|
+
*) tar -xzf "$tarball" -C "$dest" ;;
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if [ "$FROM_SOURCE" = "1" ]; then
|
|
62
|
+
# --- contributor path (requires repo access): clone + build ----------------
|
|
63
|
+
command -v git >/dev/null 2>&1 || fail "git is required for source installs."
|
|
64
|
+
command -v npm >/dev/null 2>&1 || fail "npm is required for source installs."
|
|
65
|
+
if [ -d "$INSTALL_DIR/.git" ]; then
|
|
66
|
+
info "Updating source checkout"
|
|
67
|
+
git -C "$INSTALL_DIR" pull --ff-only -q
|
|
68
|
+
else
|
|
69
|
+
info "Cloning $REPO_URL (ref: $REF)"
|
|
70
|
+
mkdir -p "$(dirname "$INSTALL_DIR")"
|
|
71
|
+
git clone -q --depth 1 --branch "$REF" "$REPO_URL" "$INSTALL_DIR"
|
|
72
|
+
fi
|
|
73
|
+
info "Building"
|
|
74
|
+
( cd "$INSTALL_DIR" && npm ci --no-fund --no-audit --loglevel=error >/dev/null && npm run build >/dev/null )
|
|
75
|
+
else
|
|
76
|
+
TMP="$(mktemp -d)"
|
|
77
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
78
|
+
|
|
79
|
+
if [ -n "${AOS_TARBALL_URL:-}" ]; then
|
|
80
|
+
# --- direct tarball (mirrors / testing): sha256 sidecar verification -----
|
|
81
|
+
info "Downloading $AOS_TARBALL_URL"
|
|
82
|
+
curl -fsSL -o "$TMP/aos.tgz" "$AOS_TARBALL_URL" || fail "Download failed."
|
|
83
|
+
curl -fsSL -o "$TMP/aos.tgz.sha256" "$AOS_TARBALL_URL.sha256" || fail "Checksum download failed."
|
|
84
|
+
EXPECTED="$(awk '{print $1}' "$TMP/aos.tgz.sha256")"
|
|
85
|
+
ACTUAL="$(node -e 'const c=require("crypto"),f=require("fs");console.log(c.createHash("sha256").update(f.readFileSync(process.argv[1])).digest("hex"))' "$TMP/aos.tgz")"
|
|
86
|
+
if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then
|
|
87
|
+
fail "Checksum verification FAILED — refusing to install."
|
|
88
|
+
fi
|
|
89
|
+
ok "Checksum verified (sha256)"
|
|
90
|
+
else
|
|
91
|
+
# --- standard path: the npm registry --------------------------------------
|
|
92
|
+
META_URL="$REG/$PKG/$VERSION"
|
|
93
|
+
info "Resolving $PKG@$VERSION from $REG"
|
|
94
|
+
META="$(curl -fsSL "$META_URL")" || fail "Could not resolve $PKG@$VERSION — is it published?"
|
|
95
|
+
RESOLVED="$(printf '%s' "$META" | node -e 'const m=JSON.parse(require("fs").readFileSync(0,"utf8"));process.stdout.write(m.version||"")')"
|
|
96
|
+
TARBALL="$(printf '%s' "$META" | node -e 'const m=JSON.parse(require("fs").readFileSync(0,"utf8"));process.stdout.write((m.dist&&m.dist.tarball)||"")')"
|
|
97
|
+
INTEGRITY="$(printf '%s' "$META" | node -e 'const m=JSON.parse(require("fs").readFileSync(0,"utf8"));process.stdout.write((m.dist&&m.dist.integrity)||"")')"
|
|
98
|
+
if [ -z "$RESOLVED" ] || [ -z "$TARBALL" ]; then
|
|
99
|
+
fail "Registry metadata is malformed."
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
info "Downloading $PKG@$RESOLVED"
|
|
103
|
+
curl -fsSL -o "$TMP/aos.tgz" "$TARBALL" || fail "Download failed."
|
|
104
|
+
|
|
105
|
+
if [ -n "$INTEGRITY" ]; then
|
|
106
|
+
node -e '
|
|
107
|
+
const crypto = require("crypto"), fs = require("fs");
|
|
108
|
+
const [file, integrity] = process.argv.slice(1);
|
|
109
|
+
const dash = integrity.indexOf("-");
|
|
110
|
+
const algo = integrity.slice(0, dash), expected = integrity.slice(dash + 1);
|
|
111
|
+
const actual = crypto.createHash(algo).update(fs.readFileSync(file)).digest("base64");
|
|
112
|
+
process.exit(actual === expected ? 0 : 1);
|
|
113
|
+
' "$TMP/aos.tgz" "$INTEGRITY" || fail "Integrity verification FAILED — refusing to install."
|
|
114
|
+
ok "Integrity verified (${INTEGRITY%%-*}, from registry)"
|
|
115
|
+
else
|
|
116
|
+
fail "Registry provided no integrity hash — refusing to install."
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
unpack_tarball "$TMP/aos.tgz" "$TMP/unpack"
|
|
121
|
+
[ -f "$TMP/unpack/dist/aos.mjs" ] || fail "Artifact is malformed (dist/aos.mjs missing)."
|
|
122
|
+
[ -d "$TMP/unpack/assets" ] || fail "Artifact is malformed (assets/ missing)."
|
|
123
|
+
|
|
124
|
+
rm -rf "$INSTALL_DIR"
|
|
125
|
+
mkdir -p "$(dirname "$INSTALL_DIR")"
|
|
126
|
+
mv "$TMP/unpack" "$INSTALL_DIR"
|
|
127
|
+
ok "Installed to $INSTALL_DIR"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# --- link the compiled bundle --------------------------------------------------
|
|
131
|
+
[ -f "$INSTALL_DIR/dist/aos.mjs" ] || fail "Compiled bundle missing at $INSTALL_DIR/dist/aos.mjs."
|
|
132
|
+
chmod +x "$INSTALL_DIR/dist/aos.mjs"
|
|
133
|
+
mkdir -p "$BIN_DIR"
|
|
134
|
+
ln -sf "$INSTALL_DIR/dist/aos.mjs" "$BIN_DIR/aos"
|
|
135
|
+
ok "Linked $BIN_DIR/aos → compiled bundle"
|
|
136
|
+
|
|
137
|
+
# --- make sure BIN_DIR is on PATH -------------------------------------------
|
|
138
|
+
ensure_path() {
|
|
139
|
+
case ":$PATH:" in
|
|
140
|
+
*":$BIN_DIR:"*) return 0 ;;
|
|
141
|
+
esac
|
|
142
|
+
local rc=""
|
|
143
|
+
local shell_name
|
|
144
|
+
shell_name="$(basename "${SHELL:-sh}")"
|
|
145
|
+
case "$shell_name" in
|
|
146
|
+
zsh) rc="$HOME/.zshrc" ;;
|
|
147
|
+
bash) rc="$HOME/.bashrc" ;;
|
|
148
|
+
fish) rc="$HOME/.config/fish/config.fish" ;;
|
|
149
|
+
*) rc="$HOME/.profile" ;;
|
|
150
|
+
esac
|
|
151
|
+
if [ "$shell_name" = "fish" ]; then
|
|
152
|
+
mkdir -p "$(dirname "$rc")"
|
|
153
|
+
if ! grep -qs "aos installer" "$rc"; then
|
|
154
|
+
printf '\n# added by aos installer\nfish_add_path %s\n' "$BIN_DIR" >> "$rc"
|
|
155
|
+
fi
|
|
156
|
+
else
|
|
157
|
+
if ! grep -qs "aos installer" "$rc"; then
|
|
158
|
+
# shellcheck disable=SC2016 # literal $PATH is intentional — it must expand when the rc runs, not now
|
|
159
|
+
printf '\n# added by aos installer\nexport PATH="%s:$PATH"\n' "$BIN_DIR" >> "$rc"
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
info "Added $BIN_DIR to PATH in $rc — restart your shell or: export PATH=\"$BIN_DIR:\$PATH\""
|
|
163
|
+
}
|
|
164
|
+
ensure_path
|
|
165
|
+
|
|
166
|
+
# --- verify ------------------------------------------------------------------
|
|
167
|
+
VERSION_OUT="$("$BIN_DIR/aos" version 2>/dev/null || true)"
|
|
168
|
+
case "$VERSION_OUT" in
|
|
169
|
+
aos*) ok "Installed: $VERSION_OUT" ;;
|
|
170
|
+
*) fail "Install verification failed — try running: node $INSTALL_DIR/dist/aos.mjs version" ;;
|
|
171
|
+
esac
|
|
172
|
+
|
|
173
|
+
cat <<'EOF'
|
|
174
|
+
|
|
175
|
+
Next steps:
|
|
176
|
+
cd <your repo> && aos init # register the project, install skills + hooks
|
|
177
|
+
aos status # see all projects and runs
|
|
178
|
+
aos console # local dashboard at http://127.0.0.1:4560
|
|
179
|
+
|
|
180
|
+
Package: https://www.npmjs.com/package/@albsugy/aos
|
|
181
|
+
EOF
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@albsugy/aos",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Agent Operations Stack — portable spec, guardrails, audit, verification, and console for operating AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aos": "dist/aos.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "bash test/smoke.sh",
|
|
14
|
+
"build": "node scripts/build.mjs",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"assets",
|
|
20
|
+
"install.sh",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"ai-agents",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"agent-ops",
|
|
30
|
+
"guardrails",
|
|
31
|
+
"audit"
|
|
32
|
+
],
|
|
33
|
+
"author": "Medhat Albsugy",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"esbuild": "^0.28.1",
|
|
37
|
+
"yaml": "^2.4.5"
|
|
38
|
+
}
|
|
39
|
+
}
|