@aria_asi/cli 0.2.29 → 0.2.30

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.
@@ -1,13 +1,12 @@
1
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.
2
+ // and auto-installs them in the background by default.
4
3
  //
5
4
  // Direction: Hamza 2026-04-26 — "does the package auto update when we
6
5
  // enhance it on our enhance? so we can continually improve all night using
7
6
  // arias background work we should imrpove to do that once we finish".
8
7
  // This is the primitive that closes the overnight-improvement loop:
9
8
  // 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.
9
+ // next `aria <cmd>` invocation; Aria upgrades itself in the background.
11
10
  //
12
11
  // Doctrine bindings:
13
12
  // - feedback_no_demos.md — real overnight evolution, real registry check
@@ -22,21 +21,30 @@
22
21
  // - Reads ~/.aria/last-update-check timestamp; if checked <24h ago, skip
23
22
  // - GET registry.npmjs.org/@aria_asi/cli for latest dist-tag
24
23
  // - semver-compare; if newer, return {updateAvailable: true, latest, current, message}
24
+ // - Launches npm install -g @aria_asi/cli@latest in the background
25
+ // unless ARIA_SELF_UPDATE_MODE=notify|off
26
+ // - Tracks per-version attempts so it doesn't thrash on every invocation
25
27
  // - Write timestamp on every check (success or skip-due-to-rate-limit)
26
28
  // - Failures are silent (network down, registry blip — never block CLI startup)
27
29
  //
28
30
  // Privacy: the request to npmjs only sends User-Agent (npm registry public).
29
31
  // No client identity, no telemetry beyond what the public registry already logs.
30
32
 
31
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
33
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, openSync } from 'node:fs';
34
+ import { spawn } from 'node:child_process';
32
35
  import { homedir } from 'node:os';
33
36
  import { join, dirname, resolve } from 'node:path';
34
37
  import { fileURLToPath } from 'node:url';
35
38
 
36
39
  const ARIA_DIR = join(homedir(), '.aria');
37
40
  const LAST_CHECK_PATH = join(ARIA_DIR, 'last-update-check');
41
+ const AUTO_UPDATE_STATE_PATH = join(ARIA_DIR, 'self-update-state.json');
42
+ const AUTO_UPDATE_LOG_PATH = join(ARIA_DIR, 'self-update.log');
38
43
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
44
+ const AUTO_UPDATE_RETRY_MS = 6 * 60 * 60 * 1000; // 6h
39
45
  const REGISTRY_URL = 'https://registry.npmjs.org/@aria_asi%2Fcli';
46
+ const PACKAGE_NAME = '@aria_asi/cli';
47
+ const AUTO_UPDATE_MODE = (process.env.ARIA_SELF_UPDATE_MODE || 'auto').toLowerCase();
40
48
 
41
49
  export interface UpdateCheckResult {
42
50
  ok: boolean;
@@ -47,6 +55,11 @@ export interface UpdateCheckResult {
47
55
  reason?: string;
48
56
  }
49
57
 
58
+ interface AutoUpdateState {
59
+ lastAttemptVersion?: string;
60
+ lastAttemptAt?: number;
61
+ }
62
+
50
63
  /**
51
64
  * Find the package's own version by walking up from this file's runtime
52
65
  * path until we hit the package.json with name @aria_asi/cli.
@@ -94,6 +107,69 @@ function compareSemver(a: string, b: string): number {
94
107
  return pa.pre.localeCompare(pb.pre);
95
108
  }
96
109
 
110
+ function loadAutoUpdateState(): AutoUpdateState {
111
+ try {
112
+ if (!existsSync(AUTO_UPDATE_STATE_PATH)) return {};
113
+ return JSON.parse(readFileSync(AUTO_UPDATE_STATE_PATH, 'utf-8')) as AutoUpdateState;
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ function saveAutoUpdateState(state: AutoUpdateState): void {
120
+ try {
121
+ if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
122
+ writeFileSync(AUTO_UPDATE_STATE_PATH, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
123
+ } catch {
124
+ // best effort only
125
+ }
126
+ }
127
+
128
+ function canRetryVersion(latest: string): boolean {
129
+ const state = loadAutoUpdateState();
130
+ if (state.lastAttemptVersion !== latest) return true;
131
+ if (!state.lastAttemptAt) return true;
132
+ return Date.now() - state.lastAttemptAt >= AUTO_UPDATE_RETRY_MS;
133
+ }
134
+
135
+ function launchAutoInstall(latest: string): { started: boolean; reason?: string } {
136
+ if (AUTO_UPDATE_MODE === 'off') {
137
+ return { started: false, reason: 'self-update disabled' };
138
+ }
139
+ if (AUTO_UPDATE_MODE === 'notify') {
140
+ return { started: false, reason: 'notify-only mode' };
141
+ }
142
+ if (!canRetryVersion(latest)) {
143
+ return { started: false, reason: 'recent attempt already made' };
144
+ }
145
+
146
+ try {
147
+ if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
148
+ const logFd = openSync(AUTO_UPDATE_LOG_PATH, 'a', 0o600);
149
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
150
+ const child = spawn(
151
+ npmCmd,
152
+ ['install', '-g', `${PACKAGE_NAME}@${latest}`],
153
+ {
154
+ detached: true,
155
+ stdio: ['ignore', logFd, logFd],
156
+ env: process.env,
157
+ },
158
+ );
159
+ child.unref();
160
+ saveAutoUpdateState({
161
+ lastAttemptVersion: latest,
162
+ lastAttemptAt: Date.now(),
163
+ });
164
+ return { started: true };
165
+ } catch (error) {
166
+ return {
167
+ started: false,
168
+ reason: error instanceof Error ? error.message : String(error),
169
+ };
170
+ }
171
+ }
172
+
97
173
  /**
98
174
  * Check the registry for the latest @aria_asi/cli version. Rate-limited
99
175
  * to once per CHECK_INTERVAL_MS (24h) via a timestamp file in ~/.aria.
@@ -150,20 +226,25 @@ export async function checkForUpdate(opts: { force?: boolean } = {}): Promise<Up
150
226
  current,
151
227
  latest,
152
228
  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.`,
229
+ message: `I have an update: v${latest} (you're on v${current}).`,
154
230
  };
155
231
  }
156
232
 
157
233
  /**
158
- * Convenience: check + print the notice if available. Non-blocking, silent
234
+ * Convenience: check + auto-install if available. Falls back to notice-only
235
+ * when auto-upgrade is disabled or can't be launched. Non-blocking, silent
159
236
  * on errors. Called from bin/aria.js on startup.
160
237
  */
161
238
  export async function maybePrintUpdateNotice(): Promise<void> {
162
239
  try {
163
240
  const result = await checkForUpdate();
164
241
  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`);
242
+ const autoInstall = result.latest ? launchAutoInstall(result.latest) : { started: false, reason: 'missing latest version' };
243
+ if (autoInstall.started) {
244
+ process.stderr.write(` ${result.message} Updating in the background now.\n`);
245
+ } else {
246
+ process.stderr.write(` ${result.message} Run 'npm install -g ${PACKAGE_NAME}@latest' if you want it immediately.\n`);
247
+ }
167
248
  }
168
249
  } catch {/* never block startup */}
169
250
  }