@donebear/cli 0.1.0
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/AGENTS.md +49 -0
- package/README.md +197 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +3676 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +263 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3676 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { createClient } from "@supabase/supabase-js";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { config } from "dotenv";
|
|
10
|
+
import { execSync, spawn } from "node:child_process";
|
|
11
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
import { styleText } from "node:util";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
|
|
16
|
+
//#region src/constants.ts
|
|
17
|
+
const CLI_NAME = "donebear";
|
|
18
|
+
const DONEBEAR_TOKEN_ENV = "DONEBEAR_TOKEN";
|
|
19
|
+
const DONEBEAR_DEBUG_ENV = "DONEBEAR_DEBUG";
|
|
20
|
+
const DONEBEAR_CONFIG_DIR_ENV = "DONEBEAR_CONFIG_DIR";
|
|
21
|
+
const DONEBEAR_SUPABASE_URL_ENV = "DONEBEAR_SUPABASE_URL";
|
|
22
|
+
const DONEBEAR_SUPABASE_KEY_ENV = "DONEBEAR_SUPABASE_PUBLISHABLE_KEY";
|
|
23
|
+
const DONEBEAR_API_URL_ENV = "DONEBEAR_API_URL";
|
|
24
|
+
const FALLBACK_SUPABASE_URL_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL"];
|
|
25
|
+
const FALLBACK_SUPABASE_KEY_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY", "SUPABASE_ANON_KEY"];
|
|
26
|
+
const FALLBACK_API_URL_ENV_KEYS = ["NEXT_PUBLIC_RESERVE_MANAGE_API"];
|
|
27
|
+
const DEFAULT_API_URL = "http://127.0.0.1:3001";
|
|
28
|
+
const DEFAULT_OAUTH_PROVIDER = "google";
|
|
29
|
+
const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
|
|
30
|
+
const DEFAULT_OAUTH_CALLBACK_PATH = "/auth/callback";
|
|
31
|
+
const DEFAULT_OAUTH_TIMEOUT_SECONDS = 180;
|
|
32
|
+
function getDefaultConfigBaseDir() {
|
|
33
|
+
if (process.platform === "win32") return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
34
|
+
return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
35
|
+
}
|
|
36
|
+
const configBaseDir = getDefaultConfigBaseDir();
|
|
37
|
+
const CONFIG_DIR = process.env[DONEBEAR_CONFIG_DIR_ENV] ?? join(configBaseDir, CLI_NAME);
|
|
38
|
+
const AUTH_FILE_PATH = join(CONFIG_DIR, "auth.json");
|
|
39
|
+
const CONTEXT_FILE_PATH = join(CONFIG_DIR, "context.json");
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/errors.ts
|
|
43
|
+
const EXIT_CODES = {
|
|
44
|
+
SUCCESS: 0,
|
|
45
|
+
ERROR: 1,
|
|
46
|
+
CANCELLED: 2,
|
|
47
|
+
AUTH_REQUIRED: 4
|
|
48
|
+
};
|
|
49
|
+
var CliError = class extends Error {
|
|
50
|
+
exitCode;
|
|
51
|
+
constructor(message, exitCode = EXIT_CODES.ERROR) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "CliError";
|
|
54
|
+
this.exitCode = exitCode;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function toCliError(error) {
|
|
58
|
+
if (error instanceof CliError) return error;
|
|
59
|
+
if (error instanceof Error) return new CliError(error.message, EXIT_CODES.ERROR);
|
|
60
|
+
return new CliError("Unknown error", EXIT_CODES.ERROR);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/storage.ts
|
|
65
|
+
function isRecord$1(value) {
|
|
66
|
+
return typeof value === "object" && value !== null;
|
|
67
|
+
}
|
|
68
|
+
function isOAuthProvider(value) {
|
|
69
|
+
return value === "google" || value === "github";
|
|
70
|
+
}
|
|
71
|
+
function parseStoredAuthSession(value) {
|
|
72
|
+
if (!isRecord$1(value)) return null;
|
|
73
|
+
if (typeof value.accessToken !== "string") return null;
|
|
74
|
+
if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
|
|
75
|
+
if (value.tokenType !== null && typeof value.tokenType !== "string") return null;
|
|
76
|
+
if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
|
|
77
|
+
if (!isOAuthProvider(value.provider)) return null;
|
|
78
|
+
const user = value.user;
|
|
79
|
+
if (user !== null && (!isRecord$1(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
|
|
80
|
+
if (typeof value.createdAt !== "string") return null;
|
|
81
|
+
const parsedUser = user === null ? null : {
|
|
82
|
+
id: user.id,
|
|
83
|
+
email: user.email ?? null
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
accessToken: value.accessToken,
|
|
87
|
+
refreshToken: value.refreshToken,
|
|
88
|
+
tokenType: value.tokenType,
|
|
89
|
+
expiresAt: value.expiresAt,
|
|
90
|
+
provider: value.provider,
|
|
91
|
+
user: parsedUser,
|
|
92
|
+
createdAt: value.createdAt
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseAuthFile(payload) {
|
|
96
|
+
const parsed = JSON.parse(payload);
|
|
97
|
+
if (!isRecord$1(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
98
|
+
const session = parseStoredAuthSession(parsed.session);
|
|
99
|
+
if (!session) throw new CliError(`Invalid auth session in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
100
|
+
return {
|
|
101
|
+
version: 1,
|
|
102
|
+
session
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function readStoredAuthSession() {
|
|
106
|
+
try {
|
|
107
|
+
return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf8")).session;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (isRecord$1(error) && error.code === "ENOENT") return null;
|
|
110
|
+
if (error instanceof CliError) throw error;
|
|
111
|
+
throw new CliError(`Failed to read credentials from ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function writeStoredAuthSession(session) {
|
|
115
|
+
const payload = {
|
|
116
|
+
version: 1,
|
|
117
|
+
session
|
|
118
|
+
};
|
|
119
|
+
await mkdir(CONFIG_DIR, {
|
|
120
|
+
recursive: true,
|
|
121
|
+
mode: 448
|
|
122
|
+
});
|
|
123
|
+
await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
mode: 384
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function clearStoredAuthSession() {
|
|
129
|
+
await rm(AUTH_FILE_PATH, { force: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/env.ts
|
|
134
|
+
const DEFAULT_ENV_FILES = [
|
|
135
|
+
".env.local",
|
|
136
|
+
".env",
|
|
137
|
+
"apps/manage-frontend/.env.local",
|
|
138
|
+
"apps/manage-api/.env"
|
|
139
|
+
];
|
|
140
|
+
let loaded = false;
|
|
141
|
+
function loadCliEnvironmentFiles() {
|
|
142
|
+
if (loaded) return;
|
|
143
|
+
for (const relativePath of DEFAULT_ENV_FILES) {
|
|
144
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
145
|
+
if (!existsSync(absolutePath)) continue;
|
|
146
|
+
config({
|
|
147
|
+
path: absolutePath,
|
|
148
|
+
override: false,
|
|
149
|
+
quiet: true
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
loaded = true;
|
|
153
|
+
}
|
|
154
|
+
function readEnvValue(key) {
|
|
155
|
+
const value = process.env[key];
|
|
156
|
+
if (typeof value !== "string") return;
|
|
157
|
+
const trimmed = value.trim();
|
|
158
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/supabase.ts
|
|
163
|
+
var MemoryStorage = class {
|
|
164
|
+
values = /* @__PURE__ */ new Map();
|
|
165
|
+
getItem(key) {
|
|
166
|
+
return this.values.get(key) ?? null;
|
|
167
|
+
}
|
|
168
|
+
setItem(key, value) {
|
|
169
|
+
this.values.set(key, value);
|
|
170
|
+
}
|
|
171
|
+
removeItem(key) {
|
|
172
|
+
this.values.delete(key);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function firstDefined$1(keys) {
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
const value = readEnvValue(key);
|
|
178
|
+
if (value) return value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function resolveSupabaseConfig() {
|
|
182
|
+
const url = readEnvValue(DONEBEAR_SUPABASE_URL_ENV) ?? firstDefined$1(FALLBACK_SUPABASE_URL_ENV_KEYS);
|
|
183
|
+
const publishableKey = readEnvValue(DONEBEAR_SUPABASE_KEY_ENV) ?? firstDefined$1(FALLBACK_SUPABASE_KEY_ENV_KEYS);
|
|
184
|
+
if (!(url && publishableKey)) throw new CliError([
|
|
185
|
+
"Supabase configuration is missing.",
|
|
186
|
+
`Set ${DONEBEAR_SUPABASE_URL_ENV} and ${DONEBEAR_SUPABASE_KEY_ENV}.`,
|
|
187
|
+
`Fallback keys are also supported: ${FALLBACK_SUPABASE_URL_ENV_KEYS.join(", ")} and ${FALLBACK_SUPABASE_KEY_ENV_KEYS.join(", ")}.`
|
|
188
|
+
].join(" "), EXIT_CODES.ERROR);
|
|
189
|
+
return {
|
|
190
|
+
url,
|
|
191
|
+
publishableKey
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function createSupabaseClient(config, options) {
|
|
195
|
+
return createClient(config.url, config.publishableKey, { auth: {
|
|
196
|
+
flowType: "pkce",
|
|
197
|
+
autoRefreshToken: false,
|
|
198
|
+
detectSessionInUrl: false,
|
|
199
|
+
persistSession: options?.persistSession ?? false,
|
|
200
|
+
storage: options?.storage
|
|
201
|
+
} });
|
|
202
|
+
}
|
|
203
|
+
function createPkceSupabaseClient(config) {
|
|
204
|
+
return createSupabaseClient(config, {
|
|
205
|
+
storage: new MemoryStorage(),
|
|
206
|
+
persistSession: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function toStoredAuthSession(session, provider) {
|
|
210
|
+
const expiresAt = typeof session.expires_at === "number" ? (/* @__PURE__ */ new Date(session.expires_at * 1e3)).toISOString() : null;
|
|
211
|
+
return {
|
|
212
|
+
accessToken: session.access_token,
|
|
213
|
+
refreshToken: session.refresh_token ?? null,
|
|
214
|
+
tokenType: session.token_type ?? null,
|
|
215
|
+
expiresAt,
|
|
216
|
+
provider,
|
|
217
|
+
user: session.user ? {
|
|
218
|
+
id: session.user.id,
|
|
219
|
+
email: session.user.email ?? null
|
|
220
|
+
} : null,
|
|
221
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function refreshSupabaseSession(config, refreshToken) {
|
|
225
|
+
const { data, error } = await createSupabaseClient(config).auth.refreshSession({ refresh_token: refreshToken });
|
|
226
|
+
if (error) throw new CliError(`Failed to refresh session: ${error.message}`, EXIT_CODES.AUTH_REQUIRED);
|
|
227
|
+
if (!data.session) throw new CliError("No session returned during refresh", EXIT_CODES.AUTH_REQUIRED);
|
|
228
|
+
return data.session;
|
|
229
|
+
}
|
|
230
|
+
async function getSupabaseUser(config, accessToken) {
|
|
231
|
+
const { data, error } = await createSupabaseClient(config).auth.getUser(accessToken);
|
|
232
|
+
if (error) throw new CliError(`Failed to load user: ${error.message}`, EXIT_CODES.ERROR);
|
|
233
|
+
if (!data.user) throw new CliError("No user returned for current token", EXIT_CODES.ERROR);
|
|
234
|
+
return data.user;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/auth-session.ts
|
|
239
|
+
function parseJwtBase64Url(input) {
|
|
240
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
241
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
242
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
243
|
+
}
|
|
244
|
+
function parseJwtClaims(token) {
|
|
245
|
+
const payloadPart = token.split(".").at(1);
|
|
246
|
+
if (!payloadPart) return null;
|
|
247
|
+
try {
|
|
248
|
+
const payload = parseJwtBase64Url(payloadPart);
|
|
249
|
+
const parsed = JSON.parse(payload);
|
|
250
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
251
|
+
const claims = parsed;
|
|
252
|
+
return {
|
|
253
|
+
exp: typeof claims.exp === "number" ? claims.exp : void 0,
|
|
254
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
255
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0
|
|
256
|
+
};
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function expiryFromClaims(claims) {
|
|
262
|
+
if (!claims || typeof claims.exp !== "number") return null;
|
|
263
|
+
return (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
|
|
264
|
+
}
|
|
265
|
+
function userFromClaims(claims) {
|
|
266
|
+
if (!(claims?.sub || claims?.email)) return null;
|
|
267
|
+
return {
|
|
268
|
+
id: claims.sub ?? "unknown",
|
|
269
|
+
email: claims.email ?? null
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function nowUnixSeconds() {
|
|
273
|
+
return Math.floor(Date.now() / 1e3);
|
|
274
|
+
}
|
|
275
|
+
function isExpired(session) {
|
|
276
|
+
if (!session.expiresAt) return false;
|
|
277
|
+
const expiresAtMs = Date.parse(session.expiresAt);
|
|
278
|
+
if (Number.isNaN(expiresAtMs)) return false;
|
|
279
|
+
return expiresAtMs <= Date.now();
|
|
280
|
+
}
|
|
281
|
+
function shouldRefresh(session) {
|
|
282
|
+
if (!session.expiresAt) return false;
|
|
283
|
+
const expiresAtMs = Date.parse(session.expiresAt);
|
|
284
|
+
if (Number.isNaN(expiresAtMs)) return false;
|
|
285
|
+
return expiresAtMs - Date.now() <= 6e4;
|
|
286
|
+
}
|
|
287
|
+
function tokenFromRaw(token, source) {
|
|
288
|
+
const claims = parseJwtClaims(token);
|
|
289
|
+
return {
|
|
290
|
+
token,
|
|
291
|
+
source,
|
|
292
|
+
expiresAt: expiryFromClaims(claims),
|
|
293
|
+
user: userFromClaims(claims)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function resolveAuthToken(context, config) {
|
|
297
|
+
if (context.tokenOverride) return tokenFromRaw(context.tokenOverride, "flag");
|
|
298
|
+
const envToken = process.env[DONEBEAR_TOKEN_ENV]?.trim();
|
|
299
|
+
if (envToken) return tokenFromRaw(envToken, "environment");
|
|
300
|
+
const session = await readStoredAuthSession();
|
|
301
|
+
if (!session) return null;
|
|
302
|
+
if (!(shouldRefresh(session) || isExpired(session))) return {
|
|
303
|
+
token: session.accessToken,
|
|
304
|
+
source: "stored",
|
|
305
|
+
expiresAt: session.expiresAt,
|
|
306
|
+
user: session.user
|
|
307
|
+
};
|
|
308
|
+
if (!(session.refreshToken && config)) {
|
|
309
|
+
if (isExpired(session)) {
|
|
310
|
+
await clearStoredAuthSession();
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
token: session.accessToken,
|
|
315
|
+
source: "stored",
|
|
316
|
+
expiresAt: session.expiresAt,
|
|
317
|
+
user: session.user
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const updatedSession = toStoredAuthSession(await refreshSupabaseSession(config, session.refreshToken), session.provider);
|
|
321
|
+
await writeStoredAuthSession(updatedSession);
|
|
322
|
+
return {
|
|
323
|
+
token: updatedSession.accessToken,
|
|
324
|
+
source: "stored",
|
|
325
|
+
expiresAt: updatedSession.expiresAt,
|
|
326
|
+
user: updatedSession.user
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function resolveTokenStatus(token) {
|
|
330
|
+
const claims = parseJwtClaims(token.token);
|
|
331
|
+
if (!claims?.exp) return {
|
|
332
|
+
expiresInSeconds: null,
|
|
333
|
+
expired: false
|
|
334
|
+
};
|
|
335
|
+
const expiresInSeconds = claims.exp - nowUnixSeconds();
|
|
336
|
+
return {
|
|
337
|
+
expiresInSeconds,
|
|
338
|
+
expired: expiresInSeconds <= 0
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/browser.ts
|
|
344
|
+
function getOpenCommand(url) {
|
|
345
|
+
if (process.platform === "darwin") return {
|
|
346
|
+
command: "open",
|
|
347
|
+
args: [url]
|
|
348
|
+
};
|
|
349
|
+
if (process.platform === "win32") return {
|
|
350
|
+
command: "cmd",
|
|
351
|
+
args: [
|
|
352
|
+
"/c",
|
|
353
|
+
"start",
|
|
354
|
+
"",
|
|
355
|
+
url
|
|
356
|
+
]
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
command: "xdg-open",
|
|
360
|
+
args: [url]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function openUrl(url) {
|
|
364
|
+
const openCommand = getOpenCommand(url);
|
|
365
|
+
return await new Promise((resolve) => {
|
|
366
|
+
try {
|
|
367
|
+
const child = spawn(openCommand.command, openCommand.args, {
|
|
368
|
+
detached: true,
|
|
369
|
+
stdio: "ignore"
|
|
370
|
+
});
|
|
371
|
+
child.once("error", () => {
|
|
372
|
+
resolve(false);
|
|
373
|
+
});
|
|
374
|
+
child.once("spawn", () => {
|
|
375
|
+
child.unref();
|
|
376
|
+
resolve(true);
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
resolve(false);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/command-parsers.ts
|
|
386
|
+
function parsePositiveInteger(value, label) {
|
|
387
|
+
const parsed = Number.parseInt(value, 10);
|
|
388
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer`);
|
|
389
|
+
return parsed;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/oauth.ts
|
|
394
|
+
function createOAuthState() {
|
|
395
|
+
return randomBytes(24).toString("hex");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/oauth-callback.ts
|
|
400
|
+
function successHtml() {
|
|
401
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication complete</h1><p>You can return to your terminal.</p></body></html>`;
|
|
402
|
+
}
|
|
403
|
+
function errorHtml(message) {
|
|
404
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication failed</h1><p>${message}</p></body></html>`;
|
|
405
|
+
}
|
|
406
|
+
async function waitForOAuthCode(options) {
|
|
407
|
+
return await new Promise((resolve, reject) => {
|
|
408
|
+
const host = options.redirectUrl.hostname;
|
|
409
|
+
const port = Number(options.redirectUrl.port);
|
|
410
|
+
const pathname = options.redirectUrl.pathname;
|
|
411
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
412
|
+
reject(new CliError("OAuth redirect URL requires an explicit port", EXIT_CODES.ERROR));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let settled = false;
|
|
416
|
+
const finish = (result) => {
|
|
417
|
+
if (settled) return;
|
|
418
|
+
settled = true;
|
|
419
|
+
clearTimeout(timeout);
|
|
420
|
+
server.close(() => {
|
|
421
|
+
if (result.error) {
|
|
422
|
+
reject(result.error);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
resolve(result.code ?? "");
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
const server = createServer((request, response) => {
|
|
429
|
+
if (!request.url) {
|
|
430
|
+
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
431
|
+
response.end(errorHtml("Missing request URL"));
|
|
432
|
+
finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const url = new URL(request.url, options.redirectUrl.origin);
|
|
436
|
+
if (url.pathname !== pathname) {
|
|
437
|
+
response.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
438
|
+
response.end(errorHtml("Invalid callback path"));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const providerError = url.searchParams.get("error");
|
|
442
|
+
if (providerError) {
|
|
443
|
+
const description = url.searchParams.get("error_description") ?? providerError;
|
|
444
|
+
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
445
|
+
response.end(errorHtml(description));
|
|
446
|
+
finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (url.searchParams.get("state") !== options.expectedState) {
|
|
450
|
+
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
451
|
+
response.end(errorHtml("State verification failed"));
|
|
452
|
+
finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const code = url.searchParams.get("code");
|
|
456
|
+
if (!code) {
|
|
457
|
+
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
458
|
+
response.end(errorHtml("Authorization code was missing"));
|
|
459
|
+
finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
463
|
+
response.end(successHtml());
|
|
464
|
+
finish({ code });
|
|
465
|
+
});
|
|
466
|
+
server.on("error", (error) => {
|
|
467
|
+
finish({ error: new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR) });
|
|
468
|
+
});
|
|
469
|
+
const timeout = setTimeout(() => {
|
|
470
|
+
finish({ error: new CliError("Timed out waiting for OAuth callback", EXIT_CODES.CANCELLED) });
|
|
471
|
+
}, options.timeoutMs);
|
|
472
|
+
server.listen(port, host);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/output.ts
|
|
478
|
+
function writeLine(stream, message) {
|
|
479
|
+
stream.write(`${message}\n`);
|
|
480
|
+
}
|
|
481
|
+
function colorize(context, format, message) {
|
|
482
|
+
if (!context.color) return message;
|
|
483
|
+
return styleText(format, message);
|
|
484
|
+
}
|
|
485
|
+
function writeJson(value) {
|
|
486
|
+
writeLine(process.stdout, JSON.stringify(value, null, 2));
|
|
487
|
+
}
|
|
488
|
+
function writeInfo(context, message) {
|
|
489
|
+
writeLine(process.stdout, colorize(context, "cyan", message));
|
|
490
|
+
}
|
|
491
|
+
function writeSuccess(context, message) {
|
|
492
|
+
writeLine(process.stdout, colorize(context, "green", message));
|
|
493
|
+
}
|
|
494
|
+
function writeWarning(context, message) {
|
|
495
|
+
writeLine(process.stderr, colorize(context, "yellow", message));
|
|
496
|
+
}
|
|
497
|
+
function writeError(context, message) {
|
|
498
|
+
writeLine(process.stderr, colorize(context, "red", message));
|
|
499
|
+
}
|
|
500
|
+
function writeDebug(context, message) {
|
|
501
|
+
if (!context.debug) return;
|
|
502
|
+
writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
|
|
503
|
+
}
|
|
504
|
+
function writeTotal(context, count) {
|
|
505
|
+
writeLine(process.stdout, colorize(context, "cyan", String(count)));
|
|
506
|
+
}
|
|
507
|
+
function escapeCsvField(value) {
|
|
508
|
+
if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replace(/"/g, "\"\"")}"`;
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
function writeCsv(headers, rows) {
|
|
512
|
+
writeLine(process.stdout, headers.map(escapeCsvField).join(","));
|
|
513
|
+
for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
|
|
514
|
+
}
|
|
515
|
+
function writeTsv(headers, rows) {
|
|
516
|
+
writeLine(process.stdout, headers.join(" "));
|
|
517
|
+
for (const row of rows) writeLine(process.stdout, row.join(" "));
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Handles --total, --format csv, --format tsv output (including --copy for tabular data).
|
|
521
|
+
* Returns true if the format was handled; caller renders text mode if false.
|
|
522
|
+
*/
|
|
523
|
+
function writeFormattedRows(context, totalCount, headers, rows) {
|
|
524
|
+
if (context.total) {
|
|
525
|
+
writeTotal(context, totalCount);
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (context.format === "csv") {
|
|
529
|
+
writeCsv(headers, rows);
|
|
530
|
+
if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
if (context.format === "tsv") {
|
|
534
|
+
writeTsv(headers, rows);
|
|
535
|
+
if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
function copyToClipboard(text) {
|
|
541
|
+
try {
|
|
542
|
+
if (process.platform === "darwin") execSync("pbcopy", { input: text });
|
|
543
|
+
else if (process.platform === "win32") execSync("clip", { input: text });
|
|
544
|
+
else try {
|
|
545
|
+
execSync("xclip -selection clipboard", { input: text });
|
|
546
|
+
} catch {
|
|
547
|
+
execSync("xsel --clipboard --input", { input: text });
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
} catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/runtime.ts
|
|
557
|
+
function parseOutputFormat(value) {
|
|
558
|
+
switch (value?.trim().toLowerCase()) {
|
|
559
|
+
case "json": return "json";
|
|
560
|
+
case "csv": return "csv";
|
|
561
|
+
case "tsv": return "tsv";
|
|
562
|
+
default: return "text";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function contextFromCommand(command) {
|
|
566
|
+
const options = command.optsWithGlobals();
|
|
567
|
+
const rawFormat = parseOutputFormat(options.format);
|
|
568
|
+
const format = options.json === true ? "json" : rawFormat;
|
|
569
|
+
return {
|
|
570
|
+
json: format === "json",
|
|
571
|
+
color: options.color !== false && process.env.NO_COLOR !== "1",
|
|
572
|
+
debug: options.debug === true || process.env[DONEBEAR_DEBUG_ENV] === "1",
|
|
573
|
+
tokenOverride: options.token?.trim() || null,
|
|
574
|
+
apiUrlOverride: options.apiUrl?.trim() || null,
|
|
575
|
+
format,
|
|
576
|
+
copy: options.copy === true,
|
|
577
|
+
total: options.total === true
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function contextFromArgv(argv) {
|
|
581
|
+
const hasJsonFlag = argv.includes("--json");
|
|
582
|
+
const rawFormat = argv.some((a) => a === "--format=json" || argv[argv.indexOf("--format") + 1] === "json" && a === "--format") ? "json" : "text";
|
|
583
|
+
const format = hasJsonFlag ? "json" : rawFormat;
|
|
584
|
+
return {
|
|
585
|
+
json: format === "json",
|
|
586
|
+
color: !argv.includes("--no-color") && process.env.NO_COLOR !== "1",
|
|
587
|
+
debug: argv.includes("--debug") || process.env[DONEBEAR_DEBUG_ENV] === "1",
|
|
588
|
+
tokenOverride: null,
|
|
589
|
+
apiUrlOverride: null,
|
|
590
|
+
format,
|
|
591
|
+
copy: argv.includes("--copy"),
|
|
592
|
+
total: argv.includes("--total")
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function runWithErrorHandling(context, action) {
|
|
596
|
+
try {
|
|
597
|
+
await action();
|
|
598
|
+
} catch (error) {
|
|
599
|
+
const cliError = toCliError(error);
|
|
600
|
+
if (context.json) writeJson({
|
|
601
|
+
ok: false,
|
|
602
|
+
error: {
|
|
603
|
+
message: cliError.message,
|
|
604
|
+
exitCode: cliError.exitCode
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
else writeError(context, cliError.message);
|
|
608
|
+
if (context.debug && error instanceof Error && typeof error.stack === "string") writeDebug(context, error.stack);
|
|
609
|
+
process.exitCode = cliError.exitCode;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/commands/auth.ts
|
|
615
|
+
const SUPPORTED_PROVIDERS = ["google", "github"];
|
|
616
|
+
function parseProvider(value) {
|
|
617
|
+
const normalized = value.trim().toLowerCase();
|
|
618
|
+
if (SUPPORTED_PROVIDERS.some((provider) => provider === normalized)) return normalized;
|
|
619
|
+
throw new Error(`Unsupported provider "${value}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}.`);
|
|
620
|
+
}
|
|
621
|
+
async function runLogin(context, options) {
|
|
622
|
+
const provider = parseProvider(options.provider);
|
|
623
|
+
const config = resolveSupabaseConfig();
|
|
624
|
+
const redirectUrl = new URL(`http://127.0.0.1:${options.port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
|
|
625
|
+
const state = createOAuthState();
|
|
626
|
+
const client = createPkceSupabaseClient(config);
|
|
627
|
+
const { data: oauthStart, error: oauthStartError } = await client.auth.signInWithOAuth({
|
|
628
|
+
provider,
|
|
629
|
+
options: {
|
|
630
|
+
redirectTo: redirectUrl.toString(),
|
|
631
|
+
queryParams: { state },
|
|
632
|
+
skipBrowserRedirect: true
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
if (oauthStartError) throw new Error(`Failed to start OAuth flow: ${oauthStartError.message}`);
|
|
636
|
+
if (!oauthStart.url) throw new Error("Supabase did not return an OAuth URL");
|
|
637
|
+
const callbackPromise = waitForOAuthCode({
|
|
638
|
+
redirectUrl,
|
|
639
|
+
expectedState: state,
|
|
640
|
+
timeoutMs: options.timeout * 1e3
|
|
641
|
+
});
|
|
642
|
+
if (!options.open) {
|
|
643
|
+
writeInfo(context, "Open this URL to continue authentication:");
|
|
644
|
+
writeInfo(context, oauthStart.url);
|
|
645
|
+
} else if (await openUrl(oauthStart.url)) writeInfo(context, "Opened browser for authentication.");
|
|
646
|
+
else {
|
|
647
|
+
writeWarning(context, "Failed to open the browser automatically. Open this URL manually:");
|
|
648
|
+
writeInfo(context, oauthStart.url);
|
|
649
|
+
}
|
|
650
|
+
const authorizationCode = await callbackPromise;
|
|
651
|
+
const { data: sessionData, error: exchangeError } = await client.auth.exchangeCodeForSession(authorizationCode);
|
|
652
|
+
if (exchangeError) throw new Error(`OAuth callback exchange failed: ${exchangeError.message}`);
|
|
653
|
+
if (!sessionData.session) throw new Error("OAuth callback did not return a session");
|
|
654
|
+
const storedSession = toStoredAuthSession(sessionData.session, provider);
|
|
655
|
+
await writeStoredAuthSession(storedSession);
|
|
656
|
+
if (context.json) {
|
|
657
|
+
writeJson({
|
|
658
|
+
ok: true,
|
|
659
|
+
provider,
|
|
660
|
+
user: storedSession.user,
|
|
661
|
+
expiresAt: storedSession.expiresAt,
|
|
662
|
+
source: "stored"
|
|
663
|
+
});
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
writeSuccess(context, "Authentication succeeded.");
|
|
667
|
+
if (storedSession.user?.email) writeInfo(context, `Signed in as ${storedSession.user.email}`);
|
|
668
|
+
}
|
|
669
|
+
async function runStatus(context) {
|
|
670
|
+
const token = await resolveAuthToken(context, getOptionalSupabaseConfig$1());
|
|
671
|
+
if (!token) {
|
|
672
|
+
if (context.json) writeJson({
|
|
673
|
+
authenticated: false,
|
|
674
|
+
source: null
|
|
675
|
+
});
|
|
676
|
+
else {
|
|
677
|
+
writeInfo(context, "Not authenticated.");
|
|
678
|
+
writeInfo(context, "Run `donebear auth login` to authenticate.");
|
|
679
|
+
}
|
|
680
|
+
process.exitCode = EXIT_CODES.AUTH_REQUIRED;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const tokenStatus = resolveTokenStatus(token);
|
|
684
|
+
if (context.json) {
|
|
685
|
+
writeJson({
|
|
686
|
+
authenticated: true,
|
|
687
|
+
source: token.source,
|
|
688
|
+
user: token.user,
|
|
689
|
+
expiresAt: token.expiresAt,
|
|
690
|
+
expiresInSeconds: tokenStatus.expiresInSeconds,
|
|
691
|
+
expired: tokenStatus.expired
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
writeSuccess(context, "Authenticated.");
|
|
696
|
+
writeInfo(context, `Source: ${token.source}`);
|
|
697
|
+
if (token.user?.email) writeInfo(context, `User: ${token.user.email}`);
|
|
698
|
+
if (token.expiresAt) writeInfo(context, `Expires at: ${token.expiresAt}`);
|
|
699
|
+
}
|
|
700
|
+
async function runLogout(context) {
|
|
701
|
+
const existingSession = await readStoredAuthSession();
|
|
702
|
+
await clearStoredAuthSession();
|
|
703
|
+
if (context.json) {
|
|
704
|
+
writeJson({
|
|
705
|
+
ok: true,
|
|
706
|
+
removedStoredCredentials: existingSession !== null
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (existingSession) writeSuccess(context, "Stored credentials removed.");
|
|
711
|
+
else writeInfo(context, "No stored credentials found.");
|
|
712
|
+
}
|
|
713
|
+
function getOptionalSupabaseConfig$1() {
|
|
714
|
+
try {
|
|
715
|
+
return resolveSupabaseConfig();
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function registerAuthCommands(program) {
|
|
721
|
+
const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
|
|
722
|
+
Examples:
|
|
723
|
+
donebear auth
|
|
724
|
+
donebear auth login --provider github
|
|
725
|
+
donebear auth status --json
|
|
726
|
+
donebear auth logout
|
|
727
|
+
`).action(async (_options, command) => {
|
|
728
|
+
const context = contextFromCommand(command);
|
|
729
|
+
await runWithErrorHandling(context, async () => {
|
|
730
|
+
await runStatus(context);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
auth.command("login").description("Authenticate with OAuth in the browser").option("-p, --provider <provider>", `OAuth provider (${SUPPORTED_PROVIDERS.join("|")})`, DEFAULT_OAUTH_PROVIDER).option("--port <port>", "Loopback callback port", (value) => parsePositiveInteger(value, "Port"), DEFAULT_OAUTH_CALLBACK_PORT).option("--timeout <seconds>", "OAuth timeout in seconds", (value) => parsePositiveInteger(value, "Timeout"), DEFAULT_OAUTH_TIMEOUT_SECONDS).option("--no-open", "Print URL instead of opening the browser").action(async (options, command) => {
|
|
734
|
+
const context = contextFromCommand(command);
|
|
735
|
+
await runWithErrorHandling(context, async () => {
|
|
736
|
+
await runLogin(context, options);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
auth.command("status").description("Check authentication status").action(async (_options, command) => {
|
|
740
|
+
const context = contextFromCommand(command);
|
|
741
|
+
await runWithErrorHandling(context, async () => {
|
|
742
|
+
await runStatus(context);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
auth.command("logout").alias("clear").description("Remove local cached credentials").action(async (_options, command) => {
|
|
746
|
+
const context = contextFromCommand(command);
|
|
747
|
+
await runWithErrorHandling(context, async () => {
|
|
748
|
+
await runLogout(context);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/context-store.ts
|
|
755
|
+
function isRecord(value) {
|
|
756
|
+
return typeof value === "object" && value !== null;
|
|
757
|
+
}
|
|
758
|
+
function parseLocalContext(value) {
|
|
759
|
+
if (!isRecord(value)) return null;
|
|
760
|
+
if (value.workspaceId !== null && typeof value.workspaceId !== "string") return null;
|
|
761
|
+
if (typeof value.clientId !== "string" || value.clientId.length === 0) return null;
|
|
762
|
+
return {
|
|
763
|
+
workspaceId: value.workspaceId,
|
|
764
|
+
clientId: value.clientId
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function parseContextFile(payload) {
|
|
768
|
+
const parsed = JSON.parse(payload);
|
|
769
|
+
if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid CLI context format in ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
770
|
+
const context = parseLocalContext(parsed.context);
|
|
771
|
+
if (!context) throw new CliError(`Invalid CLI context in ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
772
|
+
return {
|
|
773
|
+
version: 1,
|
|
774
|
+
context
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function writeContext(context) {
|
|
778
|
+
const payload = {
|
|
779
|
+
version: 1,
|
|
780
|
+
context
|
|
781
|
+
};
|
|
782
|
+
await mkdir(CONFIG_DIR, {
|
|
783
|
+
recursive: true,
|
|
784
|
+
mode: 448
|
|
785
|
+
});
|
|
786
|
+
await writeFile(CONTEXT_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
787
|
+
encoding: "utf8",
|
|
788
|
+
mode: 384
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async function readLocalContext() {
|
|
792
|
+
try {
|
|
793
|
+
return parseContextFile(await readFile(CONTEXT_FILE_PATH, "utf8")).context;
|
|
794
|
+
} catch (error) {
|
|
795
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
796
|
+
if (error instanceof CliError) throw error;
|
|
797
|
+
throw new CliError(`Failed to read context from ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function ensureLocalContext() {
|
|
801
|
+
const existing = await readLocalContext();
|
|
802
|
+
if (existing) return existing;
|
|
803
|
+
const created = {
|
|
804
|
+
workspaceId: null,
|
|
805
|
+
clientId: randomUUID()
|
|
806
|
+
};
|
|
807
|
+
await writeContext(created);
|
|
808
|
+
return created;
|
|
809
|
+
}
|
|
810
|
+
async function setCurrentWorkspace(workspaceId) {
|
|
811
|
+
const nextContext = {
|
|
812
|
+
...await ensureLocalContext(),
|
|
813
|
+
workspaceId
|
|
814
|
+
};
|
|
815
|
+
await writeContext(nextContext);
|
|
816
|
+
return nextContext;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/manage-api.ts
|
|
821
|
+
const TASKS_QUERY = `
|
|
822
|
+
query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
|
|
823
|
+
tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
824
|
+
nodes {
|
|
825
|
+
id
|
|
826
|
+
title
|
|
827
|
+
description
|
|
828
|
+
createdAt
|
|
829
|
+
updatedAt
|
|
830
|
+
completedAt
|
|
831
|
+
archivedAt
|
|
832
|
+
start
|
|
833
|
+
startDate
|
|
834
|
+
startBucket
|
|
835
|
+
todayIndexReferenceDate
|
|
836
|
+
deadlineAt
|
|
837
|
+
creatorId
|
|
838
|
+
workspaceId
|
|
839
|
+
projectId
|
|
840
|
+
teamId
|
|
841
|
+
assigneeId
|
|
842
|
+
headingId
|
|
843
|
+
}
|
|
844
|
+
pageInfo {
|
|
845
|
+
hasNextPage
|
|
846
|
+
endCursor
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
`;
|
|
851
|
+
const PROJECTS_QUERY = `
|
|
852
|
+
query DonebearProjects($first: Int!, $after: String, $workspaceId: ID!) {
|
|
853
|
+
projects(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
854
|
+
nodes {
|
|
855
|
+
id
|
|
856
|
+
key
|
|
857
|
+
name
|
|
858
|
+
description
|
|
859
|
+
status
|
|
860
|
+
sortOrder
|
|
861
|
+
targetDate
|
|
862
|
+
completedAt
|
|
863
|
+
archivedAt
|
|
864
|
+
createdAt
|
|
865
|
+
updatedAt
|
|
866
|
+
workspaceId
|
|
867
|
+
creatorId
|
|
868
|
+
}
|
|
869
|
+
pageInfo {
|
|
870
|
+
hasNextPage
|
|
871
|
+
endCursor
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
`;
|
|
876
|
+
const LABELS_QUERY = `
|
|
877
|
+
query DonebearLabels($first: Int!, $after: String, $workspaceId: ID!) {
|
|
878
|
+
labels(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
879
|
+
nodes {
|
|
880
|
+
id
|
|
881
|
+
title
|
|
882
|
+
workspaceId
|
|
883
|
+
createdAt
|
|
884
|
+
updatedAt
|
|
885
|
+
}
|
|
886
|
+
pageInfo {
|
|
887
|
+
hasNextPage
|
|
888
|
+
endCursor
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
`;
|
|
893
|
+
const TEAMS_QUERY = `
|
|
894
|
+
query DonebearTeams($first: Int!, $after: String, $workspaceId: ID!) {
|
|
895
|
+
teams(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
896
|
+
nodes {
|
|
897
|
+
id
|
|
898
|
+
key
|
|
899
|
+
name
|
|
900
|
+
description
|
|
901
|
+
workspaceId
|
|
902
|
+
createdAt
|
|
903
|
+
updatedAt
|
|
904
|
+
archivedAt
|
|
905
|
+
}
|
|
906
|
+
pageInfo {
|
|
907
|
+
hasNextPage
|
|
908
|
+
endCursor
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
`;
|
|
913
|
+
function normalizeApiUrl(url) {
|
|
914
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
915
|
+
}
|
|
916
|
+
function firstDefined(keys) {
|
|
917
|
+
for (const key of keys) {
|
|
918
|
+
const value = readEnvValue(key);
|
|
919
|
+
if (value) return value;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function extractErrorMessage(value) {
|
|
923
|
+
if (typeof value === "string") return value;
|
|
924
|
+
if (typeof value !== "object" || value === null) return "Request failed";
|
|
925
|
+
const record = value;
|
|
926
|
+
if (typeof record.message === "string") return record.message;
|
|
927
|
+
if (typeof record.error === "string") return record.error;
|
|
928
|
+
return "Request failed";
|
|
929
|
+
}
|
|
930
|
+
async function request(_context, endpoint, options) {
|
|
931
|
+
const headers = new Headers({
|
|
932
|
+
Authorization: `Bearer ${options.token}`,
|
|
933
|
+
Accept: "application/json"
|
|
934
|
+
});
|
|
935
|
+
if (options.body !== void 0) headers.set("Content-Type", "application/json");
|
|
936
|
+
const response = await fetch(endpoint, {
|
|
937
|
+
method: options.method ?? "GET",
|
|
938
|
+
headers,
|
|
939
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
940
|
+
});
|
|
941
|
+
if (response.ok) return response;
|
|
942
|
+
const raw = await response.text();
|
|
943
|
+
let parsed = null;
|
|
944
|
+
try {
|
|
945
|
+
parsed = raw.length > 0 ? JSON.parse(raw) : null;
|
|
946
|
+
} catch {
|
|
947
|
+
parsed = null;
|
|
948
|
+
}
|
|
949
|
+
let message = `HTTP ${response.status}`;
|
|
950
|
+
if (parsed !== null) message = extractErrorMessage(parsed);
|
|
951
|
+
else if (raw.length > 0) message = raw;
|
|
952
|
+
const exitCode = response.status === 401 || response.status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
|
|
953
|
+
throw new CliError(message, exitCode);
|
|
954
|
+
}
|
|
955
|
+
async function requestJson(context, endpoint, options) {
|
|
956
|
+
return await (await request(context, endpoint, options)).json();
|
|
957
|
+
}
|
|
958
|
+
async function requestNdjson(context, endpoint, options) {
|
|
959
|
+
const lines = (await (await request(context, endpoint, {
|
|
960
|
+
method: "POST",
|
|
961
|
+
token: options.token,
|
|
962
|
+
body: options.body
|
|
963
|
+
})).text()).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
964
|
+
const parsed = [];
|
|
965
|
+
for (const line of lines) parsed.push(JSON.parse(line));
|
|
966
|
+
return parsed;
|
|
967
|
+
}
|
|
968
|
+
async function requestGraphql(context, endpoint, options) {
|
|
969
|
+
const response = await requestJson(context, endpoint, {
|
|
970
|
+
method: "POST",
|
|
971
|
+
token: options.token,
|
|
972
|
+
body: {
|
|
973
|
+
query: options.query,
|
|
974
|
+
variables: options.variables
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
if (Array.isArray(response.errors) && response.errors.length > 0) {
|
|
978
|
+
const first = response.errors[0];
|
|
979
|
+
throw new CliError(typeof first?.message === "string" ? first.message : "GraphQL request failed", EXIT_CODES.ERROR);
|
|
980
|
+
}
|
|
981
|
+
if (!response.data) throw new CliError("GraphQL request returned no data", EXIT_CODES.ERROR);
|
|
982
|
+
return response.data;
|
|
983
|
+
}
|
|
984
|
+
function toTaskRecord(value) {
|
|
985
|
+
if (typeof value !== "object" || value === null) return null;
|
|
986
|
+
const row = value;
|
|
987
|
+
if (row.__class !== "Task") return null;
|
|
988
|
+
if (typeof row.id !== "string" || typeof row.title !== "string") return null;
|
|
989
|
+
if (typeof row.creatorId !== "string" || typeof row.workspaceId !== "string" || typeof row.start !== "string" || typeof row.startBucket !== "string") return null;
|
|
990
|
+
return {
|
|
991
|
+
id: row.id,
|
|
992
|
+
title: row.title,
|
|
993
|
+
description: typeof row.description === "string" ? row.description : null,
|
|
994
|
+
createdAt: toNullableNumber(row.createdAt),
|
|
995
|
+
updatedAt: toNullableNumber(row.updatedAt),
|
|
996
|
+
completedAt: toNullableNumber(row.completedAt),
|
|
997
|
+
archivedAt: toNullableNumber(row.archivedAt),
|
|
998
|
+
start: row.start,
|
|
999
|
+
startDate: toNullableNumber(row.startDate),
|
|
1000
|
+
startBucket: row.startBucket,
|
|
1001
|
+
todayIndexReferenceDate: toNullableNumber(row.todayIndexReferenceDate),
|
|
1002
|
+
deadlineAt: toNullableNumber(row.deadlineAt),
|
|
1003
|
+
creatorId: row.creatorId,
|
|
1004
|
+
workspaceId: row.workspaceId,
|
|
1005
|
+
projectId: typeof row.projectId === "string" ? row.projectId : null,
|
|
1006
|
+
teamId: typeof row.teamId === "string" ? row.teamId : null,
|
|
1007
|
+
assigneeId: typeof row.assigneeId === "string" ? row.assigneeId : null,
|
|
1008
|
+
headingId: typeof row.headingId === "string" ? row.headingId : null
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function toNullableNumber(input) {
|
|
1012
|
+
if (input === null || typeof input === "undefined") return null;
|
|
1013
|
+
return typeof input === "number" ? input : null;
|
|
1014
|
+
}
|
|
1015
|
+
function parseIsoDateToEpoch(value) {
|
|
1016
|
+
if (typeof value !== "string") return null;
|
|
1017
|
+
const parsed = Date.parse(value);
|
|
1018
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1019
|
+
}
|
|
1020
|
+
function toTaskRecordFromGraphqlNode(node) {
|
|
1021
|
+
return {
|
|
1022
|
+
id: node.id,
|
|
1023
|
+
title: node.title,
|
|
1024
|
+
description: node.description,
|
|
1025
|
+
createdAt: parseIsoDateToEpoch(node.createdAt),
|
|
1026
|
+
updatedAt: parseIsoDateToEpoch(node.updatedAt),
|
|
1027
|
+
completedAt: parseIsoDateToEpoch(node.completedAt),
|
|
1028
|
+
archivedAt: parseIsoDateToEpoch(node.archivedAt),
|
|
1029
|
+
start: node.start,
|
|
1030
|
+
startDate: parseIsoDateToEpoch(node.startDate),
|
|
1031
|
+
startBucket: node.startBucket,
|
|
1032
|
+
todayIndexReferenceDate: parseIsoDateToEpoch(node.todayIndexReferenceDate),
|
|
1033
|
+
deadlineAt: parseIsoDateToEpoch(node.deadlineAt),
|
|
1034
|
+
creatorId: node.creatorId,
|
|
1035
|
+
workspaceId: node.workspaceId,
|
|
1036
|
+
projectId: node.projectId,
|
|
1037
|
+
teamId: node.teamId,
|
|
1038
|
+
assigneeId: node.assigneeId,
|
|
1039
|
+
headingId: node.headingId
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function toProjectRecordFromGraphqlNode(node) {
|
|
1043
|
+
return {
|
|
1044
|
+
id: node.id,
|
|
1045
|
+
key: node.key,
|
|
1046
|
+
name: node.name,
|
|
1047
|
+
description: node.description,
|
|
1048
|
+
status: node.status,
|
|
1049
|
+
sortOrder: node.sortOrder,
|
|
1050
|
+
targetDate: parseIsoDateToEpoch(node.targetDate),
|
|
1051
|
+
completedAt: parseIsoDateToEpoch(node.completedAt),
|
|
1052
|
+
archivedAt: parseIsoDateToEpoch(node.archivedAt),
|
|
1053
|
+
createdAt: parseIsoDateToEpoch(node.createdAt),
|
|
1054
|
+
updatedAt: parseIsoDateToEpoch(node.updatedAt),
|
|
1055
|
+
workspaceId: node.workspaceId,
|
|
1056
|
+
creatorId: node.creatorId
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function toLabelRecordFromGraphqlNode(node) {
|
|
1060
|
+
return {
|
|
1061
|
+
id: node.id,
|
|
1062
|
+
title: node.title,
|
|
1063
|
+
workspaceId: node.workspaceId,
|
|
1064
|
+
createdAt: parseIsoDateToEpoch(node.createdAt),
|
|
1065
|
+
updatedAt: parseIsoDateToEpoch(node.updatedAt)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function toTeamRecordFromGraphqlNode(node) {
|
|
1069
|
+
return {
|
|
1070
|
+
id: node.id,
|
|
1071
|
+
key: node.key,
|
|
1072
|
+
name: node.name,
|
|
1073
|
+
description: node.description,
|
|
1074
|
+
workspaceId: node.workspaceId,
|
|
1075
|
+
createdAt: parseIsoDateToEpoch(node.createdAt),
|
|
1076
|
+
updatedAt: parseIsoDateToEpoch(node.updatedAt),
|
|
1077
|
+
archivedAt: parseIsoDateToEpoch(node.archivedAt)
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function toChecklistItemRecord(value) {
|
|
1081
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1082
|
+
const row = value;
|
|
1083
|
+
if (row.__class !== "TaskChecklistItem") return null;
|
|
1084
|
+
if (typeof row.id !== "string" || typeof row.title !== "string") return null;
|
|
1085
|
+
if (typeof row.taskId !== "string" || typeof row.workspaceId !== "string") return null;
|
|
1086
|
+
return {
|
|
1087
|
+
id: row.id,
|
|
1088
|
+
title: row.title,
|
|
1089
|
+
taskId: row.taskId,
|
|
1090
|
+
workspaceId: row.workspaceId,
|
|
1091
|
+
sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
|
|
1092
|
+
completedAt: toNullableNumber(row.completedAt),
|
|
1093
|
+
createdAt: toNullableNumber(row.createdAt),
|
|
1094
|
+
updatedAt: toNullableNumber(row.updatedAt)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function resolveApiBaseUrl(context) {
|
|
1098
|
+
const fromContext = context.apiUrlOverride?.trim();
|
|
1099
|
+
if (fromContext) return normalizeApiUrl(fromContext);
|
|
1100
|
+
return normalizeApiUrl(readEnvValue(DONEBEAR_API_URL_ENV) ?? firstDefined(FALLBACK_API_URL_ENV_KEYS) ?? DEFAULT_API_URL);
|
|
1101
|
+
}
|
|
1102
|
+
async function listWorkspaces(context, options) {
|
|
1103
|
+
return (await requestJson(context, `${options.baseUrl}/api/workspaces`, { token: options.token })).workspaces;
|
|
1104
|
+
}
|
|
1105
|
+
async function createWorkspace(context, options) {
|
|
1106
|
+
return (await requestJson(context, `${options.baseUrl}/api/workspaces`, {
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
token: options.token,
|
|
1109
|
+
body: options.body
|
|
1110
|
+
})).workspace;
|
|
1111
|
+
}
|
|
1112
|
+
function joinWorkspace(context, options) {
|
|
1113
|
+
return requestJson(context, `${options.baseUrl}/api/workspaces/join`, {
|
|
1114
|
+
method: "POST",
|
|
1115
|
+
token: options.token,
|
|
1116
|
+
body: { code: options.code }
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async function loadTasksByWorkspace(context, options) {
|
|
1120
|
+
const endpoint = `${options.baseUrl}/graphql`;
|
|
1121
|
+
const results = [];
|
|
1122
|
+
let after = null;
|
|
1123
|
+
let pageCount = 0;
|
|
1124
|
+
while (true) {
|
|
1125
|
+
pageCount += 1;
|
|
1126
|
+
if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
|
|
1127
|
+
const data = await requestGraphql(context, endpoint, {
|
|
1128
|
+
token: options.token,
|
|
1129
|
+
query: TASKS_QUERY,
|
|
1130
|
+
variables: {
|
|
1131
|
+
first: 100,
|
|
1132
|
+
after,
|
|
1133
|
+
workspaceId: options.workspaceId
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
for (const node of data.tasks.nodes) results.push(toTaskRecordFromGraphqlNode(node));
|
|
1137
|
+
const nextCursor = data.tasks.pageInfo.hasNextPage ? data.tasks.pageInfo.endCursor : null;
|
|
1138
|
+
if (!nextCursor) break;
|
|
1139
|
+
after = nextCursor;
|
|
1140
|
+
}
|
|
1141
|
+
return results;
|
|
1142
|
+
}
|
|
1143
|
+
async function loadTaskById(context, options) {
|
|
1144
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1145
|
+
const body = { requests: [{
|
|
1146
|
+
modelName: "Task",
|
|
1147
|
+
indexedKey: "id",
|
|
1148
|
+
keyValue: options.taskId
|
|
1149
|
+
}] };
|
|
1150
|
+
const rows = await requestNdjson(context, url, {
|
|
1151
|
+
token: options.token,
|
|
1152
|
+
body
|
|
1153
|
+
});
|
|
1154
|
+
for (const row of rows) {
|
|
1155
|
+
const task = toTaskRecord(row);
|
|
1156
|
+
if (task) return task;
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
async function mutateTask(context, options) {
|
|
1161
|
+
const response = await requestJson(context, `${options.baseUrl}/sync/mutate`, {
|
|
1162
|
+
method: "POST",
|
|
1163
|
+
token: options.token,
|
|
1164
|
+
body: options.request
|
|
1165
|
+
});
|
|
1166
|
+
if (!response.success) {
|
|
1167
|
+
const error = response.results.find((result) => !result.success);
|
|
1168
|
+
if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
|
|
1169
|
+
throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function loadProjectsByWorkspace(context, options) {
|
|
1173
|
+
const endpoint = `${options.baseUrl}/graphql`;
|
|
1174
|
+
const results = [];
|
|
1175
|
+
let after = null;
|
|
1176
|
+
let pageCount = 0;
|
|
1177
|
+
while (true) {
|
|
1178
|
+
pageCount += 1;
|
|
1179
|
+
if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
|
|
1180
|
+
const data = await requestGraphql(context, endpoint, {
|
|
1181
|
+
token: options.token,
|
|
1182
|
+
query: PROJECTS_QUERY,
|
|
1183
|
+
variables: {
|
|
1184
|
+
first: 100,
|
|
1185
|
+
after,
|
|
1186
|
+
workspaceId: options.workspaceId
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
for (const node of data.projects.nodes) results.push(toProjectRecordFromGraphqlNode(node));
|
|
1190
|
+
const nextCursor = data.projects.pageInfo.hasNextPage ? data.projects.pageInfo.endCursor : null;
|
|
1191
|
+
if (!nextCursor) break;
|
|
1192
|
+
after = nextCursor;
|
|
1193
|
+
}
|
|
1194
|
+
return results;
|
|
1195
|
+
}
|
|
1196
|
+
async function loadLabelsByWorkspace(context, options) {
|
|
1197
|
+
const endpoint = `${options.baseUrl}/graphql`;
|
|
1198
|
+
const results = [];
|
|
1199
|
+
let after = null;
|
|
1200
|
+
let pageCount = 0;
|
|
1201
|
+
while (true) {
|
|
1202
|
+
pageCount += 1;
|
|
1203
|
+
if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
|
|
1204
|
+
const data = await requestGraphql(context, endpoint, {
|
|
1205
|
+
token: options.token,
|
|
1206
|
+
query: LABELS_QUERY,
|
|
1207
|
+
variables: {
|
|
1208
|
+
first: 100,
|
|
1209
|
+
after,
|
|
1210
|
+
workspaceId: options.workspaceId
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
for (const node of data.labels.nodes) results.push(toLabelRecordFromGraphqlNode(node));
|
|
1214
|
+
const nextCursor = data.labels.pageInfo.hasNextPage ? data.labels.pageInfo.endCursor : null;
|
|
1215
|
+
if (!nextCursor) break;
|
|
1216
|
+
after = nextCursor;
|
|
1217
|
+
}
|
|
1218
|
+
return results;
|
|
1219
|
+
}
|
|
1220
|
+
async function loadTeamsByWorkspace(context, options) {
|
|
1221
|
+
const endpoint = `${options.baseUrl}/graphql`;
|
|
1222
|
+
const results = [];
|
|
1223
|
+
let after = null;
|
|
1224
|
+
let pageCount = 0;
|
|
1225
|
+
while (true) {
|
|
1226
|
+
pageCount += 1;
|
|
1227
|
+
if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
|
|
1228
|
+
const data = await requestGraphql(context, endpoint, {
|
|
1229
|
+
token: options.token,
|
|
1230
|
+
query: TEAMS_QUERY,
|
|
1231
|
+
variables: {
|
|
1232
|
+
first: 100,
|
|
1233
|
+
after,
|
|
1234
|
+
workspaceId: options.workspaceId
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
for (const node of data.teams.nodes) results.push(toTeamRecordFromGraphqlNode(node));
|
|
1238
|
+
const nextCursor = data.teams.pageInfo.hasNextPage ? data.teams.pageInfo.endCursor : null;
|
|
1239
|
+
if (!nextCursor) break;
|
|
1240
|
+
after = nextCursor;
|
|
1241
|
+
}
|
|
1242
|
+
return results;
|
|
1243
|
+
}
|
|
1244
|
+
async function loadChecklistItemsByTask(context, options) {
|
|
1245
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1246
|
+
const body = { requests: [{
|
|
1247
|
+
modelName: "TaskChecklistItem",
|
|
1248
|
+
indexedKey: "taskId",
|
|
1249
|
+
keyValue: options.taskId
|
|
1250
|
+
}] };
|
|
1251
|
+
const rows = await requestNdjson(context, url, {
|
|
1252
|
+
token: options.token,
|
|
1253
|
+
body
|
|
1254
|
+
});
|
|
1255
|
+
const results = [];
|
|
1256
|
+
for (const row of rows) {
|
|
1257
|
+
const item = toChecklistItemRecord(row);
|
|
1258
|
+
if (item) results.push(item);
|
|
1259
|
+
}
|
|
1260
|
+
return results;
|
|
1261
|
+
}
|
|
1262
|
+
async function mutateModel(context, options) {
|
|
1263
|
+
const response = await requestJson(context, `${options.baseUrl}/sync/mutate`, {
|
|
1264
|
+
method: "POST",
|
|
1265
|
+
token: options.token,
|
|
1266
|
+
body: options.request
|
|
1267
|
+
});
|
|
1268
|
+
if (!response.success) {
|
|
1269
|
+
const error = response.results.find((result) => !result.success);
|
|
1270
|
+
if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
|
|
1271
|
+
throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function listWorkspaceMembers(context, options) {
|
|
1275
|
+
const data = await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/members`, { token: options.token });
|
|
1276
|
+
return Array.isArray(data) ? data : data.members ?? [];
|
|
1277
|
+
}
|
|
1278
|
+
async function listWorkspaceInvitations(context, options) {
|
|
1279
|
+
const data = await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/invitations`, { token: options.token });
|
|
1280
|
+
return Array.isArray(data) ? data : data.invitations ?? [];
|
|
1281
|
+
}
|
|
1282
|
+
async function createWorkspaceInvitation(context, options) {
|
|
1283
|
+
const url = `${options.baseUrl}/api/workspaces/${options.workspaceId}/invitations`;
|
|
1284
|
+
const body = {};
|
|
1285
|
+
if (options.email !== void 0) body.email = options.email;
|
|
1286
|
+
if (options.role !== void 0) body.role = options.role;
|
|
1287
|
+
const data = await requestJson(context, url, {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
token: options.token,
|
|
1290
|
+
body
|
|
1291
|
+
});
|
|
1292
|
+
if ("id" in data && typeof data.id === "string") return data;
|
|
1293
|
+
const nested = data.invitation;
|
|
1294
|
+
if (nested) return nested;
|
|
1295
|
+
throw new CliError("Invalid response from invitation endpoint", EXIT_CODES.ERROR);
|
|
1296
|
+
}
|
|
1297
|
+
async function listWorkspaceHistory(context, options) {
|
|
1298
|
+
const params = new URLSearchParams({ limit: String(options.limit) });
|
|
1299
|
+
if (options.model) params.set("model", options.model);
|
|
1300
|
+
return (await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/history?${params.toString()}`, { token: options.token })).history;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/command-context.ts
|
|
1305
|
+
function getOptionalSupabaseConfig() {
|
|
1306
|
+
try {
|
|
1307
|
+
return resolveSupabaseConfig();
|
|
1308
|
+
} catch {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async function requireAuthToken(context) {
|
|
1313
|
+
const resolvedToken = await resolveAuthToken(context, getOptionalSupabaseConfig());
|
|
1314
|
+
if (!resolvedToken) throw new CliError("Not authenticated. Run `donebear auth login`.", EXIT_CODES.AUTH_REQUIRED);
|
|
1315
|
+
return {
|
|
1316
|
+
token: resolvedToken.token,
|
|
1317
|
+
resolvedToken
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
async function resolveCommandContext(context) {
|
|
1321
|
+
const { token, resolvedToken } = await requireAuthToken(context);
|
|
1322
|
+
if (!token) throw new CliError("Missing access token", EXIT_CODES.AUTH_REQUIRED);
|
|
1323
|
+
return {
|
|
1324
|
+
token,
|
|
1325
|
+
resolvedToken,
|
|
1326
|
+
apiBaseUrl: resolveApiBaseUrl(context),
|
|
1327
|
+
localContext: await ensureLocalContext()
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function requireUserId(resolvedToken) {
|
|
1331
|
+
const userId = resolvedToken.user?.id ?? null;
|
|
1332
|
+
if (!(typeof userId === "string" && userId.length > 0)) throw new CliError("Could not determine user id from auth token", EXIT_CODES.AUTH_REQUIRED);
|
|
1333
|
+
return userId;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region src/task-defaults.ts
|
|
1338
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
1339
|
+
function startOfDayEpoch(now) {
|
|
1340
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
1341
|
+
}
|
|
1342
|
+
function buildTaskStartFields(view, now = /* @__PURE__ */ new Date()) {
|
|
1343
|
+
const today = startOfDayEpoch(now);
|
|
1344
|
+
const tomorrow = today + DAY_MS;
|
|
1345
|
+
switch (view) {
|
|
1346
|
+
case "inbox": return {
|
|
1347
|
+
start: "not_started",
|
|
1348
|
+
startBucket: "today",
|
|
1349
|
+
startDate: null,
|
|
1350
|
+
todayIndexReferenceDate: null
|
|
1351
|
+
};
|
|
1352
|
+
case "anytime": return {
|
|
1353
|
+
start: "started",
|
|
1354
|
+
startBucket: "today",
|
|
1355
|
+
startDate: null,
|
|
1356
|
+
todayIndexReferenceDate: null
|
|
1357
|
+
};
|
|
1358
|
+
case "today": return {
|
|
1359
|
+
start: "started",
|
|
1360
|
+
startBucket: "today",
|
|
1361
|
+
startDate: today,
|
|
1362
|
+
todayIndexReferenceDate: today
|
|
1363
|
+
};
|
|
1364
|
+
case "upcoming": return {
|
|
1365
|
+
start: "started",
|
|
1366
|
+
startBucket: "upcoming",
|
|
1367
|
+
startDate: tomorrow,
|
|
1368
|
+
todayIndexReferenceDate: null
|
|
1369
|
+
};
|
|
1370
|
+
case "someday": return {
|
|
1371
|
+
start: "someday",
|
|
1372
|
+
startBucket: "today",
|
|
1373
|
+
startDate: null,
|
|
1374
|
+
todayIndexReferenceDate: null
|
|
1375
|
+
};
|
|
1376
|
+
default: return {
|
|
1377
|
+
start: "not_started",
|
|
1378
|
+
startBucket: "today",
|
|
1379
|
+
startDate: null,
|
|
1380
|
+
todayIndexReferenceDate: null
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function getTaskState(task) {
|
|
1385
|
+
if (task.archivedAt !== null) return "archived";
|
|
1386
|
+
if (task.completedAt !== null) return "done";
|
|
1387
|
+
return "open";
|
|
1388
|
+
}
|
|
1389
|
+
function formatEpochDate(epochMs) {
|
|
1390
|
+
if (epochMs === null) return null;
|
|
1391
|
+
const date = new Date(epochMs);
|
|
1392
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
1393
|
+
return date.toISOString();
|
|
1394
|
+
}
|
|
1395
|
+
function formatShortDate(epochMs) {
|
|
1396
|
+
const iso = formatEpochDate(epochMs);
|
|
1397
|
+
if (!iso) return null;
|
|
1398
|
+
return iso.slice(0, 10);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
//#endregion
|
|
1402
|
+
//#region src/command-helpers.ts
|
|
1403
|
+
function normalizeWorkspaceRef(value) {
|
|
1404
|
+
const normalized = value?.trim() ?? "";
|
|
1405
|
+
return normalized.length > 0 ? normalized : null;
|
|
1406
|
+
}
|
|
1407
|
+
function parseTaskState(value) {
|
|
1408
|
+
switch (value.trim().toLowerCase()) {
|
|
1409
|
+
case "open":
|
|
1410
|
+
case "todo":
|
|
1411
|
+
case "pending": return "open";
|
|
1412
|
+
case "done":
|
|
1413
|
+
case "complete":
|
|
1414
|
+
case "completed": return "done";
|
|
1415
|
+
case "archived":
|
|
1416
|
+
case "archive": return "archived";
|
|
1417
|
+
case "all": return "all";
|
|
1418
|
+
default: throw new Error(`Invalid state "${value}". Expected one of: open, done, archived, all.`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function toTaskMarker(task) {
|
|
1422
|
+
const state = getTaskState(task);
|
|
1423
|
+
if (state === "done") return "[x]";
|
|
1424
|
+
if (state === "archived") return "[-]";
|
|
1425
|
+
return "[ ]";
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/workspace-context.ts
|
|
1430
|
+
const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1431
|
+
function normalize(value) {
|
|
1432
|
+
return value.trim().toLowerCase();
|
|
1433
|
+
}
|
|
1434
|
+
function byReference(workspaces, reference) {
|
|
1435
|
+
const normalized = normalize(reference);
|
|
1436
|
+
for (const workspace of workspaces) {
|
|
1437
|
+
if (workspace.id === reference) return workspace;
|
|
1438
|
+
if (workspace.urlKey && normalize(workspace.urlKey) === normalized) return workspace;
|
|
1439
|
+
if (normalize(workspace.name) === normalized) return workspace;
|
|
1440
|
+
}
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
function toWorkspaceUrlKey(name) {
|
|
1444
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 50);
|
|
1445
|
+
}
|
|
1446
|
+
function isValidWorkspaceUrlKey(value) {
|
|
1447
|
+
return WORKSPACE_URL_KEY_REGEX.test(value) && value.length >= 3;
|
|
1448
|
+
}
|
|
1449
|
+
async function resolveWorkspace(context, options) {
|
|
1450
|
+
const workspaces = await listWorkspaces(context, {
|
|
1451
|
+
token: options.token,
|
|
1452
|
+
baseUrl: options.apiBaseUrl
|
|
1453
|
+
});
|
|
1454
|
+
if (workspaces.length === 0) throw new Error("No workspaces available for this account");
|
|
1455
|
+
if (options.workspaceRef) {
|
|
1456
|
+
const selected = byReference(workspaces, options.workspaceRef);
|
|
1457
|
+
if (!selected) throw new Error(`Workspace "${options.workspaceRef}" not found. Run \`donebear workspace list\`.`);
|
|
1458
|
+
return {
|
|
1459
|
+
workspace: selected,
|
|
1460
|
+
autoSelected: false
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
if (options.storedWorkspaceId) {
|
|
1464
|
+
const selected = byReference(workspaces, options.storedWorkspaceId);
|
|
1465
|
+
if (selected) return {
|
|
1466
|
+
workspace: selected,
|
|
1467
|
+
autoSelected: false
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
if (workspaces.length === 1) {
|
|
1471
|
+
const workspace = workspaces[0];
|
|
1472
|
+
if (!workspace) throw new Error("No workspaces available for this account");
|
|
1473
|
+
await setCurrentWorkspace(workspace.id);
|
|
1474
|
+
return {
|
|
1475
|
+
workspace,
|
|
1476
|
+
autoSelected: true
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
throw new Error("No default workspace selected. Run `donebear workspace use <id-or-slug>`.");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region src/commands/history.ts
|
|
1484
|
+
const DEFAULT_HISTORY_LIMIT = 50;
|
|
1485
|
+
const ACTION_LABELS = {
|
|
1486
|
+
I: "CREATE",
|
|
1487
|
+
U: "UPDATE",
|
|
1488
|
+
A: "ARCHIVE",
|
|
1489
|
+
D: "DELETE",
|
|
1490
|
+
V: "UNARCHIVE"
|
|
1491
|
+
};
|
|
1492
|
+
function formatActionCode(action) {
|
|
1493
|
+
return ACTION_LABELS[action] ?? action;
|
|
1494
|
+
}
|
|
1495
|
+
function formatHistoryDate(isoString) {
|
|
1496
|
+
const date = new Date(isoString);
|
|
1497
|
+
if (Number.isNaN(date.getTime())) return isoString;
|
|
1498
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
1499
|
+
}
|
|
1500
|
+
function extractPayloadTitle(entry) {
|
|
1501
|
+
if (!entry.payload) return "";
|
|
1502
|
+
const title = entry.payload.title;
|
|
1503
|
+
if (typeof title === "string") return ` (title: "${title}")`;
|
|
1504
|
+
return "";
|
|
1505
|
+
}
|
|
1506
|
+
async function runHistory(context, workspaceOverride, options) {
|
|
1507
|
+
const commandContext = await resolveCommandContext(context);
|
|
1508
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
1509
|
+
token: commandContext.token,
|
|
1510
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
1511
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
1512
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
1513
|
+
});
|
|
1514
|
+
try {
|
|
1515
|
+
const history = await listWorkspaceHistory(context, {
|
|
1516
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
1517
|
+
token: commandContext.token,
|
|
1518
|
+
workspaceId: workspace.id,
|
|
1519
|
+
limit: options.limit,
|
|
1520
|
+
model: options.model
|
|
1521
|
+
});
|
|
1522
|
+
if (context.json) {
|
|
1523
|
+
writeJson({
|
|
1524
|
+
workspace,
|
|
1525
|
+
history
|
|
1526
|
+
});
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const historyHeaders = [
|
|
1530
|
+
"date",
|
|
1531
|
+
"action",
|
|
1532
|
+
"model",
|
|
1533
|
+
"modelId",
|
|
1534
|
+
"title"
|
|
1535
|
+
];
|
|
1536
|
+
const historyRows = history.map((entry) => [
|
|
1537
|
+
formatHistoryDate(entry.createdAt),
|
|
1538
|
+
formatActionCode(entry.action),
|
|
1539
|
+
entry.modelName,
|
|
1540
|
+
entry.modelId.slice(0, 8),
|
|
1541
|
+
typeof entry.payload?.title === "string" ? entry.payload.title : ""
|
|
1542
|
+
]);
|
|
1543
|
+
if (writeFormattedRows(context, history.length, historyHeaders, historyRows)) return;
|
|
1544
|
+
if (history.length === 0) {
|
|
1545
|
+
writeInfo(context, "No history entries found.");
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
writeInfo(context, `History for ${workspace.name}:`);
|
|
1549
|
+
for (const entry of history) {
|
|
1550
|
+
const date = formatHistoryDate(entry.createdAt);
|
|
1551
|
+
const action = formatActionCode(entry.action);
|
|
1552
|
+
const modelId = entry.modelId.slice(0, 8);
|
|
1553
|
+
const title = extractPayloadTitle(entry);
|
|
1554
|
+
writeInfo(context, `${date} ${action} ${entry.modelName} ${modelId}...${title}`);
|
|
1555
|
+
}
|
|
1556
|
+
} catch {
|
|
1557
|
+
writeWarning(context, "History is not available for this server version.");
|
|
1558
|
+
writeWarning(context, "Upgrade your manage-api to enable audit logs.");
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function registerHistoryCommand(program, workspaceOverride) {
|
|
1562
|
+
program.command("history").description("Show recent workspace audit log").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--model <model>", "Filter by model name: Task|Project|Label|Team").option("-n, --limit <count>", "Maximum number of entries", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_HISTORY_LIMIT).addHelpText("after", `
|
|
1563
|
+
Examples:
|
|
1564
|
+
donebear history
|
|
1565
|
+
donebear history --model Task --limit 20
|
|
1566
|
+
donebear history --format csv
|
|
1567
|
+
`).action(async (options, command) => {
|
|
1568
|
+
const context = contextFromCommand(command);
|
|
1569
|
+
await runWithErrorHandling(context, async () => {
|
|
1570
|
+
await runHistory(context, workspaceOverride, options);
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region src/commands/interactive.ts
|
|
1577
|
+
const HISTORY_FILE = join(CONFIG_DIR, "history.json");
|
|
1578
|
+
const MAX_HISTORY = 200;
|
|
1579
|
+
function isHistoryFile(value) {
|
|
1580
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1581
|
+
const record = value;
|
|
1582
|
+
return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
|
|
1583
|
+
}
|
|
1584
|
+
async function loadHistory(historyFile) {
|
|
1585
|
+
try {
|
|
1586
|
+
const raw = await readFile(historyFile, "utf8");
|
|
1587
|
+
const parsed = JSON.parse(raw);
|
|
1588
|
+
if (isHistoryFile(parsed)) return parsed.entries;
|
|
1589
|
+
return [];
|
|
1590
|
+
} catch {
|
|
1591
|
+
return [];
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function tokenize(input) {
|
|
1595
|
+
const tokens = [];
|
|
1596
|
+
let current = "";
|
|
1597
|
+
let inQuote = false;
|
|
1598
|
+
let quoteChar = "";
|
|
1599
|
+
for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
|
|
1600
|
+
else current += char;
|
|
1601
|
+
else if (char === "\"" || char === "'") {
|
|
1602
|
+
inQuote = true;
|
|
1603
|
+
quoteChar = char;
|
|
1604
|
+
} else if (char === " ") {
|
|
1605
|
+
if (current.length > 0) {
|
|
1606
|
+
tokens.push(current);
|
|
1607
|
+
current = "";
|
|
1608
|
+
}
|
|
1609
|
+
} else current += char;
|
|
1610
|
+
if (current.length > 0) tokens.push(current);
|
|
1611
|
+
return tokens;
|
|
1612
|
+
}
|
|
1613
|
+
function buildCompleter(program) {
|
|
1614
|
+
return (line) => {
|
|
1615
|
+
const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
|
|
1616
|
+
const hits = allCommands.filter((c) => c.startsWith(line));
|
|
1617
|
+
return [hits.length > 0 ? hits : allCommands, line];
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
async function runInteractiveMode(context, program) {
|
|
1621
|
+
const history = await loadHistory(HISTORY_FILE);
|
|
1622
|
+
const rl = createInterface({
|
|
1623
|
+
input: process.stdin,
|
|
1624
|
+
output: process.stdout,
|
|
1625
|
+
prompt: "donebear> ",
|
|
1626
|
+
historySize: MAX_HISTORY,
|
|
1627
|
+
completer: buildCompleter(program)
|
|
1628
|
+
});
|
|
1629
|
+
for (const entry of [...history].reverse()) rl.history.push(entry);
|
|
1630
|
+
writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
|
|
1631
|
+
writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
|
|
1632
|
+
rl.prompt();
|
|
1633
|
+
const sessionEntries = [];
|
|
1634
|
+
for await (const line of rl) {
|
|
1635
|
+
const trimmed = line.trim();
|
|
1636
|
+
if (!trimmed) {
|
|
1637
|
+
rl.prompt();
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
|
|
1641
|
+
const tokens = tokenize(trimmed);
|
|
1642
|
+
try {
|
|
1643
|
+
await program.parseAsync([
|
|
1644
|
+
"node",
|
|
1645
|
+
"donebear",
|
|
1646
|
+
...tokens
|
|
1647
|
+
]);
|
|
1648
|
+
} catch {}
|
|
1649
|
+
sessionEntries.push(trimmed);
|
|
1650
|
+
rl.prompt();
|
|
1651
|
+
}
|
|
1652
|
+
rl.close();
|
|
1653
|
+
if (sessionEntries.length > 0) try {
|
|
1654
|
+
const payload = {
|
|
1655
|
+
version: 1,
|
|
1656
|
+
entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY)
|
|
1657
|
+
};
|
|
1658
|
+
await mkdir(CONFIG_DIR, {
|
|
1659
|
+
recursive: true,
|
|
1660
|
+
mode: 448
|
|
1661
|
+
});
|
|
1662
|
+
await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
1663
|
+
encoding: "utf8",
|
|
1664
|
+
mode: 384
|
|
1665
|
+
});
|
|
1666
|
+
} catch {}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
//#endregion
|
|
1670
|
+
//#region src/commands/label.ts
|
|
1671
|
+
function sortLabels(labels, sort) {
|
|
1672
|
+
const sorted = [...labels];
|
|
1673
|
+
if (sort === "alpha") sorted.sort((a, b) => a.title.localeCompare(b.title));
|
|
1674
|
+
else sorted.sort((a, b) => a.title.localeCompare(b.title));
|
|
1675
|
+
return sorted;
|
|
1676
|
+
}
|
|
1677
|
+
async function resolveLabel(context, options) {
|
|
1678
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
1679
|
+
token: options.commandContext.token,
|
|
1680
|
+
apiBaseUrl: options.commandContext.apiBaseUrl,
|
|
1681
|
+
workspaceRef: options.workspaceRef,
|
|
1682
|
+
storedWorkspaceId: options.commandContext.localContext.workspaceId
|
|
1683
|
+
});
|
|
1684
|
+
const labels = await loadLabelsByWorkspace(context, {
|
|
1685
|
+
baseUrl: options.commandContext.apiBaseUrl,
|
|
1686
|
+
token: options.commandContext.token,
|
|
1687
|
+
workspaceId: workspace.id
|
|
1688
|
+
});
|
|
1689
|
+
const normalized = options.labelRef.trim().toLowerCase();
|
|
1690
|
+
const found = labels.find((l) => l.id === options.labelRef || l.title.toLowerCase() === normalized || l.id.startsWith(options.labelRef));
|
|
1691
|
+
if (!found) throw new Error(`Label "${options.labelRef}" not found.`);
|
|
1692
|
+
return found;
|
|
1693
|
+
}
|
|
1694
|
+
async function runLabelList(context, options) {
|
|
1695
|
+
const commandContext = await resolveCommandContext(context);
|
|
1696
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
1697
|
+
token: commandContext.token,
|
|
1698
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
1699
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace),
|
|
1700
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
1701
|
+
});
|
|
1702
|
+
const sorted = sortLabels(await loadLabelsByWorkspace(context, {
|
|
1703
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
1704
|
+
token: commandContext.token,
|
|
1705
|
+
workspaceId: workspace.id
|
|
1706
|
+
}), options.sort);
|
|
1707
|
+
if (context.json) {
|
|
1708
|
+
writeJson({
|
|
1709
|
+
workspace,
|
|
1710
|
+
autoSelected,
|
|
1711
|
+
total: sorted.length,
|
|
1712
|
+
labels: sorted
|
|
1713
|
+
});
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const headers = [
|
|
1717
|
+
"id",
|
|
1718
|
+
"title",
|
|
1719
|
+
"createdAt",
|
|
1720
|
+
"updatedAt"
|
|
1721
|
+
];
|
|
1722
|
+
const rows = sorted.map((l) => [
|
|
1723
|
+
l.id,
|
|
1724
|
+
l.title,
|
|
1725
|
+
formatEpochDate(l.createdAt) ?? "",
|
|
1726
|
+
formatEpochDate(l.updatedAt) ?? ""
|
|
1727
|
+
]);
|
|
1728
|
+
if (writeFormattedRows(context, sorted.length, headers, rows)) return;
|
|
1729
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
1730
|
+
if (sorted.length === 0) {
|
|
1731
|
+
writeInfo(context, "No labels found.");
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
writeInfo(context, `${workspace.name} (${sorted.length} label(s))`);
|
|
1735
|
+
for (const label of sorted) writeInfo(context, `${label.id.slice(0, 8)} ${label.title}`);
|
|
1736
|
+
if (context.copy) copyToClipboard(sorted.map((l) => `${l.id.slice(0, 8)} ${l.title}`).join("\n"));
|
|
1737
|
+
}
|
|
1738
|
+
async function runLabelShow(context, labelRef, options) {
|
|
1739
|
+
const label = await resolveLabel(context, {
|
|
1740
|
+
labelRef,
|
|
1741
|
+
commandContext: await resolveCommandContext(context),
|
|
1742
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
1743
|
+
});
|
|
1744
|
+
if (context.json) {
|
|
1745
|
+
writeJson({
|
|
1746
|
+
label,
|
|
1747
|
+
createdAtIso: formatEpochDate(label.createdAt),
|
|
1748
|
+
updatedAtIso: formatEpochDate(label.updatedAt)
|
|
1749
|
+
});
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
writeSuccess(context, label.title);
|
|
1753
|
+
writeInfo(context, `id: ${label.id}`);
|
|
1754
|
+
writeInfo(context, `workspaceId: ${label.workspaceId}`);
|
|
1755
|
+
writeInfo(context, `createdAt: ${formatEpochDate(label.createdAt) ?? "(none)"}`);
|
|
1756
|
+
writeInfo(context, `updatedAt: ${formatEpochDate(label.updatedAt) ?? "(none)"}`);
|
|
1757
|
+
}
|
|
1758
|
+
function registerLabelCommands(program, workspaceOverride) {
|
|
1759
|
+
const label = program.command("label").alias("labels").description("Manage labels").addHelpText("after", `
|
|
1760
|
+
Examples:
|
|
1761
|
+
donebear label
|
|
1762
|
+
donebear label list --sort alpha
|
|
1763
|
+
donebear label show bug
|
|
1764
|
+
`).action(async (_options, command) => {
|
|
1765
|
+
const context = contextFromCommand(command);
|
|
1766
|
+
await runWithErrorHandling(context, async () => {
|
|
1767
|
+
await runLabelList(context, { sort: "alpha" });
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
label.command("list").alias("ls").description("List labels").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--sort <order>", "Sort order: alpha|count", "alpha").action(async (options, command) => {
|
|
1771
|
+
const context = contextFromCommand(command);
|
|
1772
|
+
await runWithErrorHandling(context, async () => {
|
|
1773
|
+
await runLabelList(context, {
|
|
1774
|
+
...options,
|
|
1775
|
+
workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
label.command("show").description("Show label details").argument("<id-or-title>", "Label id, title, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (labelRef, options, command) => {
|
|
1780
|
+
const context = contextFromCommand(command);
|
|
1781
|
+
await runWithErrorHandling(context, async () => {
|
|
1782
|
+
await runLabelShow(context, labelRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
//#endregion
|
|
1788
|
+
//#region src/commands/project.ts
|
|
1789
|
+
const ISO_YMD_REGEX$1 = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1790
|
+
function parseProjectStatus(value) {
|
|
1791
|
+
switch (value.trim().toLowerCase()) {
|
|
1792
|
+
case "active": return "active";
|
|
1793
|
+
case "done":
|
|
1794
|
+
case "complete":
|
|
1795
|
+
case "completed": return "done";
|
|
1796
|
+
case "archived":
|
|
1797
|
+
case "archive": return "archived";
|
|
1798
|
+
case "all": return "all";
|
|
1799
|
+
default: throw new Error(`Invalid status "${value}". Expected one of: active, done, archived, all.`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function getProjectStatus(project) {
|
|
1803
|
+
if (project.archivedAt !== null) return "archived";
|
|
1804
|
+
if (project.completedAt !== null) return "done";
|
|
1805
|
+
return "active";
|
|
1806
|
+
}
|
|
1807
|
+
function parseTargetDate(value) {
|
|
1808
|
+
if (!value) return null;
|
|
1809
|
+
const trimmed = value.trim();
|
|
1810
|
+
if (ISO_YMD_REGEX$1.exec(trimmed)) {
|
|
1811
|
+
const year = Number.parseInt(trimmed.slice(0, 4), 10);
|
|
1812
|
+
const month = Number.parseInt(trimmed.slice(5, 7), 10);
|
|
1813
|
+
const day = Number.parseInt(trimmed.slice(8, 10), 10);
|
|
1814
|
+
const date = new Date(year, month - 1, day);
|
|
1815
|
+
if (Number.isNaN(date.getTime())) throw new Error(`Invalid target date: ${value}`);
|
|
1816
|
+
return date.getTime();
|
|
1817
|
+
}
|
|
1818
|
+
const parsed = Date.parse(trimmed);
|
|
1819
|
+
if (Number.isNaN(parsed)) throw new Error(`Invalid target date: ${value}`);
|
|
1820
|
+
return parsed;
|
|
1821
|
+
}
|
|
1822
|
+
const WHITESPACE_REGEX = /\s+/;
|
|
1823
|
+
function autoGenerateKey(name) {
|
|
1824
|
+
return name.trim().split(WHITESPACE_REGEX).map((word) => word[0]?.toUpperCase() ?? "").join("").replace(/[^A-Z0-9]/g, "").slice(0, 5) || "PROJ";
|
|
1825
|
+
}
|
|
1826
|
+
function filterProjects(projects, status) {
|
|
1827
|
+
if (status === "all") return projects;
|
|
1828
|
+
return projects.filter((p) => getProjectStatus(p) === status);
|
|
1829
|
+
}
|
|
1830
|
+
function sortProjectsByUpdatedDesc(projects) {
|
|
1831
|
+
return [...projects].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
1832
|
+
}
|
|
1833
|
+
async function resolveProjectReference(context, options) {
|
|
1834
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
1835
|
+
token: options.commandContext.token,
|
|
1836
|
+
apiBaseUrl: options.commandContext.apiBaseUrl,
|
|
1837
|
+
workspaceRef: options.workspaceRef,
|
|
1838
|
+
storedWorkspaceId: options.commandContext.localContext.workspaceId
|
|
1839
|
+
});
|
|
1840
|
+
const projects = await loadProjectsByWorkspace(context, {
|
|
1841
|
+
baseUrl: options.commandContext.apiBaseUrl,
|
|
1842
|
+
token: options.commandContext.token,
|
|
1843
|
+
workspaceId: workspace.id
|
|
1844
|
+
});
|
|
1845
|
+
const normalized = options.projectRef.trim().toLowerCase();
|
|
1846
|
+
const found = projects.find((p) => p.id === options.projectRef || p.key.toLowerCase() === normalized || p.id.startsWith(options.projectRef));
|
|
1847
|
+
if (!found) throw new Error(`Project "${options.projectRef}" not found.`);
|
|
1848
|
+
return {
|
|
1849
|
+
project: found,
|
|
1850
|
+
workspace
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
async function runProjectList(context, options) {
|
|
1854
|
+
const commandContext = await resolveCommandContext(context);
|
|
1855
|
+
const requestedStatus = parseProjectStatus(options.status);
|
|
1856
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
1857
|
+
token: commandContext.token,
|
|
1858
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
1859
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace),
|
|
1860
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
1861
|
+
});
|
|
1862
|
+
const filtered = filterProjects(sortProjectsByUpdatedDesc(await loadProjectsByWorkspace(context, {
|
|
1863
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
1864
|
+
token: commandContext.token,
|
|
1865
|
+
workspaceId: workspace.id
|
|
1866
|
+
})), requestedStatus);
|
|
1867
|
+
if (context.json) {
|
|
1868
|
+
writeJson({
|
|
1869
|
+
workspace,
|
|
1870
|
+
autoSelected,
|
|
1871
|
+
status: requestedStatus,
|
|
1872
|
+
total: filtered.length,
|
|
1873
|
+
projects: filtered
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const headers = [
|
|
1878
|
+
"id",
|
|
1879
|
+
"key",
|
|
1880
|
+
"name",
|
|
1881
|
+
"status",
|
|
1882
|
+
"targetDate",
|
|
1883
|
+
"updatedAt"
|
|
1884
|
+
];
|
|
1885
|
+
const rows = filtered.map((p) => [
|
|
1886
|
+
p.id,
|
|
1887
|
+
p.key,
|
|
1888
|
+
p.name,
|
|
1889
|
+
getProjectStatus(p),
|
|
1890
|
+
formatShortDate(p.targetDate) ?? "",
|
|
1891
|
+
formatShortDate(p.updatedAt) ?? ""
|
|
1892
|
+
]);
|
|
1893
|
+
if (writeFormattedRows(context, filtered.length, headers, rows)) return;
|
|
1894
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
1895
|
+
if (filtered.length === 0) {
|
|
1896
|
+
writeInfo(context, "No projects found.");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
writeInfo(context, `${workspace.name} (${filtered.length} project(s))`);
|
|
1900
|
+
for (const project of filtered) {
|
|
1901
|
+
const status = getProjectStatus(project);
|
|
1902
|
+
const target = formatShortDate(project.targetDate);
|
|
1903
|
+
const suffix = target ? ` due ${target}` : "";
|
|
1904
|
+
writeInfo(context, `${project.id.slice(0, 8)} [${project.key}] ${project.name} (${status})${suffix}`);
|
|
1905
|
+
}
|
|
1906
|
+
if (context.copy) copyToClipboard(filtered.map((p) => `${p.id.slice(0, 8)} [${p.key}] ${p.name} (${getProjectStatus(p)})`).join("\n"));
|
|
1907
|
+
}
|
|
1908
|
+
async function runProjectShow(context, projectRef, options) {
|
|
1909
|
+
const { project, workspace } = await resolveProjectReference(context, {
|
|
1910
|
+
projectRef,
|
|
1911
|
+
commandContext: await resolveCommandContext(context),
|
|
1912
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
1913
|
+
});
|
|
1914
|
+
if (context.json) {
|
|
1915
|
+
writeJson({
|
|
1916
|
+
project,
|
|
1917
|
+
status: getProjectStatus(project),
|
|
1918
|
+
workspace,
|
|
1919
|
+
createdAtIso: formatEpochDate(project.createdAt),
|
|
1920
|
+
updatedAtIso: formatEpochDate(project.updatedAt),
|
|
1921
|
+
completedAtIso: formatEpochDate(project.completedAt),
|
|
1922
|
+
archivedAtIso: formatEpochDate(project.archivedAt),
|
|
1923
|
+
targetDateIso: formatEpochDate(project.targetDate)
|
|
1924
|
+
});
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
writeSuccess(context, project.name);
|
|
1928
|
+
writeInfo(context, `id: ${project.id}`);
|
|
1929
|
+
writeInfo(context, `key: ${project.key}`);
|
|
1930
|
+
writeInfo(context, `status: ${getProjectStatus(project)}`);
|
|
1931
|
+
writeInfo(context, `workspaceId: ${project.workspaceId}`);
|
|
1932
|
+
writeInfo(context, `targetDate: ${formatEpochDate(project.targetDate) ?? "(none)"}`);
|
|
1933
|
+
writeInfo(context, `createdAt: ${formatEpochDate(project.createdAt) ?? "(none)"}`);
|
|
1934
|
+
writeInfo(context, `updatedAt: ${formatEpochDate(project.updatedAt) ?? "(none)"}`);
|
|
1935
|
+
if (project.description) writeInfo(context, `description: ${project.description}`);
|
|
1936
|
+
}
|
|
1937
|
+
async function runProjectAdd(context, name, options) {
|
|
1938
|
+
const commandContext = await resolveCommandContext(context);
|
|
1939
|
+
const userId = requireUserId(commandContext.resolvedToken);
|
|
1940
|
+
const normalizedName = name.trim();
|
|
1941
|
+
if (normalizedName.length === 0) throw new Error("Project name cannot be empty.");
|
|
1942
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
1943
|
+
token: commandContext.token,
|
|
1944
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
1945
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace),
|
|
1946
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
1947
|
+
});
|
|
1948
|
+
const projectId = randomUUID();
|
|
1949
|
+
const now = Date.now();
|
|
1950
|
+
const key = options.key?.trim().toUpperCase() || autoGenerateKey(normalizedName);
|
|
1951
|
+
await mutateModel(context, {
|
|
1952
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
1953
|
+
token: commandContext.token,
|
|
1954
|
+
request: {
|
|
1955
|
+
batchId: randomUUID(),
|
|
1956
|
+
transactions: [{
|
|
1957
|
+
clientTxId: randomUUID(),
|
|
1958
|
+
clientId: commandContext.localContext.clientId,
|
|
1959
|
+
modelName: "Project",
|
|
1960
|
+
modelId: projectId,
|
|
1961
|
+
action: "INSERT",
|
|
1962
|
+
payload: {
|
|
1963
|
+
name: normalizedName,
|
|
1964
|
+
key,
|
|
1965
|
+
description: options.description?.trim() || null,
|
|
1966
|
+
status: "active",
|
|
1967
|
+
sortOrder: 0,
|
|
1968
|
+
targetDate: parseTargetDate(options.targetDate),
|
|
1969
|
+
completedAt: null,
|
|
1970
|
+
archivedAt: null,
|
|
1971
|
+
creatorId: userId,
|
|
1972
|
+
workspaceId: workspace.id,
|
|
1973
|
+
createdAt: now,
|
|
1974
|
+
updatedAt: now
|
|
1975
|
+
}
|
|
1976
|
+
}]
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
if (context.json) {
|
|
1980
|
+
writeJson({
|
|
1981
|
+
created: true,
|
|
1982
|
+
id: projectId,
|
|
1983
|
+
key,
|
|
1984
|
+
workspaceId: workspace.id,
|
|
1985
|
+
workspaceName: workspace.name,
|
|
1986
|
+
autoSelected
|
|
1987
|
+
});
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
1991
|
+
writeSuccess(context, `Added project: ${normalizedName}`);
|
|
1992
|
+
writeInfo(context, `id: ${projectId}`);
|
|
1993
|
+
writeInfo(context, `key: ${key}`);
|
|
1994
|
+
}
|
|
1995
|
+
function buildProjectUpdates(options) {
|
|
1996
|
+
if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
|
|
1997
|
+
if (options.clearTargetDate && typeof options.targetDate === "string") throw new Error("Use either --target-date or --clear-target-date, not both.");
|
|
1998
|
+
const updates = {};
|
|
1999
|
+
if (typeof options.name === "string") {
|
|
2000
|
+
const name = options.name.trim();
|
|
2001
|
+
if (name.length === 0) throw new Error("Project name cannot be empty.");
|
|
2002
|
+
updates.name = name;
|
|
2003
|
+
}
|
|
2004
|
+
if (options.clearDescription) updates.description = null;
|
|
2005
|
+
else if (typeof options.description === "string") {
|
|
2006
|
+
const desc = options.description.trim();
|
|
2007
|
+
updates.description = desc.length > 0 ? desc : null;
|
|
2008
|
+
}
|
|
2009
|
+
if (options.clearTargetDate) updates.targetDate = null;
|
|
2010
|
+
else if (typeof options.targetDate === "string") updates.targetDate = parseTargetDate(options.targetDate);
|
|
2011
|
+
if (typeof options.status === "string") applyStatusToUpdates(updates, parseProjectStatus(options.status));
|
|
2012
|
+
if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --name, --description, --clear-description, --status, --target-date, or --clear-target-date.");
|
|
2013
|
+
updates.updatedAt = Date.now();
|
|
2014
|
+
return updates;
|
|
2015
|
+
}
|
|
2016
|
+
function applyStatusToUpdates(updates, status) {
|
|
2017
|
+
if (status === "done") {
|
|
2018
|
+
updates.completedAt = Date.now();
|
|
2019
|
+
updates.archivedAt = null;
|
|
2020
|
+
} else if (status === "active") {
|
|
2021
|
+
updates.completedAt = null;
|
|
2022
|
+
updates.archivedAt = null;
|
|
2023
|
+
} else if (status === "archived") updates.archivedAt = Date.now();
|
|
2024
|
+
}
|
|
2025
|
+
async function runProjectEdit(context, projectRef, options) {
|
|
2026
|
+
const updates = buildProjectUpdates(options);
|
|
2027
|
+
const commandContext = await resolveCommandContext(context);
|
|
2028
|
+
const { project } = await resolveProjectReference(context, {
|
|
2029
|
+
projectRef,
|
|
2030
|
+
commandContext,
|
|
2031
|
+
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
2032
|
+
});
|
|
2033
|
+
await mutateModel(context, {
|
|
2034
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2035
|
+
token: commandContext.token,
|
|
2036
|
+
request: {
|
|
2037
|
+
batchId: randomUUID(),
|
|
2038
|
+
transactions: [{
|
|
2039
|
+
clientTxId: randomUUID(),
|
|
2040
|
+
clientId: commandContext.localContext.clientId,
|
|
2041
|
+
modelName: "Project",
|
|
2042
|
+
modelId: project.id,
|
|
2043
|
+
action: "UPDATE",
|
|
2044
|
+
payload: updates
|
|
2045
|
+
}]
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
if (context.json) {
|
|
2049
|
+
writeJson({
|
|
2050
|
+
ok: true,
|
|
2051
|
+
id: project.id,
|
|
2052
|
+
action: "UPDATE"
|
|
2053
|
+
});
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
writeSuccess(context, `Updated project: ${project.id}`);
|
|
2057
|
+
}
|
|
2058
|
+
async function runProjectMutation(context, projectRef, payload) {
|
|
2059
|
+
const commandContext = await resolveCommandContext(context);
|
|
2060
|
+
const { project } = await resolveProjectReference(context, {
|
|
2061
|
+
projectRef,
|
|
2062
|
+
commandContext,
|
|
2063
|
+
workspaceRef: payload.workspaceRef
|
|
2064
|
+
});
|
|
2065
|
+
await mutateModel(context, {
|
|
2066
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2067
|
+
token: commandContext.token,
|
|
2068
|
+
request: {
|
|
2069
|
+
batchId: randomUUID(),
|
|
2070
|
+
transactions: [{
|
|
2071
|
+
clientTxId: randomUUID(),
|
|
2072
|
+
clientId: commandContext.localContext.clientId,
|
|
2073
|
+
modelName: "Project",
|
|
2074
|
+
modelId: project.id,
|
|
2075
|
+
action: payload.action,
|
|
2076
|
+
payload: payload.data
|
|
2077
|
+
}]
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
if (context.json) {
|
|
2081
|
+
writeJson({
|
|
2082
|
+
ok: true,
|
|
2083
|
+
id: project.id,
|
|
2084
|
+
action: payload.action
|
|
2085
|
+
});
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
writeSuccess(context, `${payload.successMessage}: ${project.id}`);
|
|
2089
|
+
}
|
|
2090
|
+
function registerProjectCommands(program, workspaceOverride) {
|
|
2091
|
+
const project = program.command("project").alias("projects").description("Manage projects").addHelpText("after", `
|
|
2092
|
+
Examples:
|
|
2093
|
+
donebear project
|
|
2094
|
+
donebear project add "Q1 Launch" --key LAUNCH
|
|
2095
|
+
donebear project list --status active
|
|
2096
|
+
donebear project edit LAUNCH --target-date 2026-03-31
|
|
2097
|
+
`).action(async (_options, command) => {
|
|
2098
|
+
const context = contextFromCommand(command);
|
|
2099
|
+
await runWithErrorHandling(context, async () => {
|
|
2100
|
+
await runProjectList(context, { status: "active" });
|
|
2101
|
+
});
|
|
2102
|
+
});
|
|
2103
|
+
project.command("list").alias("ls").description("List projects").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --status <status>", "active|done|archived|all", "active").action(async (options, command) => {
|
|
2104
|
+
const context = contextFromCommand(command);
|
|
2105
|
+
await runWithErrorHandling(context, async () => {
|
|
2106
|
+
await runProjectList(context, {
|
|
2107
|
+
...options,
|
|
2108
|
+
workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
2111
|
+
});
|
|
2112
|
+
project.command("show").description("Show project details").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
|
|
2113
|
+
const context = contextFromCommand(command);
|
|
2114
|
+
await runWithErrorHandling(context, async () => {
|
|
2115
|
+
await runProjectShow(context, projectRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
project.command("add").alias("create").description("Create a project").argument("<name>", "Project name").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--key <KEY>", "Short project key (auto-generated if not provided)").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").action(async (name, options, command) => {
|
|
2119
|
+
const context = contextFromCommand(command);
|
|
2120
|
+
await runWithErrorHandling(context, async () => {
|
|
2121
|
+
await runProjectAdd(context, name, {
|
|
2122
|
+
...options,
|
|
2123
|
+
workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
project.command("edit").description("Update project fields").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--name <name>", "Replace project name").option("-d, --description <text>", "Replace project description").option("--clear-description", "Clear project description").option("--status <status>", "Set status (active|done|archived)").option("--target-date <date>", "Set target date (YYYY-MM-DD)").option("--clear-target-date", "Clear target date").action(async (projectRef, options, command) => {
|
|
2128
|
+
const context = contextFromCommand(command);
|
|
2129
|
+
await runWithErrorHandling(context, async () => {
|
|
2130
|
+
await runProjectEdit(context, projectRef, {
|
|
2131
|
+
...options,
|
|
2132
|
+
workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
project.command("done").alias("complete").description("Mark a project as completed").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
|
|
2137
|
+
const context = contextFromCommand(command);
|
|
2138
|
+
await runWithErrorHandling(context, async () => {
|
|
2139
|
+
const now = Date.now();
|
|
2140
|
+
await runProjectMutation(context, projectRef, {
|
|
2141
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2142
|
+
action: "UPDATE",
|
|
2143
|
+
data: {
|
|
2144
|
+
completedAt: now,
|
|
2145
|
+
updatedAt: now
|
|
2146
|
+
},
|
|
2147
|
+
successMessage: "Marked done"
|
|
2148
|
+
});
|
|
2149
|
+
});
|
|
2150
|
+
});
|
|
2151
|
+
project.command("archive").description("Archive a project").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
|
|
2152
|
+
const context = contextFromCommand(command);
|
|
2153
|
+
await runWithErrorHandling(context, async () => {
|
|
2154
|
+
await runProjectMutation(context, projectRef, {
|
|
2155
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2156
|
+
action: "ARCHIVE",
|
|
2157
|
+
data: { archivedAt: Date.now() },
|
|
2158
|
+
successMessage: "Archived"
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2161
|
+
});
|
|
2162
|
+
project.command("unarchive").alias("restore").description("Unarchive a project").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
|
|
2163
|
+
const context = contextFromCommand(command);
|
|
2164
|
+
await runWithErrorHandling(context, async () => {
|
|
2165
|
+
await runProjectMutation(context, projectRef, {
|
|
2166
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2167
|
+
action: "UNARCHIVE",
|
|
2168
|
+
data: {},
|
|
2169
|
+
successMessage: "Unarchived"
|
|
2170
|
+
});
|
|
2171
|
+
});
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
//#endregion
|
|
2176
|
+
//#region src/commands/task.ts
|
|
2177
|
+
const ISO_YMD_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2178
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2179
|
+
const DEFAULT_TASK_LIST_LIMIT = 20;
|
|
2180
|
+
const VALID_TASK_VIEWS = new Set([
|
|
2181
|
+
"inbox",
|
|
2182
|
+
"anytime",
|
|
2183
|
+
"today",
|
|
2184
|
+
"upcoming",
|
|
2185
|
+
"someday"
|
|
2186
|
+
]);
|
|
2187
|
+
function parseTaskView(value) {
|
|
2188
|
+
const normalized = value.trim().toLowerCase();
|
|
2189
|
+
if (VALID_TASK_VIEWS.has(normalized)) return normalized;
|
|
2190
|
+
throw new Error(`Invalid view "${value}". Expected one of: inbox, anytime, today, upcoming, someday.`);
|
|
2191
|
+
}
|
|
2192
|
+
function parseDeadline(value) {
|
|
2193
|
+
if (!value) return null;
|
|
2194
|
+
const trimmed = value.trim();
|
|
2195
|
+
if (ISO_YMD_REGEX.exec(trimmed)) {
|
|
2196
|
+
const year = Number.parseInt(trimmed.slice(0, 4), 10);
|
|
2197
|
+
const month = Number.parseInt(trimmed.slice(5, 7), 10);
|
|
2198
|
+
const day = Number.parseInt(trimmed.slice(8, 10), 10);
|
|
2199
|
+
const date = new Date(year, month - 1, day);
|
|
2200
|
+
if (Number.isNaN(date.getTime())) throw new Error(`Invalid deadline date: ${value}`);
|
|
2201
|
+
return date.getTime();
|
|
2202
|
+
}
|
|
2203
|
+
const parsed = Date.parse(trimmed);
|
|
2204
|
+
if (Number.isNaN(parsed)) throw new Error(`Invalid deadline date: ${value}`);
|
|
2205
|
+
return parsed;
|
|
2206
|
+
}
|
|
2207
|
+
function sortTasksByUpdatedDesc(tasks) {
|
|
2208
|
+
return [...tasks].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
2209
|
+
}
|
|
2210
|
+
async function resolveTaskReference(context, options) {
|
|
2211
|
+
const taskRef = options.taskRef.trim();
|
|
2212
|
+
if (!taskRef) throw new Error("Task id is required.");
|
|
2213
|
+
const exact = await loadTaskById(context, {
|
|
2214
|
+
baseUrl: options.commandContext.apiBaseUrl,
|
|
2215
|
+
token: options.commandContext.token,
|
|
2216
|
+
taskId: taskRef
|
|
2217
|
+
});
|
|
2218
|
+
if (exact) return exact;
|
|
2219
|
+
if (UUID_REGEX.test(taskRef)) throw new Error(`Task "${taskRef}" not found.`);
|
|
2220
|
+
if (taskRef.length < 4) throw new Error("Task id prefix must be at least 4 characters.");
|
|
2221
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
2222
|
+
token: options.commandContext.token,
|
|
2223
|
+
apiBaseUrl: options.commandContext.apiBaseUrl,
|
|
2224
|
+
workspaceRef: options.workspaceRef,
|
|
2225
|
+
storedWorkspaceId: options.commandContext.localContext.workspaceId
|
|
2226
|
+
});
|
|
2227
|
+
const matches = (await loadTasksByWorkspace(context, {
|
|
2228
|
+
baseUrl: options.commandContext.apiBaseUrl,
|
|
2229
|
+
token: options.commandContext.token,
|
|
2230
|
+
workspaceId: workspace.id
|
|
2231
|
+
})).filter((task) => task.id.startsWith(taskRef));
|
|
2232
|
+
if (matches.length === 0) throw new Error(`Task "${taskRef}" not found.`);
|
|
2233
|
+
if (matches.length === 1) {
|
|
2234
|
+
const [match] = matches;
|
|
2235
|
+
if (!match) throw new Error(`Task "${taskRef}" not found.`);
|
|
2236
|
+
if (autoSelected && !context.json) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
2237
|
+
return match;
|
|
2238
|
+
}
|
|
2239
|
+
throw new Error(`Task prefix "${taskRef}" is ambiguous (${matches.length} matches). Use the full id.`);
|
|
2240
|
+
}
|
|
2241
|
+
function filterTasks(tasks, options) {
|
|
2242
|
+
const stateFiltered = options.state === "all" ? tasks : tasks.filter((task) => getTaskState(task) === options.state);
|
|
2243
|
+
if (!options.search) return stateFiltered;
|
|
2244
|
+
const query = options.search.trim().toLowerCase();
|
|
2245
|
+
if (query.length === 0) return stateFiltered;
|
|
2246
|
+
return stateFiltered.filter((task) => {
|
|
2247
|
+
const title = task.title.toLowerCase();
|
|
2248
|
+
const notes = task.description?.toLowerCase() ?? "";
|
|
2249
|
+
return title.includes(query) || notes.includes(query);
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
async function runTaskList(context, options, workspaceOverride) {
|
|
2253
|
+
const commandContext = await resolveCommandContext(context);
|
|
2254
|
+
const requestedState = parseTaskState(options.state);
|
|
2255
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
2256
|
+
token: commandContext.token,
|
|
2257
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2258
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2259
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2260
|
+
});
|
|
2261
|
+
const filtered = filterTasks(sortTasksByUpdatedDesc(await loadTasksByWorkspace(context, {
|
|
2262
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2263
|
+
token: commandContext.token,
|
|
2264
|
+
workspaceId: workspace.id
|
|
2265
|
+
})), {
|
|
2266
|
+
state: requestedState,
|
|
2267
|
+
search: options.search?.trim() ?? null
|
|
2268
|
+
});
|
|
2269
|
+
const limited = filtered.slice(0, options.limit);
|
|
2270
|
+
if (context.json) {
|
|
2271
|
+
writeJson({
|
|
2272
|
+
workspace,
|
|
2273
|
+
autoSelected,
|
|
2274
|
+
state: requestedState,
|
|
2275
|
+
total: filtered.length,
|
|
2276
|
+
count: limited.length,
|
|
2277
|
+
tasks: limited
|
|
2278
|
+
});
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const headers = [
|
|
2282
|
+
"id",
|
|
2283
|
+
"title",
|
|
2284
|
+
"state",
|
|
2285
|
+
"deadline"
|
|
2286
|
+
];
|
|
2287
|
+
const rows = limited.map((task) => [
|
|
2288
|
+
task.id,
|
|
2289
|
+
task.title,
|
|
2290
|
+
getTaskState(task),
|
|
2291
|
+
formatShortDate(task.deadlineAt) ?? ""
|
|
2292
|
+
]);
|
|
2293
|
+
if (writeFormattedRows(context, filtered.length, headers, rows)) return;
|
|
2294
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
2295
|
+
if (limited.length === 0) {
|
|
2296
|
+
writeInfo(context, "No tasks found.");
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
const lines = [];
|
|
2300
|
+
lines.push(`${workspace.name} (${limited.length}/${filtered.length})`);
|
|
2301
|
+
for (const task of limited) {
|
|
2302
|
+
const marker = toTaskMarker(task);
|
|
2303
|
+
const deadline = formatShortDate(task.deadlineAt);
|
|
2304
|
+
const suffix = deadline ? ` due ${deadline}` : "";
|
|
2305
|
+
lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
|
|
2306
|
+
}
|
|
2307
|
+
for (const line of lines) writeInfo(context, line);
|
|
2308
|
+
}
|
|
2309
|
+
async function runTaskShow(context, taskRef, options, workspaceOverride) {
|
|
2310
|
+
const task = await resolveTaskReference(context, {
|
|
2311
|
+
taskRef,
|
|
2312
|
+
commandContext: await resolveCommandContext(context),
|
|
2313
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
2314
|
+
});
|
|
2315
|
+
if (context.json) {
|
|
2316
|
+
writeJson({
|
|
2317
|
+
task,
|
|
2318
|
+
state: getTaskState(task),
|
|
2319
|
+
createdAtIso: formatEpochDate(task.createdAt),
|
|
2320
|
+
updatedAtIso: formatEpochDate(task.updatedAt),
|
|
2321
|
+
completedAtIso: formatEpochDate(task.completedAt),
|
|
2322
|
+
archivedAtIso: formatEpochDate(task.archivedAt),
|
|
2323
|
+
deadlineAtIso: formatEpochDate(task.deadlineAt)
|
|
2324
|
+
});
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
writeSuccess(context, task.title);
|
|
2328
|
+
writeInfo(context, `id: ${task.id}`);
|
|
2329
|
+
writeInfo(context, `state: ${getTaskState(task)}`);
|
|
2330
|
+
writeInfo(context, `workspaceId: ${task.workspaceId}`);
|
|
2331
|
+
writeInfo(context, `start: ${task.start}`);
|
|
2332
|
+
writeInfo(context, `startBucket: ${task.startBucket}`);
|
|
2333
|
+
writeInfo(context, `deadline: ${formatEpochDate(task.deadlineAt) ?? "(none)"}`);
|
|
2334
|
+
if (task.projectId) writeInfo(context, `projectId: ${task.projectId}`);
|
|
2335
|
+
if (task.teamId) writeInfo(context, `teamId: ${task.teamId}`);
|
|
2336
|
+
if (task.assigneeId) writeInfo(context, `assigneeId: ${task.assigneeId}`);
|
|
2337
|
+
if (task.description) writeInfo(context, `notes: ${task.description}`);
|
|
2338
|
+
}
|
|
2339
|
+
async function runTaskAdd(context, title, options, workspaceOverride) {
|
|
2340
|
+
const commandContext = await resolveCommandContext(context);
|
|
2341
|
+
const userId = requireUserId(commandContext.resolvedToken);
|
|
2342
|
+
const normalizedTitle = title.trim();
|
|
2343
|
+
if (normalizedTitle.length === 0) throw new Error("Task title cannot be empty.");
|
|
2344
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
2345
|
+
token: commandContext.token,
|
|
2346
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2347
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2348
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2349
|
+
});
|
|
2350
|
+
const [projectId, teamId] = await Promise.all([options.project ? loadProjectsByWorkspace(context, {
|
|
2351
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2352
|
+
token: commandContext.token,
|
|
2353
|
+
workspaceId: workspace.id
|
|
2354
|
+
}).then((projects) => resolveEntityId(projects, options.project, "Project")) : Promise.resolve(null), options.team ? loadTeamsByWorkspace(context, {
|
|
2355
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2356
|
+
token: commandContext.token,
|
|
2357
|
+
workspaceId: workspace.id
|
|
2358
|
+
}).then((teams) => resolveEntityId(teams, options.team, "Team")) : Promise.resolve(null)]);
|
|
2359
|
+
const taskId = randomUUID();
|
|
2360
|
+
const now = Date.now();
|
|
2361
|
+
const view = parseTaskView(options.when);
|
|
2362
|
+
const startFields = buildTaskStartFields(view);
|
|
2363
|
+
await mutateTask(context, {
|
|
2364
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2365
|
+
token: commandContext.token,
|
|
2366
|
+
request: {
|
|
2367
|
+
batchId: randomUUID(),
|
|
2368
|
+
transactions: [{
|
|
2369
|
+
clientTxId: randomUUID(),
|
|
2370
|
+
clientId: commandContext.localContext.clientId,
|
|
2371
|
+
modelName: "Task",
|
|
2372
|
+
modelId: taskId,
|
|
2373
|
+
action: "INSERT",
|
|
2374
|
+
payload: {
|
|
2375
|
+
title: normalizedTitle,
|
|
2376
|
+
description: options.notes?.trim() || null,
|
|
2377
|
+
creatorId: userId,
|
|
2378
|
+
workspaceId: workspace.id,
|
|
2379
|
+
createdAt: now,
|
|
2380
|
+
updatedAt: now,
|
|
2381
|
+
sortOrder: 0,
|
|
2382
|
+
todaySortOrder: 0,
|
|
2383
|
+
completedAt: null,
|
|
2384
|
+
archivedAt: null,
|
|
2385
|
+
deadlineAt: parseDeadline(options.deadline),
|
|
2386
|
+
projectId,
|
|
2387
|
+
teamId,
|
|
2388
|
+
...startFields
|
|
2389
|
+
}
|
|
2390
|
+
}]
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
if (context.json) {
|
|
2394
|
+
writeJson({
|
|
2395
|
+
created: true,
|
|
2396
|
+
id: taskId,
|
|
2397
|
+
workspaceId: workspace.id,
|
|
2398
|
+
workspaceName: workspace.name,
|
|
2399
|
+
autoSelected,
|
|
2400
|
+
view,
|
|
2401
|
+
projectId,
|
|
2402
|
+
teamId
|
|
2403
|
+
});
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
2407
|
+
writeSuccess(context, `Added task: ${normalizedTitle}`);
|
|
2408
|
+
writeInfo(context, `id: ${taskId}`);
|
|
2409
|
+
}
|
|
2410
|
+
async function runTaskMutation(context, taskRef, payload) {
|
|
2411
|
+
const commandContext = await resolveCommandContext(context);
|
|
2412
|
+
const task = await resolveTaskReference(context, {
|
|
2413
|
+
taskRef,
|
|
2414
|
+
commandContext,
|
|
2415
|
+
workspaceRef: payload.workspaceRef
|
|
2416
|
+
});
|
|
2417
|
+
await mutateTask(context, {
|
|
2418
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2419
|
+
token: commandContext.token,
|
|
2420
|
+
request: {
|
|
2421
|
+
batchId: randomUUID(),
|
|
2422
|
+
transactions: [{
|
|
2423
|
+
clientTxId: randomUUID(),
|
|
2424
|
+
clientId: commandContext.localContext.clientId,
|
|
2425
|
+
modelName: "Task",
|
|
2426
|
+
modelId: task.id,
|
|
2427
|
+
action: payload.action,
|
|
2428
|
+
payload: payload.data
|
|
2429
|
+
}]
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
if (context.json) {
|
|
2433
|
+
writeJson({
|
|
2434
|
+
ok: true,
|
|
2435
|
+
id: task.id,
|
|
2436
|
+
action: payload.action
|
|
2437
|
+
});
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
writeSuccess(context, `${payload.successMessage}: ${task.id}`);
|
|
2441
|
+
}
|
|
2442
|
+
function resolveEntityId(entities, ref, label) {
|
|
2443
|
+
const trimmed = ref.trim();
|
|
2444
|
+
const lower = trimmed.toLowerCase();
|
|
2445
|
+
const found = entities.find((e) => e.id === trimmed || e.key.toLowerCase() === lower || e.id.startsWith(trimmed));
|
|
2446
|
+
if (!found) throw new Error(`${label} "${ref}" not found.`);
|
|
2447
|
+
return found.id;
|
|
2448
|
+
}
|
|
2449
|
+
async function resolveProjectId(context, commandContext, projectRef, workspaceRef) {
|
|
2450
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
2451
|
+
token: commandContext.token,
|
|
2452
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2453
|
+
workspaceRef,
|
|
2454
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2455
|
+
});
|
|
2456
|
+
return resolveEntityId(await loadProjectsByWorkspace(context, {
|
|
2457
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2458
|
+
token: commandContext.token,
|
|
2459
|
+
workspaceId: workspace.id
|
|
2460
|
+
}), projectRef, "Project");
|
|
2461
|
+
}
|
|
2462
|
+
async function resolveTeamId(context, commandContext, teamRef, workspaceRef) {
|
|
2463
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
2464
|
+
token: commandContext.token,
|
|
2465
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2466
|
+
workspaceRef,
|
|
2467
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2468
|
+
});
|
|
2469
|
+
return resolveEntityId(await loadTeamsByWorkspace(context, {
|
|
2470
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2471
|
+
token: commandContext.token,
|
|
2472
|
+
workspaceId: workspace.id
|
|
2473
|
+
}), teamRef, "Team");
|
|
2474
|
+
}
|
|
2475
|
+
function validateTaskEditOptions(options) {
|
|
2476
|
+
if (options.clearNotes && typeof options.notes === "string") throw new Error("Use either --notes or --clear-notes, not both.");
|
|
2477
|
+
if (options.clearDeadline && typeof options.deadline === "string") throw new Error("Use either --deadline or --clear-deadline, not both.");
|
|
2478
|
+
if (options.clearProject && typeof options.project === "string") throw new Error("Use either --project or --clear-project, not both.");
|
|
2479
|
+
if (options.clearTeam && typeof options.team === "string") throw new Error("Use either --team or --clear-team, not both.");
|
|
2480
|
+
}
|
|
2481
|
+
function applyScalarEdits(updates, options) {
|
|
2482
|
+
if (typeof options.title === "string") {
|
|
2483
|
+
const title = options.title.trim();
|
|
2484
|
+
if (title.length === 0) throw new Error("Task title cannot be empty.");
|
|
2485
|
+
updates.title = title;
|
|
2486
|
+
}
|
|
2487
|
+
if (options.clearNotes) updates.description = null;
|
|
2488
|
+
else if (typeof options.notes === "string") {
|
|
2489
|
+
const notes = options.notes.trim();
|
|
2490
|
+
updates.description = notes.length > 0 ? notes : null;
|
|
2491
|
+
}
|
|
2492
|
+
if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
|
|
2493
|
+
if (options.clearDeadline) updates.deadlineAt = null;
|
|
2494
|
+
else if (typeof options.deadline === "string") updates.deadlineAt = parseDeadline(options.deadline);
|
|
2495
|
+
}
|
|
2496
|
+
async function runTaskEdit(context, taskRef, options, workspaceOverride) {
|
|
2497
|
+
validateTaskEditOptions(options);
|
|
2498
|
+
const updates = {};
|
|
2499
|
+
applyScalarEdits(updates, options);
|
|
2500
|
+
const wsRef = workspaceOverride ?? normalizeWorkspaceRef(options.workspace);
|
|
2501
|
+
const commandContext = await resolveCommandContext(context);
|
|
2502
|
+
if (options.clearProject) updates.projectId = null;
|
|
2503
|
+
else if (typeof options.project === "string") updates.projectId = await resolveProjectId(context, commandContext, options.project, wsRef);
|
|
2504
|
+
if (options.clearTeam) updates.teamId = null;
|
|
2505
|
+
else if (typeof options.team === "string") updates.teamId = await resolveTeamId(context, commandContext, options.team, wsRef);
|
|
2506
|
+
if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --notes, --clear-notes, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
|
|
2507
|
+
updates.updatedAt = Date.now();
|
|
2508
|
+
await runTaskMutation(context, taskRef, {
|
|
2509
|
+
workspaceRef: wsRef,
|
|
2510
|
+
action: "UPDATE",
|
|
2511
|
+
data: updates,
|
|
2512
|
+
successMessage: "Updated"
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
async function runTaskRead(context, taskRef, options, workspaceOverride) {
|
|
2516
|
+
const task = await resolveTaskReference(context, {
|
|
2517
|
+
taskRef,
|
|
2518
|
+
commandContext: await resolveCommandContext(context),
|
|
2519
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
2520
|
+
});
|
|
2521
|
+
if (context.json) {
|
|
2522
|
+
writeJson({
|
|
2523
|
+
id: task.id,
|
|
2524
|
+
description: task.description
|
|
2525
|
+
});
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
writeInfo(context, task.description ?? "(empty)");
|
|
2529
|
+
}
|
|
2530
|
+
async function runTaskAppend(context, taskRef, text, options, workspaceOverride) {
|
|
2531
|
+
const task = await resolveTaskReference(context, {
|
|
2532
|
+
taskRef,
|
|
2533
|
+
commandContext: await resolveCommandContext(context),
|
|
2534
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
2535
|
+
});
|
|
2536
|
+
const newDesc = `${task.description ?? ""}\n${text.trim()}`;
|
|
2537
|
+
await runTaskMutation(context, task.id, {
|
|
2538
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2539
|
+
action: "UPDATE",
|
|
2540
|
+
data: {
|
|
2541
|
+
description: newDesc,
|
|
2542
|
+
updatedAt: Date.now()
|
|
2543
|
+
},
|
|
2544
|
+
successMessage: "Appended"
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
async function runTaskPrepend(context, taskRef, text, options, workspaceOverride) {
|
|
2548
|
+
const task = await resolveTaskReference(context, {
|
|
2549
|
+
taskRef,
|
|
2550
|
+
commandContext: await resolveCommandContext(context),
|
|
2551
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
2552
|
+
});
|
|
2553
|
+
const existing = task.description ?? "";
|
|
2554
|
+
const newDesc = existing.length > 0 ? `${text.trim()}\n${existing}` : text.trim();
|
|
2555
|
+
await runTaskMutation(context, task.id, {
|
|
2556
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2557
|
+
action: "UPDATE",
|
|
2558
|
+
data: {
|
|
2559
|
+
description: newDesc,
|
|
2560
|
+
updatedAt: Date.now()
|
|
2561
|
+
},
|
|
2562
|
+
successMessage: "Prepended"
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
async function runTaskRandom(context, options, workspaceOverride) {
|
|
2566
|
+
const commandContext = await resolveCommandContext(context);
|
|
2567
|
+
const requestedState = options.state ? parseTaskState(options.state) : "open";
|
|
2568
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
2569
|
+
token: commandContext.token,
|
|
2570
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2571
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2572
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2573
|
+
});
|
|
2574
|
+
const filtered = filterTasks(await loadTasksByWorkspace(context, {
|
|
2575
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2576
|
+
token: commandContext.token,
|
|
2577
|
+
workspaceId: workspace.id
|
|
2578
|
+
}), {
|
|
2579
|
+
state: requestedState,
|
|
2580
|
+
search: null
|
|
2581
|
+
});
|
|
2582
|
+
if (filtered.length === 0) {
|
|
2583
|
+
if (context.json) {
|
|
2584
|
+
writeJson({ task: null });
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
writeInfo(context, "No tasks found.");
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const task = filtered[Math.floor(Math.random() * filtered.length)];
|
|
2591
|
+
if (!task) throw new Error("Failed to pick a random task.");
|
|
2592
|
+
if (context.json) {
|
|
2593
|
+
writeJson({
|
|
2594
|
+
task,
|
|
2595
|
+
state: getTaskState(task),
|
|
2596
|
+
createdAtIso: formatEpochDate(task.createdAt),
|
|
2597
|
+
updatedAtIso: formatEpochDate(task.updatedAt),
|
|
2598
|
+
completedAtIso: formatEpochDate(task.completedAt),
|
|
2599
|
+
archivedAtIso: formatEpochDate(task.archivedAt),
|
|
2600
|
+
deadlineAtIso: formatEpochDate(task.deadlineAt)
|
|
2601
|
+
});
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
2605
|
+
writeSuccess(context, task.title);
|
|
2606
|
+
writeInfo(context, `id: ${task.id}`);
|
|
2607
|
+
writeInfo(context, `state: ${getTaskState(task)}`);
|
|
2608
|
+
writeInfo(context, `workspaceId: ${task.workspaceId}`);
|
|
2609
|
+
writeInfo(context, `start: ${task.start}`);
|
|
2610
|
+
writeInfo(context, `startBucket: ${task.startBucket}`);
|
|
2611
|
+
writeInfo(context, `deadline: ${formatEpochDate(task.deadlineAt) ?? "(none)"}`);
|
|
2612
|
+
if (task.description) writeInfo(context, `notes: ${task.description}`);
|
|
2613
|
+
}
|
|
2614
|
+
async function resolveChecklistItemReference(context, commandContext, taskId, itemRef) {
|
|
2615
|
+
const trimmed = itemRef.trim();
|
|
2616
|
+
if (!trimmed) throw new Error("Checklist item id is required.");
|
|
2617
|
+
const items = await loadChecklistItemsByTask(context, {
|
|
2618
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2619
|
+
token: commandContext.token,
|
|
2620
|
+
taskId
|
|
2621
|
+
});
|
|
2622
|
+
const exact = items.find((item) => item.id === trimmed);
|
|
2623
|
+
if (exact) return exact;
|
|
2624
|
+
const matches = items.filter((item) => item.id.startsWith(trimmed));
|
|
2625
|
+
if (matches.length === 0) throw new Error(`Checklist item "${trimmed}" not found.`);
|
|
2626
|
+
if (matches.length === 1) {
|
|
2627
|
+
const [match] = matches;
|
|
2628
|
+
if (!match) throw new Error(`Checklist item "${trimmed}" not found.`);
|
|
2629
|
+
return match;
|
|
2630
|
+
}
|
|
2631
|
+
throw new Error(`Checklist item prefix "${trimmed}" is ambiguous (${matches.length} matches). Use the full id.`);
|
|
2632
|
+
}
|
|
2633
|
+
async function runChecklistList(context, taskId, workspaceOption) {
|
|
2634
|
+
const commandContext = await resolveCommandContext(context);
|
|
2635
|
+
const task = await resolveTaskReference(context, {
|
|
2636
|
+
taskRef: taskId,
|
|
2637
|
+
commandContext,
|
|
2638
|
+
workspaceRef: workspaceOption?.trim() ?? null
|
|
2639
|
+
});
|
|
2640
|
+
const sorted = [...await loadChecklistItemsByTask(context, {
|
|
2641
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2642
|
+
token: commandContext.token,
|
|
2643
|
+
taskId: task.id
|
|
2644
|
+
})].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
2645
|
+
if (context.json) {
|
|
2646
|
+
writeJson({
|
|
2647
|
+
taskId: task.id,
|
|
2648
|
+
items: sorted
|
|
2649
|
+
});
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
if (sorted.length === 0) {
|
|
2653
|
+
writeInfo(context, "No checklist items.");
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
writeInfo(context, `Checklist for: ${task.title}`);
|
|
2657
|
+
for (const item of sorted) writeInfo(context, `${item.completedAt !== null ? "[x]" : "[ ]"} ${item.id.slice(0, 8)} ${item.title}`);
|
|
2658
|
+
}
|
|
2659
|
+
function registerTaskCommands(program, workspaceOverride) {
|
|
2660
|
+
const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
|
|
2661
|
+
Examples:
|
|
2662
|
+
donebear task
|
|
2663
|
+
donebear task add "Write launch post" --when today
|
|
2664
|
+
donebear task list --state done --limit 50
|
|
2665
|
+
donebear task edit 8f2c1a --deadline 2026-03-05
|
|
2666
|
+
`).action(async (_options, command) => {
|
|
2667
|
+
const context = contextFromCommand(command);
|
|
2668
|
+
await runWithErrorHandling(context, async () => {
|
|
2669
|
+
await runTaskList(context, {
|
|
2670
|
+
state: "open",
|
|
2671
|
+
limit: DEFAULT_TASK_LIST_LIMIT
|
|
2672
|
+
}, workspaceOverride);
|
|
2673
|
+
});
|
|
2674
|
+
});
|
|
2675
|
+
task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or notes").action(async (options, command) => {
|
|
2676
|
+
const context = contextFromCommand(command);
|
|
2677
|
+
await runWithErrorHandling(context, async () => {
|
|
2678
|
+
await runTaskList(context, options, workspaceOverride);
|
|
2679
|
+
});
|
|
2680
|
+
});
|
|
2681
|
+
task.command("show").description("Show task details").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2682
|
+
const context = contextFromCommand(command);
|
|
2683
|
+
await runWithErrorHandling(context, async () => {
|
|
2684
|
+
await runTaskShow(context, taskId, options, workspaceOverride);
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
task.command("read").description("Print the description (notes) of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2688
|
+
const context = contextFromCommand(command);
|
|
2689
|
+
await runWithErrorHandling(context, async () => {
|
|
2690
|
+
await runTaskRead(context, taskId, options, workspaceOverride);
|
|
2691
|
+
});
|
|
2692
|
+
});
|
|
2693
|
+
task.command("append").description("Append text to a task's description").argument("<id>", "Task id or unique prefix").argument("<text>", "Text to append").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, text, options, command) => {
|
|
2694
|
+
const context = contextFromCommand(command);
|
|
2695
|
+
await runWithErrorHandling(context, async () => {
|
|
2696
|
+
await runTaskAppend(context, taskId, text, options, workspaceOverride);
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
task.command("prepend").description("Prepend text to a task's description").argument("<id>", "Task id or unique prefix").argument("<text>", "Text to prepend").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, text, options, command) => {
|
|
2700
|
+
const context = contextFromCommand(command);
|
|
2701
|
+
await runWithErrorHandling(context, async () => {
|
|
2702
|
+
await runTaskPrepend(context, taskId, text, options, workspaceOverride);
|
|
2703
|
+
});
|
|
2704
|
+
});
|
|
2705
|
+
task.command("random").description("Show a random task").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").action(async (options, command) => {
|
|
2706
|
+
const context = contextFromCommand(command);
|
|
2707
|
+
await runWithErrorHandling(context, async () => {
|
|
2708
|
+
await runTaskRandom(context, options, workspaceOverride);
|
|
2709
|
+
});
|
|
2710
|
+
});
|
|
2711
|
+
task.command("add").alias("create").description("Create a task").argument("<title>", "Task title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").action(async (title, options, command) => {
|
|
2712
|
+
const context = contextFromCommand(command);
|
|
2713
|
+
await runWithErrorHandling(context, async () => {
|
|
2714
|
+
await runTaskAdd(context, title, options, workspaceOverride);
|
|
2715
|
+
});
|
|
2716
|
+
});
|
|
2717
|
+
task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--notes <text>", "Replace task notes (empty string clears)").option("--clear-notes", "Clear task notes").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
|
|
2718
|
+
const context = contextFromCommand(command);
|
|
2719
|
+
await runWithErrorHandling(context, async () => {
|
|
2720
|
+
await runTaskEdit(context, taskId, options, workspaceOverride);
|
|
2721
|
+
});
|
|
2722
|
+
});
|
|
2723
|
+
task.command("done").alias("complete").description("Mark a task as completed").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2724
|
+
const context = contextFromCommand(command);
|
|
2725
|
+
await runWithErrorHandling(context, async () => {
|
|
2726
|
+
const now = Date.now();
|
|
2727
|
+
await runTaskMutation(context, taskId, {
|
|
2728
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2729
|
+
action: "UPDATE",
|
|
2730
|
+
data: {
|
|
2731
|
+
completedAt: now,
|
|
2732
|
+
updatedAt: now
|
|
2733
|
+
},
|
|
2734
|
+
successMessage: "Marked done"
|
|
2735
|
+
});
|
|
2736
|
+
});
|
|
2737
|
+
});
|
|
2738
|
+
task.command("reopen").alias("undo").description("Reopen a completed task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2739
|
+
const context = contextFromCommand(command);
|
|
2740
|
+
await runWithErrorHandling(context, async () => {
|
|
2741
|
+
await runTaskMutation(context, taskId, {
|
|
2742
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2743
|
+
action: "UPDATE",
|
|
2744
|
+
data: {
|
|
2745
|
+
completedAt: null,
|
|
2746
|
+
updatedAt: Date.now()
|
|
2747
|
+
},
|
|
2748
|
+
successMessage: "Reopened"
|
|
2749
|
+
});
|
|
2750
|
+
});
|
|
2751
|
+
});
|
|
2752
|
+
task.command("archive").description("Archive a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2753
|
+
const context = contextFromCommand(command);
|
|
2754
|
+
await runWithErrorHandling(context, async () => {
|
|
2755
|
+
await runTaskMutation(context, taskId, {
|
|
2756
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2757
|
+
action: "ARCHIVE",
|
|
2758
|
+
data: { archivedAt: Date.now() },
|
|
2759
|
+
successMessage: "Archived"
|
|
2760
|
+
});
|
|
2761
|
+
});
|
|
2762
|
+
});
|
|
2763
|
+
task.command("unarchive").alias("restore").description("Unarchive a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2764
|
+
const context = contextFromCommand(command);
|
|
2765
|
+
await runWithErrorHandling(context, async () => {
|
|
2766
|
+
await runTaskMutation(context, taskId, {
|
|
2767
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2768
|
+
action: "UNARCHIVE",
|
|
2769
|
+
data: {},
|
|
2770
|
+
successMessage: "Unarchived"
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
});
|
|
2774
|
+
const checklist = task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-id>", "Task id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
2775
|
+
const context = contextFromCommand(command);
|
|
2776
|
+
await runWithErrorHandling(context, async () => {
|
|
2777
|
+
await runChecklistList(context, taskId, options.workspace);
|
|
2778
|
+
});
|
|
2779
|
+
});
|
|
2780
|
+
checklist.command("add").description("Add a checklist item").argument("<task-id>", "Task id or prefix").argument("<title>", "Item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, title, options, command) => {
|
|
2781
|
+
const context = contextFromCommand(command);
|
|
2782
|
+
await runWithErrorHandling(context, async () => {
|
|
2783
|
+
const commandContext = await resolveCommandContext(context);
|
|
2784
|
+
const task = await resolveTaskReference(context, {
|
|
2785
|
+
taskRef: taskId,
|
|
2786
|
+
commandContext,
|
|
2787
|
+
workspaceRef: options.workspace?.trim() ?? null
|
|
2788
|
+
});
|
|
2789
|
+
const itemId = randomUUID();
|
|
2790
|
+
const now = Date.now();
|
|
2791
|
+
await mutateModel(context, {
|
|
2792
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2793
|
+
token: commandContext.token,
|
|
2794
|
+
request: {
|
|
2795
|
+
batchId: randomUUID(),
|
|
2796
|
+
transactions: [{
|
|
2797
|
+
clientTxId: randomUUID(),
|
|
2798
|
+
clientId: commandContext.localContext.clientId,
|
|
2799
|
+
modelName: "TaskChecklistItem",
|
|
2800
|
+
modelId: itemId,
|
|
2801
|
+
action: "INSERT",
|
|
2802
|
+
payload: {
|
|
2803
|
+
title: title.trim(),
|
|
2804
|
+
taskId: task.id,
|
|
2805
|
+
workspaceId: task.workspaceId,
|
|
2806
|
+
sortOrder: now,
|
|
2807
|
+
completedAt: null,
|
|
2808
|
+
createdAt: now,
|
|
2809
|
+
updatedAt: now
|
|
2810
|
+
}
|
|
2811
|
+
}]
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
if (context.json) {
|
|
2815
|
+
writeJson({
|
|
2816
|
+
created: true,
|
|
2817
|
+
id: itemId
|
|
2818
|
+
});
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
writeSuccess(context, `Added: ${title.trim()}`);
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
checklist.command("done").description("Mark a checklist item as complete").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
|
|
2825
|
+
const context = contextFromCommand(command);
|
|
2826
|
+
await runWithErrorHandling(context, async () => {
|
|
2827
|
+
const commandContext = await resolveCommandContext(context);
|
|
2828
|
+
const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
|
|
2829
|
+
taskRef: taskId,
|
|
2830
|
+
commandContext,
|
|
2831
|
+
workspaceRef: options.workspace?.trim() ?? null
|
|
2832
|
+
})).id, itemId);
|
|
2833
|
+
const now = Date.now();
|
|
2834
|
+
await mutateModel(context, {
|
|
2835
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2836
|
+
token: commandContext.token,
|
|
2837
|
+
request: {
|
|
2838
|
+
batchId: randomUUID(),
|
|
2839
|
+
transactions: [{
|
|
2840
|
+
clientTxId: randomUUID(),
|
|
2841
|
+
clientId: commandContext.localContext.clientId,
|
|
2842
|
+
modelName: "TaskChecklistItem",
|
|
2843
|
+
modelId: item.id,
|
|
2844
|
+
action: "UPDATE",
|
|
2845
|
+
payload: {
|
|
2846
|
+
completedAt: now,
|
|
2847
|
+
updatedAt: now
|
|
2848
|
+
}
|
|
2849
|
+
}]
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
if (context.json) {
|
|
2853
|
+
writeJson({
|
|
2854
|
+
ok: true,
|
|
2855
|
+
id: item.id
|
|
2856
|
+
});
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
writeSuccess(context, `Marked done: ${item.title}`);
|
|
2860
|
+
});
|
|
2861
|
+
});
|
|
2862
|
+
checklist.command("reopen").description("Reopen a completed checklist item").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
|
|
2863
|
+
const context = contextFromCommand(command);
|
|
2864
|
+
await runWithErrorHandling(context, async () => {
|
|
2865
|
+
const commandContext = await resolveCommandContext(context);
|
|
2866
|
+
const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
|
|
2867
|
+
taskRef: taskId,
|
|
2868
|
+
commandContext,
|
|
2869
|
+
workspaceRef: options.workspace?.trim() ?? null
|
|
2870
|
+
})).id, itemId);
|
|
2871
|
+
const now = Date.now();
|
|
2872
|
+
await mutateModel(context, {
|
|
2873
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2874
|
+
token: commandContext.token,
|
|
2875
|
+
request: {
|
|
2876
|
+
batchId: randomUUID(),
|
|
2877
|
+
transactions: [{
|
|
2878
|
+
clientTxId: randomUUID(),
|
|
2879
|
+
clientId: commandContext.localContext.clientId,
|
|
2880
|
+
modelName: "TaskChecklistItem",
|
|
2881
|
+
modelId: item.id,
|
|
2882
|
+
action: "UPDATE",
|
|
2883
|
+
payload: {
|
|
2884
|
+
completedAt: null,
|
|
2885
|
+
updatedAt: now
|
|
2886
|
+
}
|
|
2887
|
+
}]
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
if (context.json) {
|
|
2891
|
+
writeJson({
|
|
2892
|
+
ok: true,
|
|
2893
|
+
id: item.id
|
|
2894
|
+
});
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
writeSuccess(context, `Reopened: ${item.title}`);
|
|
2898
|
+
});
|
|
2899
|
+
});
|
|
2900
|
+
checklist.command("edit").description("Edit a checklist item title").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").requiredOption("--title <title>", "New item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
|
|
2901
|
+
const context = contextFromCommand(command);
|
|
2902
|
+
await runWithErrorHandling(context, async () => {
|
|
2903
|
+
const newTitle = options.title.trim();
|
|
2904
|
+
if (newTitle.length === 0) throw new Error("Checklist item title cannot be empty.");
|
|
2905
|
+
const commandContext = await resolveCommandContext(context);
|
|
2906
|
+
const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
|
|
2907
|
+
taskRef: taskId,
|
|
2908
|
+
commandContext,
|
|
2909
|
+
workspaceRef: options.workspace?.trim() ?? null
|
|
2910
|
+
})).id, itemId);
|
|
2911
|
+
const now = Date.now();
|
|
2912
|
+
await mutateModel(context, {
|
|
2913
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2914
|
+
token: commandContext.token,
|
|
2915
|
+
request: {
|
|
2916
|
+
batchId: randomUUID(),
|
|
2917
|
+
transactions: [{
|
|
2918
|
+
clientTxId: randomUUID(),
|
|
2919
|
+
clientId: commandContext.localContext.clientId,
|
|
2920
|
+
modelName: "TaskChecklistItem",
|
|
2921
|
+
modelId: item.id,
|
|
2922
|
+
action: "UPDATE",
|
|
2923
|
+
payload: {
|
|
2924
|
+
title: newTitle,
|
|
2925
|
+
updatedAt: now
|
|
2926
|
+
}
|
|
2927
|
+
}]
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
if (context.json) {
|
|
2931
|
+
writeJson({
|
|
2932
|
+
ok: true,
|
|
2933
|
+
id: item.id
|
|
2934
|
+
});
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
writeSuccess(context, `Updated: ${newTitle}`);
|
|
2938
|
+
});
|
|
2939
|
+
});
|
|
2940
|
+
checklist.command("remove").alias("rm").description("Remove a checklist item").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
|
|
2941
|
+
const context = contextFromCommand(command);
|
|
2942
|
+
await runWithErrorHandling(context, async () => {
|
|
2943
|
+
const commandContext = await resolveCommandContext(context);
|
|
2944
|
+
const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
|
|
2945
|
+
taskRef: taskId,
|
|
2946
|
+
commandContext,
|
|
2947
|
+
workspaceRef: options.workspace?.trim() ?? null
|
|
2948
|
+
})).id, itemId);
|
|
2949
|
+
await mutateModel(context, {
|
|
2950
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2951
|
+
token: commandContext.token,
|
|
2952
|
+
request: {
|
|
2953
|
+
batchId: randomUUID(),
|
|
2954
|
+
transactions: [{
|
|
2955
|
+
clientTxId: randomUUID(),
|
|
2956
|
+
clientId: commandContext.localContext.clientId,
|
|
2957
|
+
modelName: "TaskChecklistItem",
|
|
2958
|
+
modelId: item.id,
|
|
2959
|
+
action: "ARCHIVE",
|
|
2960
|
+
payload: {}
|
|
2961
|
+
}]
|
|
2962
|
+
}
|
|
2963
|
+
});
|
|
2964
|
+
if (context.json) {
|
|
2965
|
+
writeJson({
|
|
2966
|
+
removed: true,
|
|
2967
|
+
id: item.id
|
|
2968
|
+
});
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
writeSuccess(context, `Removed: ${item.title}`);
|
|
2972
|
+
});
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
//#endregion
|
|
2977
|
+
//#region src/commands/search.ts
|
|
2978
|
+
const DEFAULT_SEARCH_LIMIT = 20;
|
|
2979
|
+
async function runSearch(context, query, options, workspaceOverride) {
|
|
2980
|
+
const commandContext = await resolveCommandContext(context);
|
|
2981
|
+
const requestedState = parseTaskState(options.state);
|
|
2982
|
+
const trimmedQuery = query.trim();
|
|
2983
|
+
if (trimmedQuery.length === 0) throw new Error("Search query cannot be empty.");
|
|
2984
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
2985
|
+
token: commandContext.token,
|
|
2986
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
2987
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
2988
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
2989
|
+
});
|
|
2990
|
+
const filtered = filterTasks(await loadTasksByWorkspace(context, {
|
|
2991
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
2992
|
+
token: commandContext.token,
|
|
2993
|
+
workspaceId: workspace.id
|
|
2994
|
+
}), {
|
|
2995
|
+
state: requestedState,
|
|
2996
|
+
search: trimmedQuery
|
|
2997
|
+
});
|
|
2998
|
+
const limited = filtered.slice(0, options.limit);
|
|
2999
|
+
if (context.json) {
|
|
3000
|
+
writeJson({
|
|
3001
|
+
workspace,
|
|
3002
|
+
autoSelected,
|
|
3003
|
+
query: trimmedQuery,
|
|
3004
|
+
state: requestedState,
|
|
3005
|
+
total: filtered.length,
|
|
3006
|
+
count: limited.length,
|
|
3007
|
+
tasks: limited
|
|
3008
|
+
});
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
const headers = [
|
|
3012
|
+
"id",
|
|
3013
|
+
"title",
|
|
3014
|
+
"state",
|
|
3015
|
+
"deadline"
|
|
3016
|
+
];
|
|
3017
|
+
const rows = limited.map((task) => [
|
|
3018
|
+
task.id,
|
|
3019
|
+
task.title,
|
|
3020
|
+
getTaskState(task),
|
|
3021
|
+
formatShortDate(task.deadlineAt) ?? ""
|
|
3022
|
+
]);
|
|
3023
|
+
if (writeFormattedRows(context, filtered.length, headers, rows)) return;
|
|
3024
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
3025
|
+
if (limited.length === 0) {
|
|
3026
|
+
writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
const lines = [];
|
|
3030
|
+
lines.push(`${workspace.name} — "${trimmedQuery}" (${limited.length}/${filtered.length})`);
|
|
3031
|
+
for (const task of limited) {
|
|
3032
|
+
const marker = toTaskMarker(task);
|
|
3033
|
+
const deadline = formatShortDate(task.deadlineAt);
|
|
3034
|
+
const suffix = deadline ? ` due ${deadline}` : "";
|
|
3035
|
+
lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
|
|
3036
|
+
}
|
|
3037
|
+
for (const line of lines) writeInfo(context, line);
|
|
3038
|
+
if (context.copy) copyToClipboard(lines.join("\n"));
|
|
3039
|
+
}
|
|
3040
|
+
function registerSearchCommand(program, workspaceOverride) {
|
|
3041
|
+
program.command("search").description("Search tasks by title or notes").argument("<query>", "Search query").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
|
|
3042
|
+
Examples:
|
|
3043
|
+
donebear search "meeting"
|
|
3044
|
+
donebear search "launch" --state open
|
|
3045
|
+
donebear search "Q1" --format csv --copy
|
|
3046
|
+
donebear search "review" --total
|
|
3047
|
+
`).action(async (query, options, command) => {
|
|
3048
|
+
const context = contextFromCommand(command);
|
|
3049
|
+
await runWithErrorHandling(context, async () => {
|
|
3050
|
+
await runSearch(context, query, options, workspaceOverride);
|
|
3051
|
+
});
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
//#endregion
|
|
3056
|
+
//#region src/commands/team.ts
|
|
3057
|
+
function findTeam(ref, teams) {
|
|
3058
|
+
const normalized = ref.trim().toLowerCase();
|
|
3059
|
+
for (const team of teams) {
|
|
3060
|
+
if (team.id === ref) return team;
|
|
3061
|
+
if (team.key.toLowerCase() === normalized) return team;
|
|
3062
|
+
if (team.id.startsWith(ref)) return team;
|
|
3063
|
+
}
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
async function runTeamList(context, workspaceOverride, options) {
|
|
3067
|
+
const commandContext = await resolveCommandContext(context);
|
|
3068
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
3069
|
+
token: commandContext.token,
|
|
3070
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3071
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
3072
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3073
|
+
});
|
|
3074
|
+
const teams = await loadTeamsByWorkspace(context, {
|
|
3075
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3076
|
+
token: commandContext.token,
|
|
3077
|
+
workspaceId: workspace.id
|
|
3078
|
+
});
|
|
3079
|
+
if (context.json) {
|
|
3080
|
+
writeJson({
|
|
3081
|
+
workspace,
|
|
3082
|
+
teams
|
|
3083
|
+
});
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
const headers = [
|
|
3087
|
+
"id",
|
|
3088
|
+
"key",
|
|
3089
|
+
"name"
|
|
3090
|
+
];
|
|
3091
|
+
const rows = teams.map((t) => [
|
|
3092
|
+
t.id,
|
|
3093
|
+
t.key,
|
|
3094
|
+
t.name
|
|
3095
|
+
]);
|
|
3096
|
+
if (writeFormattedRows(context, teams.length, headers, rows)) return;
|
|
3097
|
+
if (teams.length === 0) {
|
|
3098
|
+
writeInfo(context, "No teams found.");
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
writeInfo(context, `Teams in ${workspace.name}:`);
|
|
3102
|
+
for (const team of teams) writeInfo(context, `${team.id.slice(0, 8)} [${team.key}] ${team.name}`);
|
|
3103
|
+
}
|
|
3104
|
+
async function runTeamShow(context, teamRef, workspaceOverride, options) {
|
|
3105
|
+
const commandContext = await resolveCommandContext(context);
|
|
3106
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
3107
|
+
token: commandContext.token,
|
|
3108
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3109
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
3110
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3111
|
+
});
|
|
3112
|
+
const teams = await loadTeamsByWorkspace(context, {
|
|
3113
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3114
|
+
token: commandContext.token,
|
|
3115
|
+
workspaceId: workspace.id
|
|
3116
|
+
});
|
|
3117
|
+
const team = findTeam(teamRef.trim(), teams);
|
|
3118
|
+
if (!team) throw new Error(`Team "${teamRef}" not found. Run \`donebear team list\`.`);
|
|
3119
|
+
if (context.json) {
|
|
3120
|
+
writeJson({
|
|
3121
|
+
workspace,
|
|
3122
|
+
team
|
|
3123
|
+
});
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
writeSuccess(context, team.name);
|
|
3127
|
+
writeInfo(context, `id: ${team.id}`);
|
|
3128
|
+
writeInfo(context, `key: ${team.key}`);
|
|
3129
|
+
writeInfo(context, `name: ${team.name}`);
|
|
3130
|
+
writeInfo(context, `description: ${team.description ?? "(none)"}`);
|
|
3131
|
+
writeInfo(context, `workspaceId: ${team.workspaceId ?? "(none)"}`);
|
|
3132
|
+
}
|
|
3133
|
+
function registerTeamCommands(program, workspaceOverride) {
|
|
3134
|
+
const team = program.command("team").alias("teams").description("Manage teams").addHelpText("after", `
|
|
3135
|
+
Examples:
|
|
3136
|
+
donebear team
|
|
3137
|
+
donebear team list
|
|
3138
|
+
donebear team show ENG
|
|
3139
|
+
donebear team list --format csv
|
|
3140
|
+
`).action(async (_options, command) => {
|
|
3141
|
+
const context = contextFromCommand(command);
|
|
3142
|
+
await runWithErrorHandling(context, async () => {
|
|
3143
|
+
await runTeamList(context, workspaceOverride, {});
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
team.command("list").alias("ls").description("List teams in the current or specified workspace").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (options, command) => {
|
|
3147
|
+
const context = contextFromCommand(command);
|
|
3148
|
+
await runWithErrorHandling(context, async () => {
|
|
3149
|
+
await runTeamList(context, workspaceOverride, options);
|
|
3150
|
+
});
|
|
3151
|
+
});
|
|
3152
|
+
team.command("show").description("Show team details").argument("<id-or-key>", "Team id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (teamRef, options, command) => {
|
|
3153
|
+
const context = contextFromCommand(command);
|
|
3154
|
+
await runWithErrorHandling(context, async () => {
|
|
3155
|
+
await runTeamShow(context, teamRef, workspaceOverride, options);
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
//#endregion
|
|
3161
|
+
//#region src/commands/today.ts
|
|
3162
|
+
const DEFAULT_TODAY_LIMIT = 20;
|
|
3163
|
+
async function runToday(context, options, workspaceOverride) {
|
|
3164
|
+
const commandContext = await resolveCommandContext(context);
|
|
3165
|
+
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
3166
|
+
token: commandContext.token,
|
|
3167
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3168
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
|
|
3169
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3170
|
+
});
|
|
3171
|
+
const todayTasks = (await loadTasksByWorkspace(context, {
|
|
3172
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3173
|
+
token: commandContext.token,
|
|
3174
|
+
workspaceId: workspace.id
|
|
3175
|
+
})).filter((task) => task.todayIndexReferenceDate !== null && task.completedAt === null && task.archivedAt === null);
|
|
3176
|
+
const limited = todayTasks.slice(0, options.limit);
|
|
3177
|
+
if (context.json) {
|
|
3178
|
+
writeJson({
|
|
3179
|
+
workspace,
|
|
3180
|
+
autoSelected,
|
|
3181
|
+
total: todayTasks.length,
|
|
3182
|
+
count: limited.length,
|
|
3183
|
+
tasks: limited
|
|
3184
|
+
});
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
const headers = [
|
|
3188
|
+
"id",
|
|
3189
|
+
"title",
|
|
3190
|
+
"state",
|
|
3191
|
+
"deadline"
|
|
3192
|
+
];
|
|
3193
|
+
const rows = limited.map((task) => [
|
|
3194
|
+
task.id,
|
|
3195
|
+
task.title,
|
|
3196
|
+
getTaskState(task),
|
|
3197
|
+
formatShortDate(task.deadlineAt) ?? ""
|
|
3198
|
+
]);
|
|
3199
|
+
if (writeFormattedRows(context, todayTasks.length, headers, rows)) return;
|
|
3200
|
+
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
3201
|
+
if (limited.length === 0) {
|
|
3202
|
+
writeInfo(context, "No tasks for today.");
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
const lines = [];
|
|
3206
|
+
lines.push(`${workspace.name} — Today (${limited.length}/${todayTasks.length})`);
|
|
3207
|
+
for (const task of limited) {
|
|
3208
|
+
const marker = toTaskMarker(task);
|
|
3209
|
+
const deadline = formatShortDate(task.deadlineAt);
|
|
3210
|
+
const suffix = deadline ? ` due ${deadline}` : "";
|
|
3211
|
+
lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
|
|
3212
|
+
}
|
|
3213
|
+
for (const line of lines) writeInfo(context, line);
|
|
3214
|
+
if (context.copy) copyToClipboard(lines.join("\n"));
|
|
3215
|
+
}
|
|
3216
|
+
function registerTodayCommand(program, workspaceOverride) {
|
|
3217
|
+
program.command("today").description("List today's tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TODAY_LIMIT).addHelpText("after", `
|
|
3218
|
+
Examples:
|
|
3219
|
+
donebear today
|
|
3220
|
+
donebear today --limit 10
|
|
3221
|
+
donebear today --format csv
|
|
3222
|
+
donebear today --total
|
|
3223
|
+
`).action(async (options, command) => {
|
|
3224
|
+
const context = contextFromCommand(command);
|
|
3225
|
+
await runWithErrorHandling(context, async () => {
|
|
3226
|
+
await runToday(context, options, workspaceOverride);
|
|
3227
|
+
});
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
//#endregion
|
|
3232
|
+
//#region src/commands/whoami.ts
|
|
3233
|
+
async function runWhoAmI(context) {
|
|
3234
|
+
const config = resolveSupabaseConfig();
|
|
3235
|
+
const resolvedToken = await resolveAuthToken(context, config);
|
|
3236
|
+
if (!resolvedToken) {
|
|
3237
|
+
process.exitCode = EXIT_CODES.AUTH_REQUIRED;
|
|
3238
|
+
if (context.json) {
|
|
3239
|
+
writeJson({
|
|
3240
|
+
ok: false,
|
|
3241
|
+
error: {
|
|
3242
|
+
message: "Not authenticated",
|
|
3243
|
+
exitCode: EXIT_CODES.AUTH_REQUIRED
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
writeInfo(context, "Not authenticated.");
|
|
3249
|
+
writeInfo(context, "Run `donebear auth login`.");
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
const user = await getSupabaseUser(config, resolvedToken.token);
|
|
3253
|
+
if (context.json) {
|
|
3254
|
+
writeJson({
|
|
3255
|
+
ok: true,
|
|
3256
|
+
user: {
|
|
3257
|
+
id: user.id,
|
|
3258
|
+
email: user.email ?? null
|
|
3259
|
+
},
|
|
3260
|
+
source: resolvedToken.source
|
|
3261
|
+
});
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
writeSuccess(context, "Authenticated user:");
|
|
3265
|
+
writeInfo(context, `id: ${user.id}`);
|
|
3266
|
+
writeInfo(context, `email: ${user.email ?? "(none)"}`);
|
|
3267
|
+
writeInfo(context, `source: ${resolvedToken.source}`);
|
|
3268
|
+
}
|
|
3269
|
+
function registerWhoAmICommand(program) {
|
|
3270
|
+
program.command("whoami").alias("me").description("Show the current authenticated user").action(async (_options, command) => {
|
|
3271
|
+
const context = contextFromCommand(command);
|
|
3272
|
+
await runWithErrorHandling(context, async () => {
|
|
3273
|
+
await runWhoAmI(context);
|
|
3274
|
+
});
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
//#endregion
|
|
3279
|
+
//#region src/commands/workspace.ts
|
|
3280
|
+
function findWorkspace(workspaceRef, workspaces) {
|
|
3281
|
+
const normalizedReference = workspaceRef.trim().toLowerCase();
|
|
3282
|
+
return workspaces.find((workspace) => {
|
|
3283
|
+
if (workspace.id === workspaceRef) return true;
|
|
3284
|
+
if (workspace.urlKey?.toLowerCase() === normalizedReference) return true;
|
|
3285
|
+
return workspace.name.toLowerCase() === normalizedReference;
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
function writeCurrentWorkspaceNotAvailable(context, message) {
|
|
3289
|
+
if (context.json) {
|
|
3290
|
+
writeJson({ workspace: null });
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
writeInfo(context, message);
|
|
3294
|
+
writeInfo(context, "Run `donebear workspace use <id-or-slug>`.");
|
|
3295
|
+
}
|
|
3296
|
+
async function runWorkspaceList(context) {
|
|
3297
|
+
const commandContext = await resolveCommandContext(context);
|
|
3298
|
+
const workspaces = await listWorkspaces(context, {
|
|
3299
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3300
|
+
token: commandContext.token
|
|
3301
|
+
});
|
|
3302
|
+
if (context.json) {
|
|
3303
|
+
writeJson({
|
|
3304
|
+
workspaces,
|
|
3305
|
+
currentWorkspaceId: commandContext.localContext.workspaceId
|
|
3306
|
+
});
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
if (workspaces.length === 0) {
|
|
3310
|
+
writeInfo(context, "No workspaces found.");
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
writeInfo(context, "Workspaces:");
|
|
3314
|
+
for (const workspace of workspaces) writeInfo(context, `${workspace.id === commandContext.localContext.workspaceId ? "*" : " "} ${workspace.id} ${workspace.name}${workspace.urlKey ? ` (${workspace.urlKey})` : ""} role=${workspace.role}`);
|
|
3315
|
+
}
|
|
3316
|
+
async function runWorkspaceCurrent(context) {
|
|
3317
|
+
const commandContext = await resolveCommandContext(context);
|
|
3318
|
+
if (!commandContext.localContext.workspaceId) {
|
|
3319
|
+
writeCurrentWorkspaceNotAvailable(context, "No default workspace selected.");
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
const workspace = (await listWorkspaces(context, {
|
|
3323
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3324
|
+
token: commandContext.token
|
|
3325
|
+
})).find((item) => item.id === commandContext.localContext.workspaceId);
|
|
3326
|
+
if (!workspace) {
|
|
3327
|
+
writeCurrentWorkspaceNotAvailable(context, "Saved workspace is no longer accessible.");
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
if (context.json) {
|
|
3331
|
+
writeJson({ workspace });
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
writeSuccess(context, `Current workspace: ${workspace.name}`);
|
|
3335
|
+
writeInfo(context, `id: ${workspace.id}`);
|
|
3336
|
+
writeInfo(context, `slug: ${workspace.urlKey ?? "(none)"}`);
|
|
3337
|
+
writeInfo(context, `role: ${workspace.role}`);
|
|
3338
|
+
}
|
|
3339
|
+
async function runWorkspaceUse(context, reference) {
|
|
3340
|
+
const commandContext = await resolveCommandContext(context);
|
|
3341
|
+
const selected = findWorkspace(reference, await listWorkspaces(context, {
|
|
3342
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3343
|
+
token: commandContext.token
|
|
3344
|
+
}));
|
|
3345
|
+
if (!selected) throw new Error(`Workspace "${reference}" not found. Run \`donebear workspace list\`.`);
|
|
3346
|
+
await setCurrentWorkspace(selected.id);
|
|
3347
|
+
if (context.json) {
|
|
3348
|
+
writeJson({
|
|
3349
|
+
workspace: selected,
|
|
3350
|
+
selected: true
|
|
3351
|
+
});
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
writeSuccess(context, `Selected workspace: ${selected.name}`);
|
|
3355
|
+
writeInfo(context, `id: ${selected.id}`);
|
|
3356
|
+
}
|
|
3357
|
+
async function runWorkspaceClear(context) {
|
|
3358
|
+
await setCurrentWorkspace(null);
|
|
3359
|
+
if (context.json) {
|
|
3360
|
+
writeJson({
|
|
3361
|
+
cleared: true,
|
|
3362
|
+
workspace: null
|
|
3363
|
+
});
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
writeSuccess(context, "Cleared current workspace.");
|
|
3367
|
+
}
|
|
3368
|
+
async function runWorkspaceCreate(context, name, options) {
|
|
3369
|
+
const commandContext = await resolveCommandContext(context);
|
|
3370
|
+
const slug = (options.slug ?? toWorkspaceUrlKey(name)).trim();
|
|
3371
|
+
if (!isValidWorkspaceUrlKey(slug)) throw new Error("Workspace slug must be lowercase alphanumeric with hyphens and at least 3 characters");
|
|
3372
|
+
const workspace = await createWorkspace(context, {
|
|
3373
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3374
|
+
token: commandContext.token,
|
|
3375
|
+
body: {
|
|
3376
|
+
name,
|
|
3377
|
+
urlKey: slug,
|
|
3378
|
+
logoUrl: options.logoUrl
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
if (options.use) await setCurrentWorkspace(workspace.id);
|
|
3382
|
+
if (context.json) {
|
|
3383
|
+
writeJson({
|
|
3384
|
+
workspace,
|
|
3385
|
+
selected: options.use
|
|
3386
|
+
});
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
writeSuccess(context, `Created workspace: ${workspace.name}`);
|
|
3390
|
+
writeInfo(context, `id: ${workspace.id}`);
|
|
3391
|
+
writeInfo(context, `slug: ${workspace.urlKey ?? slug}`);
|
|
3392
|
+
if (options.use) writeInfo(context, "Set as current workspace.");
|
|
3393
|
+
}
|
|
3394
|
+
async function runWorkspaceJoin(context, code, options) {
|
|
3395
|
+
const commandContext = await resolveCommandContext(context);
|
|
3396
|
+
const trimmedCode = code.trim();
|
|
3397
|
+
if (!trimmedCode) throw new Error("Invitation code is required.");
|
|
3398
|
+
const joined = await joinWorkspace(context, {
|
|
3399
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3400
|
+
token: commandContext.token,
|
|
3401
|
+
code: trimmedCode
|
|
3402
|
+
});
|
|
3403
|
+
if (options.use) await setCurrentWorkspace(joined.workspace.id);
|
|
3404
|
+
if (context.json) {
|
|
3405
|
+
writeJson({
|
|
3406
|
+
workspace: joined.workspace,
|
|
3407
|
+
role: joined.role,
|
|
3408
|
+
selected: options.use
|
|
3409
|
+
});
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
writeSuccess(context, `Joined workspace: ${joined.workspace.name}`);
|
|
3413
|
+
writeInfo(context, `id: ${joined.workspace.id}`);
|
|
3414
|
+
writeInfo(context, `role: ${joined.role}`);
|
|
3415
|
+
if (options.use) writeInfo(context, "Set as current workspace.");
|
|
3416
|
+
}
|
|
3417
|
+
async function runWorkspaceMembers(context, workspaceRefArg, workspaceOverride, options) {
|
|
3418
|
+
const commandContext = await resolveCommandContext(context);
|
|
3419
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
3420
|
+
token: commandContext.token,
|
|
3421
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3422
|
+
workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
|
|
3423
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3424
|
+
});
|
|
3425
|
+
const members = await listWorkspaceMembers(context, {
|
|
3426
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3427
|
+
token: commandContext.token,
|
|
3428
|
+
workspaceId: workspace.id
|
|
3429
|
+
});
|
|
3430
|
+
if (context.json) {
|
|
3431
|
+
writeJson({
|
|
3432
|
+
workspace,
|
|
3433
|
+
members
|
|
3434
|
+
});
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
if (context.total) {
|
|
3438
|
+
writeTotal(context, members.length);
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
const format = options.format?.trim().toLowerCase() ?? context.format;
|
|
3442
|
+
if (format === "csv") {
|
|
3443
|
+
writeCsv([
|
|
3444
|
+
"userId",
|
|
3445
|
+
"name",
|
|
3446
|
+
"email",
|
|
3447
|
+
"role"
|
|
3448
|
+
], members.map((m) => [
|
|
3449
|
+
m.userId,
|
|
3450
|
+
m.user.name ?? "",
|
|
3451
|
+
m.user.email ?? "",
|
|
3452
|
+
m.role
|
|
3453
|
+
]));
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
if (format === "tsv") {
|
|
3457
|
+
writeTsv([
|
|
3458
|
+
"userId",
|
|
3459
|
+
"name",
|
|
3460
|
+
"email",
|
|
3461
|
+
"role"
|
|
3462
|
+
], members.map((m) => [
|
|
3463
|
+
m.userId,
|
|
3464
|
+
m.user.name ?? "",
|
|
3465
|
+
m.user.email ?? "",
|
|
3466
|
+
m.role
|
|
3467
|
+
]));
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
if (members.length === 0) {
|
|
3471
|
+
writeInfo(context, "No members found.");
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
writeInfo(context, `Members of ${workspace.name}:`);
|
|
3475
|
+
for (const member of members) writeInfo(context, `${member.userId} ${member.user.name ?? "(no name)"} ${member.user.email ?? "(no email)"} ${member.role}`);
|
|
3476
|
+
}
|
|
3477
|
+
async function runWorkspaceInvitations(context, workspaceRefArg, workspaceOverride) {
|
|
3478
|
+
const commandContext = await resolveCommandContext(context);
|
|
3479
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
3480
|
+
token: commandContext.token,
|
|
3481
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3482
|
+
workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
|
|
3483
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3484
|
+
});
|
|
3485
|
+
const invitations = await listWorkspaceInvitations(context, {
|
|
3486
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3487
|
+
token: commandContext.token,
|
|
3488
|
+
workspaceId: workspace.id
|
|
3489
|
+
});
|
|
3490
|
+
if (context.json) {
|
|
3491
|
+
writeJson({
|
|
3492
|
+
workspace,
|
|
3493
|
+
invitations
|
|
3494
|
+
});
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (invitations.length === 0) {
|
|
3498
|
+
writeInfo(context, "No invitations found.");
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
writeInfo(context, `Invitations for ${workspace.name}:`);
|
|
3502
|
+
for (const invitation of invitations) {
|
|
3503
|
+
const used = invitation.usedAt ? "yes" : "no";
|
|
3504
|
+
writeInfo(context, `${invitation.id.slice(0, 8)} ${invitation.email ?? "(open)"} ${invitation.role} expires=${invitation.expiresAt} used=${used}`);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
async function runWorkspaceInvite(context, workspaceRefArg, workspaceOverride, options) {
|
|
3508
|
+
const commandContext = await resolveCommandContext(context);
|
|
3509
|
+
const { workspace } = await resolveWorkspace(context, {
|
|
3510
|
+
token: commandContext.token,
|
|
3511
|
+
apiBaseUrl: commandContext.apiBaseUrl,
|
|
3512
|
+
workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
|
|
3513
|
+
storedWorkspaceId: commandContext.localContext.workspaceId
|
|
3514
|
+
});
|
|
3515
|
+
const invitation = await createWorkspaceInvitation(context, {
|
|
3516
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
3517
|
+
token: commandContext.token,
|
|
3518
|
+
workspaceId: workspace.id,
|
|
3519
|
+
email: options.email,
|
|
3520
|
+
role: options.role ?? "member"
|
|
3521
|
+
});
|
|
3522
|
+
if (context.json) {
|
|
3523
|
+
writeJson({
|
|
3524
|
+
workspace,
|
|
3525
|
+
invitation
|
|
3526
|
+
});
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
writeSuccess(context, `Invitation created for ${workspace.name}`);
|
|
3530
|
+
writeInfo(context, `code: ${invitation.code}`);
|
|
3531
|
+
writeInfo(context, `role: ${invitation.role}`);
|
|
3532
|
+
writeInfo(context, `expires: ${invitation.expiresAt}`);
|
|
3533
|
+
if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
|
|
3534
|
+
const link = `${commandContext.apiBaseUrl}/invite/${invitation.code}`;
|
|
3535
|
+
writeInfo(context, `link: ${link}`);
|
|
3536
|
+
if (context.copy) {
|
|
3537
|
+
if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function registerWorkspaceCommands(program, workspaceOverride) {
|
|
3541
|
+
const workspace = program.command("workspace").alias("ws").description("Manage workspaces").addHelpText("after", `
|
|
3542
|
+
Examples:
|
|
3543
|
+
donebear workspace
|
|
3544
|
+
donebear workspace use personal
|
|
3545
|
+
donebear workspace create "Personal" --slug personal
|
|
3546
|
+
donebear workspace join ABCD2345
|
|
3547
|
+
donebear workspace members
|
|
3548
|
+
donebear workspace invitations
|
|
3549
|
+
donebear workspace invite --email user@example.com --role member
|
|
3550
|
+
`).action(async (_options, command) => {
|
|
3551
|
+
const context = contextFromCommand(command);
|
|
3552
|
+
await runWithErrorHandling(context, async () => {
|
|
3553
|
+
await runWorkspaceList(context);
|
|
3554
|
+
});
|
|
3555
|
+
});
|
|
3556
|
+
workspace.command("list").alias("ls").description("List workspaces available to the current user").action(async (_options, command) => {
|
|
3557
|
+
const context = contextFromCommand(command);
|
|
3558
|
+
await runWithErrorHandling(context, async () => {
|
|
3559
|
+
await runWorkspaceList(context);
|
|
3560
|
+
});
|
|
3561
|
+
});
|
|
3562
|
+
workspace.command("current").description("Show the current default workspace").action(async (_options, command) => {
|
|
3563
|
+
const context = contextFromCommand(command);
|
|
3564
|
+
await runWithErrorHandling(context, async () => {
|
|
3565
|
+
await runWorkspaceCurrent(context);
|
|
3566
|
+
});
|
|
3567
|
+
});
|
|
3568
|
+
workspace.command("use").description("Set the default workspace").argument("<workspace>", "Workspace id, slug, or exact name").action(async (workspaceRef, _options, command) => {
|
|
3569
|
+
const context = contextFromCommand(command);
|
|
3570
|
+
await runWithErrorHandling(context, async () => {
|
|
3571
|
+
await runWorkspaceUse(context, workspaceRef);
|
|
3572
|
+
});
|
|
3573
|
+
});
|
|
3574
|
+
workspace.command("clear").description("Clear the default workspace").action(async (_options, command) => {
|
|
3575
|
+
const context = contextFromCommand(command);
|
|
3576
|
+
await runWithErrorHandling(context, async () => {
|
|
3577
|
+
await runWorkspaceClear(context);
|
|
3578
|
+
});
|
|
3579
|
+
});
|
|
3580
|
+
workspace.command("create").description("Create a workspace").argument("<name>", "Workspace display name").option("--slug <slug>", "Workspace urlKey slug").option("--logo-url <url>", "Workspace logo URL").option("--no-use", "Do not set as current workspace").action(async (name, options, command) => {
|
|
3581
|
+
const context = contextFromCommand(command);
|
|
3582
|
+
await runWithErrorHandling(context, async () => {
|
|
3583
|
+
await runWorkspaceCreate(context, name, options);
|
|
3584
|
+
});
|
|
3585
|
+
});
|
|
3586
|
+
workspace.command("join").description("Join a workspace with an invitation code").argument("<code>", "Invitation code").option("--no-use", "Do not set joined workspace as current").action(async (code, options, command) => {
|
|
3587
|
+
const context = contextFromCommand(command);
|
|
3588
|
+
await runWithErrorHandling(context, async () => {
|
|
3589
|
+
await runWorkspaceJoin(context, code, options);
|
|
3590
|
+
});
|
|
3591
|
+
});
|
|
3592
|
+
workspace.command("members").description("List members of the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--format <format>", "Output format: text|json|csv|tsv").option("--total", "Print member count only").action(async (workspaceRef, options, command) => {
|
|
3593
|
+
const context = contextFromCommand(command);
|
|
3594
|
+
await runWithErrorHandling(context, async () => {
|
|
3595
|
+
await runWorkspaceMembers(context, workspaceRef, workspaceOverride, options);
|
|
3596
|
+
});
|
|
3597
|
+
});
|
|
3598
|
+
workspace.command("invitations").alias("invites").description("List pending invitations for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").action(async (workspaceRef, _options, command) => {
|
|
3599
|
+
const context = contextFromCommand(command);
|
|
3600
|
+
await runWithErrorHandling(context, async () => {
|
|
3601
|
+
await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
|
|
3602
|
+
});
|
|
3603
|
+
});
|
|
3604
|
+
workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
|
|
3605
|
+
const context = contextFromCommand(command);
|
|
3606
|
+
await runWithErrorHandling(context, async () => {
|
|
3607
|
+
await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
|
|
3608
|
+
});
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
//#endregion
|
|
3613
|
+
//#region src/cli.ts
|
|
3614
|
+
const packageManifest = createRequire(import.meta.url)("../package.json");
|
|
3615
|
+
const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/i;
|
|
3616
|
+
function extractWorkspaceTarget(argv) {
|
|
3617
|
+
const processedArgv = [...argv];
|
|
3618
|
+
const firstArg = processedArgv[2];
|
|
3619
|
+
if (firstArg) {
|
|
3620
|
+
const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
|
|
3621
|
+
if (match?.[1]) {
|
|
3622
|
+
processedArgv.splice(2, 1);
|
|
3623
|
+
return {
|
|
3624
|
+
processedArgv,
|
|
3625
|
+
workspaceOverride: match[1].trim()
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
return {
|
|
3630
|
+
processedArgv,
|
|
3631
|
+
workspaceOverride: null
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
async function main() {
|
|
3635
|
+
loadCliEnvironmentFiles();
|
|
3636
|
+
const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
|
|
3637
|
+
const program = new Command();
|
|
3638
|
+
program.name(CLI_NAME).description("Done Bear CLI for auth, workspace, and task workflows").version(packageManifest.version).showHelpAfterError().showSuggestionAfterError().option("--json", "Output machine-readable JSON").option("--format <format>", "Output format: text|json|csv|tsv", "text").option("--copy", "Copy output to clipboard").option("--total", "Print count only").option("--token <token>", "Use an explicit token (overrides DONEBEAR_TOKEN)").option("--api-url <url>", "Manage API base URL (overrides DONEBEAR_API_URL)").option("--debug", "Enable debug logs", process.env[DONEBEAR_DEBUG_ENV] === "1").option("--no-color", "Disable color output").addHelpText("after", `
|
|
3639
|
+
Quick start:
|
|
3640
|
+
donebear auth login
|
|
3641
|
+
donebear workspace
|
|
3642
|
+
donebear task add "Plan week" --when today
|
|
3643
|
+
donebear task
|
|
3644
|
+
donebear today
|
|
3645
|
+
donebear search "meeting"
|
|
3646
|
+
donebear project
|
|
3647
|
+
donebear label
|
|
3648
|
+
|
|
3649
|
+
Workspace targeting:
|
|
3650
|
+
donebear workspace=myorg task list
|
|
3651
|
+
|
|
3652
|
+
Use --json for scripting, --format csv|tsv for export, --total for counts.
|
|
3653
|
+
`);
|
|
3654
|
+
registerAuthCommands(program);
|
|
3655
|
+
registerWorkspaceCommands(program, workspaceOverride);
|
|
3656
|
+
registerTaskCommands(program, workspaceOverride);
|
|
3657
|
+
registerProjectCommands(program, workspaceOverride);
|
|
3658
|
+
registerLabelCommands(program, workspaceOverride);
|
|
3659
|
+
registerTeamCommands(program, workspaceOverride);
|
|
3660
|
+
registerTodayCommand(program, workspaceOverride);
|
|
3661
|
+
registerSearchCommand(program, workspaceOverride);
|
|
3662
|
+
registerHistoryCommand(program, workspaceOverride);
|
|
3663
|
+
registerWhoAmICommand(program);
|
|
3664
|
+
const hasUserArgs = processedArgv.length <= 2;
|
|
3665
|
+
const isTTY = process.stdin.isTTY === true;
|
|
3666
|
+
if (hasUserArgs && isTTY) {
|
|
3667
|
+
await runInteractiveMode(contextFromArgv(processedArgv), program);
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
await program.parseAsync(processedArgv);
|
|
3671
|
+
}
|
|
3672
|
+
await runWithErrorHandling(contextFromArgv(process.argv), main);
|
|
3673
|
+
|
|
3674
|
+
//#endregion
|
|
3675
|
+
export { };
|
|
3676
|
+
//# sourceMappingURL=cli.mjs.map
|