Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
253c029
feat(cloud): jcode login/logout via device code flow (M2)
cnjack Jul 20, 2026
342051f
feat(cloud): relay connector - auto-connect on jcode web start (M3)
cnjack Jul 20, 2026
f394769
fix(cloud): durable agent_message synthesized on agent_done (M4 review)
cnjack Jul 20, 2026
b6ef9c5
feat(cloud): E2E encryption - CEK, AES-256-GCM envelopes, pairing CLI…
cnjack Jul 20, 2026
3328e90
docs(cloud): user guide docs/cloud.md + jcode cloud guide command (M7)
cnjack Jul 20, 2026
d6411fa
test(prompts): isolate HOME in TestMemoryLoader_EmptyDir (pre-push fix)
cnjack Jul 20, 2026
9448395
feat(cloud): desktop cloud badge + optional toggle (M9)
cnjack Jul 20, 2026
3a256b2
test(cloud): drop unused return in supervisorTestCreds (lint)
cnjack Jul 20, 2026
36a61b0
refactor(command): fold newCloudConnector into supervisor path (lint)
cnjack Jul 20, 2026
d87b276
feat(cloud): visual login/pairing/QR-code in web UI (M11)
cnjack Jul 21, 2026
1ad091a
feat(cloud): compose facets execution + capabilities report + attachm…
cnjack Jul 21, 2026
dc11f76
docs(agents): cloud relay area rules + HOME-isolated tests
cnjack Jul 21, 2026
c04b7a0
feat(command): JCODE_NO_BROWSER=1 disables browser auto-open in login
cnjack Jul 21, 2026
61186b2
feat(cloud): report actual e2ee state in register (M13)
cnjack Jul 21, 2026
fcb2cf8
feat(jcode-ui): product composer in packages (jcode-ui/product) + rel…
cnjack Jul 21, 2026
8e5a624
feat(cloud): device fingerprint idempotency + per-session cloud sync …
cnjack Jul 21, 2026
9253bc7
style(cloud): parseMachineGUID naming (revive)
cnjack Jul 21, 2026
888536e
feat(web): copy buttons in cloud popover + mode ceiling hook + full_a…
cnjack Jul 21, 2026
2c2c7a8
feat(web): Settings as first-class view with Cloud section (M18)
cnjack Jul 21, 2026
b70ce9a
merge: origin/main into feat/jcode-device-relay (resolve handleNewSes…
cnjack Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,35 @@ Checklist before publish:
- **Don't skip `env.ResolvePath()`.** Raw path concatenation can escape the working directory without warning.
- **Don't import `internal/tui` from non-TUI packages.** The handler interface is the decoupling boundary.
- **Don't depend on `jcode-ui` / `jcode-ui-core` via `file:` or `workspace:*`.** Consumers and `packages/jcode-ui`→core must use registry version ranges so publish and local installs match.
- **Don't let tests read the real HOME.** The pre-push hook runs the full suite; tests must `t.Setenv("HOME", t.TempDir())` whenever code under test resolves `config.ConfigDir()` (a real `~/.jcode/AGENTS.md` breaks `internal/prompts`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document Windows-safe HOME isolation.

Overriding only HOME does not isolate os.UserHomeDir() on Windows; the existing setHome helper also sets USERPROFILE. Update this rule to set both variables to the same temporary directory, or instruct tests to reuse setHome(t), otherwise tests can still read a real ~/.jcode/AGENTS.md on Windows.

Proposed documentation change
-- `t.Setenv("HOME", t.TempDir())` whenever code under test resolves `config.ConfigDir()` (a real `~/.jcode/AGENTS.md` breaks `internal/prompts`).
+- Set both `HOME` and `USERPROFILE` to the same `t.TempDir()` value whenever code under test resolves `config.ConfigDir()` (or reuse the existing `setHome(t)` helper); a real `~/.jcode/AGENTS.md` breaks `internal/prompts`.
📝 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
- **Don't let tests read the real HOME.** The pre-push hook runs the full suite; tests must `t.Setenv("HOME", t.TempDir())` whenever code under test resolves `config.ConfigDir()` (a real `~/.jcode/AGENTS.md` breaks `internal/prompts`).
- **Don't let tests read the real HOME.** The pre-push hook runs the full suite; tests must set both `HOME` and `USERPROFILE` to the same `t.TempDir()` value (or reuse `setHome(t)`) whenever code under test resolves `config.ConfigDir()` (a real `~/.jcode/AGENTS.md` breaks `internal/prompts`).
🧰 Tools
🪛 LanguageTool

[style] ~206-~206: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...o publish and local installs match. - Don't let tests read the real HOME. The ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 206, Update the test HOME-isolation rule in AGENTS.md to
require setting both HOME and USERPROFILE to the same temporary directory, or
instruct tests to reuse the existing setHome(t) helper. Preserve the guidance
about isolating tests that resolve config.ConfigDir() and preventing reads from
the real .jcode/AGENTS.md.


---

## Cloud relay (`internal/cloud/`)

jcode can log into jcloud (device code) and be remote-controlled via an
outbound-only relay; design contract lives in the cloud repo
(`docs/17-jcode-device-relay.md`). Durable rules:

- **Layout:** `credentials.go` (~/.jcode/cloud.json, 0600), `client.go`
(transport, 32MB response cap — attachment commands are large),
`connector.go` + `supervisor.go` (lifecycle, runtime auto_connect toggle),
`events.go` (WS event pump: durable vs ephemeral classification +
`agent_message` synthesis), `sessions.go` (index + capabilities mirror),
`crypto.go` (CEK, AES-256-GCM envelopes, P-256 ECIES, BIP39),
`pairing_inbox.go` (pairing approvals; QR offers auto-approve).
- **E2EE is the default.** All relay payloads are sealed once a CEK exists;
gray plaintext is only for pre-CEK or `cloud.e2ee:false`. Never add an
uplink path that bypasses `sealUplink`/`openDownlink`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **The connector is a client of the local control plane** (`/api/*` on
loopback) — it must never change engine behavior or block `jcode web`;
failures log and back off (≤60s).
- **Contract changes are cross-repo:** bump the cloud orchestrator first
(strict decode rejects unknown fields), keep `docs/17` in sync, and extend
the matching e2e journey in the cloud repo (`e2e/j7`–`j11`).
- **Cross-implementation crypto vectors** live outside the repos at
`jcode-cloud-relay/shared/test-vectors.json`; `TestSharedVectorsFile` skips
cleanly when the sibling dir is absent.

---

Expand Down Expand Up @@ -232,3 +261,4 @@ Checklist before publish:
- **Colors:** every color must come from a CSS custom property (jcode-ui tokens / `tokens.generated.css`). Never hardcode hex/rgb/`#fff`/`white` in components.
- **Themes:** edit `internal/theme/palette.go` and run `make generate` — never edit `tokens.generated.css` or `themes.generated.ts` by hand.
- **Reusable chat UI:** import from the `jcode-ui` package (registry); implement/fix library code under `packages/jcode-ui` and publish.
- **Product composer:** the desktop input experience (`ChatInput` + `WorkspacePicker` / `BranchPicker` / `GoalBanner`) lives in `packages/jcode-ui/src/product/` and is consumed as `jcode-ui/product`. The components are Redux/fetch/Tauri/i18next-free — hosts inject a `ProductComposerHost` (state + actions + strings + icons); the web adapter is `web/src/app/composerHost.ts`. Package tests: `cd packages/jcode-ui && pnpm test` (vitest).
3 changes: 3 additions & 0 deletions cmd/jcode/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func main() {
command.NewSessionsCmd(),
command.NewUpdateCmd(),
command.NewMemoryCmd(),
command.NewLoginCmd(),
command.NewLogoutCmd(),
command.NewCloudCmd(),
)

if err := rootCmd.Execute(); err != nil {
Expand Down
6 changes: 5 additions & 1 deletion desktop/src-tauri/src/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ pub fn start(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
"127.0.0.1",
"--open=false",
])
.current_dir(workdir);
.current_dir(workdir)
// Marks the process as desktop-launched: the sidecar reports
// platform="desktop" at cloud device register (JCODE_DESKTOP detection
// in internal/cloud/connector.go).
.env("JCODE_DESKTOP", "1");

// GUI launches hand us launchd's minimal environment — no Homebrew, no
// profile PATH — so the sidecar can't find `rg`, `git`, node, etc. Overlay
Expand Down
114 changes: 114 additions & 0 deletions docs/cloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# jcode 云端(jcloud)用户指南

把本地 jcode(CLI 或桌面版)登录到 jcode Cloud 后,这台机器就成为一台可被远程查看和控制的"设备":你可以从 cloud 控制台(浏览器)或手机 app 随时给它派活、看会话、处理审批。本文面向终端用户,覆盖登录、远程使用、配对加密和密钥恢复。

> 命令速查:`jcode cloud guide` 会打印本文的精简版。

## 1. 什么是"设备"

- 一台运行 jcode 并通过 `jcode login` 登录到 jcode Cloud 的机器 = 一台设备。登录信息保存在本机 `~/.jcode/cloud.json`。
- 会话始终在你的机器上运行和保存;云端只做中转(relay):你的机器主动向云端发起连接,无需开放入站端口、无需 VPN。
- 从控制台或手机发起的会话就是你本机的会话,会出现在本地 sessions 列表里。

## 2. 快速上手

在你要控制的机器上运行:

```bash
jcode login
```

1. CLI 打印一个验证码(user code)和一个确认链接,并尝试自动打开浏览器。
2. 在浏览器里确认这次登录(确认页地址为云端控制台的 `/device` 路由)。
3. 确认后 CLI 自动完成登录,设备随即上线:打开控制台的设备列表(`/devices`)就能看到它。

常用变体:

```bash
jcode login --cloud https://your-cloud.example.com # self-host 云端(必须 https;仅 localhost/127.0.0.1 开发场景允许 http)
jcode login --name "我的工作站" # 自定义设备名(默认用主机名)
jcode login --status # 查看当前登录状态
```

默认云端地址是 `https://cloud.j-code.net`。

登录后,设备随 `jcode web` 启动自动保持连接(可在 `~/.jcode/config.json` 的 `cloud` 块中用 `auto_connect: false` 关闭)。

## 3. 远程使用

打开控制台 **设备列表**(`/devices`)或手机 app 首页:

- 点进一台设备:发起新会话(可带 `plan` / `full_access` 等模式)、浏览历史会话。
- 点进一个会话:实时跟进输出、发消息追加指令、**停止**运行、处理**权限审批**——和在本机上操作一致。
- **设备离线时**:仍可翻看会话历史,但发消息、停止、审批都会被禁用,直到设备重新上线。

## 4. 配对与端到端加密

会话内容**端到端加密**(AES-256-GCM):云端只存密文和路由元数据,**云端看不到会话内容**。因此每个新客户端(控制台的一个新浏览器、一部手机)必须先与设备"配对"拿到密钥,否则只能看到配对引导卡片。

配对流程:

1. 在新客户端上点击"配对"发起请求(请求 10 分钟有效)。
2. 在设备上查看并批准:

```bash
jcode cloud pairings # 列出待批准的配对请求(含客户端备注名)
jcode cloud approve <id> # 批准:把加密后的密钥交给该客户端
jcode cloud deny <id> # 拒绝
```

批准后客户端自动完成配对,之后即可正常读写加密会话。换浏览器/换手机需要重新配对。

## 5. 密钥与恢复

加密密钥(CEK)在你的第一台设备上生成,永不明文离开你的设备。

- **备份**(强烈建议第一时间做):

```bash
jcode cloud key show-phrase # 二次确认后显示 24 词恢复短语
```

恢复短语能解密账号下全部同步内容——请抄写下来离线妥善保存,不要分享给任何人。它不会保存在其他任何地方。

- **恢复**:所有设备都丢失后,在新登录的设备上输入短语重建密钥:

```bash
jcode cloud key recover
```

- **换钥**:生成新一代密钥(key_gen+1);已配对的客户端需重新配对才能读取新内容:

```bash
jcode cloud rotate-key
```

## 6. 常用命令

| 命令 | 说明 |
|------|------|
| `jcode login` | 登录 jcloud(设备码流程)。`--cloud <url>` 指定云端(默认 `https://cloud.j-code.net`,self-host 必须 https),`--name` 指定设备名 |
| `jcode login --status` | 显示当前登录状态并退出 |
| `jcode logout` | 退出登录:吊销设备令牌并清除本地凭据 |
| `jcode cloud status` | 显示云端地址、设备 id、密钥代数和连通性(心跳探测) |
| `jcode cloud pairings` | 列出待批准的配对请求 |
| `jcode cloud approve <pairing_id>` | 批准配对(为该客户端包裹密钥) |
| `jcode cloud deny <pairing_id>` | 拒绝配对 |
| `jcode cloud key show-phrase` | 显示 CEK 的 24 词恢复短语 |
| `jcode cloud key recover` | 用恢复短语重建 CEK |
| `jcode cloud rotate-key` | 生成新 CEK(key_gen+1);已配对客户端需重新配对 |
| `jcode cloud guide` | 打印本指南的精简版 |

**E2EE 开关**:端到端加密默认开启。需要临时关闭(明文上行,用于灰度回滚或排查)时,在 `~/.jcode/config.json` 中设置:

```json
{ "cloud": { "e2ee": false } }
```

然后重启 `jcode web` 生效。

## 7. 常见问题

- **控制台设备列表是空的?** 先在机器上运行 `jcode login` 并完成浏览器确认;控制台须用用户会话登录(服务令牌看不到设备)。
- **新浏览器里会话全是"未配对"提示?** 这是 E2EE 的正常状态——按第 4 节完成配对即可。
- **设备显示离线?** 确认机器上 `jcode web` 在运行且能访问云端;`jcode cloud status` 会做心跳探测并显示连通性。
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ require (
tinygo.org/x/bluetooth v0.15.0
)

require github.com/tyler-smith/go-bip39 v1.1.0

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
Expand Down Expand Up @@ -60,7 +62,7 @@ require (
github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9
github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/eino-contrib/jsonschema v1.0.3
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down Expand Up @@ -104,7 +106,7 @@ require (
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/tinygo-org/pio v0.3.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
Expand Down Expand Up @@ -330,18 +332,23 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
75 changes: 75 additions & 0 deletions internal/cloud/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// auth.go holds the shared login/logout helpers used by both the CLI
// (`jcode login` / `jcode logout`) and the web API (POST /api/cloud/login,
// /api/cloud/logout) so the two entry points can never drift apart.
package cloud

import (
"context"
"encoding/json"
"os"

"github.com/cnjack/jcode/internal/config"
)

// Logout signs the device out of jcloud: the device token is revoked remotely
// (best effort — a network failure or a missing server endpoint must not trap
// the local credentials), the credentials file and the in-process CEK cache
// are cleared, and config.cloud is updated (enabled=false, URL and user
// preferences preserved). warnf receives non-fatal warnings (revoke/config
// failures); it may be nil. A missing credentials file is a no-op.
func Logout(ctx context.Context, warnf func(format string, args ...any)) error {
warn := func(format string, args ...any) {
if warnf != nil {
warnf(format, args...)
}
}
creds, err := LoadCredentials()
if err != nil {
return err
}
if creds == nil {
return nil // not logged in
}

client := NewClient(creds.CloudURL)
if err := client.RevokeDevice(ctx, creds.DeviceToken); err != nil {
warn("failed to revoke device token on %s: %v — clearing local credentials anyway", creds.CloudURL, err)
}

if err := DeleteCredentials(); err != nil {
return err
}
ResetCEKCache()

if err := UpdateConfigCloud("", false); err != nil {
warn("failed to update %s: %v", config.ConfigPath(), err)
}
return nil
}

// UpdateConfigCloud sets config.cloud while preserving the stored url (when
// the url argument is empty, i.e. logout) and the user's auto_connect/e2ee
// preferences. Login/logout must not require a fully configured provider set,
// so a LoadConfig failure falls back to a best-effort raw read of the file
// (unknown fields may be dropped in that case).
func UpdateConfigCloud(url string, enabled bool) error {
cfg, err := config.LoadConfig()
if err != nil {
cfg = &config.Config{}
if data, readErr := os.ReadFile(config.ConfigPath()); readErr == nil {
_ = json.Unmarshal(data, cfg)
}
}
current := cfg.CloudSettings()
if url == "" {
url = current.URL
}
cfg.SetCloud(&config.CloudConfig{
Enabled: enabled,
URL: url,
AutoConnect: current.AutoConnect,
E2EE: current.E2EE,
SyncDefault: current.SyncDefault,
})
return config.SaveConfig(cfg)
}
Comment on lines +55 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== SetAutoConnect (in-memory s.cfg writer) =="
rg -nP -A22 'func \(s \*Supervisor\) SetAutoConnect' internal/cloud/supervisor.go
echo "== Does anything refresh s.cfg after login's UpdateConfigCloud? =="
rg -nP 's\.cfg|SetCloud|LoadConfig|UpdateConfigCloud' internal/web/cloud_login.go internal/cloud/supervisor.go

Repository: cnjack/jcode

Length of output: 1934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/web/cloud_login.go relevant sections =="
sed -n '180,330p' internal/web/cloud_login.go

echo
echo "== internal/cloud/supervisor.go relevant sections =="
sed -n '1,260p' internal/cloud/supervisor.go

echo
echo "== config helpers =="
rg -nP -A8 -B4 'func (LoadConfig|SaveConfig|SetCloud|CloudSettings|CloudAutoConnect|CloudE2EE|ConfigPath)\b' internal/config -g '*.go'

Repository: cnjack/jcode

Length of output: 16423


Refresh the shared cloud config after login
internal/cloud/auth.go:55-74 writes a fresh config.json, but Supervisor.s.cfg stays stale. A later SetAutoConnect save can write the old Enabled value back to disk and undo the login change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cloud/auth.go` around lines 55 - 74, Update UpdateConfigCloud to
refresh the shared Supervisor.s.cfg after saving the cloud configuration,
ensuring subsequent SetAutoConnect operations use the newly persisted Enabled
value. Reuse the existing supervisor configuration state and preserve the
current URL and cloud-setting update behavior.

59 changes: 59 additions & 0 deletions internal/cloud/backoff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cloud

import (
"context"
"sync"
"time"
)

// Backoff is a thread-safe exponential backoff with a cap, used by every
// connector retry loop (register, poll, WS reconnect). It is injectable so
// tests can shrink the delays.
type Backoff struct {
Min time.Duration
Max time.Duration

mu sync.Mutex
cur time.Duration
}

// NewBackoff returns a Backoff starting at min and capped at max.
func NewBackoff(min, max time.Duration) *Backoff {
return &Backoff{Min: min, Max: max}
}

// Next returns the current delay and doubles it (capped at Max).
func (b *Backoff) Next() time.Duration {
b.mu.Lock()
defer b.mu.Unlock()
if b.cur <= 0 {
b.cur = b.Min
} else {
b.cur *= 2
if b.cur > b.Max {
b.cur = b.Max
}
}
return b.cur
}

// Reset clears the backoff after a success.
func (b *Backoff) Reset() {
b.mu.Lock()
b.cur = 0
b.mu.Unlock()
}

// Wait sleeps for the next backoff delay or until ctx is cancelled (in which
// case it returns ctx.Err()).
func (b *Backoff) Wait(ctx context.Context) error {
d := b.Next()
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
Loading
Loading