stella-coder 5.2.1 → 5.3.1
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/package.json +1 -1
- package/stella-cli/adb.mjs +200 -0
- package/stella-cli/charts.mjs +411 -0
- package/stella-cli/game-engine.mjs +708 -0
- package/stella-cli/gdrive-backup.mjs +338 -0
- package/stella-cli/gmail.mjs +415 -0
- package/stella-cli/home-assistant.mjs +168 -0
- package/stella-cli/index.mjs +923 -0
- package/stella-cli/yandex-maps.mjs +426 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import { execSync } from "node:child_process"
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), ".stella", "gdrive")
|
|
7
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, "token.json")
|
|
8
|
+
const CREDS_FILE = path.join(CONFIG_DIR, "credentials.json")
|
|
9
|
+
const BACKUPS_DIR = path.join(CONFIG_DIR, "backups")
|
|
10
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "backup-history.json")
|
|
11
|
+
|
|
12
|
+
function ensureDirs() {
|
|
13
|
+
for (const dir of [CONFIG_DIR, BACKUPS_DIR]) {
|
|
14
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getOAuth2URL(clientId, redirectUri) {
|
|
19
|
+
const scopes = ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.metadata.readonly"]
|
|
20
|
+
const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, response_type: "code", scope: scopes.join(" "), access_type: "offline", prompt: "consent" })
|
|
21
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
25
|
+
const resp = await fetch("https://oauth2.googleapis.com/token", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
28
|
+
body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, code, grant_type: "authorization_code", redirect_uri: redirectUri }).toString(),
|
|
29
|
+
})
|
|
30
|
+
return await resp.json()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function refreshTokenValue(clientId, clientSecret, rt) {
|
|
34
|
+
const resp = await fetch("https://oauth2.googleapis.com/token", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
37
|
+
body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, refresh_token: rt, grant_type: "refresh_token" }).toString(),
|
|
38
|
+
})
|
|
39
|
+
return await resp.json()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadToken() {
|
|
43
|
+
if (!fs.existsSync(TOKEN_FILE)) return null
|
|
44
|
+
return JSON.parse(fs.readFileSync(TOKEN_FILE, "utf8"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveToken(token) {
|
|
48
|
+
ensureDirs()
|
|
49
|
+
fs.writeFileSync(TOKEN_FILE, JSON.stringify(token, null, 2))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadCredentials() {
|
|
53
|
+
if (!fs.existsSync(CREDS_FILE)) return null
|
|
54
|
+
return JSON.parse(fs.readFileSync(CREDS_FILE, "utf8"))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadHistory() {
|
|
58
|
+
if (!fs.existsSync(HISTORY_FILE)) return []
|
|
59
|
+
return JSON.parse(fs.readFileSync(HISTORY_FILE, "utf8"))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function saveHistory(history) {
|
|
63
|
+
ensureDirs()
|
|
64
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getDirSize(dirPath) {
|
|
68
|
+
let size = 0
|
|
69
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
const fullPath = path.join(dirPath, item.name)
|
|
72
|
+
if (item.isDirectory()) {
|
|
73
|
+
size += getDirSize(fullPath)
|
|
74
|
+
} else {
|
|
75
|
+
size += fs.statSync(fullPath).size
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return size
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getFilesRecursive(dirPath, base = "") {
|
|
82
|
+
const files = []
|
|
83
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
const relPath = path.join(base, item.name)
|
|
86
|
+
const fullPath = path.join(dirPath, item.name)
|
|
87
|
+
if (item.isDirectory()) {
|
|
88
|
+
files.push(...getFilesRecursive(fullPath, relPath))
|
|
89
|
+
} else {
|
|
90
|
+
files.push({ relPath, fullPath })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createZip(sourceDir, outputPath) {
|
|
97
|
+
const zipPath = outputPath.replace(/\.zip$/, "") + ".zip"
|
|
98
|
+
try {
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
const ps = `Compress-Archive -Path "${sourceDir}\\*" -DestinationPath "${zipPath}" -Force`
|
|
101
|
+
execSync(`powershell -Command "${ps}"`, { stdio: "ignore" })
|
|
102
|
+
} else {
|
|
103
|
+
execSync(`cd "${path.dirname(sourceDir)}" && zip -r "${zipPath}" "${path.basename(sourceDir)}"`, { stdio: "ignore" })
|
|
104
|
+
}
|
|
105
|
+
return zipPath
|
|
106
|
+
} catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class GDriveBackup {
|
|
112
|
+
constructor() {
|
|
113
|
+
ensureDirs()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isConfigured() {
|
|
117
|
+
return fs.existsSync(CREDS_FILE) && fs.existsSync(TOKEN_FILE)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getSetupURL() {
|
|
121
|
+
const creds = loadCredentials()
|
|
122
|
+
if (!creds?.installed?.client_id) return null
|
|
123
|
+
const redirectUri = creds.installed.redirect_uris?.[0] || "http://localhost:3457/callback"
|
|
124
|
+
return getOAuth2URL(creds.installed.client_id, redirectUri)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async completeAuth(code) {
|
|
128
|
+
const creds = loadCredentials()
|
|
129
|
+
if (!creds?.installed) return { success: false, error: "No credentials.json" }
|
|
130
|
+
const redirectUri = creds.installed.redirect_uris?.[0] || "http://localhost:3457/callback"
|
|
131
|
+
const token = await exchangeCode(creds.installed.client_id, creds.installed.client_secret, code, redirectUri)
|
|
132
|
+
if (token.error) return { success: false, error: token.error_description || token.error }
|
|
133
|
+
saveToken(token)
|
|
134
|
+
return { success: true }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getAccessToken() {
|
|
138
|
+
const creds = loadCredentials()
|
|
139
|
+
const token = loadToken()
|
|
140
|
+
if (!creds || !token) return null
|
|
141
|
+
if (token.expiry_date && token.expiry_date > Date.now()) return token.access_token
|
|
142
|
+
const refreshed = await refreshTokenValue(creds.installed.client_id, creds.installed.client_secret, token.refresh_token)
|
|
143
|
+
if (refreshed.access_token) {
|
|
144
|
+
token.access_token = refreshed.access_token
|
|
145
|
+
token.expiry_date = Date.now() + (refreshed.expires_in || 3600) * 1000
|
|
146
|
+
saveToken(token)
|
|
147
|
+
return refreshed.access_token
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async uploadFile(filePath, folderId = "root") {
|
|
153
|
+
const accessToken = await this.getAccessToken()
|
|
154
|
+
if (!accessToken) return { success: false, error: "Not authenticated. Run /gdrive-setup first." }
|
|
155
|
+
|
|
156
|
+
const fileName = path.basename(filePath)
|
|
157
|
+
const fileContent = fs.readFileSync(filePath)
|
|
158
|
+
const metadata = { name: fileName, parents: [folderId] }
|
|
159
|
+
|
|
160
|
+
const boundary = `stella_${Date.now()}`
|
|
161
|
+
const body = [
|
|
162
|
+
`--${boundary}`,
|
|
163
|
+
"Content-Type: application/json; charset=UTF-8",
|
|
164
|
+
"",
|
|
165
|
+
JSON.stringify(metadata),
|
|
166
|
+
`--${boundary}`,
|
|
167
|
+
"Content-Type: application/octet-stream",
|
|
168
|
+
"",
|
|
169
|
+
"",
|
|
170
|
+
].join("\r\n")
|
|
171
|
+
const bodyEnd = `\r\n--${boundary}--`
|
|
172
|
+
const fullBody = Buffer.concat([Buffer.from(body), fileContent, Buffer.from(bodyEnd)])
|
|
173
|
+
|
|
174
|
+
const resp = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": `multipart/related; boundary=${boundary}` },
|
|
177
|
+
body: fullBody,
|
|
178
|
+
})
|
|
179
|
+
const result = await resp.json()
|
|
180
|
+
if (result.id) {
|
|
181
|
+
return { success: true, fileId: result.id, name: result.name, webViewLink: result.webViewLink }
|
|
182
|
+
}
|
|
183
|
+
return { success: false, error: result.error?.message || "Upload failed" }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async backupProject(projectPath, name) {
|
|
187
|
+
if (!fs.existsSync(projectPath)) return { success: false, error: "Path not found" }
|
|
188
|
+
|
|
189
|
+
const backupName = name || `backup_${path.basename(projectPath)}_${new Date().toISOString().replace(/[:.]/g, "-")}`
|
|
190
|
+
const stat = fs.statSync(projectPath)
|
|
191
|
+
|
|
192
|
+
if (stat.isDirectory()) {
|
|
193
|
+
const zipPath = path.join(BACKUPS_DIR, `${backupName}.zip`)
|
|
194
|
+
const created = createZip(projectPath, zipPath)
|
|
195
|
+
if (!created) return { success: false, error: "Failed to create zip" }
|
|
196
|
+
|
|
197
|
+
const uploadResult = await this.uploadFile(zipPath)
|
|
198
|
+
const history = loadHistory()
|
|
199
|
+
history.push({
|
|
200
|
+
name: backupName,
|
|
201
|
+
path: projectPath,
|
|
202
|
+
fileId: uploadResult.fileId,
|
|
203
|
+
size: fs.statSync(zipPath).size,
|
|
204
|
+
date: new Date().toISOString(),
|
|
205
|
+
type: "directory",
|
|
206
|
+
})
|
|
207
|
+
saveHistory(history)
|
|
208
|
+
return { success: true, ...uploadResult, backupName, size: fs.statSync(zipPath).size }
|
|
209
|
+
} else {
|
|
210
|
+
const uploadResult = await this.uploadFile(projectPath)
|
|
211
|
+
const history = loadHistory()
|
|
212
|
+
history.push({
|
|
213
|
+
name: backupName,
|
|
214
|
+
path: projectPath,
|
|
215
|
+
fileId: uploadResult.fileId,
|
|
216
|
+
size: stat.size,
|
|
217
|
+
date: new Date().toISOString(),
|
|
218
|
+
type: "file",
|
|
219
|
+
})
|
|
220
|
+
saveHistory(history)
|
|
221
|
+
return { success: true, ...uploadResult, backupName, size: stat.size }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async listBackups() {
|
|
226
|
+
const accessToken = await this.getAccessToken()
|
|
227
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
228
|
+
|
|
229
|
+
const params = new URLSearchParams({
|
|
230
|
+
q: "trashed=false",
|
|
231
|
+
fields: "files(id,name,size,createdTime,modifiedTime,mimeType)",
|
|
232
|
+
orderBy: "createdTime desc",
|
|
233
|
+
pageSize: "50",
|
|
234
|
+
})
|
|
235
|
+
const resp = await fetch(`https://www.googleapis.com/drive/v3/files?${params.toString()}`, {
|
|
236
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
237
|
+
})
|
|
238
|
+
const data = await resp.json()
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
files: (data.files || []).map(f => ({
|
|
242
|
+
id: f.id,
|
|
243
|
+
name: f.name,
|
|
244
|
+
size: parseInt(f.size || "0"),
|
|
245
|
+
created: f.createdTime,
|
|
246
|
+
modified: f.modifiedTime,
|
|
247
|
+
mimeType: f.mimeType,
|
|
248
|
+
})),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async downloadFile(fileId, savePath) {
|
|
253
|
+
const accessToken = await this.getAccessToken()
|
|
254
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
255
|
+
|
|
256
|
+
const resp = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
|
|
257
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
258
|
+
})
|
|
259
|
+
const buffer = Buffer.from(await resp.arrayBuffer())
|
|
260
|
+
fs.writeFileSync(savePath, buffer)
|
|
261
|
+
return { success: true, path: savePath, size: buffer.length }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async deleteFile(fileId) {
|
|
265
|
+
const accessToken = await this.getAccessToken()
|
|
266
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
267
|
+
|
|
268
|
+
await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
|
|
269
|
+
method: "DELETE",
|
|
270
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
271
|
+
})
|
|
272
|
+
return { success: true }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async createFolder(name) {
|
|
276
|
+
const accessToken = await this.getAccessToken()
|
|
277
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
278
|
+
|
|
279
|
+
const resp = await fetch("https://www.googleapis.com/drive/v3/files", {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
282
|
+
body: JSON.stringify({ name, mimeType: "application/vnd.google-apps.folder" }),
|
|
283
|
+
})
|
|
284
|
+
const result = await resp.json()
|
|
285
|
+
if (result.id) return { success: true, folderId: result.id, name: result.name }
|
|
286
|
+
return { success: false, error: result.error?.message }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async searchFiles(query) {
|
|
290
|
+
const accessToken = await this.getAccessToken()
|
|
291
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
292
|
+
|
|
293
|
+
const params = new URLSearchParams({
|
|
294
|
+
q: `name contains '${query}' and trashed=false`,
|
|
295
|
+
fields: "files(id,name,size,createdTime,mimeType)",
|
|
296
|
+
pageSize: "20",
|
|
297
|
+
})
|
|
298
|
+
const resp = await fetch(`https://www.googleapis.com/drive/v3/files?${params.toString()}`, {
|
|
299
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
300
|
+
})
|
|
301
|
+
const data = await resp.json()
|
|
302
|
+
return { success: true, files: data.files || [] }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async getQuota() {
|
|
306
|
+
const accessToken = await this.getAccessToken()
|
|
307
|
+
if (!accessToken) return { success: false, error: "Not authenticated" }
|
|
308
|
+
|
|
309
|
+
const resp = await fetch("https://www.googleapis.com/drive/v3/about?fields=storageQuota", {
|
|
310
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
311
|
+
})
|
|
312
|
+
const data = await resp.json()
|
|
313
|
+
const q = data.storageQuota || {}
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
used: parseInt(q.usage || "0"),
|
|
317
|
+
limit: parseInt(q.limit || "0"),
|
|
318
|
+
usedFormatted: formatBytes(parseInt(q.usage || "0")),
|
|
319
|
+
limitFormatted: formatBytes(parseInt(q.limit || "0")),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getHistory() {
|
|
324
|
+
return loadHistory()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async restoreBackup(backupEntry, restorePath) {
|
|
328
|
+
return this.downloadFile(backupEntry.fileId, restorePath)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formatBytes(bytes) {
|
|
333
|
+
if (bytes === 0) return "0 B"
|
|
334
|
+
const k = 1024
|
|
335
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"]
|
|
336
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
337
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
|
|
338
|
+
}
|