Skip to content

feat(compute): add compute service CLI commands#52

Merged
tonychang04 merged 3 commits intomainfrom
feat/compute-cli
Apr 8, 2026
Merged

feat(compute): add compute service CLI commands#52
tonychang04 merged 3 commits intomainfrom
feat/compute-cli

Conversation

@tonychang04
Copy link
Copy Markdown
Contributor

@tonychang04 tonychang04 commented Apr 6, 2026

Summary

  • Add 9 commands under insforge compute: list, get, create, deploy, update, delete, start, stop, logs
  • deploy command shells out to flyctl deploy --remote-only for remote Docker builds (no local Docker needed)
  • Generates temporary fly.toml with InsForge naming, restores original after deploy
  • Reads existing fly.toml for port/memory/region defaults
  • Idempotent deploys — redeploys if service already exists
  • All commands follow existing CLI patterns: --json output, semantic exit codes, usage reporting

Depends on: InsForge/InsForge#1062 (backend deploy + sync endpoints)

Test plan

  • E2E tested all 9 commands against local backend + real Fly.io
  • compute deploy tested with real Dockerfile project (audio-analyzer) — remote build, deploy, health check all pass
  • compute list table output verified with multiple services
  • --json output verified for all commands
  • CLI builds successfully with tsup

🤖 Generated with Claude Code

Note

Add compute CLI command group for managing compute services

  • Adds a new top-level compute command to the CLI with subcommands: list, get, create, update, delete, start, stop, logs, and deploy.
  • Each subcommand authenticates and calls the corresponding /api/compute/services REST endpoint, outputting JSON or human-readable text.
  • The deploy subcommand builds and deploys a local Dockerfile to Fly.io: it checks for flyctl and FLY_API_TOKEN, parses or generates a fly.toml, runs flyctl deploy, and syncs state with the backend via PATCH.
  • Updates ossFetch error handling to prefer message over error, append nextActions when present, and surface a specific message for 404s on /api/compute paths.
  • Integration tests covering the full create/get/logs/stop/start/delete lifecycle are added, gated by INTEGRATION_TEST_ENABLED.

Macroscope summarized 1a38fee.

Summary by CodeRabbit

  • New Features
    • Added compute service management commands: create, list, get, deploy, update, delete, start, stop, and logs. Commands support JSON output and configurable service options (image, port, CPU, memory, region, env).
  • Tests
    • Added integration tests covering the full compute lifecycle: create → get/list → logs → stop → start → delete.

Add 9 commands under `insforge compute`:
- list: list all compute services
- get: get service details
- create: deploy from a pre-built Docker image
- deploy: build Dockerfile remotely via flyctl and deploy to Fly.io
- update: update service config (image, cpu, memory, port, env)
- delete: destroy service and Fly.io resources
- start: start a stopped service
- stop: stop a running service
- logs: view machine lifecycle events

The deploy command shells out to flyctl for remote Docker builds
(no local Docker required), generates a temporary fly.toml, and
syncs machine state back to the InsForge backend after deployment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

Walkthrough

Adds a new top-level compute CLI command and nine subcommands (create, delete, deploy, get, list, logs, start, stop, update) plus an integration test suite; each subcommand enforces auth, calls OSS API endpoints, and supports JSON/human output and usage reporting.

Changes

Cohort / File(s) Summary
CLI Integration
src/index.ts
Registers the compute command and wires all compute subcommand registration functions.
Compute Subcommands
src/commands/compute/create.ts, src/commands/compute/delete.ts, src/commands/compute/deploy.ts, src/commands/compute/get.ts, src/commands/compute/list.ts, src/commands/compute/logs.ts, src/commands/compute/start.ts, src/commands/compute/stop.ts, src/commands/compute/update.ts
Adds nine subcommand handlers that enforce authentication, build and send HTTP requests to /api/compute/..., parse JSON, branch output on root --json, call reportCliUsage, and route errors via handleError. deploy includes fly.toml backup/restore, flyctl invocation, and sync logic.
Integration Tests
src/integration/compute.test.ts
New Vitest integration suite exercising compute lifecycle (list, create, get, logs, stop, start, delete) when INTEGRATION_TEST_ENABLED is enabled; creates a test service and performs cleanup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • track connection #11 — Adds or modifies reportCliUsage(...) instrumentation used by the new compute subcommands.

Poem

🐰 Soft paws tap keys at break of day,

I wired nine tunnels where services play,
Create, deploy, start, stop — all in line,
Logs and updates, each endpoint will shine,
Hop on, CLI friend, the compute burrow's fine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(compute): add compute service CLI commands' accurately and clearly summarizes the main change: adding a new compute service command with multiple subcommands to the CLI.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/compute-cli

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +98 to +102
function getFlyToken(): string {
const config = getProjectConfig();
// The backend knows the Fly token, but we need it for flyctl.
// Check env var first (user may have it set), then fall back to asking.
const token = process.env.FLY_API_TOKEN;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low compute/deploy.ts:98

getFlyToken calls getProjectConfig() and assigns the result to config, but then ignores it and only reads from process.env.FLY_API_TOKEN. The dead variable suggests the config might have been intended as a fallback source for the token, or the line should simply be removed.

 function getFlyToken(): string {
-  const config = getProjectConfig();
-  // The backend knows the Fly token, but we need it for flyctl.
   // Check env var first (user may have it set), then fall back to asking.
   const token = process.env.FLY_API_TOKEN;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/commands/compute/deploy.ts around lines 98-102:

`getFlyToken` calls `getProjectConfig()` and assigns the result to `config`, but then ignores it and only reads from `process.env.FLY_API_TOKEN`. The dead variable suggests the config might have been intended as a fallback source for the token, or the line should simply be removed.

Evidence trail:
src/commands/compute/deploy.ts lines 98-110 (REVIEWED_COMMIT): `getFlyToken()` function shows `const config = getProjectConfig();` on line 99, followed by code that only uses `process.env.FLY_API_TOKEN` and never references `config`.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
src/commands/compute/deploy.ts (1)

99-100: Remove unused config binding (lint warning).

Line 99 assigns config but never uses it. This is currently flagged by static analysis and should be cleaned up.

Suggested fix
 function getFlyToken(): string {
-  const config = getProjectConfig();
+  getProjectConfig();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/deploy.ts` around lines 99 - 100, Remove the unused
local binding by deleting the line that calls getProjectConfig() (the `const
config = getProjectConfig();` assignment) since `config` is never referenced;
ensure any needed project configuration is fetched only where actually used (or
rename to `_config` if intentionally unused) and keep the surrounding comment
about the Fly token intact.
src/commands/compute/get.ts (1)

23-31: Inconsistent fallback handling for optional fields.

Only endpointUrl (line 30) has a nullish coalescing fallback (?? 'n/a'). If other fields are missing from the API response, they'll print as undefined (e.g., "Status: undefined"). Consider applying consistent fallback handling for a more robust user experience.

💡 Optional: Apply consistent fallbacks
-          outputInfo(`Name:      ${service.name}`);
-          outputInfo(`ID:        ${service.id}`);
-          outputInfo(`Status:    ${service.status}`);
-          outputInfo(`Image:     ${service.imageUrl}`);
-          outputInfo(`CPU:       ${service.cpu}`);
-          outputInfo(`Memory:    ${service.memory}MB`);
-          outputInfo(`Region:    ${service.region}`);
-          outputInfo(`Endpoint:  ${service.endpointUrl ?? 'n/a'}`);
-          outputInfo(`Created:   ${service.createdAt}`);
+          outputInfo(`Name:      ${service.name ?? 'n/a'}`);
+          outputInfo(`ID:        ${service.id ?? 'n/a'}`);
+          outputInfo(`Status:    ${service.status ?? 'n/a'}`);
+          outputInfo(`Image:     ${service.imageUrl ?? 'n/a'}`);
+          outputInfo(`CPU:       ${service.cpu ?? 'n/a'}`);
+          outputInfo(`Memory:    ${service.memory ?? 'n/a'}MB`);
+          outputInfo(`Region:    ${service.region ?? 'n/a'}`);
+          outputInfo(`Endpoint:  ${service.endpointUrl ?? 'n/a'}`);
+          outputInfo(`Created:   ${service.createdAt ?? 'n/a'}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/get.ts` around lines 23 - 31, The output lines use
outputInfo with several service.* fields but only service.endpointUrl uses a
fallback, causing other missing fields to render "undefined"; update each output
to provide a consistent fallback (e.g., use nullish coalescing `?? 'n/a'` or a
more specific default like `'0'`/`'unknown'` where appropriate) for
service.name, service.id, service.status, service.imageUrl, service.cpu,
service.memory, service.region, service.endpointUrl, and service.createdAt so
the CLI prints stable values when the API omits fields; keep using the same
outputInfo calls and only change the interpolated expressions to include the
fallback.
src/commands/compute/create.ts (1)

27-29: Consider validating numeric options before sending to API.

If a user provides non-numeric input (e.g., --port abc), Number() returns NaN, which gets serialized and sent to the server. While the server should validate, client-side validation provides better UX with immediate feedback.

💡 Optional: Add validation for port and memory
+        const port = Number(opts.port);
+        const memory = Number(opts.memory);
+        if (Number.isNaN(port) || port <= 0) {
+          throw new CLIError('Invalid value for --port: must be a positive number');
+        }
+        if (Number.isNaN(memory) || memory <= 0) {
+          throw new CLIError('Invalid value for --memory: must be a positive number');
+        }
+
         const body: Record<string, unknown> = {
           name: opts.name,
           imageUrl: opts.image,
-          port: Number(opts.port),
+          port,
           cpu: opts.cpu,
-          memory: Number(opts.memory),
+          memory,
           region: opts.region,
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/create.ts` around lines 27 - 29, Validate numeric CLI
options before sending to the API: check opts.port and opts.memory (and any
other numeric opts) after parsing and before building the payload (where you
currently use Number(opts.port) and Number(opts.memory)); ensure port is a valid
integer within allowed range and memory is a finite positive number (use
Number.isInteger/Number.isFinite checks or parseInt/parseFloat + isNaN), and
display a clear error message and exit the command if validation fails instead
of sending NaN to the server. Reference the code that constructs the payload
(the lines using Number(opts.port), opts.cpu, Number(opts.memory)) to implement
the checks and early-return on invalid input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/compute/deploy.ts`:
- Around line 239-244: The unconditional synchronous cleanup in the finally
block (unlinkSync/renameSync operating on existingTomlPath/backupTomlPath with
hadExistingToml) can throw and mask the real deploy result; wrap the unlinkSync
and renameSync calls in their own try/catch so any FS errors are logged or
warned (using the existing logger or console.warn) and not rethrown, ensuring
the original deploy error/result is preserved — do this inside the same finally
block around the unlinkSync/renameSync operations and avoid altering the outer
control flow (do not convert these to throws or let them overwrite the primary
error).
- Around line 208-212: Remove passing flyToken via the CLI and avoid leaving an
unconsumed pipe: update the spawn call that creates child (function using spawn
with args including '--access-token', flyAppId, json, dir) to drop the
'--access-token' / flyToken argument and instead set FLY_API_TOKEN in the spawn
options.env (merge with process.env). Also change stdio so when json is true use
'ignore' (not 'pipe') and when json is false keep 'inherit', ensuring no
stdout/stderr pipes remain unconsumed to prevent hangs.

In `@src/commands/compute/list.ts`:
- Around line 24-27: The early return inside the services.length === 0 branch
prevents the success usage reporting from running for an empty "compute list"
result; instead of returning immediately after console.log('No compute services
found.'), invoke the existing success usage-reporting call (the same reporter
used later for successful runs) before returning or refactor to let the common
success-reporting path execute for both empty and non-empty results, ensuring
the services check (services) and the console.log('No compute services found.')
message remain but do not skip the success-reporting step.
- Line 35: The code uses the expression `${s.memory ?? '-'}MB` which yields
`-MB` when s.memory is null/undefined; update the memory cell formatting to only
append "MB" when s.memory is present (e.g., use a conditional that returns
`${s.memory}MB` when s.memory exists, otherwise return `'-'`)—look for the
memory formatting in the list output where s.memory is referenced and replace
the interpolation accordingly.

In `@src/commands/compute/logs.ts`:
- Around line 27-30: The early return when logs is empty (the if
(!Array.isArray(logs) || logs.length === 0) block) prevents the subsequent
success usage reporting for the "compute logs" command from running; instead of
returning there, ensure you still execute the existing success/usage reporting
call after the logs check (i.e., do not return early — either run the reporting
before returning or remove the return and let the normal success reporting flow
execute), keeping the existing variable names (logs) and the surrounding compute
logs command handler intact.
- Around line 18-21: The current coercion "const limit = Number(opts.limit) ||
50" silently accepts invalid or negative values; replace it with explicit
validation: check opts.limit (the CLI flag) and if present parse it with
parseInt, ensure it's a positive integer (>0), otherwise return/throw a
user-facing error (or print validation message) instead of proceeding; only when
opts.limit is undefined use the default 50; then pass the validated numeric
limit into the ossFetch call for
`/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}` to avoid
sending bad query values.

In `@src/commands/compute/update.ts`:
- Around line 25-27: The handlers that populate the request body (the local body
object) currently coerce opts.port and opts.memory with Number without
validation; instead, in the update handler use explicit numeric validation: when
opts.port or opts.memory are present, parse them (Number or parseInt as
appropriate) and check isFinite/!isNaN (and for port optionally that the value
is an integer in valid port range) before assigning to body.port/body.memory; if
validation fails, abort with a clear error message (e.g., throw or process.exit
with explanation) rather than inserting NaN into the payload; apply the same
pattern for opts.cpu if it must be numeric.

---

Nitpick comments:
In `@src/commands/compute/create.ts`:
- Around line 27-29: Validate numeric CLI options before sending to the API:
check opts.port and opts.memory (and any other numeric opts) after parsing and
before building the payload (where you currently use Number(opts.port) and
Number(opts.memory)); ensure port is a valid integer within allowed range and
memory is a finite positive number (use Number.isInteger/Number.isFinite checks
or parseInt/parseFloat + isNaN), and display a clear error message and exit the
command if validation fails instead of sending NaN to the server. Reference the
code that constructs the payload (the lines using Number(opts.port), opts.cpu,
Number(opts.memory)) to implement the checks and early-return on invalid input.

In `@src/commands/compute/deploy.ts`:
- Around line 99-100: Remove the unused local binding by deleting the line that
calls getProjectConfig() (the `const config = getProjectConfig();` assignment)
since `config` is never referenced; ensure any needed project configuration is
fetched only where actually used (or rename to `_config` if intentionally
unused) and keep the surrounding comment about the Fly token intact.

In `@src/commands/compute/get.ts`:
- Around line 23-31: The output lines use outputInfo with several service.*
fields but only service.endpointUrl uses a fallback, causing other missing
fields to render "undefined"; update each output to provide a consistent
fallback (e.g., use nullish coalescing `?? 'n/a'` or a more specific default
like `'0'`/`'unknown'` where appropriate) for service.name, service.id,
service.status, service.imageUrl, service.cpu, service.memory, service.region,
service.endpointUrl, and service.createdAt so the CLI prints stable values when
the API omits fields; keep using the same outputInfo calls and only change the
interpolated expressions to include the fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb7d80b9-6e98-4adf-9594-2e961a8271d6

📥 Commits

Reviewing files that changed from the base of the PR and between cb78753 and bd90346.

📒 Files selected for processing (10)
  • src/commands/compute/create.ts
  • src/commands/compute/delete.ts
  • src/commands/compute/deploy.ts
  • src/commands/compute/get.ts
  • src/commands/compute/list.ts
  • src/commands/compute/logs.ts
  • src/commands/compute/start.ts
  • src/commands/compute/stop.ts
  • src/commands/compute/update.ts
  • src/index.ts

Comment on lines +208 to +212
const child = spawn(
'flyctl',
['deploy', '--remote-only', '--app', flyAppId, '--access-token', flyToken, '--yes'],
{ cwd: dir, stdio: json ? 'pipe' : 'inherit' },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does flyctl deploysupport authentication via theFLY_API_TOKEN environment variable, and what are the security implications of passing tokens as command-line arguments versus environment variables?

💡 Result:

Yes—flyctl deploy (and flyctl in general) can authenticate using a token from an environment variable:

  • Fly’s official “Integrating flyctl” docs state that FLY_ACCESS_TOKEN can be used to pass an access token to flyctl (the same token you’d use for deploys). [1]
  • Fly’s “Access tokens” docs show flyctl being authenticated via FLY_API_TOKEN=… in an example, and Fly’s official GitHub Action for deployments uses FLY_API_TOKEN in the environment when running flyctl deploy. [2][3]

In practice, FLY_API_TOKEN is widely supported/used in CI examples, while FLY_ACCESS_TOKEN is the documented variable name in the “integrating” page. [1][3]

Security implications: CLI args vs environment variables

Passing tokens as command-line arguments (e.g., --access-token ...) is usually riskier because process arguments are commonly visible to other local users and tooling (e.g., ps, /proc/<pid>/cmdline) while the process runs, and they’re easy to accidentally capture in shell history and logs. [4]

Environment variables are often safer than args, but still not “secret storage.” They can be inherited by child processes and may be readable by other processes in the same environment/container; OWASP notes that when secrets are provided via environment variables, other processes in the container can read them. [5]

Rule of thumb: prefer a CI secret store that injects the token as an env var at runtime (as Fly’s action does), avoid putting secrets directly in command lines, and ensure logs don’t echo either. [3][4]

Sources: [1] Fly Docs – Integrating flyctl, [2] Fly Docs – Access tokens, [3] superfly/flyctl-actions README, [4] BashFAQ/069 (process args visibility), [5] OWASP Microservice Security (secrets via env vars visible to processes).


🏁 Script executed:

head -250 src/commands/compute/deploy.ts | tail -50

Repository: InsForge/CLI

Length of output: 1878


Remove --access-token argument and pass token via environment variable to prevent secret exposure; fix unconsumed pipe that can cause hangs.

The code passes flyToken via the --access-token CLI argument, which is visible to other processes and users (via ps, /proc/<pid>/cmdline, etc.). Additionally, in JSON mode, stdio: 'pipe' is set but never consumed, leaving output buffered until the process exits—this can cause hangs on verbose output.

Use the FLY_API_TOKEN environment variable instead (supported by flyctl), and change stdio to 'ignore' in JSON mode since the output is not consumed by the code:

Suggested fix
             const child = spawn(
               'flyctl',
-              ['deploy', '--remote-only', '--app', flyAppId, '--access-token', flyToken, '--yes'],
-              { cwd: dir, stdio: json ? 'pipe' : 'inherit' },
+              ['deploy', '--remote-only', '--app', flyAppId, '--yes'],
+              {
+                cwd: dir,
+                stdio: json ? 'ignore' : 'inherit',
+                env: { ...process.env, FLY_API_TOKEN: flyToken },
+              },
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const child = spawn(
'flyctl',
['deploy', '--remote-only', '--app', flyAppId, '--access-token', flyToken, '--yes'],
{ cwd: dir, stdio: json ? 'pipe' : 'inherit' },
);
const child = spawn(
'flyctl',
['deploy', '--remote-only', '--app', flyAppId, '--yes'],
{
cwd: dir,
stdio: json ? 'ignore' : 'inherit',
env: { ...process.env, FLY_API_TOKEN: flyToken },
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/deploy.ts` around lines 208 - 212, Remove passing
flyToken via the CLI and avoid leaving an unconsumed pipe: update the spawn call
that creates child (function using spawn with args including '--access-token',
flyAppId, json, dir) to drop the '--access-token' / flyToken argument and
instead set FLY_API_TOKEN in the spawn options.env (merge with process.env).
Also change stdio so when json is true use 'ignore' (not 'pipe') and when json
is false keep 'inherit', ensuring no stdout/stderr pipes remain unconsumed to
prevent hangs.

Comment on lines +239 to +244
} finally {
// Restore original fly.toml
unlinkSync(existingTomlPath);
if (hadExistingToml) {
renameSync(backupTomlPath, existingTomlPath);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cleanup can throw and mask the real deploy result.

Lines 241-244 run unconditional sync FS operations in finally. If cleanup fails, it can overwrite the primary error (or turn a successful deploy into a failed command).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/deploy.ts` around lines 239 - 244, The unconditional
synchronous cleanup in the finally block (unlinkSync/renameSync operating on
existingTomlPath/backupTomlPath with hadExistingToml) can throw and mask the
real deploy result; wrap the unlinkSync and renameSync calls in their own
try/catch so any FS errors are logged or warned (using the existing logger or
console.warn) and not rethrown, ensuring the original deploy error/result is
preserved — do this inside the same finally block around the
unlinkSync/renameSync operations and avoid altering the outer control flow (do
not convert these to throws or let them overwrite the primary error).

Comment on lines +24 to +27
if (services.length === 0) {
console.log('No compute services found.');
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty-list path skips success usage reporting.

At Line 26, returning early means Line 40 is never reached, so successful compute list runs with no services are not reported.

Also applies to: 40-40

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/list.ts` around lines 24 - 27, The early return inside
the services.length === 0 branch prevents the success usage reporting from
running for an empty "compute list" result; instead of returning immediately
after console.log('No compute services found.'), invoke the existing success
usage-reporting call (the same reporter used later for successful runs) before
returning or refactor to let the common success-reporting path execute for both
empty and non-empty results, ensuring the services check (services) and the
console.log('No compute services found.') message remain but do not skip the
success-reporting step.

String(s.status ?? '-'),
String(s.imageUrl ?? '-'),
String(s.cpu ?? '-'),
`${s.memory ?? '-'}MB`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid -MB placeholder in table output.

Line 35 prints -MB when memory is absent, which looks malformed. Prefer '-' for missing values.

Suggested fix
-              `${s.memory ?? '-'}MB`,
+              s.memory == null ? '-' : `${String(s.memory)}MB`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`${s.memory ?? '-'}MB`,
s.memory == null ? '-' : `${String(s.memory)}MB`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/list.ts` at line 35, The code uses the expression
`${s.memory ?? '-'}MB` which yields `-MB` when s.memory is null/undefined;
update the memory cell formatting to only append "MB" when s.memory is present
(e.g., use a conditional that returns `${s.memory}MB` when s.memory exists,
otherwise return `'-'`)—look for the memory formatting in the list output where
s.memory is referenced and replace the interpolation accordingly.

Comment on lines +18 to +21
const limit = Number(opts.limit) || 50;
const res = await ossFetch(
`/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate --limit as a positive integer instead of silently coercing.

At Line 18, invalid values (e.g. --limit abc, --limit -5) are coerced/fallback in a way that can hide user errors and send bad query values.

Suggested fix
-        const limit = Number(opts.limit) || 50;
+        const limit = Number.parseInt(String(opts.limit), 10);
+        if (!Number.isInteger(limit) || limit <= 0) {
+          throw new Error('--limit must be a positive integer');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/logs.ts` around lines 18 - 21, The current coercion
"const limit = Number(opts.limit) || 50" silently accepts invalid or negative
values; replace it with explicit validation: check opts.limit (the CLI flag) and
if present parse it with parseInt, ensure it's a positive integer (>0),
otherwise return/throw a user-facing error (or print validation message) instead
of proceeding; only when opts.limit is undefined use the default 50; then pass
the validated numeric limit into the ossFetch call for
`/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}` to avoid
sending bad query values.

Comment on lines +27 to +30
if (!Array.isArray(logs) || logs.length === 0) {
console.log('No logs found.');
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Early return skips success usage reporting.

At Line 29, returning on empty logs bypasses Line 36, so successful compute logs calls are not fully tracked.

Suggested fix
         if (json) {
           outputJson(logs);
         } else {
           if (!Array.isArray(logs) || logs.length === 0) {
             console.log('No logs found.');
-            return;
+            await reportCliUsage('cli.compute.logs', true);
+            return;
           }
           for (const entry of logs) {
             const ts = new Date(entry.timestamp).toISOString();
             console.log(`${ts}  ${entry.message}`);
           }
         }
         await reportCliUsage('cli.compute.logs', true);

Also applies to: 36-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/logs.ts` around lines 27 - 30, The early return when
logs is empty (the if (!Array.isArray(logs) || logs.length === 0) block)
prevents the subsequent success usage reporting for the "compute logs" command
from running; instead of returning there, ensure you still execute the existing
success/usage reporting call after the logs check (i.e., do not return early —
either run the reporting before returning or remove the return and let the
normal success reporting flow execute), keeping the existing variable names
(logs) and the surrounding compute logs command handler intact.

Comment on lines +25 to +27
if (opts.port) body.port = Number(opts.port);
if (opts.cpu) body.cpu = opts.cpu;
if (opts.memory) body.memory = Number(opts.memory);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate numeric flags before adding them to request body.

At Lines 25-27, invalid values (--port abc, --memory nope) are coerced and can produce malformed payload fields, causing unclear backend failures.

Suggested fix
+        const parsePositiveInt = (value: string, flag: string): number => {
+          const parsed = Number.parseInt(value, 10);
+          if (!Number.isInteger(parsed) || parsed <= 0) {
+            throw new CLIError(`${flag} must be a positive integer`);
+          }
+          return parsed;
+        };
+
         const body: Record<string, unknown> = {};
         if (opts.image) body.imageUrl = opts.image;
-        if (opts.port) body.port = Number(opts.port);
+        if (opts.port) body.port = parsePositiveInt(opts.port, '--port');
         if (opts.cpu) body.cpu = opts.cpu;
-        if (opts.memory) body.memory = Number(opts.memory);
+        if (opts.memory) body.memory = parsePositiveInt(opts.memory, '--memory');
         if (opts.region) body.region = opts.region;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/compute/update.ts` around lines 25 - 27, The handlers that
populate the request body (the local body object) currently coerce opts.port and
opts.memory with Number without validation; instead, in the update handler use
explicit numeric validation: when opts.port or opts.memory are present, parse
them (Number or parseInt as appropriate) and check isFinite/!isNaN (and for port
optionally that the value is an integer in valid port range) before assigning to
body.port/body.memory; if validation fails, abort with a clear error message
(e.g., throw or process.exit with explanation) rather than inserting NaN into
the payload; apply the same pattern for opts.cpu if it must be numeric.

Tests the full CRUD lifecycle: list, create, get, logs, stop, start,
delete — all via --json output against a real backend + Fly.io.
Follows existing integration test pattern (INTEGRATION_TEST_ENABLED gate).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/integration/compute.test.ts`:
- Around line 124-131: Introduce a shared variable (e.g., createdServiceName:
string | undefined) at the top of the test file, set it in the create test where
the service name is generated, preserve the value in the delete test before
clearing any ids, and then in the it('compute list --json should be empty after
delete', ...) test use parseJsonOutput(result.stdout) and assert that no item
equals that specific createdServiceName (instead of checking the 'cli-test-'
prefix) so the assertion only verifies the service you created was removed.
- Line 50: The test at src/integration/compute.test.ts currently asserts
payload.status === 'running' immediately after calling the create command; since
src/commands/compute/create.ts returns the API's immediate response and
provisioning is asynchronous, change the test to either (a) retry/poll the
service status (e.g., loop with delay and timeout) until payload.status ===
'running' or a timeout is reached, using a small retry helper, or (b) relax the
assertion to allow intermediate statuses by asserting
expect(['creating','starting','running']).toContain(payload.status); update the
assertion line that references payload.status accordingly and/or add the retry
helper call before the assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcafc6a5-92e7-47b3-b2e1-7c607504b22f

📥 Commits

Reviewing files that changed from the base of the PR and between bd90346 and efa9854.

📒 Files selected for processing (1)
  • src/integration/compute.test.ts

expect(payload).toHaveProperty('name');
expect(payload).toHaveProperty('status');
expect(payload).toHaveProperty('endpointUrl');
expect(payload.status).toBe('running');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential flaky test: Service may not be 'running' immediately after creation.

Based on src/commands/compute/create.ts, the create command returns the API's immediate response without polling. Since Fly.io service provisioning is asynchronous, the status might be 'creating', 'starting', or another intermediate value rather than 'running'.

Consider either:

  1. Polling/retrying until status becomes 'running' with a timeout
  2. Asserting against a set of acceptable statuses: expect(['creating', 'starting', 'running']).toContain(payload.status)
♻️ Suggested fix using retry with timeout
-    expect(payload.status).toBe('running');
+    // Service may still be provisioning; poll until running or timeout
+    let currentStatus = payload.status;
+    const maxWaitMs = 60_000;
+    const pollIntervalMs = 2_000;
+    const start = Date.now();
+    while (currentStatus !== 'running' && Date.now() - start < maxWaitMs) {
+      await new Promise((r) => setTimeout(r, pollIntervalMs));
+      const getResult = await runCli(['--json', 'compute', 'get', payload.id as string], { apiUrl });
+      if (getResult.code === 0) {
+        const getPayload = parseJsonOutput(getResult.stdout) as Record<string, unknown>;
+        currentStatus = getPayload.status;
+      }
+    }
+    expect(currentStatus).toBe('running');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(payload.status).toBe('running');
// Service may still be provisioning; poll until running or timeout
let currentStatus = payload.status;
const maxWaitMs = 60_000;
const pollIntervalMs = 2_000;
const start = Date.now();
while (currentStatus !== 'running' && Date.now() - start < maxWaitMs) {
await new Promise((r) => setTimeout(r, pollIntervalMs));
const getResult = await runCli(['--json', 'compute', 'get', payload.id as string], { apiUrl });
if (getResult.code === 0) {
const getPayload = parseJsonOutput(getResult.stdout) as Record<string, unknown>;
currentStatus = getPayload.status;
}
}
expect(currentStatus).toBe('running');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/integration/compute.test.ts` at line 50, The test at
src/integration/compute.test.ts currently asserts payload.status === 'running'
immediately after calling the create command; since
src/commands/compute/create.ts returns the API's immediate response and
provisioning is asynchronous, change the test to either (a) retry/poll the
service status (e.g., loop with delay and timeout) until payload.status ===
'running' or a timeout is reached, using a small retry helper, or (b) relax the
assertion to allow intermediate statuses by asserting
expect(['creating','starting','running']).toContain(payload.status); update the
assertion line that references payload.status accordingly and/or add the retry
helper call before the assertion.

Comment on lines +124 to +131
it('compute list --json should be empty after delete', async () => {
const result = await runCli(['--json', 'compute', 'list'], { apiUrl });
expectCliSuccess(result);

const payload = parseJsonOutput(result.stdout) as Record<string, unknown>[];
const found = payload.find((s) => (s.name as string)?.startsWith('cli-test-'));
expect(found).toBeUndefined();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fragile assertion: Checking all 'cli-test-*' services may cause false failures.

This test asserts that no services with names starting with 'cli-test-' exist. However, if parallel test runs or previous crashed runs left orphaned services, this will fail even though this specific test's delete succeeded.

Consider storing the service name before deletion (in the delete test) and verifying that specific name is absent:

♻️ Suggested fix

Add a variable to track the name:

let createdServiceName: string | undefined;

In the create test, store it:

    createdServiceId = payload.id as string;
+   createdServiceName = payload.name as string;

In the delete test, preserve the name before clearing the id:

+   const deletedName = createdServiceName;
    createdServiceId = undefined;
+   createdServiceName = undefined;

Then in this test, check for the specific name:

-   const found = payload.find((s) => (s.name as string)?.startsWith('cli-test-'));
-   expect(found).toBeUndefined();
+   const found = payload.find((s) => s.name === deletedName);
+   expect(found, `Service "${deletedName}" should not exist after deletion`).toBeUndefined();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/integration/compute.test.ts` around lines 124 - 131, Introduce a shared
variable (e.g., createdServiceName: string | undefined) at the top of the test
file, set it in the create test where the service name is generated, preserve
the value in the delete test before clearing any ids, and then in the
it('compute list --json should be empty after delete', ...) test use
parseJsonOutput(result.stdout) and assert that no item equals that specific
createdServiceName (instead of checking the 'cli-test-' prefix) so the assertion
only verifies the service you created was removed.

…te 404

Error responses now include backend-provided nextActions guidance.
Compute-specific 404s get a clear "upgrade or contact admin" message
for backward compatibility with older backends.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants