@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/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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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
+ }