reallink-cli 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,7 @@ reallink token create --name "ci-token" --scope trace:read --scope trace:write
53
53
  reallink token revoke --token-id tok_xxx
54
54
 
55
55
  reallink file list --project-id prj_xxx
56
+ reallink file tree --project-id prj_xxx
56
57
  reallink file get --asset-id ast_xxx
57
58
  reallink file stat --asset-id ast_xxx
58
59
  reallink file download --asset-id ast_xxx --output ./downloads/report.json
@@ -60,15 +61,25 @@ reallink file download --asset-id ast_xxx --output ./downloads/report.json --res
60
61
  reallink file upload --project-id prj_xxx --source ./local/report.json --path reports/daily
61
62
  reallink file mkdir --project-id prj_xxx --path reports/archive
62
63
  reallink file move --asset-id ast_xxx --file-name reports/archive/report.json
64
+ reallink file move-folder --project-id prj_xxx --source-path reports/archive --target-path reports/2026
63
65
  reallink file remove --asset-id ast_xxx
64
66
 
67
+ reallink logs status
68
+ reallink logs consent --enable
69
+ reallink logs tail --lines 120
70
+ reallink logs upload --project-id prj_xxx --dry-run
71
+ reallink logs upload --project-id prj_xxx --clear-on-success
72
+
65
73
  reallink tool list
66
74
  reallink tool register --manifest ./spec/tools/trace-placeholder.tool.jsonc
75
+ reallink tool publish --tool-id trace.placeholder --channel ga --visibility public
67
76
  reallink tool enable --tool-id trace.placeholder --project-id prj_xxx
68
77
  reallink tool enable --tool-id trace.placeholder --user-id usr_xxx
69
78
  reallink tool run --tool-id trace.placeholder --project-id prj_xxx --input-file ./run-input.jsonc --idempotency-key run-001
70
79
  reallink tool runs --project-id prj_xxx
71
80
  reallink tool get-run --run-id trun_xxx
81
+ reallink tool run-events --run-id trun_xxx --limit 200
82
+ reallink tool run-events --run-id trun_xxx --stage-prefix workflow. --status succeeded
72
83
 
73
84
  reallink link unreal --project-id prj_xxx --uproject "D:/Games/MyGame/MyGame.uproject" --engine-root "D:/Epic/UE_5.4" --set-default
74
85
  reallink link list
@@ -100,9 +111,24 @@ reallink link plugin install --project-id prj_xxx --name RealLinkUnreal --url ht
100
111
  - Login stores a local session at:
101
112
  - Windows: `%APPDATA%\\reallink\\session.json`
102
113
  - Linux/macOS: `${XDG_CONFIG_HOME:-~/.config}/reallink/session.json`
114
+ - Access token TTL: 15 minutes (auto-refreshed by CLI on 401).
115
+ - Refresh/session TTL: 30 days.
103
116
  - You only need to log in again when the saved session is invalid/expired or you explicitly run `reallink logout`.
104
117
  - `reallink logout` revokes the current server session (`/auth/logout`) and clears the local session file.
105
118
 
119
+ ## CLI Logs & Crash Reports
120
+
121
+ - Local logs are written under:
122
+ - Windows: `%APPDATA%\\reallink\\logs\\`
123
+ - Linux/macOS: `${XDG_CONFIG_HOME:-~/.config}/reallink/logs/`
124
+ - Runtime log file: `runtime.jsonl`
125
+ - Crash reports: `crash/crash-<epoch>.json`
126
+ - Uploading logs requires explicit consent:
127
+ - `reallink logs consent --enable`
128
+ - Upload destination path in project storage:
129
+ - `.reallink/logs/cli/runtime/*`
130
+ - `.reallink/logs/cli/crash/*`
131
+
106
132
  ## Unreal Link Architecture
107
133
 
108
134
  `reallink link ...` provides a Vercel-style "link local project to remote project" workflow for local Unreal development.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallink-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Rust-based CLI for Reallink auth and API operations",
5
5
  "bin": {
6
6
  "reallink": "bin/reallink.cjs"
Binary file
package/rust/Cargo.lock CHANGED
@@ -993,7 +993,7 @@ dependencies = [
993
993
 
994
994
  [[package]]
995
995
  name = "reallink-cli"
996
- version = "0.1.12"
996
+ version = "0.1.13"
997
997
  dependencies = [
998
998
  "anyhow",
999
999
  "clap",
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "reallink-cli"
3
- version = "0.1.12"
3
+ version = "0.1.13"
4
4
  edition = "2021"
5
5
  description = "CLI for Reallink auth and token workflows"
6
6
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
  // Do not edit by hand. Update packages/shared/src/api-contract.ts and re-run codegen.
3
3
  #![allow(dead_code)]
4
4
 
5
- pub const API_CONTRACT_VERSION: &str = "2026.03.0";
5
+ pub const API_CONTRACT_VERSION: &str = "2026.03.1";
6
6
  pub const API_TOKEN_SCOPES: &[&str] = &[
7
7
  "core:read",
8
8
  "core:write",
@@ -0,0 +1,277 @@
1
+ use anyhow::{Context, Result};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::collections::VecDeque;
4
+ use std::fs::{self, OpenOptions};
5
+ use std::io::Write;
6
+ use std::path::{Path, PathBuf};
7
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
+
9
+ const LOGS_DIR: &str = "logs";
10
+ const CRASH_DIR: &str = "crash";
11
+ const CONSENT_FILE: &str = "consent.json";
12
+ const RUNTIME_LOG_FILE: &str = "runtime.jsonl";
13
+
14
+ #[derive(Debug, Clone, Serialize, Deserialize)]
15
+ #[serde(rename_all = "camelCase")]
16
+ pub struct LogConsentConfig {
17
+ pub upload_enabled: bool,
18
+ pub asked_at_epoch_ms: Option<u128>,
19
+ pub updated_at_epoch_ms: u128,
20
+ }
21
+
22
+ impl Default for LogConsentConfig {
23
+ fn default() -> Self {
24
+ Self {
25
+ upload_enabled: false,
26
+ asked_at_epoch_ms: None,
27
+ updated_at_epoch_ms: now_epoch_ms(),
28
+ }
29
+ }
30
+ }
31
+
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ #[serde(rename_all = "camelCase")]
34
+ pub struct RuntimeLogEvent {
35
+ pub ts_epoch_ms: u128,
36
+ pub level: String,
37
+ pub command: String,
38
+ pub message: String,
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ pub duration_ms: Option<u128>,
41
+ #[serde(skip_serializing_if = "Option::is_none")]
42
+ pub exit_code: Option<i32>,
43
+ }
44
+
45
+ #[derive(Debug, Clone, Serialize, Deserialize)]
46
+ #[serde(rename_all = "camelCase")]
47
+ pub struct CrashReport {
48
+ pub ts_epoch_ms: u128,
49
+ pub command: String,
50
+ pub message: String,
51
+ #[serde(skip_serializing_if = "Option::is_none")]
52
+ pub detail: Option<String>,
53
+ }
54
+
55
+ #[derive(Debug, Clone, Serialize)]
56
+ #[serde(rename_all = "camelCase")]
57
+ pub struct LogUploadCandidate {
58
+ pub local_path: PathBuf,
59
+ pub remote_path: String,
60
+ pub content_type: String,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Serialize)]
64
+ #[serde(rename_all = "camelCase")]
65
+ pub struct LogStatus {
66
+ pub logs_root: String,
67
+ pub runtime_log_path: String,
68
+ pub crash_dir: String,
69
+ pub consent_file_path: String,
70
+ pub consent: LogConsentConfig,
71
+ pub runtime_exists: bool,
72
+ pub crash_report_count: usize,
73
+ }
74
+
75
+ fn now_epoch_ms() -> u128 {
76
+ SystemTime::now()
77
+ .duration_since(UNIX_EPOCH)
78
+ .unwrap_or_else(|_| Duration::from_secs(0))
79
+ .as_millis()
80
+ }
81
+
82
+ fn write_atomic(path: &Path, payload: &[u8]) -> Result<()> {
83
+ let temp_path = path.with_extension(format!("tmp.{}", now_epoch_ms()));
84
+ fs::write(&temp_path, payload)
85
+ .with_context(|| format!("Failed to write temporary file {}", temp_path.display()))?;
86
+ if path.exists() {
87
+ fs::remove_file(path).with_context(|| format!("Failed to replace {}", path.display()))?;
88
+ }
89
+ fs::rename(&temp_path, path).with_context(|| {
90
+ format!(
91
+ "Failed to move temporary file {} to {}",
92
+ temp_path.display(),
93
+ path.display()
94
+ )
95
+ })?;
96
+ Ok(())
97
+ }
98
+
99
+ fn logs_root(state_root: &Path) -> PathBuf {
100
+ state_root.join(LOGS_DIR)
101
+ }
102
+
103
+ fn crash_dir(state_root: &Path) -> PathBuf {
104
+ logs_root(state_root).join(CRASH_DIR)
105
+ }
106
+
107
+ fn consent_path(state_root: &Path) -> PathBuf {
108
+ logs_root(state_root).join(CONSENT_FILE)
109
+ }
110
+
111
+ fn runtime_log_path(state_root: &Path) -> PathBuf {
112
+ logs_root(state_root).join(RUNTIME_LOG_FILE)
113
+ }
114
+
115
+ fn ensure_logs_dirs(state_root: &Path) -> Result<()> {
116
+ fs::create_dir_all(logs_root(state_root))
117
+ .with_context(|| format!("Failed to create {}", logs_root(state_root).display()))?;
118
+ fs::create_dir_all(crash_dir(state_root))
119
+ .with_context(|| format!("Failed to create {}", crash_dir(state_root).display()))?;
120
+ Ok(())
121
+ }
122
+
123
+ pub fn load_consent(state_root: &Path) -> Result<LogConsentConfig> {
124
+ let path = consent_path(state_root);
125
+ if !path.exists() {
126
+ return Ok(LogConsentConfig::default());
127
+ }
128
+ let raw =
129
+ fs::read(&path).with_context(|| format!("Failed to read consent file {}", path.display()))?;
130
+ let parsed: LogConsentConfig = serde_json::from_slice(&raw)
131
+ .with_context(|| format!("Invalid consent file {}", path.display()))?;
132
+ Ok(parsed)
133
+ }
134
+
135
+ pub fn set_upload_consent(state_root: &Path, enabled: bool) -> Result<LogConsentConfig> {
136
+ ensure_logs_dirs(state_root)?;
137
+ let mut current = load_consent(state_root)?;
138
+ let now = now_epoch_ms();
139
+ current.upload_enabled = enabled;
140
+ current.updated_at_epoch_ms = now;
141
+ if current.asked_at_epoch_ms.is_none() {
142
+ current.asked_at_epoch_ms = Some(now);
143
+ }
144
+ save_consent(state_root, &current)?;
145
+ Ok(current)
146
+ }
147
+
148
+ pub fn save_consent(state_root: &Path, config: &LogConsentConfig) -> Result<()> {
149
+ ensure_logs_dirs(state_root)?;
150
+ let path = consent_path(state_root);
151
+ let payload = serde_json::to_vec_pretty(config)?;
152
+ write_atomic(&path, &payload)?;
153
+ Ok(())
154
+ }
155
+
156
+ pub fn append_runtime_event(state_root: &Path, event: &RuntimeLogEvent) -> Result<()> {
157
+ ensure_logs_dirs(state_root)?;
158
+ let path = runtime_log_path(state_root);
159
+ let mut writer = OpenOptions::new()
160
+ .create(true)
161
+ .append(true)
162
+ .open(&path)
163
+ .with_context(|| format!("Failed to open runtime log {}", path.display()))?;
164
+ let line = serde_json::to_string(event)?;
165
+ writer
166
+ .write_all(format!("{}\n", line).as_bytes())
167
+ .with_context(|| format!("Failed to write runtime log {}", path.display()))?;
168
+ Ok(())
169
+ }
170
+
171
+ pub fn write_crash_report(state_root: &Path, report: &CrashReport) -> Result<PathBuf> {
172
+ ensure_logs_dirs(state_root)?;
173
+ let file_name = format!("crash-{}.json", report.ts_epoch_ms);
174
+ let path = crash_dir(state_root).join(file_name);
175
+ let payload = serde_json::to_vec_pretty(report)?;
176
+ write_atomic(&path, &payload)?;
177
+ Ok(path)
178
+ }
179
+
180
+ fn file_name_string(path: &Path) -> Option<String> {
181
+ path.file_name()
182
+ .and_then(|value| value.to_str())
183
+ .map(|value| value.to_string())
184
+ }
185
+
186
+ fn content_type_for_path(path: &Path) -> String {
187
+ match path.extension().and_then(|value| value.to_str()) {
188
+ Some("json") => "application/json".to_string(),
189
+ Some("jsonl") => "application/x-ndjson".to_string(),
190
+ Some("log") | Some("txt") => "text/plain".to_string(),
191
+ _ => "application/octet-stream".to_string(),
192
+ }
193
+ }
194
+
195
+ pub fn list_upload_candidates(
196
+ state_root: &Path,
197
+ include_runtime: bool,
198
+ include_crash: bool,
199
+ ) -> Result<Vec<LogUploadCandidate>> {
200
+ ensure_logs_dirs(state_root)?;
201
+ let mut output = Vec::new();
202
+
203
+ if include_runtime {
204
+ let runtime_path = runtime_log_path(state_root);
205
+ if runtime_path.exists() && runtime_path.is_file() {
206
+ if let Some(file_name) = file_name_string(&runtime_path) {
207
+ output.push(LogUploadCandidate {
208
+ local_path: runtime_path.clone(),
209
+ remote_path: format!(".reallink/logs/cli/runtime/{}", file_name),
210
+ content_type: content_type_for_path(&runtime_path),
211
+ });
212
+ }
213
+ }
214
+ }
215
+
216
+ if include_crash {
217
+ let mut crash_files: Vec<PathBuf> = fs::read_dir(crash_dir(state_root))
218
+ .with_context(|| format!("Failed to read {}", crash_dir(state_root).display()))?
219
+ .filter_map(|entry| entry.ok().map(|item| item.path()))
220
+ .filter(|path| path.is_file())
221
+ .collect();
222
+ crash_files.sort();
223
+ for path in crash_files {
224
+ if let Some(file_name) = file_name_string(&path) {
225
+ output.push(LogUploadCandidate {
226
+ local_path: path.clone(),
227
+ remote_path: format!(".reallink/logs/cli/crash/{}", file_name),
228
+ content_type: content_type_for_path(&path),
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ Ok(output)
235
+ }
236
+
237
+ pub fn read_runtime_tail(state_root: &Path, lines: usize) -> Result<Vec<String>> {
238
+ let path = runtime_log_path(state_root);
239
+ if !path.exists() {
240
+ return Ok(Vec::new());
241
+ }
242
+ let content = fs::read_to_string(&path)
243
+ .with_context(|| format!("Failed to read runtime log {}", path.display()))?;
244
+ if lines == 0 {
245
+ return Ok(Vec::new());
246
+ }
247
+ let mut deque = VecDeque::with_capacity(lines);
248
+ for line in content.lines() {
249
+ if deque.len() == lines {
250
+ deque.pop_front();
251
+ }
252
+ deque.push_back(line.to_string());
253
+ }
254
+ Ok(deque.into_iter().collect())
255
+ }
256
+
257
+ pub fn status(state_root: &Path) -> Result<LogStatus> {
258
+ ensure_logs_dirs(state_root)?;
259
+ let runtime_path = runtime_log_path(state_root);
260
+ let crash_directory = crash_dir(state_root);
261
+ let crash_count = fs::read_dir(&crash_directory)
262
+ .with_context(|| format!("Failed to read crash directory {}", crash_directory.display()))?
263
+ .filter_map(|entry| entry.ok())
264
+ .filter(|entry| entry.path().is_file())
265
+ .count();
266
+ let consent = load_consent(state_root)?;
267
+
268
+ Ok(LogStatus {
269
+ logs_root: logs_root(state_root).display().to_string(),
270
+ runtime_log_path: runtime_path.display().to_string(),
271
+ crash_dir: crash_directory.display().to_string(),
272
+ consent_file_path: consent_path(state_root).display().to_string(),
273
+ consent,
274
+ runtime_exists: runtime_path.exists(),
275
+ crash_report_count: crash_count,
276
+ })
277
+ }