gworkspace 0.2.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/dist/cli.js ADDED
@@ -0,0 +1,99 @@
1
+ import { authorize, commandAuthLogin, commandAuthLogout, commandAuthStatus, } from './auth/index.js';
2
+ import { resolveCredentialsPath } from './config.js';
3
+ import { commandCalendarList } from './commands/calendar.js';
4
+ import { commandChatMessages, commandChatSpaces } from './commands/chat.js';
5
+ import { commandDriveGet, commandDriveRecent, commandDriveSearch } from './commands/drive.js';
6
+ import { commandGmailGet, commandGmailSearch } from './commands/gmail.js';
7
+ import { commandTimeDate, commandTimeNow, commandTimeZone } from './commands/time.js';
8
+ import { optString, parseArgs } from './lib/args.js';
9
+ import { fail } from './lib/io.js';
10
+ import { usage } from './usage.js';
11
+ export async function runCli(argv) {
12
+ const { positional, options } = parseArgs(argv);
13
+ const [domain, command, subcommand] = positional;
14
+ const helpRequested = options.help === true || options.h === true;
15
+ if (!domain)
16
+ usage();
17
+ if (domain === 'help')
18
+ usage(command);
19
+ if (helpRequested)
20
+ usage(domain);
21
+ if (!command && ['auth', 'calendar', 'gmail', 'drive', 'chat', 'time'].includes(domain)) {
22
+ usage(domain);
23
+ }
24
+ if (domain === 'auth' && command === 'status')
25
+ commandAuthStatus();
26
+ if (domain === 'auth' && command === 'login')
27
+ await commandAuthLogin(options);
28
+ if (domain === 'auth' && command === 'logout')
29
+ commandAuthLogout();
30
+ if (domain === 'time' && command === 'now')
31
+ commandTimeNow();
32
+ if (domain === 'time' && command === 'date')
33
+ commandTimeDate();
34
+ if (domain === 'time' && command === 'zone')
35
+ commandTimeZone();
36
+ const alias = domain;
37
+ if (alias === 'calendar_getEvents') {
38
+ const auth = await authorize(resolveCredentialsPath(options));
39
+ const patched = {
40
+ ...options,
41
+ from: optString(options, 'timeMin') || optString(options, 'from') || true,
42
+ to: optString(options, 'timeMax') || optString(options, 'to') || true,
43
+ max: optString(options, 'maxResults') || optString(options, 'max') || true,
44
+ };
45
+ await commandCalendarList(auth, patched);
46
+ }
47
+ if (alias === 'gmail_search') {
48
+ const auth = await authorize(resolveCredentialsPath(options));
49
+ await commandGmailSearch(auth, options);
50
+ }
51
+ if (alias === 'drive_search') {
52
+ const auth = await authorize(resolveCredentialsPath(options));
53
+ await commandDriveSearch(auth, options);
54
+ }
55
+ if (domain === 'calendar' && command === 'list') {
56
+ const auth = await authorize(resolveCredentialsPath(options));
57
+ await commandCalendarList(auth, {
58
+ ...options,
59
+ today: subcommand === 'today' ? true : options.today,
60
+ });
61
+ }
62
+ if (domain === 'gmail' && command === 'search') {
63
+ const auth = await authorize(resolveCredentialsPath(options));
64
+ await commandGmailSearch(auth, options);
65
+ }
66
+ if (domain === 'gmail' && command === 'list' && subcommand === 'today') {
67
+ const auth = await authorize(resolveCredentialsPath(options));
68
+ await commandGmailSearch(auth, { ...options, today: true });
69
+ }
70
+ if (domain === 'gmail' && command === 'get') {
71
+ const auth = await authorize(resolveCredentialsPath(options));
72
+ await commandGmailGet(auth, options);
73
+ }
74
+ if (domain === 'drive' && command === 'search') {
75
+ const auth = await authorize(resolveCredentialsPath(options));
76
+ await commandDriveSearch(auth, options);
77
+ }
78
+ if (domain === 'drive' && command === 'recent') {
79
+ const auth = await authorize(resolveCredentialsPath(options));
80
+ await commandDriveRecent(auth, options);
81
+ }
82
+ if (domain === 'drive' && command === 'get') {
83
+ const auth = await authorize(resolveCredentialsPath(options));
84
+ await commandDriveGet(auth, options);
85
+ }
86
+ if (domain === 'chat' && command === 'spaces') {
87
+ const auth = await authorize(resolveCredentialsPath(options));
88
+ await commandChatSpaces(auth, options);
89
+ }
90
+ if (domain === 'chat' && command === 'messages') {
91
+ const auth = await authorize(resolveCredentialsPath(options));
92
+ await commandChatMessages(auth, options);
93
+ }
94
+ if (domain === 'chat' && command === 'list' && subcommand === 'today') {
95
+ const auth = await authorize(resolveCredentialsPath(options));
96
+ await commandChatMessages(auth, { ...options, today: true });
97
+ }
98
+ fail(`Unknown command: ${[domain, command, subcommand].filter(Boolean).join(' ')}`);
99
+ }
@@ -0,0 +1,47 @@
1
+ import { google } from 'googleapis';
2
+ import { optNumber, optString } from '../lib/args.js';
3
+ import { output } from '../lib/io.js';
4
+ function getTodayRangeIso() {
5
+ const start = new Date();
6
+ start.setHours(0, 0, 0, 0);
7
+ const end = new Date(start);
8
+ end.setDate(end.getDate() + 1);
9
+ return { start: start.toISOString(), end: end.toISOString() };
10
+ }
11
+ export async function commandCalendarList(auth, options) {
12
+ const calendar = google.calendar({ version: 'v3', auth });
13
+ const fromOption = optString(options, 'from');
14
+ const toOption = optString(options, 'to');
15
+ const useToday = options.today === true;
16
+ const todayRange = getTodayRangeIso();
17
+ const now = new Date();
18
+ const from = fromOption || (useToday ? todayRange.start : now.toISOString());
19
+ const to = toOption || (useToday ? todayRange.end : new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString());
20
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 250));
21
+ const res = await calendar.events.list({
22
+ calendarId: optString(options, 'calendarId') || 'primary',
23
+ timeMin: from,
24
+ timeMax: to,
25
+ singleEvents: true,
26
+ orderBy: 'startTime',
27
+ maxResults: max,
28
+ });
29
+ const items = res.data.items || [];
30
+ output({
31
+ ok: true,
32
+ action: 'calendar.list',
33
+ today: useToday,
34
+ from,
35
+ to,
36
+ count: items.length,
37
+ events: items.map((item) => ({
38
+ id: item.id,
39
+ status: item.status,
40
+ summary: item.summary,
41
+ description: item.description,
42
+ start: item.start,
43
+ end: item.end,
44
+ htmlLink: item.htmlLink,
45
+ })),
46
+ });
47
+ }
@@ -0,0 +1,82 @@
1
+ import { google } from 'googleapis';
2
+ import { optNumber, optString } from '../lib/args.js';
3
+ import { fail, output } from '../lib/io.js';
4
+ function getTodayStartLocal() {
5
+ const start = new Date();
6
+ start.setHours(0, 0, 0, 0);
7
+ return start;
8
+ }
9
+ function isTodayLocal(isoValue) {
10
+ if (typeof isoValue !== 'string')
11
+ return false;
12
+ const date = new Date(isoValue);
13
+ if (Number.isNaN(date.getTime()))
14
+ return false;
15
+ const start = getTodayStartLocal();
16
+ const end = new Date(start);
17
+ end.setDate(end.getDate() + 1);
18
+ return date >= start && date < end;
19
+ }
20
+ export async function commandChatSpaces(auth, options) {
21
+ const chat = google.chat({ version: 'v1', auth });
22
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 1000));
23
+ const res = await chat.spaces.list({
24
+ pageSize: max,
25
+ pageToken: optString(options, 'pageToken'),
26
+ filter: optString(options, 'filter'),
27
+ });
28
+ const spaces = res.data.spaces || [];
29
+ output({
30
+ ok: true,
31
+ action: 'chat.spaces',
32
+ count: spaces.length,
33
+ nextPageToken: res.data.nextPageToken || null,
34
+ spaces: spaces.map((space) => ({
35
+ name: space.name,
36
+ displayName: space.displayName,
37
+ spaceType: space.spaceType,
38
+ spaceThreadingState: space.spaceThreadingState,
39
+ createTime: space.createTime,
40
+ lastActiveTime: space.lastActiveTime,
41
+ membershipCount: space.membershipCount,
42
+ singleUserBotDm: space.singleUserBotDm,
43
+ externalUserAllowed: space.externalUserAllowed,
44
+ })),
45
+ });
46
+ }
47
+ export async function commandChatMessages(auth, options) {
48
+ const parent = optString(options, 'space');
49
+ if (!parent)
50
+ fail('Missing required --space for chat messages.');
51
+ const chat = google.chat({ version: 'v1', auth });
52
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 1000));
53
+ const useToday = options.today === true;
54
+ const res = await chat.spaces.messages.list({
55
+ parent,
56
+ pageSize: max,
57
+ pageToken: optString(options, 'pageToken'),
58
+ filter: optString(options, 'filter'),
59
+ orderBy: optString(options, 'orderBy') || (useToday ? 'createTime desc' : undefined),
60
+ });
61
+ const allMessages = res.data.messages || [];
62
+ const messages = useToday
63
+ ? allMessages.filter((message) => isTodayLocal(message.createTime))
64
+ : allMessages;
65
+ output({
66
+ ok: true,
67
+ action: 'chat.messages',
68
+ today: useToday,
69
+ space: parent,
70
+ count: messages.length,
71
+ nextPageToken: res.data.nextPageToken || null,
72
+ messages: messages.map((message) => ({
73
+ name: message.name,
74
+ createTime: message.createTime,
75
+ lastUpdateTime: message.lastUpdateTime,
76
+ sender: message.sender?.name || null,
77
+ thread: message.thread?.name || null,
78
+ text: message.text || '',
79
+ argumentText: message.argumentText || '',
80
+ })),
81
+ });
82
+ }
@@ -0,0 +1,52 @@
1
+ import { google } from 'googleapis';
2
+ import { optNumber, optString } from '../lib/args.js';
3
+ import { fail, output } from '../lib/io.js';
4
+ export async function commandDriveSearch(auth, options) {
5
+ const drive = google.drive({ version: 'v3', auth });
6
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 200));
7
+ const res = await drive.files.list({
8
+ q: optString(options, 'query') || 'trashed = false',
9
+ pageSize: max,
10
+ fields: 'files(id,name,mimeType,modifiedTime,owners,webViewLink),nextPageToken',
11
+ includeItemsFromAllDrives: true,
12
+ supportsAllDrives: true,
13
+ });
14
+ const files = res.data.files || [];
15
+ output({
16
+ ok: true,
17
+ action: 'drive.search',
18
+ count: files.length,
19
+ nextPageToken: res.data.nextPageToken || null,
20
+ files,
21
+ });
22
+ }
23
+ export async function commandDriveRecent(auth, options) {
24
+ const drive = google.drive({ version: 'v3', auth });
25
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 200));
26
+ const res = await drive.files.list({
27
+ q: 'trashed = false',
28
+ orderBy: 'modifiedTime desc',
29
+ pageSize: max,
30
+ fields: 'files(id,name,mimeType,modifiedTime,owners,webViewLink),nextPageToken',
31
+ includeItemsFromAllDrives: true,
32
+ supportsAllDrives: true,
33
+ });
34
+ output({
35
+ ok: true,
36
+ action: 'drive.recent',
37
+ count: (res.data.files || []).length,
38
+ files: res.data.files || [],
39
+ });
40
+ }
41
+ export async function commandDriveGet(auth, options) {
42
+ const id = optString(options, 'id');
43
+ if (!id)
44
+ fail('Missing required --id for drive get.');
45
+ const drive = google.drive({ version: 'v3', auth });
46
+ const res = await drive.files.get({
47
+ fileId: id,
48
+ fields: 'id,name,mimeType,modifiedTime,owners,webViewLink,size',
49
+ supportsAllDrives: true,
50
+ });
51
+ output({ ok: true, action: 'drive.get', file: res.data });
52
+ }
@@ -0,0 +1,72 @@
1
+ import { google } from 'googleapis';
2
+ import { optNumber, optString } from '../lib/args.js';
3
+ import { fail, output } from '../lib/io.js';
4
+ function formatGmailDate(d) {
5
+ const year = d.getFullYear();
6
+ const month = `${d.getMonth() + 1}`.padStart(2, '0');
7
+ const day = `${d.getDate()}`.padStart(2, '0');
8
+ return `${year}/${month}/${day}`;
9
+ }
10
+ function getTodayGmailQuery() {
11
+ const start = new Date();
12
+ start.setHours(0, 0, 0, 0);
13
+ const end = new Date(start);
14
+ end.setDate(end.getDate() + 1);
15
+ return `after:${formatGmailDate(start)} before:${formatGmailDate(end)}`;
16
+ }
17
+ export async function commandGmailSearch(auth, options) {
18
+ const gmail = google.gmail({ version: 'v1', auth });
19
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 100));
20
+ const baseQuery = optString(options, 'query') || '';
21
+ const useToday = options.today === true;
22
+ const query = useToday ? `${baseQuery} ${getTodayGmailQuery()}`.trim() : baseQuery;
23
+ const res = await gmail.users.messages.list({
24
+ userId: 'me',
25
+ q: query,
26
+ maxResults: max,
27
+ pageToken: optString(options, 'pageToken'),
28
+ });
29
+ const messages = res.data.messages || [];
30
+ output({
31
+ ok: true,
32
+ action: 'gmail.search',
33
+ today: useToday,
34
+ query,
35
+ count: messages.length,
36
+ nextPageToken: res.data.nextPageToken || null,
37
+ messages: messages.map((m) => ({ id: m.id, threadId: m.threadId })),
38
+ });
39
+ }
40
+ export async function commandGmailGet(auth, options) {
41
+ const id = optString(options, 'id');
42
+ if (!id)
43
+ fail('Missing required --id for gmail get.');
44
+ const gmail = google.gmail({ version: 'v1', auth });
45
+ const res = await gmail.users.messages.get({
46
+ userId: 'me',
47
+ id,
48
+ format: 'metadata',
49
+ metadataHeaders: ['From', 'To', 'Subject', 'Date'],
50
+ });
51
+ const headers = res.data.payload?.headers || [];
52
+ const map = headers.reduce((acc, h) => {
53
+ if (h.name && h.value)
54
+ acc[h.name.toLowerCase()] = h.value;
55
+ return acc;
56
+ }, {});
57
+ output({
58
+ ok: true,
59
+ action: 'gmail.get',
60
+ message: {
61
+ id: res.data.id,
62
+ threadId: res.data.threadId,
63
+ labelIds: res.data.labelIds || [],
64
+ snippet: res.data.snippet || '',
65
+ subject: map.subject || '',
66
+ from: map.from || '',
67
+ to: map.to || '',
68
+ date: map.date || '',
69
+ internalDate: res.data.internalDate || null,
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,31 @@
1
+ import { output } from '../lib/io.js';
2
+ export function commandTimeNow() {
3
+ const now = new Date();
4
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
5
+ output({
6
+ ok: true,
7
+ action: 'time.now',
8
+ utc: now.toISOString(),
9
+ localDate: now.toLocaleDateString('en-CA', { timeZone }),
10
+ localTime: now.toLocaleTimeString('en-GB', { hour12: false, timeZone }),
11
+ timeZone,
12
+ });
13
+ }
14
+ export function commandTimeDate() {
15
+ const now = new Date();
16
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
17
+ output({
18
+ ok: true,
19
+ action: 'time.date',
20
+ utc: now.toISOString().slice(0, 10),
21
+ local: now.toLocaleDateString('en-CA', { timeZone }),
22
+ timeZone,
23
+ });
24
+ }
25
+ export function commandTimeZone() {
26
+ output({
27
+ ok: true,
28
+ action: 'time.zone',
29
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
30
+ });
31
+ }
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { optString } from './lib/args.js';
5
+ export const SCOPES = [
6
+ 'https://www.googleapis.com/auth/calendar.readonly',
7
+ 'https://www.googleapis.com/auth/gmail.readonly',
8
+ 'https://www.googleapis.com/auth/drive.readonly',
9
+ 'https://www.googleapis.com/auth/chat.spaces.readonly',
10
+ 'https://www.googleapis.com/auth/chat.messages.readonly',
11
+ ];
12
+ export const CONFIG_DIR = process.env.GWORKSPACE_CONFIG_DIR ||
13
+ path.join(os.homedir(), '.config', 'gworkspace');
14
+ export const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
15
+ export const CREDENTIALS_PATH = process.env.GOOGLE_OAUTH_CREDENTIALS ||
16
+ path.join(CONFIG_DIR, 'credentials.json');
17
+ export function ensureConfigDir() {
18
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ }
20
+ export function resolveCredentialsPath(options) {
21
+ const fromOption = optString(options, 'credentials');
22
+ return fromOption ? path.resolve(fromOption) : CREDENTIALS_PATH;
23
+ }
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './cli.js';
3
+ import { fail } from './lib/io.js';
4
+ runCli(process.argv.slice(2)).catch((error) => {
5
+ const msg = error instanceof Error ? error.message : String(error);
6
+ fail('Command failed.', msg);
7
+ });
@@ -0,0 +1,65 @@
1
+ export function parseArgs(argv) {
2
+ const positional = [];
3
+ const options = {};
4
+ const setOpt = (key, value) => {
5
+ const current = options[key];
6
+ if (current === undefined) {
7
+ options[key] = value;
8
+ return;
9
+ }
10
+ if (Array.isArray(current)) {
11
+ options[key] = [...current, value];
12
+ return;
13
+ }
14
+ options[key] = [current, value];
15
+ };
16
+ for (let i = 0; i < argv.length; i += 1) {
17
+ const token = argv[i];
18
+ if (!token)
19
+ continue;
20
+ if (token === '--') {
21
+ positional.push(...argv.slice(i + 1));
22
+ break;
23
+ }
24
+ if (token === '-h') {
25
+ setOpt('help', true);
26
+ setOpt('h', true);
27
+ continue;
28
+ }
29
+ if (!token.startsWith('--')) {
30
+ positional.push(token);
31
+ continue;
32
+ }
33
+ const eq = token.indexOf('=');
34
+ if (eq > 2) {
35
+ setOpt(token.slice(2, eq), token.slice(eq + 1));
36
+ continue;
37
+ }
38
+ const key = token.slice(2);
39
+ const next = argv[i + 1];
40
+ if (!next || next.startsWith('--')) {
41
+ setOpt(key, true);
42
+ continue;
43
+ }
44
+ setOpt(key, next);
45
+ i += 1;
46
+ }
47
+ return { positional, options };
48
+ }
49
+ export function optString(options, key) {
50
+ const value = options[key];
51
+ if (value === undefined || typeof value === 'boolean')
52
+ return undefined;
53
+ if (Array.isArray(value))
54
+ return String(value[value.length - 1]);
55
+ return String(value);
56
+ }
57
+ export function optNumber(options, key, fallback) {
58
+ const raw = optString(options, key);
59
+ if (!raw)
60
+ return fallback;
61
+ const parsed = Number.parseInt(raw, 10);
62
+ if (Number.isNaN(parsed))
63
+ return fallback;
64
+ return parsed;
65
+ }
package/dist/lib/io.js ADDED
@@ -0,0 +1,11 @@
1
+ export function output(payload, code = 0) {
2
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
3
+ process.exit(code);
4
+ }
5
+ export function fail(message, details) {
6
+ output({
7
+ ok: false,
8
+ error: message,
9
+ details: details ?? null,
10
+ }, 1);
11
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/usage.js ADDED
@@ -0,0 +1,90 @@
1
+ import { CREDENTIALS_PATH, TOKEN_PATH } from './config.js';
2
+ const HELP = {
3
+ auth: {
4
+ summary: 'Authenticate gw with Google Workspace',
5
+ commands: [
6
+ 'gw auth login [--credentials path/to/credentials.json] [--no-open]',
7
+ 'gw auth status',
8
+ 'gw auth logout',
9
+ ],
10
+ },
11
+ calendar: {
12
+ summary: 'Read calendar events',
13
+ commands: [
14
+ 'gw calendar list --from <ISO> --to <ISO> [--calendarId primary] [--max 20]',
15
+ 'gw calendar list today [--calendarId primary] [--max 20]',
16
+ 'gw calendar_getEvents --timeMin <ISO> --timeMax <ISO> [--calendarId primary]',
17
+ ],
18
+ },
19
+ gmail: {
20
+ summary: 'Search and read Gmail messages',
21
+ commands: [
22
+ 'gw gmail search --query "newer_than:7d" [--max 20]',
23
+ 'gw gmail list today [--max 20]',
24
+ 'gw gmail get --id <messageId>',
25
+ 'gw gmail_search --query "..." --max 20',
26
+ ],
27
+ },
28
+ drive: {
29
+ summary: 'Search and inspect Drive files',
30
+ commands: [
31
+ 'gw drive search --query "trashed = false" [--max 20]',
32
+ 'gw drive recent [--max 20]',
33
+ 'gw drive get --id <fileId>',
34
+ 'gw drive_search --query "..." --max 20',
35
+ ],
36
+ },
37
+ chat: {
38
+ summary: 'Read Google Chat spaces and messages',
39
+ commands: [
40
+ 'gw chat spaces [--max 20] [--filter "spaceType = SPACE"]',
41
+ 'gw chat messages --space spaces/<spaceId> [--max 20]',
42
+ 'gw chat list today --space spaces/<spaceId> [--max 20]',
43
+ ],
44
+ },
45
+ time: {
46
+ summary: 'Print local time context',
47
+ commands: ['gw time now', 'gw time date', 'gw time zone'],
48
+ },
49
+ };
50
+ function printText(text) {
51
+ process.stdout.write(`${text}\n`);
52
+ process.exit(0);
53
+ }
54
+ export function usage(topic) {
55
+ const normalized = topic?.trim().toLowerCase();
56
+ const selected = normalized ? HELP[normalized] : undefined;
57
+ if (selected) {
58
+ const topicName = normalized;
59
+ const commands = selected.commands.map((line) => ` ${line}`).join('\n');
60
+ printText([
61
+ `${topicName.toUpperCase()} COMMANDS`,
62
+ commands,
63
+ '',
64
+ 'LEARN MORE',
65
+ ` Use \`gw ${topicName} --help\` for command usage.`,
66
+ ].join('\n'));
67
+ }
68
+ const core = Object.entries(HELP)
69
+ .map(([name, def]) => ` ${name.padEnd(14)}${def.summary}`)
70
+ .join('\n');
71
+ printText([
72
+ 'Work seamlessly with Google Workspace from the command line.',
73
+ '',
74
+ 'USAGE',
75
+ ' gw <command> <subcommand> [flags]',
76
+ '',
77
+ 'CORE COMMANDS',
78
+ core,
79
+ '',
80
+ 'FLAGS',
81
+ ' --help Show help for command',
82
+ '',
83
+ 'CONFIG FILES',
84
+ ` credentials: ${CREDENTIALS_PATH}`,
85
+ ` token: ${TOKEN_PATH}`,
86
+ '',
87
+ 'LEARN MORE',
88
+ ' Use `gw <command> --help` for more information about a command.',
89
+ ].join('\n'));
90
+ }
package/docs/auth.md ADDED
@@ -0,0 +1,56 @@
1
+ # Auth Flow
2
+
3
+ `gw auth login` uses a browser OAuth loop with Google Desktop OAuth credentials:
4
+
5
+ 1. Start a localhost callback server on a random free port.
6
+ 2. Generate Google OAuth URL with the configured scopes.
7
+ 3. Open the browser automatically.
8
+ 4. Receive callback at `/oauth2callback`.
9
+ 5. Exchange auth code for tokens and store token JSON.
10
+
11
+ ## Files
12
+
13
+ - Credentials: `~/.config/gworkspace/credentials.json` (default)
14
+ - Token: `~/.config/gworkspace/token.json`
15
+
16
+ Use an explicit credentials path:
17
+
18
+ ```bash
19
+ gw auth login --credentials /path/to/credentials.json
20
+ ```
21
+
22
+ Or env var:
23
+
24
+ ```bash
25
+ export GOOGLE_OAUTH_CREDENTIALS=/path/to/credentials.json
26
+ ```
27
+
28
+ ## Flags
29
+
30
+ - `--no-open`: prints/uses auth URL without auto-launching browser.
31
+ - `--credentials`: explicit Desktop OAuth credentials file path.
32
+
33
+ ## Create Credentials (Google Cloud Console)
34
+
35
+ 1. Open Google Cloud Console, select or create a project.
36
+ 2. Enable APIs you need: Calendar, Gmail, Drive, Google Chat.
37
+ 3. Configure OAuth consent screen.
38
+ 4. Go to Credentials -> Create Credentials -> OAuth client ID.
39
+ 5. Select Application type `Desktop app`.
40
+ 6. Download the JSON and save it as `~/.config/gworkspace/credentials.json`.
41
+
42
+ ## Scope updates
43
+
44
+ If new scopes are added (for example Chat read-only scopes), your existing token may not include them.
45
+ Run:
46
+
47
+ ```bash
48
+ gw auth logout
49
+ gw auth login
50
+ ```
51
+
52
+ ## Troubleshooting
53
+
54
+ - `Credentials file not found`: create/download Desktop OAuth credentials and pass `--credentials`, or place the file at `~/.config/gworkspace/credentials.json`.
55
+ - `Invalid credentials file`: use Desktop OAuth client JSON with `installed` or `web` payload.
56
+ - `Authentication timed out`: reopen login and complete consent in browser within 5 minutes.