Skip to content

feat(cli): olares-cli profile + files (ls/upload/download/cat/rm) + skills#2949

Merged
eball merged 13 commits intomainfrom
cli/profile-auth-phase1
Apr 26, 2026
Merged

feat(cli): olares-cli profile + files (ls/upload/download/cat/rm) + skills#2949
eball merged 13 commits intomainfrom
cli/profile-auth-phase1

Conversation

@pengpeng
Copy link
Copy Markdown
Member

@pengpeng pengpeng commented Apr 25, 2026

Summary

Bring olares-cli to a usable baseline for profile-based authentication, end-to-end Drive file management, and the per-user Market app-store, plus ship Cursor agent skills so AI assistants can drive the CLI safely.

All changes are confined to cli/ (including cli/skills/). The branch stacks the original feature commits plus follow-ups (Bugbot / encoding / package layout / market). High-level breakdown:

1. Profile / auth subsystem (b9a7bde0, ce392322, 53c49514)

  • New olares-cli profile command group: login (password + TOTP), import (refresh-token bootstrap), list, use, remove, plus a global --profile flag.
  • New cli/pkg/auth package wraps Authelia firstFactor + loginTerminus with the same shape as the LarePass TS client, including correct 2FA semantics:
    • tok.FA2 is the only server-driven gate that escalates to a TOTP-bearing second login.
    • LoginRequest.NeedTwoFactor is only for activation/reset flows that need to switch targetURL; it must not be set during normal interactive login.
    • performPasswordReset now mirrors the TS sequence: pre-reset login (no 2FA) → ResetPassword → post-reset login (with 2FA).
    • Documented in cli/docs/notes/auth-2fa-semantics.md.
  • Phase 2: tokens move from plain JSON into the OS keychain
    • macOS: Keychain (service OlaresCli).
    • Linux: AES-256-GCM file under ~/.local/share/olares-cli/.
    • Windows: DPAPI-encrypted blob under HKCU\Software\OlaresCli\keychain.
  • Wizard refactored to consume cli/pkg/auth instead of duplicating login/refresh logic.

2. olares-cli files commands (5efe2a0d, f5aad739 + follow-ups)

  • ls: directory listing via GET /api/resources/<encPath>/. FrontendPath.URLPath() uses the same JS encodeUrl semantics as the other verbs (shared encodepath package) so ls / download / cat / rm / upload agree on wire paths for names with +, !*'(), spaces, etc.
  • upload: resumable, chunked upload that ports the LarePass Resumable.js protocol to Go
    • Implementation: cli/internal/files/upload — directory-aware BuildPlan, server-driven resume via /upload/file-uploaded-bytes/, and file-level concurrency. Path / query encoding uses cli/internal/files/encodepath (JS encodeURIComponent parity).
    • Documented quirk: POST /api/resources/<dir>/ auto-renames an existing dir to Foo (1). The CLI therefore does not pre-mkdir the destination root or empty subdirs — it relies on implicit creation during chunk POSTs.
  • download, cat, rm (cli/internal/files/download, cli/internal/files/rm):
    • download: file or recursive directory; resume via Range, atomic .tmp + rename for full downloads, retry on transient 5xx. Resume retries re-read the local file size before each attempt so a partial append cannot re-send a stale Range: header.
    • cat: streams a single file's bytes to stdout via GET /api/raw/....
    • rm: Unix-like UX (-r, -f, multi-arg, TTY confirmation), batched per-parent DELETE /api/resources/<parent>/ with { "dirents": [...] }.
    • Worked around a backend quirk: GET /api/resources/<file> (no trailing slash) returns HTTP 500 because the handler tries to embed file content. Stat mirrors the web app — list the parent dir and look the basename up in items.
  • cli/cmd/ctl/files/root.go registers all five verbs.
  • Unit tests under cli/internal/files/{upload,download,rm,encodepath}/ and cli/cmd/ctl/files/ (parsing, transfer, retry/resume, delete planning / wire shape).

Package layout: Drive-specific libraries live under cli/internal/files/ (encodepath, upload, download, rm) so they are not mistaken for a stable public API. The legacy root cli/pkg/files (installer download URLs, rate limiter, etc.) remains in pkg for existing bootstrap / storage / terminus importers.

3. olares-cli market commands (b3ba0047)

New market command tree under cli/cmd/ctl/market/ that talks to the per-user Market app-store v2 API at <MarketURL>/app-store/api/v2. It uses the same cmdutil.Factory + X-Authorization transport as files, so it does not depend on kubeconfig. The existing cli/cmd/ctl/app/ tree is kept intact so reviewers can diff the two side by side.

  • Catalog (read-only): list (with -c category filter and -a all-sources), categories, get.
  • Runtime: status with the "not installed" UX fix and the source-fallback hint (App is installed under source 'Y' (not 'X')); supports --watch for op-agnostic recovery (confirming a previously fire-and-forget install reaches running).
  • Lifecycle (mutating, all support --watch): install, upgrade, uninstall, clone, stop, resume, cancel. Per-verb gotchas: clone --title is required (≤ 30 chars), uninstall --cascade / --delete-data, cancel is op-agnostic.
  • Local sources: upload (single file or directory of .tgz / .tar.gz) and delete, restricted to -s upload|studio|cli.

--watch machinery (cli/cmd/ctl/market/watch.go):

  • Polls GetMarketState every --watch-interval (default 2s) until terminal state, bounded by --watch-timeout (default 15m).
  • Per-op terminal sets are derived from the backend's ApplicationManagerState enum (framework/app-service/api/app.bytetrade.io/v1alpha1/appmanager_states.go).
  • matchOpType gates "success" against the in-flight OpType so an upgrade issued on an already-running app cannot return success on tick zero. cancel and status --watch are deliberately op-agnostic.
  • signal.NotifyContext cleans up Ctrl-C with watch canceled by user. The underlying mutation is not stopped — re-attach via status --watch.
  • Output: TTY emits one info line per state transition; -o json emits a single final OperationResult with new finalState / finalOpType fields (omitempty, so non-watch JSON output is byte-identical); -q suppresses transitions but exit code still propagates.

Plumbing:

  • cli/pkg/olares.ID.MarketURL(localPrefix) derives https://market.<localPrefix><terminusName> in the same shape as the existing FilesURL / DesktopURL / VaultURL.
  • credential.ResolvedProfile.MarketURL and buildResolved propagate it through the factory.
  • cmd/ctl/root.go registers market.NewMarketCommand(factory) next to app / profile / files.

Tests: cli/cmd/ctl/market/watch_test.go covers the classifier (per-op terminal sets, OpType gating, cancel op-agnostic path, op-agnostic status --watch) and end-to-end waitForTerminal against an httptest.Server (state transitions, timeout surfacing last-seen state, JSON output stability).

4. Agent skills (68fa9424, b3ba0047)

  • cli/skills/olares-shared/SKILL.md: profile model, login modes, keychain layout, re-auth state machine, error → recovery table, security rules.
  • cli/skills/olares-files/SKILL.md: 3-segment frontend path, trailing-slash conventions, the two server quirks above, X-Authorization transport, per-verb cheatsheet for ls/upload/download/cat/rm (links point at cli/internal/files/... where relevant).
  • cli/skills/olares-market/SKILL.md: source resolution (market.olares catalog vs cli/upload/studio local sources), the install/upgrade/uninstall/stop/resume/cancel state machine, OpType-vs-State race-safety, full market cheatsheet, dedicated --watch section with per-op terminal-set table, op-agnostic status --watch recovery, mermaid flowchart, errors → fixes, typical workflows, and security rules. Defers profile / login / 401-403 recovery to olares-shared.
  • Frontmatter follows the established skill schema (name / version / description / metadata.requires.bins / metadata.cliHelp) so Cursor's skill loader can pick them up.

Test plan

  • cd cli && go build ./... && go vet ./...
  • cd cli && go test ./pkg/auth/... ./internal/files/... ./cmd/ctl/files/... ./cmd/ctl/market/...
  • Manual: olares-cli profile login against a real Olares instance with TOTP enabled — verify the prompt order matches the TS client (password → TOTP only when fa2 is true).
  • Manual: olares-cli profile import from a refresh token, then profile list / profile use.
  • Manual: olares-cli files ls drive/Home/.
  • Manual: olares-cli files upload ./somefile drive/Home/Documents/ (existing dir → no Documents (1) should appear) and same for a directory.
  • Manual: olares-cli files download drive/Home/Documents/somefile ./out and ... -r drive/Home/Documents/ ./outdir.
  • Manual: olares-cli files cat drive/Home/Code/opencode.json (the original HTTP 500 repro — should now stream).
  • Manual: olares-cli files rm drive/Home/tmp/foo.txt and ... -r drive/Home/tmp/.
  • Manual: olares-cli market list / ... categories / ... get firefox against a live cluster.
  • Manual: olares-cli market install firefox --watch blocks until running, prints transitions, exit 0.
  • Manual: olares-cli market install nonexistent-app --watch exits non-zero with installFailed or downloadFailed.
  • Manual: Forgot-watch recovery — olares-cli market install firefox then olares-cli market status firefox --watch reaches running.
  • Manual: olares-cli market upgrade firefox --version <newer> --watch waits past the stale-OpType tick before declaring success.
  • Manual: olares-cli market clone firefox --title "Firefox (work)" --watch lands on running.
  • Manual: olares-cli market stop firefox --watch then ... resume firefox --watch.
  • Manual: olares-cli market cancel firefox --watch against an in-flight install lands on installingCanceled.
  • Manual: Ctrl-C mid-watch produces watch canceled by user and non-zero exit; status --watch re-attaches cleanly.
  • Manual: olares-cli market upload ./myapp-1.0.0.tgz then ... install myapp -s cli --watch.
  • Verify cli/skills/*/SKILL.md are picked up by Cursor (skill list shows olares-shared / olares-files / olares-market).

Made with Cursor


Note

Medium Risk
Adds substantial new CLI surfaces that perform remote file transfer/deletion and app lifecycle operations; mistakes in path handling, retries/resume, or watch state classification could lead to data loss or confusing/incorrect command outcomes.

Overview
Adds a new olares-cli files subtree with front-end path parsing and URL encoding plus verbs for directory listing, resumable chunked uploads, resumable/parallel downloads (including recursive directory pulls), binary-safe cat, and Unix-style remote deletion with -r/-f confirmation and batched deletes.

Adds a new profile-based olares-cli market subtree that talks to the per-user app-store v2 API, including catalog queries (list, categories, get), lifecycle actions (install, upgrade, uninstall, clone, stop, resume, cancel), local chart upload/delete, and a shared --watch poller that waits for terminal states and surfaces final state/optype in structured output.

Minor repo hygiene updates include ignoring generated cli/docs/ and top-level /files/ and /market/ directories in .gitignore.

Reviewed by Cursor Bugbot for commit eb951e7. Bugbot is set up for automated code reviews on this repo. Configure here.

Introduce the multi-profile credential subsystem for olares-cli, covering
the "operate on behalf of a user" scenario via password login and refresh
token import, while keeping the existing kubeconfig-based commands
(osinfo/os/node/gpu/amdgpu/disk) untouched and orthogonal.

New packages:
- pkg/cliconfig: ~/.olares-cli/{config.json,tokens.json} layout, file
  perms (0700/0600), atomic write, MultiProfileConfig with current /
  previous profile pointers, FindByOlaresID, etc.
- pkg/olares: olaresId -> auth/vault/desktop URL derivation (single
  source of truth for all per-profile URLs).
- pkg/auth: stateless protocol layer.
  * login.go (mode A): /api/firstfactor with optional TOTP, password
    salting, cookie-jar HTTP client.
  * refresh.go (mode B): one-shot /api/refresh exchange used to bootstrap
    an access token from a user-supplied refresh token.
  * token_store.go: plaintext-JSON TokenStore (Phase 1) with Get / Set /
    Delete / List / MarkInvalidated. StoredToken includes InvalidatedAt
    (Phase 1 defines, Phase 2 writes on /api/refresh 401/403); Set()
    defensively zeroes it on every fresh grant.
  * jwt.go: ExpiresAt(token) only - no signature verification, no other
    claims exposed (username/groups/mfa/jid are explicitly untrusted).
- pkg/credential: Provider chain + ResolvedProfile.
  * DefaultProvider resolves Profile + token, returning typed errors with
    invalidated > expired > ok priority (ErrTokenInvalidated /
    ErrTokenExpired / ErrNotLoggedIn), each with a "run profile login"
    CTA. Profile config is never silently mutated.
  * EnvProvider stub for future Phase 3 in-cluster scenario.
- pkg/cmdutil/Factory: lazy DI for Credential / ResolvedProfile / HTTP
  client (auto-injects Authorization: Bearer; no auto-refresh in Phase 1).

New commands (cmd/ctl/profile, no separate `auth` namespace):
- profile list / use / remove / login / import.
- login & import auto-create the profile on first use, reuse-and-overwrite
  when the existing token is missing/expired/invalidated, and reject only
  when a still-valid token is present (with a profile-remove hint).
- list shows STATUS: logged-in (Xh) / expired / invalidated / never.

Also:
- Register profile command in cmd/ctl/root.go.
- Ignore cli/docs/ (design notes are local-only, not part of the
  shipping repo).

Phase 1 trade-offs (deferred to Phase 2): OS keychain backend, automatic
refresh-with-lock (sync.Map + flock + double-check), LarePass OAuth
device-flow login, wizard activation -> profile bridge.

Made-with: Cursor
Closes the Phase 1 loop with a first authenticated command and rolls
in the audit fixes from the cleanup pass.

`files ls <fileType>/<extend>[/<subPath>]`:
- Parse + validate the 3-segment front-end path used by files-backend
  (drive/Home, drive/Data, sync/<repo>, awss3/<account>/<bucket>, ...);
  unknown fileType / bad drive extend are rejected client-side before
  any HTTP.
- Percent-encode each segment via url.PathEscape (mirrors the web
  app's encodeUrl) so filenames with `#`, `?`, `+`, spaces, `%`,
  non-ASCII survive the trip to /api/resources.
- Render a one-line header (`<path>  (N dirs, M files, modified ...)`)
  followed by a MODE / SIZE / TYPE / MODIFIED / NAME table; MODE
  decodes os.FileMode (drwxr-xr-x / -rw-r--r-- / Lrwxr-xr-x) and TYPE
  surfaces the backend's class (video / image / audio / pdf / text /
  blob / ...). `--json` passes the raw response through verbatim.
- 401/403 reuses DefaultProvider's "run profile login" CTA; other
  non-2xx surface the backend's error/code+message JSON verbatim.

Supporting plumbing:
- olares.ID.FilesURL derives `https://files.<terminusName>` (matches
  the web app's getModuleSever('files')).
- ResolvedProfile + DefaultProvider expose FilesURL so commands don't
  have to redo the derivation.
- Factory's HTTP client now injects the access token via the custom
  `X-Authorization` header (was `Authorization: Bearer`). Confirmed
  via l4-bfl-proxy + BFL filters that the standard Authorization
  header is filtered at the edge and never reaches per-user services;
  the web app uses the same X-Authorization path.
- Root command grows a persistent `--profile` flag bound straight onto
  the shared Factory.ProfileOverride, so any subcommand that calls
  factory.ResolveProfile honors it without re-declaring the flag.
- Drop the unused Factory.Stderr field.

.gitignore: exclude the local `/files/` and `/market/` checkouts that
are kept beside the repo for cross-reference but must never be
committed.

Tests: ParseFrontendPath (15 cases incl. URL-escape table covering
`#?+%` / spaces / non-ASCII), formatSize (10 cases incl. 1023/1024/1MB
boundary), formatMode (incl. the live-observed dir mode 2147484141 →
drwxr-xr-x), formatType, formatHTTPError (401/403, error/code+message,
raw fallback), renderListing (header + counts + dirs-first sort).

Made-with: Cursor
Replaces the Phase 1 plaintext ~/.olares-cli/tokens.json store with a
per-OS keychain backend so refresh tokens no longer sit on disk in clear
text. The auth.TokenStore surface is unchanged; production code now
constructs a keychainStore via auth.NewTokenStore, while tests inject an
in-memory fake through auth.NewTokenStoreWith.

Backends (cli/internal/keychain):
  - darwin: system Keychain for the master key, AES-256-GCM file blobs
    under StorageDir; falls back to a file-only master key if the system
    keychain is unavailable (sandbox / CI).
  - linux/other: file-based master key + AES-256-GCM blobs under
    StorageDir, all 0600/0700.
  - windows: DPAPI-encrypted values under HKCU\Software\OlaresCli\keychain.

Hardening / UX in the same change:
  - keychainStore.List tolerates a single corrupted entry (warn to stderr,
    skip) instead of aborting the whole `profile list`.
  - StorageDir falls back to os.TempDir with a stderr warning when
    UserHomeDir is unresolvable, so we never silently write to '/'.
  - keychain.Backend(service) reports the active backend label
    (system-keychain / file-fallback / file / registry+dpapi); printed
    after every successful login/import so users notice when they land
    on the file fallback.
  - keychain.PurgeService is invoked when the last profile is removed,
    cleaning up the master key + storage dir / registry subkey so we
    don't leave orphan secrets behind.
  - wrapError stays terse by default and only attaches the verbose
    troubleshooting hint when OLARES_CLI_DEBUG is set.

Refactor:
  - AES-GCM constants and the safeFileName helper live in a single
    aesgcm.go (//go:build !windows) so darwin and linux can't drift on
    the on-disk envelope.
  - The in-memory keychain fake is promoted to its own keychainfake
    subpackage and shared by pkg/auth and cmd/ctl/profile tests.

Docs in cli/internal/keychain/doc.go explain the deltas vs. lark-cli's
upstream copy, including why olares-cli keeps the KeychainAccess
interface + keychainfake (keychainStore has MarkInvalidated / List /
InvalidatedAt semantics that need unit-test coverage).

Made-with: Cursor
Restructure cli/pkg/auth so it mirrors the two-tiered authentication
pattern in apps/packages/app/src/utils/{account.ts,BindTerminusBusiness.ts}
1:1, and migrate the wizard package off its own duplicated copies of the
salt math, cookie jar, and 2FA wiring.

- pkg/auth.LoginRequest: drop the hallucinated TargetURL/SkipSecondFactor
  fields; add NeedTwoFactor (controls vault vs desktop targetURL only)
  and AcceptCookie (1:1 with TS `acceptCookie` arg). Docstrings cite the
  TS file + line numbers so the next change does not have to reverse-
  engineer ground truth.
- pkg/auth.FirstFactor: new exported low-level primitive that POSTs
  /api/firstfactor and returns the raw token without inspecting `fa2`,
  matching TS onFirstFactor (account.ts L7-71).
- pkg/auth.Login: rewritten as a thin wrapper around the shared
  firstFactorWithClient + optional /api/secondfactor/totp escalation,
  matching TS loginTerminus (BindTerminusBusiness.ts L353-446). Gate
  uses tok.FA2 only, deliberately diverging from TS's
  `tok.FA2 || needTwoFactor` because the CLI has no caller-side
  knowledge to defensively force 2FA and OR'ing would surface a
  spurious ErrTOTPRequired the moment a caller probes with the desktop
  targetURL.
- pkg/wizard.UserBindTerminus: switch from auth.Login to auth.FirstFactor
  with NeedTwoFactor=false / AcceptCookie=false, mirroring TS L58-66.
  This restores `olares-cli wizard activate`, which broke when the
  unified auth.Login enforced a 2FA gate the original wizard flow
  intentionally bypassed (no MFA seed exists at signup time).
- pkg/wizard.LoginTerminus: pass NeedTwoFactor through and set
  AcceptCookie=true to match TS L364-372; keep the eager-TOTP and
  ErrTOTPRequired retry paths that bridge the CLI to the MFA-store
  TOTP source.
- profile login: pass NeedTwoFactor=true so /api/firstfactor uses the
  desktop targetURL and Authelia honestly reports fa2=true on
  2FA-enabled accounts. With the previous vault targetURL the server
  silently downgraded to fa2=false and the TOTP prompt never fired.

Made-with: Cursor
Adds four new verbs to `olares-cli files`, mirroring the LarePass web app's
Drive UX over the per-user files-backend on `files.<terminusName>`:

- `files upload`   — resumable chunked upload (Drive v2 protocol: upload-link
  + file-uploaded-bytes + Content-Range POSTs). Recurses into local
  directories; the destination directory must pre-exist on the server because
  POST /api/resources/<dir>/ auto-renames an existing dir to "Dir (1)" instead
  of returning 409.
- `files download` — single-file with server-driven `Range:` resume + atomic
  `.tmp`+rename overwrite, or recursive directory mirroring with
  `--parallel N` errgroup-bounded concurrency.
- `files cat`      — streams `/api/raw/<path>?inline=true` to stdout, refusing
  directories up-front for a friendlier message than the backend's terse 400.
- `files rm`       — Unix-like `-r/-R` and `-f/--force`; groups targets by
  parent dir and issues one `DELETE /api/resources/<parent>/` per group with
  `{"dirents":[...]}` in the body, matching the frontend's batchDelete wire.

`Stat` uses the parent-listing strategy (list parent dir, find leaf in items)
rather than `GET /api/resources/<file>` directly, because the backend's
single-file List handler hard-codes `Content: true` and 500s on most real
files. This matches what the LarePass web app does.

All four commands reuse the existing 3-segment `FrontendPath` parser and
the `X-Authorization` access-token transport from `pkg/cmdutil/factory.go`.
httptest-driven unit tests cover resume / overwrite / retries / range
headers (download), plan grouping + dir-without-r refusal + wire shape (rm),
encodeURIComponent parity + chunked upload protocol (upload).

Made-with: Cursor
Two new SKILL.md files under cli/skills/, modeled after larksuite-cli's
lark-shared / lark-drive style, so AI coding assistants get reliable
guidance on:

- olares-shared: profile model, password+TOTP login (mode A), refresh-token
  import (mode B), profile use/list/remove + global --profile, OS keychain
  storage layout, the re-authentication state machine, and a recovery
  table for HTTP 401/403 / already-authenticated / 2FA-required errors.
- olares-files: the 3-segment frontend path schema, trailing-slash
  conventions, two server-side quirks the agent MUST respect (POST mkdir
  auto-renames existing dirs to "Foo (1)"; GET single-file resource
  returns HTTP 500 — Stat must list parents instead), X-Authorization
  transport, and per-verb cheatsheets for ls / upload / download / cat /
  rm with wire shapes and key flags.

The frontmatter follows the lark-cli schema (name / version / description
with trigger keywords / metadata.requires.bins / metadata.cliHelp), so
Cursor's skill loader can pick them up from cli/skills/<name>/SKILL.md.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
olares-docs Ignored Ignored Preview Apr 26, 2026 8:51am

Request Review

Comment thread cli/internal/files/download/download.go
Comment thread cli/cmd/ctl/files/download.go
Comment thread cli/cmd/ctl/files/upload.go Outdated
- Re-stat the local file before each download attempt in --resume mode
  so Range: bytes=N- tracks partial progress after a failed append;
  accumulate per-attempt bytes for the success return and align error
  returns with that accounting.
- Add TestDownloadFile_ResumeRetryRefreshesRange (partial 206 + cut,
  then second GET with bytes=6-).
- Drop redundant atomic adds under mutex in files upload/download
  commands; use formatBytes from ls.go and remove duplicate humanBytes.

Made-with: Cursor
Comment thread cli/cmd/ctl/files/path.go Outdated
Comment thread .gitignore
framework/app-service/bin

/files/
/market/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gitignore entries appear accidentally committed for local dirs

Low Severity

The /files/ and /market/ entries at the repository root gitignore look like developer-local directory exclusions (likely locally-cloned sibling repos) rather than project-wide patterns. No such directories exist in the repository structure. Committing these pollutes the shared .gitignore with entries that only serve one developer's workspace layout.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8cdfdc. Configure here.

Bugbot: FrontendPath.URLPath used url.PathEscape per segment while
download/cat/rm/upload use upload.EncodeURL, diverging for '+', '!*()',
etc. Delegate URLPath to EncodeURL(p.String()) and extend path tests
(report (1).txt, x+y → x%2By). Update ls command comment.

Made-with: Cursor
Comment thread cli/cmd/ctl/files/upload.go
Move EncodeURL / EncodeURIComponent out of package upload into a small
shared package so download, rm, path, and upload all depend on the same
wire encoder without cmd/pkg layers importing upload only for URLs.

- Add cli/pkg/files/encodepath with tests (former upload/encode*.go).
- Update upload api/uploader, download client, rm, FrontendPath.URLPath.
- Clarify download/list.go is for the download walker, not `files ls`.

Made-with: Cursor
Comment thread .gitignore
.vscode
.DS_Store
cli/output
cli/docs/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gitignore prevents PR deliverable docs from being tracked

Medium Severity

The new cli/docs/ gitignore entry prevents all files under that directory from being committed, but the PR description explicitly lists cli/docs/notes/auth-2fa-semantics.md as a deliverable documenting the 2FA authentication semantics. This file would never be tracked by git.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ad2cad. Configure here.

Comment thread cli/cmd/ctl/files/path.go
Relocate encodepath, download, rm, and upload from cli/pkg/files to
cli/internal/files so they are clearly olares-cli implementation details
(not a stable public library surface). The legacy root package
cli/pkg/files (installer URLs, rate limiter, etc.) stays in pkg for
existing importers (storage, bootstrap, terminus, ...).

Update `files` Cobra imports and olares-files SKILL links.

Made-with: Cursor
@pengpeng
Copy link
Copy Markdown
Member Author

Branch status: cli/profile-auth-phase1 is up to date with origin (latest 542005f8).

Recent follow-ups since the original description:

  • Resume downloads: re-stat local offset before each retry; accumulate bytes across resume attempts; remove redundant atomic under mutex; dedupe formatBytes in ls/upload/download commands.
  • files ls wire paths: FrontendPath.URLPath() now uses the same JS encodeUrl semantics as other verbs (via shared encoder).
  • Shared encoder lives in cli/internal/files/encodepath; Drive clients (download, upload, rm) live under cli/internal/files/... so they are not mistaken for a public library API. Legacy cli/pkg/files (installer URLs, etc.) stays in pkg for existing importers.

Tests run locally: go test ./internal/files/... ./cmd/ctl/files/..., go vet, go build ./....

Add a profile-authenticated `market` command tree that talks to the per-user
Market app-store v2 API (`<MarketURL>/app-store/api/v2`) using the same
`cmdutil.Factory` / `X-Authorization` transport as `olares-cli files`. The
existing `cli/cmd/ctl/app/` tree is kept intact so reviewers can diff the two
side by side.

Verbs:
- Catalog (read-only): `list`, `categories`, `get`.
- Runtime: `status` with the "not installed" UX fix, the source-fallback
  hint, and `--watch` for op-agnostic recovery (e.g. confirming a previously
  fire-and-forget install reaches `running`).
- Lifecycle (mutating, all support `--watch`): `install`, `upgrade`,
  `uninstall`, `clone`, `stop`, `resume`, `cancel`.
- Local sources: `upload` / `delete`, restricted to `-s upload|studio|cli`.

The `--watch` machinery (`cli/cmd/ctl/market/watch.go`) blocks until the
backend reaches a terminal state. Per-op success/failure sets are derived
from the backend's `ApplicationManagerState` enum; `matchOpType` gates
"success" against the in-flight `OpType` so an `upgrade` issued on an
already-`running` app cannot return success on tick zero. `cancel` and
`status --watch` are deliberately op-agnostic. Output: TTY emits one info
line per state transition; `-o json` emits a single final
`OperationResult` with the new `finalState` / `finalOpType` fields
(`omitempty`, so non-watch JSON output is byte-identical). Ctrl-C exits
cleanly via `signal.NotifyContext` with `watch canceled by user`; the
underlying mutation is not stopped — re-attach via `status --watch`.

Plumbing:
- `cli/pkg/olares.ID.MarketURL(localPrefix)` derives the market origin in
  the same shape as the existing `FilesURL` / `DesktopURL` / `VaultURL`.
- `credential.ResolvedProfile.MarketURL` and `buildResolved` propagate it
  through the factory, so commands never touch kubeconfig.
- `cmd/ctl/root.go` registers `market.NewMarketCommand(factory)` next to
  the existing `app` / `profile` / `files` commands.

Skill:
- `cli/skills/olares-market/SKILL.md` mirrors the `olares-files` /
  `olares-shared` shape (frontmatter, top callout, core concepts → auth
  transport → command cheatsheet → `--watch` section → errors → workflows
  → security rules). Defers profile / login / 401-403 recovery to
  `olares-shared` instead of duplicating it.

Tests: `cli/cmd/ctl/market/watch_test.go` covers the classifier (per-op
terminal sets, OpType gating, `cancel` op-agnostic path, op-agnostic
`status --watch`) and end-to-end `waitForTerminal` against an
`httptest.Server` (state transitions, timeout surfacing last-seen state,
JSON output stability).

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 6 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b3ba004. Configure here.

Comment thread cli/cmd/ctl/market/get.go
return ""
}
fieldName := path[len(path)-1]
i18nPath := append(path[:len(path)-1], "i18n")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Slice append mutates caller's variadic path argument

Medium Severity

resolveI18nField uses append(path[:len(path)-1], "i18n") which overwrites the last element of the original path slice's backing array, since the sub-slice has spare capacity. While fieldName is captured before the mutation, the path slice passed by the caller is corrupted. When called from printAppDetail with a literal variadic like "app_info", "app_entry", "i18n", "title", Go allocates a fresh backing array per call so it's safe today — but if path were ever reused or if the compiler optimized the allocation, the last element would be silently replaced with "i18n". The idiomatic fix is to copy the slice before appending.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3ba004. Configure here.

task.RelativePath, uploaded, total,
formatBytes(uploaded), formatBytes(total))
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Progress callback closure captures shared mutable variable

Low Severity

The lastReported variable inside the progress closure is local to each goroutine, so it works correctly for throttling. However, the fmt.Fprintf(out, ...) calls from multiple goroutines writing to the same out writer concurrently have no synchronization. With --parallel > 1, interleaved writes to out can produce garbled output lines. The same issue exists in download.go's runDownloadDir.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3ba004. Configure here.

Comment thread cli/cmd/ctl/files/path.go
eball added 2 commits April 26, 2026 16:49
…to cli/profile-auth-phase1

* 'cli/profile-auth-phase1' of github.com:beclab/olares:
  feat(cli): add olares-cli market commands with --watch + skill
@eball eball merged commit ba4f4e1 into main Apr 26, 2026
14 checks passed
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