aethel 0.3.1 → 0.3.3
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/CHANGELOG.md +8 -0
- package/package.json +2 -1
- package/src/cli.js +74 -19
- package/src/core/auth.js +15 -0
- package/src/core/drive-api.js +18 -0
- package/src/core/repository.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3 (2026-04-05)
|
|
4
|
+
|
|
5
|
+
- Persist credentials to ~/.config/aethel/ after auth for seamless init
|
|
6
|
+
|
|
7
|
+
## 0.3.2 (2026-04-05)
|
|
8
|
+
|
|
9
|
+
- Add --version flag, interactive init folder selection, and release script
|
|
10
|
+
|
|
3
11
|
## 0.3.1 (2026-04-05)
|
|
4
12
|
|
|
5
13
|
- Improve setup SOP: default credentials to ~/.config/aethel/ with guided error message
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aethel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Git-style Google Drive sync CLI with interactive TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"auth": "node src/cli.js auth",
|
|
38
38
|
"clean": "node src/cli.js clean",
|
|
39
39
|
"install:cli": "bash scripts/install.sh",
|
|
40
|
+
"release": "bash scripts/release.sh",
|
|
40
41
|
"pack:check": "npm pack --dry-run",
|
|
41
42
|
"prepublishOnly": "npm test && npm run pack:check",
|
|
42
43
|
"test": "node --test",
|
package/src/cli.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import readline from "node:readline/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
5
8
|
import { Command } from "commander";
|
|
6
|
-
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"));
|
|
12
|
+
|
|
13
|
+
function getGitHash() {
|
|
14
|
+
try {
|
|
15
|
+
return execSync("git rev-parse --short HEAD", { cwd: __dirname, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const gitHash = getGitHash();
|
|
22
|
+
const versionString = gitHash ? `${pkg.version} (${gitHash})` : pkg.version;
|
|
23
|
+
import { persistCredentials, resolveCredentialsPath, resolveTokenPath } from "./core/auth.js";
|
|
7
24
|
import {
|
|
8
25
|
initWorkspace,
|
|
9
26
|
requireRoot,
|
|
@@ -102,8 +119,11 @@ async function handleAuth(options) {
|
|
|
102
119
|
const repo = await openRepo(options, { requireWorkspace: false });
|
|
103
120
|
const account = await repo.getAccountInfo();
|
|
104
121
|
|
|
122
|
+
const credentialsPath = resolveCredentialsPath(options.credentials);
|
|
123
|
+
await persistCredentials(credentialsPath);
|
|
124
|
+
|
|
105
125
|
console.log("OAuth initialization completed.");
|
|
106
|
-
console.log(`Credentials path: ${
|
|
126
|
+
console.log(`Credentials path: ${credentialsPath}`);
|
|
107
127
|
console.log(`Token path: ${resolveTokenPath(options.token)}`);
|
|
108
128
|
console.log(`Authenticated user: ${account.name}`);
|
|
109
129
|
console.log(`Authenticated email: ${account.email}`);
|
|
@@ -145,26 +165,59 @@ async function handleClean(options) {
|
|
|
145
165
|
|
|
146
166
|
async function handleInit(options) {
|
|
147
167
|
const localPath = path.resolve(options.localPath);
|
|
168
|
+
let driveFolderId = options.driveFolder || null;
|
|
169
|
+
let driveFolderName = options.driveFolderName || null;
|
|
170
|
+
|
|
171
|
+
// Interactive folder selection when no --drive-folder is provided
|
|
172
|
+
if (!driveFolderId) {
|
|
173
|
+
const repo = await openRepo(options, { requireWorkspace: false });
|
|
174
|
+
console.log("Fetching root-level Drive folders...");
|
|
175
|
+
const folders = await repo.listRootFolders();
|
|
176
|
+
|
|
177
|
+
if (folders.length === 0) {
|
|
178
|
+
console.log("No folders found in Drive root. Syncing entire My Drive.");
|
|
179
|
+
driveFolderName = "My Drive";
|
|
180
|
+
} else {
|
|
181
|
+
console.log("\nDrive folders:");
|
|
182
|
+
console.log(" 0) My Drive (entire drive)");
|
|
183
|
+
for (const [i, folder] of folders.entries()) {
|
|
184
|
+
console.log(` ${i + 1}) ${folder.name}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
188
|
+
try {
|
|
189
|
+
const answer = await rl.question(`\nSelect a folder [0-${folders.length}]: `);
|
|
190
|
+
const index = Number.parseInt(answer, 10);
|
|
191
|
+
|
|
192
|
+
if (index === 0 || answer.trim() === "") {
|
|
193
|
+
driveFolderName = "My Drive";
|
|
194
|
+
} else if (index >= 1 && index <= folders.length) {
|
|
195
|
+
const selected = folders[index - 1];
|
|
196
|
+
driveFolderId = selected.id;
|
|
197
|
+
driveFolderName = selected.name;
|
|
198
|
+
} else {
|
|
199
|
+
console.log("Invalid selection. Aborting.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
rl.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
148
207
|
|
|
149
208
|
if (!fs.existsSync(localPath)) {
|
|
150
209
|
await fs.promises.mkdir(localPath, { recursive: true });
|
|
151
210
|
}
|
|
152
211
|
|
|
153
|
-
const root = initWorkspace(
|
|
154
|
-
localPath,
|
|
155
|
-
options.driveFolder || null,
|
|
156
|
-
options.driveFolderName || "My Drive"
|
|
157
|
-
);
|
|
212
|
+
const root = initWorkspace(localPath, driveFolderId, driveFolderName || "My Drive");
|
|
158
213
|
|
|
159
214
|
const created = createDefaultIgnoreFile(root);
|
|
160
|
-
console.log(
|
|
215
|
+
console.log(`\nInitialised Aethel workspace at ${root}`);
|
|
161
216
|
if (created) {
|
|
162
217
|
console.log(" Created .aethelignore with default patterns");
|
|
163
218
|
}
|
|
164
|
-
if (
|
|
165
|
-
console.log(
|
|
166
|
-
` Drive folder: ${options.driveFolderName || "My Drive"} (${options.driveFolder})`
|
|
167
|
-
);
|
|
219
|
+
if (driveFolderId) {
|
|
220
|
+
console.log(` Drive folder: ${driveFolderName} (${driveFolderId})`);
|
|
168
221
|
} else {
|
|
169
222
|
console.log(" Syncing entire My Drive");
|
|
170
223
|
}
|
|
@@ -816,6 +869,7 @@ async function main() {
|
|
|
816
869
|
|
|
817
870
|
program
|
|
818
871
|
.name("aethel")
|
|
872
|
+
.version(versionString, "-v, --version")
|
|
819
873
|
.description("Git-like Google Drive sync management and cleanup")
|
|
820
874
|
.showHelpAfterError();
|
|
821
875
|
|
|
@@ -835,13 +889,14 @@ async function main() {
|
|
|
835
889
|
.option("--confirm <phrase>", "Confirmation phrase required for --execute", "")
|
|
836
890
|
).action(handleClean);
|
|
837
891
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
892
|
+
addAuthOptions(
|
|
893
|
+
program
|
|
894
|
+
.command("init")
|
|
895
|
+
.description("Initialise a sync workspace")
|
|
896
|
+
.option("--local-path <path>", "Local directory to sync", ".")
|
|
897
|
+
.option("--drive-folder <id>", "Drive folder ID to sync (omit for interactive selection)")
|
|
898
|
+
.option("--drive-folder-name <name>", "Display name for the Drive folder")
|
|
899
|
+
).action(handleInit);
|
|
845
900
|
|
|
846
901
|
addAuthOptions(program.command("status").description("Show sync status")).action(
|
|
847
902
|
handleStatus
|
package/src/core/auth.js
CHANGED
|
@@ -29,6 +29,21 @@ function resolvePath(candidatePath, fallbackFileName) {
|
|
|
29
29
|
return path.join(CONFIG_DIR, fallbackFileName);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export { CONFIG_DIR };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Copy a credentials file into ~/.config/aethel/ so future commands
|
|
36
|
+
* work without --credentials. No-op if the file is already there.
|
|
37
|
+
*/
|
|
38
|
+
export async function persistCredentials(sourcePath) {
|
|
39
|
+
const dest = path.join(CONFIG_DIR, DEFAULT_CREDENTIALS_PATH);
|
|
40
|
+
const resolved = path.resolve(sourcePath);
|
|
41
|
+
if (resolved === dest) return;
|
|
42
|
+
if (fsSyncFallback.existsSync(dest)) return;
|
|
43
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
44
|
+
await fs.copyFile(resolved, dest);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
export function resolveCredentialsPath(customPath) {
|
|
33
48
|
return resolvePath(
|
|
34
49
|
customPath || process.env.GOOGLE_DRIVE_CREDENTIALS_PATH,
|
package/src/core/drive-api.js
CHANGED
|
@@ -685,6 +685,24 @@ export async function getAccountInfo(drive) {
|
|
|
685
685
|
};
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
export async function listRootFolders(drive) {
|
|
689
|
+
const items = [];
|
|
690
|
+
let pageToken = null;
|
|
691
|
+
|
|
692
|
+
do {
|
|
693
|
+
const response = await drive.files.list({
|
|
694
|
+
q: `'root' in parents and mimeType = '${FOLDER_MIME}' and trashed = false`,
|
|
695
|
+
fields: "nextPageToken, files(id, name, modifiedTime)",
|
|
696
|
+
pageSize: PAGE_SIZE,
|
|
697
|
+
pageToken,
|
|
698
|
+
});
|
|
699
|
+
items.push(...(response.data.files || []));
|
|
700
|
+
pageToken = response.data.nextPageToken;
|
|
701
|
+
} while (pageToken);
|
|
702
|
+
|
|
703
|
+
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
704
|
+
}
|
|
705
|
+
|
|
688
706
|
export async function listAccessibleFiles(drive, includeSharedDrives = false) {
|
|
689
707
|
const richFields =
|
|
690
708
|
"nextPageToken, files(id, name, mimeType, size, modifiedTime, parents, ownedByMe, shared, driveId, owners(displayName,emailAddress), capabilities(canAddChildren,canEdit,canTrash,canDelete,canRename))";
|
package/src/core/repository.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getAccountInfo,
|
|
27
27
|
getRemoteState,
|
|
28
28
|
listAccessibleFiles,
|
|
29
|
+
listRootFolders,
|
|
29
30
|
syncLocalDirectoryToParent,
|
|
30
31
|
uploadLocalEntry,
|
|
31
32
|
withDriveRetry,
|
|
@@ -201,6 +202,10 @@ export class Repository {
|
|
|
201
202
|
|
|
202
203
|
// ── File browser (TUI) ─────────────────────────────────────────────
|
|
203
204
|
|
|
205
|
+
async listRootFolders() {
|
|
206
|
+
return listRootFolders(this.drive);
|
|
207
|
+
}
|
|
208
|
+
|
|
204
209
|
async listRemoteFiles({ includeSharedDrives = false } = {}) {
|
|
205
210
|
return listAccessibleFiles(this.drive, includeSharedDrives);
|
|
206
211
|
}
|