Skip to main content

atmos/os_lib/aura/builtins/
cloud.rs

1// 分割: aura/builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ6)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// `drive.ls([folder_id])` — 指定フォルダ直下(省略時は Drive ルート直下)の一覧。
6pub(crate) fn builtin_drive_ls(env: &mut Env, args: &[AST]) -> Result<Value, String> {
7    let vals = eval_args(env, args)?;
8    let folder_id = vals.first().map(unquote);
9    let auth = crate::os_lib::cloud::load_auth();
10    if !auth.is_logged_in() {
11        return Err(String::from(
12            "Not logged in to Google Drive. Log in via the drive app first.",
13        ));
14    }
15    match crate::os_lib::cloud::drive_list_files_in_folder(
16        &auth.access_token,
17        folder_id.as_deref(),
18    ) {
19        Ok(files) => {
20            let mut out = alloc::format!("Drive listing ({} items):\n", files.len());
21            for f in &files {
22                let tag = if f.mime_type == crate::os_lib::cloud::MIME_FOLDER {
23                    "[D]"
24                } else if f.mime_type == crate::os_lib::cloud::MIME_GOOGLE_DOC {
25                    "[G]"
26                } else {
27                    "   "
28                };
29                out.push_str(&alloc::format!("{} {}  ({})\n", tag, f.name, f.id));
30            }
31            Ok(Value::Str(out))
32        }
33        Err(e) => Err(alloc::format!("drive ls error: {}", e)),
34    }
35}
36
37/// `drive.cat(file_id)` — ファイル本文を取得(mime_type を自動判別)。
38pub(crate) fn builtin_drive_cat(env: &mut Env, args: &[AST]) -> Result<Value, String> {
39    let vals = eval_args(env, args)?;
40    if vals.is_empty() {
41        return Err(String::from("USAGE: drive.cat(\"file_id\")"));
42    }
43    let file_id = unquote(&vals[0]);
44    let auth = crate::os_lib::cloud::load_auth();
45    if !auth.is_logged_in() {
46        return Err(String::from("Not logged in to Google Drive."));
47    }
48    match crate::os_lib::cloud::drive_cat(&auth.access_token, &file_id) {
49        Ok(text) => Ok(Value::Str(text)),
50        Err(e) => Err(alloc::format!("drive cat error: {}", e)),
51    }
52}
53
54/// `drive.download(file_id, local_name)` — バイナリ本文を生バイト列のまま
55/// ローカル FS に保存する(画像/zip 等、lossy な drive.cat では壊れるファイル向け)。
56pub(crate) fn builtin_drive_download(env: &mut Env, args: &[AST]) -> Result<Value, String> {
57    let vals = eval_args(env, args)?;
58    if vals.len() < 2 {
59        return Err(String::from("USAGE: drive.download(\"file_id\" \"local_name\")"));
60    }
61    let file_id = unquote(&vals[0]);
62    let local_name = unquote(&vals[1]);
63    let auth = crate::os_lib::cloud::load_auth();
64    if !auth.is_logged_in() {
65        return Err(String::from("Not logged in to Google Drive."));
66    }
67    match crate::os_lib::cloud::drive_download_binary(&auth.access_token, &file_id) {
68        Ok(bytes) => {
69            let len = bytes.len();
70            let fs = crate::kernel::fs::get_fs();
71            match fs.save_file(&local_name, &bytes, "downloaded") {
72                Ok(()) => Ok(Value::Str(alloc::format!(
73                    "Downloaded {} bytes to '{}'.",
74                    len, local_name
75                ))),
76                Err(e) => Err(alloc::format!("drive download save error: {}", e)),
77            }
78        }
79        Err(e) => Err(alloc::format!("drive download error: {}", e)),
80    }
81}
82
83/// `drive.upload(name, content)` — プレーンテキストを新規ファイルとして作成。
84pub(crate) fn builtin_drive_upload(env: &mut Env, args: &[AST]) -> Result<Value, String> {
85    let vals = eval_args(env, args)?;
86    if vals.len() < 2 {
87        return Err(String::from("USAGE: drive.upload(\"name\" \"content\")"));
88    }
89    let name = unquote(&vals[0]);
90    let content = unquote(&vals[1]);
91    let auth = crate::os_lib::cloud::load_auth();
92    if !auth.is_logged_in() {
93        return Err(String::from("Not logged in to Google Drive."));
94    }
95    match crate::os_lib::cloud::drive_create_text(&auth.access_token, &name, &content) {
96        Ok(id) => Ok(Value::Str(alloc::format!(
97            "Uploaded '{}' (id={})",
98            name, id
99        ))),
100        Err(e) => Err(alloc::format!("drive upload error: {}", e)),
101    }
102}
103
104/// `drive.rm(file_id)` — ファイルを削除。
105pub(crate) fn builtin_drive_rm(env: &mut Env, args: &[AST]) -> Result<Value, String> {
106    let vals = eval_args(env, args)?;
107    if vals.is_empty() {
108        return Err(String::from("USAGE: drive.rm(\"file_id\")"));
109    }
110    let file_id = unquote(&vals[0]);
111    let auth = crate::os_lib::cloud::load_auth();
112    if !auth.is_logged_in() {
113        return Err(String::from("Not logged in to Google Drive."));
114    }
115    match crate::os_lib::cloud::drive_delete(&auth.access_token, &file_id) {
116        Ok(()) => Ok(Value::Str(alloc::format!("Deleted {}", file_id))),
117        Err(e) => Err(alloc::format!("drive rm error: {}", e)),
118    }
119}
120
121// ============ gh(GitHub CUI) ============
122// 認証状態は gh_auth.json に保存(cloud::load_gh_auth/save_gh_auth)。
123// デバイスフローは client_secret 不要なため、login → poll の2コマンドで完結する。
124
125/// `gh.login(client_id)` — デバイスコードを要求し、user_code/URL を表示する。
126/// client_id と device_code を gh_auth.json に保存し、次の `gh.poll()` で使う。
127pub(crate) fn builtin_gh_login(env: &mut Env, args: &[AST]) -> Result<Value, String> {
128    let vals = eval_args(env, args)?;
129    if vals.is_empty() {
130        return Err(String::from("USAGE: gh.login(\"client_id\")"));
131    }
132    let client_id = unquote(&vals[0]);
133    match crate::os_lib::cloud::github_request_device_code(&client_id) {
134        Ok(dc) if !dc.user_code.is_empty() => {
135            let mut auth = crate::os_lib::cloud::load_gh_auth();
136            auth.client_id = client_id;
137            auth.device_code = dc.device_code;
138            auth.access_token = String::new();
139            // device_code を保存できなければ、この後の `gh.poll()` は
140            // 「保留中のログインが無い」と言って必ず失敗する。手順を案内する前に
141            // 失敗として返す(従来は保存結果を捨てて案内だけ出していた)。
142            crate::os_lib::cloud::save_gh_auth(&auth)
143                .map_err(|e| alloc::format!("gh login: failed to save device code: {}", e))?;
144            Ok(Value::Str(alloc::format!(
145                "Open {} and enter code: {}\nThen run gh.poll() to complete login.",
146                dc.verification_url, dc.user_code
147            )))
148        }
149        Ok(_) => Err(String::from(
150            "Device code request returned empty (check client_id/scope).",
151        )),
152        Err(e) => Err(alloc::format!("gh login error: {}", e)),
153    }
154}
155
156/// `gh.poll()` — 直前の `gh.login()` で保存した device_code でトークンを1回確認する。
157pub(crate) fn builtin_gh_poll(_env: &mut Env, _args: &[AST]) -> Result<Value, String> {
158    let mut auth = crate::os_lib::cloud::load_gh_auth();
159    if auth.device_code.is_empty() {
160        return Err(String::from("No pending login. Run gh.login(client_id) first."));
161    }
162    match crate::os_lib::cloud::github_poll_token(&auth.client_id, &auth.device_code) {
163        Ok(tok) if !tok.access_token.is_empty() => {
164            auth.access_token = tok.access_token;
165            auth.device_code = String::new();
166            // トークンを保存できていないのに「ログインした」と言わない
167            // (次回起動でログイン状態が消えるのに気づけない)。
168            if let Err(e) = crate::os_lib::cloud::save_gh_auth(&auth) {
169                return Err(alloc::format!(
170                    "Logged in, but the token could not be saved ({}). It will be lost on reboot.",
171                    e
172                ));
173            }
174            Ok(Value::Str(String::from("Logged in to GitHub.")))
175        }
176        Ok(tok) if tok.error == "authorization_pending" => Ok(Value::Str(String::from(
177            "Still pending. Authorize in the browser, then run gh.poll() again.",
178        ))),
179        Ok(tok) if tok.error == "slow_down" => Ok(Value::Str(String::from(
180            "Slow down: wait a few seconds, then run gh.poll() again.",
181        ))),
182        Ok(tok) if !tok.error.is_empty() => Err(alloc::format!("gh poll error: {}", tok.error)),
183        Ok(_) => Ok(Value::Str(String::from("No token yet. Run gh.poll() again."))),
184        Err(e) => Err(alloc::format!("gh poll error: {}", e)),
185    }
186}
187
188/// `gh.user()` — 認証済みユーザ情報。
189pub(crate) fn builtin_gh_user(_env: &mut Env, _args: &[AST]) -> Result<Value, String> {
190    let auth = crate::os_lib::cloud::load_gh_auth();
191    if !auth.is_logged_in() {
192        return Err(String::from(
193            "Not logged in to GitHub. Run gh.login(client_id) first.",
194        ));
195    }
196    match crate::os_lib::cloud::github_get_user(&auth.access_token) {
197        Ok(u) => Ok(Value::Str(alloc::format!(
198            "login={} name={} id={}",
199            u.login, u.name, u.id
200        ))),
201        Err(e) => Err(alloc::format!("gh user error: {}", e)),
202    }
203}
204
205/// `gh.repos()` — 認証済みユーザのリポジトリ一覧。
206pub(crate) fn builtin_gh_repos(_env: &mut Env, _args: &[AST]) -> Result<Value, String> {
207    let auth = crate::os_lib::cloud::load_gh_auth();
208    if !auth.is_logged_in() {
209        return Err(String::from(
210            "Not logged in to GitHub. Run gh.login(client_id) first.",
211        ));
212    }
213    match crate::os_lib::cloud::github_list_repos(&auth.access_token) {
214        Ok(repos) => {
215            let mut out = alloc::format!("GitHub repos ({}):\n", repos.len());
216            for r in &repos {
217                let vis = if r.private { "private" } else { "public" };
218                out.push_str(&alloc::format!("{}  ({})\n", r.full_name, vis));
219            }
220            Ok(Value::Str(out))
221        }
222        Err(e) => Err(alloc::format!("gh repos error: {}", e)),
223    }
224}
225
226/// `gh.logout()` — トークンのみ破棄(client_id は残す)。
227pub(crate) fn builtin_gh_logout(_env: &mut Env, _args: &[AST]) -> Result<Value, String> {
228    let mut auth = crate::os_lib::cloud::load_gh_auth();
229    auth.access_token = String::new();
230    auth.device_code = String::new();
231    match crate::os_lib::cloud::save_gh_auth(&auth) {
232        Ok(()) => Ok(Value::Str(String::from("Logged out of GitHub."))),
233        Err(e) => Err(alloc::format!("gh logout error: {}", e)),
234    }
235}
236