aethel 0.3.1 → 0.3.2

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 (2026-04-05)
4
+
5
+ - Add --version flag, interactive init folder selection, and release script
6
+
3
7
  ## 0.3.1 (2026-04-05)
4
8
 
5
9
  - 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.1",
3
+ "version": "0.3.2",
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,8 +1,25 @@
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";
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;
6
23
  import { resolveCredentialsPath, resolveTokenPath } from "./core/auth.js";
7
24
  import {
8
25
  initWorkspace,
@@ -145,26 +162,59 @@ async function handleClean(options) {
145
162
 
146
163
  async function handleInit(options) {
147
164
  const localPath = path.resolve(options.localPath);
165
+ let driveFolderId = options.driveFolder || null;
166
+ let driveFolderName = options.driveFolderName || null;
167
+
168
+ // Interactive folder selection when no --drive-folder is provided
169
+ if (!driveFolderId) {
170
+ const repo = await openRepo(options, { requireWorkspace: false });
171
+ console.log("Fetching root-level Drive folders...");
172
+ const folders = await repo.listRootFolders();
173
+
174
+ if (folders.length === 0) {
175
+ console.log("No folders found in Drive root. Syncing entire My Drive.");
176
+ driveFolderName = "My Drive";
177
+ } else {
178
+ console.log("\nDrive folders:");
179
+ console.log(" 0) My Drive (entire drive)");
180
+ for (const [i, folder] of folders.entries()) {
181
+ console.log(` ${i + 1}) ${folder.name}`);
182
+ }
183
+
184
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
185
+ try {
186
+ const answer = await rl.question(`\nSelect a folder [0-${folders.length}]: `);
187
+ const index = Number.parseInt(answer, 10);
188
+
189
+ if (index === 0 || answer.trim() === "") {
190
+ driveFolderName = "My Drive";
191
+ } else if (index >= 1 && index <= folders.length) {
192
+ const selected = folders[index - 1];
193
+ driveFolderId = selected.id;
194
+ driveFolderName = selected.name;
195
+ } else {
196
+ console.log("Invalid selection. Aborting.");
197
+ return;
198
+ }
199
+ } finally {
200
+ rl.close();
201
+ }
202
+ }
203
+ }
148
204
 
149
205
  if (!fs.existsSync(localPath)) {
150
206
  await fs.promises.mkdir(localPath, { recursive: true });
151
207
  }
152
208
 
153
- const root = initWorkspace(
154
- localPath,
155
- options.driveFolder || null,
156
- options.driveFolderName || "My Drive"
157
- );
209
+ const root = initWorkspace(localPath, driveFolderId, driveFolderName || "My Drive");
158
210
 
159
211
  const created = createDefaultIgnoreFile(root);
160
- console.log(`Initialised Aethel workspace at ${root}`);
212
+ console.log(`\nInitialised Aethel workspace at ${root}`);
161
213
  if (created) {
162
214
  console.log(" Created .aethelignore with default patterns");
163
215
  }
164
- if (options.driveFolder) {
165
- console.log(
166
- ` Drive folder: ${options.driveFolderName || "My Drive"} (${options.driveFolder})`
167
- );
216
+ if (driveFolderId) {
217
+ console.log(` Drive folder: ${driveFolderName} (${driveFolderId})`);
168
218
  } else {
169
219
  console.log(" Syncing entire My Drive");
170
220
  }
@@ -816,6 +866,7 @@ async function main() {
816
866
 
817
867
  program
818
868
  .name("aethel")
869
+ .version(versionString, "-v, --version")
819
870
  .description("Git-like Google Drive sync management and cleanup")
820
871
  .showHelpAfterError();
821
872
 
@@ -835,13 +886,14 @@ async function main() {
835
886
  .option("--confirm <phrase>", "Confirmation phrase required for --execute", "")
836
887
  ).action(handleClean);
837
888
 
838
- program
839
- .command("init")
840
- .description("Initialise a sync workspace")
841
- .option("--local-path <path>", "Local directory to sync", ".")
842
- .option("--drive-folder <id>", "Drive folder ID to sync")
843
- .option("--drive-folder-name <name>", "Display name for the Drive folder")
844
- .action(handleInit);
889
+ addAuthOptions(
890
+ program
891
+ .command("init")
892
+ .description("Initialise a sync workspace")
893
+ .option("--local-path <path>", "Local directory to sync", ".")
894
+ .option("--drive-folder <id>", "Drive folder ID to sync (omit for interactive selection)")
895
+ .option("--drive-folder-name <name>", "Display name for the Drive folder")
896
+ ).action(handleInit);
845
897
 
846
898
  addAuthOptions(program.command("status").description("Show sync status")).action(
847
899
  handleStatus
@@ -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))";
@@ -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
  }