groove-dev 0.27.202 → 0.27.204

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.202",
3
+ "version": "0.27.204",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.202",
3
+ "version": "0.27.204",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -18,7 +18,7 @@ import { join } from 'path';
18
18
  import os from 'os';
19
19
  import { pipeline } from 'stream/promises';
20
20
  import { Transform } from 'stream';
21
- import { AXOM_REQUIREMENTS } from './axom-server.js';
21
+ import { AXOM_REQUIREMENTS, detectRuntime } from './axom-server.js';
22
22
 
23
23
  export class AxomInstaller {
24
24
  constructor(daemon, opts = {}) {
@@ -34,11 +34,18 @@ export class AxomInstaller {
34
34
  // so up front rather than offering a button that fails on click.
35
35
  getStatus() {
36
36
  const manifestUrl = this.daemon.config?.axom?.manifestUrl || null;
37
+ // Already-installed runtimes are NOT gated. Distribution gating controls
38
+ // downloading; a machine that already has Axom (the Spark, a dev box, an
39
+ // operator install) must be able to start it. Conflating the two told a
40
+ // user standing on a working runtime that it was "coming soon".
41
+ const runtime = detectRuntime(this.daemon.config?.axom?.command);
37
42
  return {
38
43
  ...this.status,
39
44
  available: !!manifestUrl,
40
45
  manifestUrl,
41
46
  unavailableReason: manifestUrl ? null : 'Coming soon',
47
+ runtimeInstalled: runtime.installed,
48
+ runtimeCommand: runtime.installed ? runtime.command : null,
42
49
  };
43
50
  }
44
51
 
@@ -8,8 +8,8 @@
8
8
  // The serve binary refuses a locked data-dir; we surface that error verbatim
9
9
  // rather than retrying around it.
10
10
 
11
- import { spawn } from 'child_process';
12
- import { mkdirSync, statfsSync } from 'fs';
11
+ import { spawn, execFileSync } from 'child_process';
12
+ import { mkdirSync, statfsSync, existsSync } from 'fs';
13
13
  import { join } from 'path';
14
14
  import os from 'os';
15
15
  import { AXOM_DEFAULT_PORT } from './axom-connector.js';
@@ -27,6 +27,27 @@ export const AXOM_REQUIREMENTS = {
27
27
  downloadGb: 4.4,
28
28
  };
29
29
 
30
+ // Is a runtime ALREADY on this machine? Distribution gating ("Coming soon")
31
+ // controls DOWNLOADING, never running — a machine that already has Axom must
32
+ // be able to start it regardless of whether this build can fetch one.
33
+ export function detectRuntime(command) {
34
+ const cmd = command || 'axom';
35
+ // A configured command may be a full invocation ("cd /x && python3 -m
36
+ // axom.cli"); probe the first token that looks like a path or binary.
37
+ const probe = cmd.trim().split(/\s+/).find((t) => !t.includes('=') && t !== 'cd') || cmd;
38
+ try {
39
+ if (probe.startsWith('/') || probe.startsWith('~')) {
40
+ return { installed: existsSync(probe.replace(/^~/, os.homedir())), command: cmd, source: 'path' };
41
+ }
42
+ execFileSync(process.platform === 'win32' ? 'where' : 'which', [probe], {
43
+ stdio: 'ignore', timeout: 5000,
44
+ });
45
+ return { installed: true, command: cmd, source: 'PATH' };
46
+ } catch {
47
+ return { installed: false, command: cmd, source: null };
48
+ }
49
+ }
50
+
30
51
  export function hardwareReport(dir = os.homedir(), requirements = AXOM_REQUIREMENTS) {
31
52
  const totalRamGb = os.totalmem() / 2 ** 30;
32
53
  let freeDiskGb = null;
@@ -206,6 +206,23 @@ describe('AxomInstaller', () => {
206
206
  assert.equal(status.manifestUrl, null);
207
207
  });
208
208
 
209
+ it('does not gate a runtime that is already installed', async () => {
210
+ // The Spark had Axom installed and got told "Coming soon" — distribution
211
+ // gating must control DOWNLOADING only, never running what's already here.
212
+ daemon.config.axom.command = process.execPath; // a path that exists
213
+ const status = installer.getStatus();
214
+ assert.equal(status.available, false); // still can't download
215
+ assert.equal(status.runtimeInstalled, true); // but CAN start
216
+ assert.equal(status.runtimeCommand, process.execPath);
217
+ });
218
+
219
+ it('reports no runtime when the configured command is absent', () => {
220
+ daemon.config.axom.command = '/nonexistent/axom-binary';
221
+ const status = installer.getStatus();
222
+ assert.equal(status.runtimeInstalled, false);
223
+ assert.equal(status.runtimeCommand, null);
224
+ });
225
+
209
226
  it('gates install server-side too — a hand-crafted POST cannot bypass it', async () => {
210
227
  await assert.rejects(() => installer.install(undefined), /Coming soon/);
211
228
  });