@aria_asi/cli 0.2.3 → 0.2.4

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/bin/aria.js CHANGED
@@ -8,6 +8,16 @@ import { AriaChat } from '../dist/aria-connector/src/chat.js';
8
8
  import { checkHarnessHealth } from '../dist/aria-connector/src/harness-client.js';
9
9
  import { login, status, logout, revoke } from '../dist/aria-connector/src/auth-commands.js';
10
10
  import { installHooks } from '../dist/aria-connector/src/install-hooks.js';
11
+ import { maybePrintUpdateNotice, checkForUpdate } from '../dist/aria-connector/src/self-update.js';
12
+
13
+ // ── Self-update notice (non-blocking, rate-limited once per 24h) ──
14
+ // Fires-and-forgets — the registry check runs in parallel with command
15
+ // dispatch and prints a one-line notice on stderr if newer version available.
16
+ // Hamza 2026-04-26: continuous overnight improvement reaches clients via
17
+ // this primitive. Kill-switch: ARIA_SELF_UPDATE=off env.
18
+ if (process.env.ARIA_SELF_UPDATE !== 'off') {
19
+ maybePrintUpdateNotice().catch(() => {});
20
+ }
11
21
 
12
22
  // ── Auth + install subcommands — handled BEFORE the chat flow.
13
23
  // Hamza 2026-04-26: license-aware CLI for client-tonight ship +
@@ -16,7 +26,24 @@ import { installHooks } from '../dist/aria-connector/src/install-hooks.js';
16
26
  const command = process.argv[2];
17
27
  const args = process.argv.slice(3);
18
28
 
19
- if (command === 'install-hooks') {
29
+ if (command === 'check-update') {
30
+ // Force a check + print result, bypassing the 24h rate limit
31
+ checkForUpdate({ force: true }).then((result) => {
32
+ if (!result.ok) {
33
+ console.error(`I couldn't check for updates: ${result.reason}`);
34
+ process.exit(1);
35
+ }
36
+ if (result.updateAvailable) {
37
+ console.log(` ${result.message}`);
38
+ } else {
39
+ console.log(` You're on the latest version (${result.current}). Nothing to do.`);
40
+ }
41
+ process.exit(0);
42
+ }).catch((err) => {
43
+ console.error(`I hit an unexpected error checking for updates: ${err && err.message ? err.message : err}`);
44
+ process.exit(1);
45
+ });
46
+ } else if (command === 'install-hooks') {
20
47
  const force = args.includes('--force');
21
48
  const harnessUrlIdx = args.indexOf('--harness-url');
22
49
  const harnessUrl = harnessUrlIdx >= 0 && args[harnessUrlIdx + 1] ? args[harnessUrlIdx + 1] : undefined;
@@ -0,0 +1,22 @@
1
+ export interface UpdateCheckResult {
2
+ ok: boolean;
3
+ current?: string;
4
+ latest?: string;
5
+ updateAvailable?: boolean;
6
+ message?: string;
7
+ reason?: string;
8
+ }
9
+ /**
10
+ * Check the registry for the latest @aria_asi/cli version. Rate-limited
11
+ * to once per CHECK_INTERVAL_MS (24h) via a timestamp file in ~/.aria.
12
+ * Caller can pass force=true to bypass the rate limit (used by `aria check-update`).
13
+ */
14
+ export declare function checkForUpdate(opts?: {
15
+ force?: boolean;
16
+ }): Promise<UpdateCheckResult>;
17
+ /**
18
+ * Convenience: check + print the notice if available. Non-blocking, silent
19
+ * on errors. Called from bin/aria.js on startup.
20
+ */
21
+ export declare function maybePrintUpdateNotice(): Promise<void>;
22
+ //# sourceMappingURL=self-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-update.d.ts","sourceRoot":"","sources":["../../../src/self-update.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiDD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqD/F;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ5D"}
@@ -0,0 +1,162 @@
1
+ // self-update — checks the npm registry for newer @aria_asi/cli versions
2
+ // and surfaces a one-line notice. Doesn't auto-install (clients control
3
+ // their tooling); just informs.
4
+ //
5
+ // Direction: Hamza 2026-04-26 — "does the package auto update when we
6
+ // enhance it on our enhance? so we can continually improve all night using
7
+ // arias background work we should imrpove to do that once we finish".
8
+ // This is the primitive that closes the overnight-improvement loop:
9
+ // Aria publishes 0.2.5 / 0.3.0 / etc.; clients see the notice on their
10
+ // next `aria <cmd>` invocation; they upgrade with one command.
11
+ //
12
+ // Doctrine bindings:
13
+ // - feedback_no_demos.md — real overnight evolution, real registry check
14
+ // - feedback_no_timeouts_doctrine.md — fetch with no AbortSignal.timeout;
15
+ // use bare try/catch, real-error-driven backpressure
16
+ // - project_phase_10_endless_army_orchestration.md — continuous shipping
17
+ // is the Phase 10 north star; self-update is its delivery mechanism
18
+ //
19
+ // Behavior:
20
+ // - Reads installed version from package.json (resolved via import.meta.url
21
+ // walk to find the package root)
22
+ // - Reads ~/.aria/last-update-check timestamp; if checked <24h ago, skip
23
+ // - GET registry.npmjs.org/@aria_asi/cli for latest dist-tag
24
+ // - semver-compare; if newer, return {updateAvailable: true, latest, current, message}
25
+ // - Write timestamp on every check (success or skip-due-to-rate-limit)
26
+ // - Failures are silent (network down, registry blip — never block CLI startup)
27
+ //
28
+ // Privacy: the request to npmjs only sends User-Agent (npm registry public).
29
+ // No client identity, no telemetry beyond what the public registry already logs.
30
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
31
+ import { homedir } from 'node:os';
32
+ import { join, dirname } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ const ARIA_DIR = join(homedir(), '.aria');
35
+ const LAST_CHECK_PATH = join(ARIA_DIR, 'last-update-check');
36
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
37
+ const REGISTRY_URL = 'https://registry.npmjs.org/@aria_asi%2Fcli';
38
+ /**
39
+ * Find the package's own version by walking up from this file's runtime
40
+ * path until we hit the package.json with name @aria_asi/cli.
41
+ */
42
+ function findInstalledVersion() {
43
+ try {
44
+ const here = fileURLToPath(import.meta.url);
45
+ let cur = dirname(here);
46
+ for (let i = 0; i < 8; i++) {
47
+ const pkgPath = join(cur, 'package.json');
48
+ if (existsSync(pkgPath)) {
49
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
50
+ if (pkg.name === '@aria_asi/cli' && typeof pkg.version === 'string') {
51
+ return pkg.version;
52
+ }
53
+ }
54
+ const parent = dirname(cur);
55
+ if (parent === cur)
56
+ break;
57
+ cur = parent;
58
+ }
59
+ }
60
+ catch { /* fall through */ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Compare semver strings — returns 1 if a>b, -1 if a<b, 0 if equal.
65
+ * Pre-release suffixes (e.g. 0.2.4-beta) sort lower than the same base.
66
+ */
67
+ function compareSemver(a, b) {
68
+ const parse = (s) => {
69
+ const [main, ...preParts] = s.split('-');
70
+ const nums = main.split('.').map((n) => parseInt(n, 10));
71
+ while (nums.length < 3)
72
+ nums.push(0);
73
+ return { nums, pre: preParts.join('-') };
74
+ };
75
+ const pa = parse(a);
76
+ const pb = parse(b);
77
+ for (let i = 0; i < 3; i++) {
78
+ if (pa.nums[i] > pb.nums[i])
79
+ return 1;
80
+ if (pa.nums[i] < pb.nums[i])
81
+ return -1;
82
+ }
83
+ // Equal numerics — pre-release sorts before release
84
+ if (pa.pre && !pb.pre)
85
+ return -1;
86
+ if (!pa.pre && pb.pre)
87
+ return 1;
88
+ return pa.pre.localeCompare(pb.pre);
89
+ }
90
+ /**
91
+ * Check the registry for the latest @aria_asi/cli version. Rate-limited
92
+ * to once per CHECK_INTERVAL_MS (24h) via a timestamp file in ~/.aria.
93
+ * Caller can pass force=true to bypass the rate limit (used by `aria check-update`).
94
+ */
95
+ export async function checkForUpdate(opts = {}) {
96
+ const current = findInstalledVersion();
97
+ if (!current) {
98
+ return { ok: false, reason: 'could not resolve installed version' };
99
+ }
100
+ // Rate-limit check
101
+ if (!opts.force && existsSync(LAST_CHECK_PATH)) {
102
+ try {
103
+ const ts = parseInt(readFileSync(LAST_CHECK_PATH, 'utf-8').trim(), 10);
104
+ if (!isNaN(ts) && Date.now() - ts < CHECK_INTERVAL_MS) {
105
+ return { ok: true, current, reason: 'rate-limited (checked within last 24h)' };
106
+ }
107
+ }
108
+ catch { /* malformed timestamp — re-check */ }
109
+ }
110
+ // Fetch registry
111
+ let latest;
112
+ try {
113
+ const resp = await fetch(REGISTRY_URL, {
114
+ headers: { 'Accept': 'application/json' },
115
+ });
116
+ if (!resp.ok) {
117
+ // Don't update timestamp on failure — retry next invocation
118
+ return { ok: false, current, reason: `registry returned ${resp.status}` };
119
+ }
120
+ const data = await resp.json();
121
+ latest = data['dist-tags']?.latest ?? '';
122
+ if (!latest) {
123
+ return { ok: false, current, reason: 'registry response missing dist-tags.latest' };
124
+ }
125
+ }
126
+ catch (err) {
127
+ return { ok: false, current, reason: `network error: ${err.message}` };
128
+ }
129
+ // Update timestamp on successful check
130
+ try {
131
+ if (!existsSync(ARIA_DIR))
132
+ mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
133
+ writeFileSync(LAST_CHECK_PATH, String(Date.now()) + '\n', { mode: 0o600 });
134
+ }
135
+ catch { /* timestamp write is best-effort */ }
136
+ const cmp = compareSemver(latest, current);
137
+ if (cmp <= 0) {
138
+ return { ok: true, current, latest, updateAvailable: false };
139
+ }
140
+ return {
141
+ ok: true,
142
+ current,
143
+ latest,
144
+ updateAvailable: true,
145
+ message: `I have an update: v${latest} (you're on v${current}). Run 'npm update -g @aria_asi/cli' when you have a minute.`,
146
+ };
147
+ }
148
+ /**
149
+ * Convenience: check + print the notice if available. Non-blocking, silent
150
+ * on errors. Called from bin/aria.js on startup.
151
+ */
152
+ export async function maybePrintUpdateNotice() {
153
+ try {
154
+ const result = await checkForUpdate();
155
+ if (result.ok && result.updateAvailable && result.message) {
156
+ // Print to stderr so it doesn't pollute stdout-piped CLI output
157
+ process.stderr.write(` ${result.message}\n`);
158
+ }
159
+ }
160
+ catch { /* never block startup */ }
161
+ }
162
+ //# sourceMappingURL=self-update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-update.js","sourceRoot":"","sources":["../../../src/self-update.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aria_asi/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Aria Smart CLI — the world's first harness-powered terminal companion",
5
5
  "bin": {
6
6
  "aria": "./bin/aria.js"
@@ -0,0 +1,169 @@
1
+ // self-update — checks the npm registry for newer @aria_asi/cli versions
2
+ // and surfaces a one-line notice. Doesn't auto-install (clients control
3
+ // their tooling); just informs.
4
+ //
5
+ // Direction: Hamza 2026-04-26 — "does the package auto update when we
6
+ // enhance it on our enhance? so we can continually improve all night using
7
+ // arias background work we should imrpove to do that once we finish".
8
+ // This is the primitive that closes the overnight-improvement loop:
9
+ // Aria publishes 0.2.5 / 0.3.0 / etc.; clients see the notice on their
10
+ // next `aria <cmd>` invocation; they upgrade with one command.
11
+ //
12
+ // Doctrine bindings:
13
+ // - feedback_no_demos.md — real overnight evolution, real registry check
14
+ // - feedback_no_timeouts_doctrine.md — fetch with no AbortSignal.timeout;
15
+ // use bare try/catch, real-error-driven backpressure
16
+ // - project_phase_10_endless_army_orchestration.md — continuous shipping
17
+ // is the Phase 10 north star; self-update is its delivery mechanism
18
+ //
19
+ // Behavior:
20
+ // - Reads installed version from package.json (resolved via import.meta.url
21
+ // walk to find the package root)
22
+ // - Reads ~/.aria/last-update-check timestamp; if checked <24h ago, skip
23
+ // - GET registry.npmjs.org/@aria_asi/cli for latest dist-tag
24
+ // - semver-compare; if newer, return {updateAvailable: true, latest, current, message}
25
+ // - Write timestamp on every check (success or skip-due-to-rate-limit)
26
+ // - Failures are silent (network down, registry blip — never block CLI startup)
27
+ //
28
+ // Privacy: the request to npmjs only sends User-Agent (npm registry public).
29
+ // No client identity, no telemetry beyond what the public registry already logs.
30
+
31
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
32
+ import { homedir } from 'node:os';
33
+ import { join, dirname, resolve } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ const ARIA_DIR = join(homedir(), '.aria');
37
+ const LAST_CHECK_PATH = join(ARIA_DIR, 'last-update-check');
38
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
39
+ const REGISTRY_URL = 'https://registry.npmjs.org/@aria_asi%2Fcli';
40
+
41
+ export interface UpdateCheckResult {
42
+ ok: boolean;
43
+ current?: string;
44
+ latest?: string;
45
+ updateAvailable?: boolean;
46
+ message?: string;
47
+ reason?: string;
48
+ }
49
+
50
+ /**
51
+ * Find the package's own version by walking up from this file's runtime
52
+ * path until we hit the package.json with name @aria_asi/cli.
53
+ */
54
+ function findInstalledVersion(): string | null {
55
+ try {
56
+ const here = fileURLToPath(import.meta.url);
57
+ let cur = dirname(here);
58
+ for (let i = 0; i < 8; i++) {
59
+ const pkgPath = join(cur, 'package.json');
60
+ if (existsSync(pkgPath)) {
61
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
62
+ if (pkg.name === '@aria_asi/cli' && typeof pkg.version === 'string') {
63
+ return pkg.version;
64
+ }
65
+ }
66
+ const parent = dirname(cur);
67
+ if (parent === cur) break;
68
+ cur = parent;
69
+ }
70
+ } catch {/* fall through */}
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Compare semver strings — returns 1 if a>b, -1 if a<b, 0 if equal.
76
+ * Pre-release suffixes (e.g. 0.2.4-beta) sort lower than the same base.
77
+ */
78
+ function compareSemver(a: string, b: string): number {
79
+ const parse = (s: string): { nums: number[]; pre: string } => {
80
+ const [main, ...preParts] = s.split('-');
81
+ const nums = main.split('.').map((n) => parseInt(n, 10));
82
+ while (nums.length < 3) nums.push(0);
83
+ return { nums, pre: preParts.join('-') };
84
+ };
85
+ const pa = parse(a);
86
+ const pb = parse(b);
87
+ for (let i = 0; i < 3; i++) {
88
+ if (pa.nums[i] > pb.nums[i]) return 1;
89
+ if (pa.nums[i] < pb.nums[i]) return -1;
90
+ }
91
+ // Equal numerics — pre-release sorts before release
92
+ if (pa.pre && !pb.pre) return -1;
93
+ if (!pa.pre && pb.pre) return 1;
94
+ return pa.pre.localeCompare(pb.pre);
95
+ }
96
+
97
+ /**
98
+ * Check the registry for the latest @aria_asi/cli version. Rate-limited
99
+ * to once per CHECK_INTERVAL_MS (24h) via a timestamp file in ~/.aria.
100
+ * Caller can pass force=true to bypass the rate limit (used by `aria check-update`).
101
+ */
102
+ export async function checkForUpdate(opts: { force?: boolean } = {}): Promise<UpdateCheckResult> {
103
+ const current = findInstalledVersion();
104
+ if (!current) {
105
+ return { ok: false, reason: 'could not resolve installed version' };
106
+ }
107
+
108
+ // Rate-limit check
109
+ if (!opts.force && existsSync(LAST_CHECK_PATH)) {
110
+ try {
111
+ const ts = parseInt(readFileSync(LAST_CHECK_PATH, 'utf-8').trim(), 10);
112
+ if (!isNaN(ts) && Date.now() - ts < CHECK_INTERVAL_MS) {
113
+ return { ok: true, current, reason: 'rate-limited (checked within last 24h)' };
114
+ }
115
+ } catch {/* malformed timestamp — re-check */}
116
+ }
117
+
118
+ // Fetch registry
119
+ let latest: string;
120
+ try {
121
+ const resp = await fetch(REGISTRY_URL, {
122
+ headers: { 'Accept': 'application/json' },
123
+ });
124
+ if (!resp.ok) {
125
+ // Don't update timestamp on failure — retry next invocation
126
+ return { ok: false, current, reason: `registry returned ${resp.status}` };
127
+ }
128
+ const data = await resp.json() as { 'dist-tags'?: Record<string, string> };
129
+ latest = data['dist-tags']?.latest ?? '';
130
+ if (!latest) {
131
+ return { ok: false, current, reason: 'registry response missing dist-tags.latest' };
132
+ }
133
+ } catch (err) {
134
+ return { ok: false, current, reason: `network error: ${(err as Error).message}` };
135
+ }
136
+
137
+ // Update timestamp on successful check
138
+ try {
139
+ if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
140
+ writeFileSync(LAST_CHECK_PATH, String(Date.now()) + '\n', { mode: 0o600 });
141
+ } catch {/* timestamp write is best-effort */}
142
+
143
+ const cmp = compareSemver(latest, current);
144
+ if (cmp <= 0) {
145
+ return { ok: true, current, latest, updateAvailable: false };
146
+ }
147
+
148
+ return {
149
+ ok: true,
150
+ current,
151
+ latest,
152
+ updateAvailable: true,
153
+ message: `I have an update: v${latest} (you're on v${current}). Run 'npm update -g @aria_asi/cli' when you have a minute.`,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Convenience: check + print the notice if available. Non-blocking, silent
159
+ * on errors. Called from bin/aria.js on startup.
160
+ */
161
+ export async function maybePrintUpdateNotice(): Promise<void> {
162
+ try {
163
+ const result = await checkForUpdate();
164
+ if (result.ok && result.updateAvailable && result.message) {
165
+ // Print to stderr so it doesn't pollute stdout-piped CLI output
166
+ process.stderr.write(` ${result.message}\n`);
167
+ }
168
+ } catch {/* never block startup */}
169
+ }