@atollhq/cli 0.1.5 → 0.1.6
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/README.md +22 -3
- package/dist/index.js +359 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ __export(config_exports, {
|
|
|
21
21
|
getProfile: () => getProfile,
|
|
22
22
|
readConfig: () => readConfig,
|
|
23
23
|
resolveConfig: () => resolveConfig,
|
|
24
|
+
resolveDefaultProject: () => resolveDefaultProject,
|
|
24
25
|
writeConfig: () => writeConfig
|
|
25
26
|
});
|
|
26
27
|
function readConfig() {
|
|
@@ -63,9 +64,13 @@ function resolveConfig(opts) {
|
|
|
63
64
|
apiKey: process.env.ATOLL_API_KEY || profile?.apiKey || (profileName ? void 0 : config.apiKey),
|
|
64
65
|
orgSlug: process.env.ATOLL_ORG || profile?.orgSlug || (profileName ? void 0 : config.orgSlug),
|
|
65
66
|
defaultTeam: process.env.ATOLL_TEAM || profile?.defaultTeam || (profileName ? void 0 : config.defaultTeam),
|
|
67
|
+
defaultProject: process.env.ATOLL_PROJECT || profile?.defaultProject || (profileName ? void 0 : config.defaultProject),
|
|
66
68
|
baseUrl: process.env.ATOLL_BASE_URL || profile?.baseUrl || (profileName ? void 0 : config.baseUrl)
|
|
67
69
|
};
|
|
68
70
|
}
|
|
71
|
+
function resolveDefaultProject(explicitProject) {
|
|
72
|
+
return explicitProject || resolveConfig().defaultProject;
|
|
73
|
+
}
|
|
69
74
|
function getApiKey(opts) {
|
|
70
75
|
return resolveConfig(opts).apiKey;
|
|
71
76
|
}
|
|
@@ -88,6 +93,8 @@ var import_node_path4 = require("path");
|
|
|
88
93
|
|
|
89
94
|
// src/commands/auth.ts
|
|
90
95
|
var import_commander = require("commander");
|
|
96
|
+
var import_node_process = require("process");
|
|
97
|
+
var import_promises = require("readline/promises");
|
|
91
98
|
init_config();
|
|
92
99
|
|
|
93
100
|
// src/lib/client.ts
|
|
@@ -191,24 +198,174 @@ var AtollClient = class {
|
|
|
191
198
|
};
|
|
192
199
|
|
|
193
200
|
// src/commands/auth.ts
|
|
201
|
+
var DEFAULT_BASE_URL2 = "https://atollhq.com";
|
|
202
|
+
function redactKey(key) {
|
|
203
|
+
if (key.length <= 8) return "********";
|
|
204
|
+
return `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
205
|
+
}
|
|
206
|
+
function profileSummaries(config = readConfig()) {
|
|
207
|
+
return Object.entries(config.profiles ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
208
|
+
name,
|
|
209
|
+
active: name === config.activeProfile,
|
|
210
|
+
apiKeySet: !!profile.apiKey,
|
|
211
|
+
orgSlug: profile.orgSlug ?? null,
|
|
212
|
+
defaultTeam: profile.defaultTeam ?? null,
|
|
213
|
+
defaultProject: profile.defaultProject ?? null,
|
|
214
|
+
baseUrl: profile.baseUrl ?? null
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
function formatProfileLine(profile) {
|
|
218
|
+
const marker = profile.active ? "*" : " ";
|
|
219
|
+
const parts = [
|
|
220
|
+
`${marker} ${profile.name}`,
|
|
221
|
+
profile.apiKeySet ? "key=set" : "key=not set",
|
|
222
|
+
profile.orgSlug ? `org=${profile.orgSlug}` : null,
|
|
223
|
+
profile.defaultTeam ? `team=${profile.defaultTeam}` : null,
|
|
224
|
+
profile.defaultProject ? `project=${profile.defaultProject}` : null,
|
|
225
|
+
profile.baseUrl ? `baseUrl=${profile.baseUrl}` : null
|
|
226
|
+
].filter(Boolean);
|
|
227
|
+
return parts.join(" ");
|
|
228
|
+
}
|
|
229
|
+
async function ask(rl, question, defaultValue, opts) {
|
|
230
|
+
const suffix = defaultValue ? ` (${opts?.defaultLabel ?? defaultValue})` : "";
|
|
231
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
232
|
+
return answer || defaultValue || "";
|
|
233
|
+
}
|
|
234
|
+
async function askRequired(rl, question, defaultValue, opts) {
|
|
235
|
+
while (true) {
|
|
236
|
+
const answer = await ask(rl, question, defaultValue, opts);
|
|
237
|
+
if (answer) return answer;
|
|
238
|
+
console.log("This value is required.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function selectOne(rl, label, items, format, opts) {
|
|
242
|
+
if (items.length === 0) return null;
|
|
243
|
+
if (items.length === 1 && !opts?.allowSkip) return items[0];
|
|
244
|
+
console.log(label);
|
|
245
|
+
items.forEach((item, index) => {
|
|
246
|
+
console.log(` ${index + 1}. ${format(item)}`);
|
|
247
|
+
});
|
|
248
|
+
if (opts?.allowSkip) console.log(" 0. Skip");
|
|
249
|
+
while (true) {
|
|
250
|
+
const defaultChoice = opts?.defaultIndex !== void 0 ? String(opts.defaultIndex + 1) : opts?.allowSkip ? "0" : "1";
|
|
251
|
+
const answer = await ask(rl, "Choose a number", defaultChoice);
|
|
252
|
+
const choice = Number.parseInt(answer, 10);
|
|
253
|
+
if (opts?.allowSkip && choice === 0) return null;
|
|
254
|
+
if (Number.isInteger(choice) && choice >= 1 && choice <= items.length) {
|
|
255
|
+
return items[choice - 1];
|
|
256
|
+
}
|
|
257
|
+
console.log("Enter a valid number from the list.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function deleteProfile(profileName) {
|
|
261
|
+
const config = readConfig();
|
|
262
|
+
if (!getProfile(config, profileName)) {
|
|
263
|
+
outputError(`Profile "${profileName}" not found.`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
delete config.profiles?.[profileName];
|
|
267
|
+
if (config.activeProfile === profileName) {
|
|
268
|
+
const nextProfile = Object.keys(config.profiles ?? {}).sort()[0];
|
|
269
|
+
if (nextProfile) config.activeProfile = nextProfile;
|
|
270
|
+
else delete config.activeProfile;
|
|
271
|
+
}
|
|
272
|
+
writeConfig(config);
|
|
273
|
+
return { activeProfile: config.activeProfile ?? null };
|
|
274
|
+
}
|
|
275
|
+
async function runProfileSetup(rl) {
|
|
276
|
+
const config = readConfig();
|
|
277
|
+
const profileName = await askRequired(rl, "Profile name", config.activeProfile || "default");
|
|
278
|
+
const existing = getProfile(config, profileName);
|
|
279
|
+
const apiKey = await askRequired(
|
|
280
|
+
rl,
|
|
281
|
+
existing?.apiKey ? `API key (${redactKey(existing.apiKey)})` : "API key",
|
|
282
|
+
existing?.apiKey,
|
|
283
|
+
existing?.apiKey ? { defaultLabel: "keep existing" } : void 0
|
|
284
|
+
);
|
|
285
|
+
const baseUrl = await ask(rl, "Base URL", existing?.baseUrl || DEFAULT_BASE_URL2);
|
|
286
|
+
console.log("Validating API key...");
|
|
287
|
+
const client = new AtollClient({ apiKey, baseUrl });
|
|
288
|
+
await client.get("/api/auth/me");
|
|
289
|
+
const { orgs } = await client.get("/api/orgs");
|
|
290
|
+
if (!orgs || orgs.length === 0) {
|
|
291
|
+
outputError("No organizations found for this API key.");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const defaultOrgIndex = existing?.orgSlug ? orgs.findIndex((org2) => org2.slug === existing.orgSlug) : -1;
|
|
295
|
+
const org = await selectOne(
|
|
296
|
+
rl,
|
|
297
|
+
"Organizations",
|
|
298
|
+
orgs,
|
|
299
|
+
(item) => `${item.name} (${item.slug})`,
|
|
300
|
+
{ defaultIndex: defaultOrgIndex >= 0 ? defaultOrgIndex : void 0 }
|
|
301
|
+
);
|
|
302
|
+
if (!org) {
|
|
303
|
+
outputError("No organization selected.");
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const { projects } = await client.get(`/api/orgs/${org.id}/projects`);
|
|
307
|
+
const defaultProjectIndex = existing?.defaultProject ? (projects ?? []).findIndex((project2) => project2.id === existing.defaultProject) : -1;
|
|
308
|
+
const project = await selectOne(
|
|
309
|
+
rl,
|
|
310
|
+
"Default project",
|
|
311
|
+
projects ?? [],
|
|
312
|
+
(item) => `${item.name} (${item.id})`,
|
|
313
|
+
{ allowSkip: true, defaultIndex: defaultProjectIndex >= 0 ? defaultProjectIndex : void 0 }
|
|
314
|
+
);
|
|
315
|
+
const defaultTeam = await ask(rl, "Default team slug or ID (optional)", existing?.defaultTeam);
|
|
316
|
+
const updatedConfig = readConfig();
|
|
317
|
+
const profile = ensureProfile(updatedConfig, profileName);
|
|
318
|
+
profile.apiKey = apiKey;
|
|
319
|
+
profile.orgSlug = org.slug;
|
|
320
|
+
if (baseUrl && baseUrl !== DEFAULT_BASE_URL2) profile.baseUrl = baseUrl;
|
|
321
|
+
else delete profile.baseUrl;
|
|
322
|
+
if (defaultTeam) profile.defaultTeam = defaultTeam;
|
|
323
|
+
else delete profile.defaultTeam;
|
|
324
|
+
if (project) profile.defaultProject = project.id;
|
|
325
|
+
else delete profile.defaultProject;
|
|
326
|
+
updatedConfig.activeProfile = profileName;
|
|
327
|
+
writeConfig(updatedConfig);
|
|
328
|
+
output(
|
|
329
|
+
{
|
|
330
|
+
status: "ok",
|
|
331
|
+
profile: profileName,
|
|
332
|
+
orgSlug: profile.orgSlug,
|
|
333
|
+
defaultTeam: profile.defaultTeam ?? null,
|
|
334
|
+
defaultProject: profile.defaultProject ?? null,
|
|
335
|
+
baseUrl: profile.baseUrl ?? null,
|
|
336
|
+
apiKey: redactKey(apiKey)
|
|
337
|
+
},
|
|
338
|
+
[
|
|
339
|
+
`\u2713 Profile "${profileName}" saved and set active`,
|
|
340
|
+
` Org: ${profile.orgSlug}`,
|
|
341
|
+
profile.defaultProject ? ` Project: ${profile.defaultProject}` : " Project: (not set)",
|
|
342
|
+
profile.defaultTeam ? ` Team: ${profile.defaultTeam}` : null,
|
|
343
|
+
profile.baseUrl ? ` Base URL: ${profile.baseUrl}` : null,
|
|
344
|
+
` API key: ${redactKey(apiKey)}`
|
|
345
|
+
].filter(Boolean).join("\n")
|
|
346
|
+
);
|
|
347
|
+
}
|
|
194
348
|
var authCommand = new import_commander.Command("auth").description("Manage authentication");
|
|
195
|
-
authCommand.command("login").description("Save an API key to ~/.atoll/config.json").requiredOption("--key <API_KEY>", "API key to store").option("--profile <name>", "Profile name to store this API key under").option("--org <slug>", "Default org slug for this profile").option("--team <team>", "Default team slug or ID for this profile").option("--base-url <url>", "Base URL for this profile").action((opts) => {
|
|
349
|
+
authCommand.command("login").description("Save an API key to ~/.atoll/config.json").requiredOption("--key <API_KEY>", "API key to store").option("--profile <name>", "Profile name to store this API key under").option("--org <slug>", "Default org slug for this profile").option("--team <team>", "Default team slug or ID for this profile").option("--project <id>", "Default project ID for this profile").option("--base-url <url>", "Base URL for this profile").action((opts) => {
|
|
196
350
|
const config = readConfig();
|
|
197
351
|
const profileName = opts.profile || process.env.ATOLL_PROFILE || config.activeProfile;
|
|
198
352
|
const orgSlug = opts.org ?? process.env.ATOLL_CLI_ORG;
|
|
199
353
|
const defaultTeam = opts.team ?? process.env.ATOLL_CLI_TEAM;
|
|
354
|
+
const defaultProject = opts.project;
|
|
200
355
|
const baseUrl = opts.baseUrl;
|
|
201
356
|
if (profileName) {
|
|
202
357
|
const profile = ensureProfile(config, profileName);
|
|
203
358
|
profile.apiKey = opts.key;
|
|
204
359
|
if (orgSlug !== void 0) profile.orgSlug = orgSlug;
|
|
205
360
|
if (defaultTeam !== void 0) profile.defaultTeam = defaultTeam;
|
|
361
|
+
if (defaultProject !== void 0) profile.defaultProject = defaultProject;
|
|
206
362
|
if (baseUrl !== void 0) profile.baseUrl = baseUrl;
|
|
207
363
|
config.activeProfile = profileName;
|
|
208
364
|
} else {
|
|
209
365
|
config.apiKey = opts.key;
|
|
210
366
|
if (orgSlug !== void 0) config.orgSlug = orgSlug;
|
|
211
367
|
if (defaultTeam !== void 0) config.defaultTeam = defaultTeam;
|
|
368
|
+
if (defaultProject !== void 0) config.defaultProject = defaultProject;
|
|
212
369
|
if (baseUrl !== void 0) config.baseUrl = baseUrl;
|
|
213
370
|
}
|
|
214
371
|
writeConfig(config);
|
|
@@ -217,6 +374,93 @@ authCommand.command("login").description("Save an API key to ~/.atoll/config.jso
|
|
|
217
374
|
profileName ? `\u2713 API key saved to profile "${profileName}" in ~/.atoll/config.json` : "\u2713 API key saved to ~/.atoll/config.json"
|
|
218
375
|
);
|
|
219
376
|
});
|
|
377
|
+
authCommand.command("setup").description("Interactively create or update an auth profile").action(async () => {
|
|
378
|
+
if (!import_node_process.stdin.isTTY || !import_node_process.stdout.isTTY) {
|
|
379
|
+
outputError("Interactive setup requires a TTY. Use `atoll auth login --profile <name> --key <API_KEY>` for non-interactive setup.");
|
|
380
|
+
process.exit(2);
|
|
381
|
+
}
|
|
382
|
+
const rl = (0, import_promises.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
383
|
+
try {
|
|
384
|
+
await runProfileSetup(rl);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
const msg = err.message || String(err);
|
|
387
|
+
if (/API (401|403):/.test(msg)) {
|
|
388
|
+
outputError("API key is invalid or expired");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
outputError(`Setup failed: ${msg}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
} finally {
|
|
394
|
+
rl.close();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
authCommand.command("manage").description("Interactively view, add, switch, or delete auth profiles").action(async () => {
|
|
398
|
+
if (!import_node_process.stdin.isTTY || !import_node_process.stdout.isTTY) {
|
|
399
|
+
outputError("Interactive profile management requires a TTY. Use `atoll auth profiles`, `atoll auth setup`, `atoll auth use`, or `atoll auth delete-profile` instead.");
|
|
400
|
+
process.exit(2);
|
|
401
|
+
}
|
|
402
|
+
const rl = (0, import_promises.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
403
|
+
try {
|
|
404
|
+
while (true) {
|
|
405
|
+
const config = readConfig();
|
|
406
|
+
const profiles = profileSummaries(config);
|
|
407
|
+
console.log("\nProfiles");
|
|
408
|
+
if (profiles.length === 0) {
|
|
409
|
+
console.log(" No profiles configured.");
|
|
410
|
+
} else {
|
|
411
|
+
for (const profile of profiles) console.log(` ${formatProfileLine(profile)}`);
|
|
412
|
+
}
|
|
413
|
+
const action = await selectOne(
|
|
414
|
+
rl,
|
|
415
|
+
"Actions",
|
|
416
|
+
["Add or update profile", "Switch active profile", "Delete profile", "Quit"],
|
|
417
|
+
(item) => item
|
|
418
|
+
);
|
|
419
|
+
if (action === "Add or update profile") {
|
|
420
|
+
await runProfileSetup(rl);
|
|
421
|
+
} else if (action === "Switch active profile") {
|
|
422
|
+
if (profiles.length === 0) {
|
|
423
|
+
console.log("No profiles to switch.");
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const selected = await selectOne(rl, "Profiles", profiles, (profile) => formatProfileLine(profile));
|
|
427
|
+
if (selected) {
|
|
428
|
+
const nextConfig = readConfig();
|
|
429
|
+
nextConfig.activeProfile = selected.name;
|
|
430
|
+
writeConfig(nextConfig);
|
|
431
|
+
console.log(`\u2713 Active profile set to "${selected.name}"`);
|
|
432
|
+
}
|
|
433
|
+
} else if (action === "Delete profile") {
|
|
434
|
+
if (profiles.length === 0) {
|
|
435
|
+
console.log("No profiles to delete.");
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const selected = await selectOne(rl, "Profiles", profiles, (profile) => formatProfileLine(profile));
|
|
439
|
+
if (!selected) continue;
|
|
440
|
+
const confirm = await ask(rl, `Delete profile "${selected.name}"? Type the profile name to confirm`);
|
|
441
|
+
if (confirm !== selected.name) {
|
|
442
|
+
console.log("Delete cancelled.");
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const result = deleteProfile(selected.name);
|
|
446
|
+
console.log(`\u2713 Deleted profile "${selected.name}"`);
|
|
447
|
+
if (result.activeProfile) console.log(` Active profile is now "${result.activeProfile}"`);
|
|
448
|
+
} else {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
const msg = err.message || String(err);
|
|
454
|
+
if (/API (401|403):/.test(msg)) {
|
|
455
|
+
outputError("API key is invalid or expired");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
outputError(`Profile management failed: ${msg}`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
} finally {
|
|
461
|
+
rl.close();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
220
464
|
authCommand.command("status").description("Show current auth status and user info").option("--profile <name>", "Profile name to check").action(async (opts) => {
|
|
221
465
|
const resolved = resolveConfig({ profile: opts.profile });
|
|
222
466
|
const apiKey = resolved.apiKey;
|
|
@@ -251,14 +495,7 @@ authCommand.command("status").description("Show current auth status and user inf
|
|
|
251
495
|
authCommand.command("profiles").description("List saved auth profiles").action(() => {
|
|
252
496
|
const config = readConfig();
|
|
253
497
|
const activeProfile = config.activeProfile;
|
|
254
|
-
const profiles =
|
|
255
|
-
name,
|
|
256
|
-
active: name === activeProfile,
|
|
257
|
-
apiKeySet: !!profile.apiKey,
|
|
258
|
-
orgSlug: profile.orgSlug ?? null,
|
|
259
|
-
defaultTeam: profile.defaultTeam ?? null,
|
|
260
|
-
baseUrl: profile.baseUrl ?? null
|
|
261
|
-
}));
|
|
498
|
+
const profiles = profileSummaries(config);
|
|
262
499
|
if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
|
|
263
500
|
output({ activeProfile: activeProfile ?? null, profiles }, "");
|
|
264
501
|
return;
|
|
@@ -268,16 +505,19 @@ authCommand.command("profiles").description("List saved auth profiles").action((
|
|
|
268
505
|
return;
|
|
269
506
|
}
|
|
270
507
|
for (const profile of profiles) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
].filter(Boolean);
|
|
279
|
-
console.log(parts.join(" "));
|
|
508
|
+
console.log(formatProfileLine(profile));
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
authCommand.command("delete-profile <profile>").description("Delete a saved auth profile").option("--force", "Delete without confirmation").action((profileName, opts) => {
|
|
512
|
+
if (!opts.force) {
|
|
513
|
+
outputError("Profile deletion requires --force. Use `atoll auth manage` for interactive deletion.");
|
|
514
|
+
process.exit(2);
|
|
280
515
|
}
|
|
516
|
+
const result = deleteProfile(profileName);
|
|
517
|
+
output(
|
|
518
|
+
{ deleted: profileName, activeProfile: result.activeProfile },
|
|
519
|
+
result.activeProfile ? `\u2713 Deleted profile "${profileName}". Active profile is now "${result.activeProfile}".` : `\u2713 Deleted profile "${profileName}". No active profile is set.`
|
|
520
|
+
);
|
|
281
521
|
});
|
|
282
522
|
authCommand.command("use <profile>").description("Set the active auth profile").action((profileName) => {
|
|
283
523
|
const config = readConfig();
|
|
@@ -314,6 +554,7 @@ authCommand.command("logout").description("Remove stored API key").option("--pro
|
|
|
314
554
|
|
|
315
555
|
// src/commands/issue.ts
|
|
316
556
|
var import_commander2 = require("commander");
|
|
557
|
+
init_config();
|
|
317
558
|
|
|
318
559
|
// src/lib/colors.ts
|
|
319
560
|
function isTTY() {
|
|
@@ -487,12 +728,12 @@ function padEnd(str, len) {
|
|
|
487
728
|
var issueCommand = new import_commander2.Command("issue").description("Manage issues").addHelpText("after", `
|
|
488
729
|
Examples:
|
|
489
730
|
$ atoll issue list
|
|
490
|
-
$ atoll issue list --status in_progress --priority 1
|
|
731
|
+
$ atoll issue list --project <project-id> --status in_progress --priority 1
|
|
491
732
|
$ atoll issue view ATOLL-42
|
|
492
|
-
$ atoll issue create --title "Fix login" --priority 1 --status todo
|
|
733
|
+
$ atoll issue create --title "Fix login" --project <project-id> --priority 1 --status todo
|
|
493
734
|
$ atoll issue update ATOLL-42 --status done
|
|
494
735
|
$ atoll issue assign ATOLL-42 --to <user-id>`);
|
|
495
|
-
issueCommand.command("list").description("List issues").option("--status <status>", `Filter by status (${VALID_STATUSES.join(", ")})`).option("--assignee <user>", "Filter by assignee ID").option("--priority <n>", "Filter by priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--limit <n>", "Max results (1-100)", parseInt).option("--offset <n>", "Results offset for pagination", parseInt).action(async (opts) => {
|
|
736
|
+
issueCommand.command("list").description("List issues").option("--status <status>", `Filter by status (${VALID_STATUSES.join(", ")})`).option("--assignee <user>", "Filter by assignee ID").option("--priority <n>", "Filter by priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--project <id>", "Filter by project ID").option("--limit <n>", "Max results (1-100)", parseInt).option("--offset <n>", "Results offset for pagination", parseInt).action(async (opts) => {
|
|
496
737
|
try {
|
|
497
738
|
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
498
739
|
outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
@@ -506,10 +747,12 @@ issueCommand.command("list").description("List issues").option("--status <status
|
|
|
506
747
|
const org = await resolveOrg(client);
|
|
507
748
|
const limit = normalizeLimit(opts.limit);
|
|
508
749
|
const offset = normalizeOffset(opts.offset);
|
|
750
|
+
const projectId = resolveDefaultProject(opts.project);
|
|
509
751
|
const params = new URLSearchParams();
|
|
510
752
|
if (opts.status) params.set("status", opts.status);
|
|
511
753
|
if (opts.assignee) params.set("assigneeId", opts.assignee);
|
|
512
754
|
if (opts.priority !== void 0) params.set("priority", String(opts.priority));
|
|
755
|
+
if (projectId) params.set("projectId", projectId);
|
|
513
756
|
params.set("limit", String(limit));
|
|
514
757
|
if (offset > 0) params.set("offset", String(offset));
|
|
515
758
|
const qs = params.toString();
|
|
@@ -596,7 +839,7 @@ Subtasks: ${enriched.sub_tasks.length}`);
|
|
|
596
839
|
}
|
|
597
840
|
issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
|
|
598
841
|
issueCommand.command("get <identifier>").description("Get issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
|
|
599
|
-
issueCommand.command("create").description("Create a new issue").requiredOption("--title <title>", "Issue title").option("--description <text>", "Issue description").option("--status <status>", `Status (${VALID_STATUSES.join(", ")})`).option("--priority <n>", "Priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).action(async (opts) => {
|
|
842
|
+
issueCommand.command("create").description("Create a new issue").requiredOption("--title <title>", "Issue title").option("--description <text>", "Issue description").option("--status <status>", `Status (${VALID_STATUSES.join(", ")})`).option("--priority <n>", "Priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--project <id>", "Project ID").action(async (opts) => {
|
|
600
843
|
try {
|
|
601
844
|
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
602
845
|
outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
@@ -609,9 +852,11 @@ issueCommand.command("create").description("Create a new issue").requiredOption(
|
|
|
609
852
|
const client = new AtollClient();
|
|
610
853
|
const org = await resolveOrg(client);
|
|
611
854
|
const body = { title: opts.title };
|
|
855
|
+
const projectId = resolveDefaultProject(opts.project);
|
|
612
856
|
if (opts.description !== void 0) body.description = opts.description;
|
|
613
857
|
if (opts.status) body.status = opts.status;
|
|
614
858
|
if (opts.priority !== void 0) body.priority = opts.priority;
|
|
859
|
+
if (projectId) body.projectId = projectId;
|
|
615
860
|
const { issue } = await client.post(`/api/orgs/${org.id}/issues`, body);
|
|
616
861
|
const projects = await fetchProjectMap(client, org.id);
|
|
617
862
|
const enriched = attachIssueUrl(issue, client.baseUrl, org.slug, projects);
|
|
@@ -1013,22 +1258,32 @@ projectCommand.command("get <projectId>").description("Get project details and i
|
|
|
1013
1258
|
|
|
1014
1259
|
// src/commands/milestone.ts
|
|
1015
1260
|
var import_commander5 = require("commander");
|
|
1261
|
+
init_config();
|
|
1016
1262
|
function progressBar2(progress, width = 20) {
|
|
1017
1263
|
const filled = Math.round(progress / 100 * width);
|
|
1018
1264
|
const empty = width - filled;
|
|
1019
1265
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${progress}%`;
|
|
1020
1266
|
}
|
|
1267
|
+
function resolveProjectOrExit(explicitProject) {
|
|
1268
|
+
const projectId = resolveDefaultProject(explicitProject);
|
|
1269
|
+
if (!projectId) {
|
|
1270
|
+
outputError("No project selected. Pass --project <id> or run `atoll config set-project <project-id>`.");
|
|
1271
|
+
process.exit(2);
|
|
1272
|
+
}
|
|
1273
|
+
return projectId;
|
|
1274
|
+
}
|
|
1021
1275
|
var milestoneCommand = new import_commander5.Command("milestone").description("Manage milestones").addHelpText("after", `
|
|
1022
1276
|
Examples:
|
|
1023
1277
|
$ atoll milestone list --project <project-id>
|
|
1024
1278
|
$ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
|
|
1025
|
-
milestoneCommand.command("list").description("List milestones for a project").
|
|
1279
|
+
milestoneCommand.command("list").description("List milestones for a project").option("--project <id>", "Project ID").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
|
|
1280
|
+
const projectId = resolveProjectOrExit(opts.project);
|
|
1026
1281
|
const client = new AtollClient();
|
|
1027
1282
|
try {
|
|
1028
1283
|
const limit = normalizeLimit(opts.limit);
|
|
1029
1284
|
const orgId = await resolveOrgId(client);
|
|
1030
1285
|
const data = await client.get(
|
|
1031
|
-
`/api/orgs/${orgId}/projects/${
|
|
1286
|
+
`/api/orgs/${orgId}/projects/${projectId}/milestones`
|
|
1032
1287
|
);
|
|
1033
1288
|
const allMilestones = data.milestones ?? [];
|
|
1034
1289
|
const milestones = allMilestones.slice(0, limit);
|
|
@@ -1066,12 +1321,13 @@ milestoneCommand.command("list").description("List milestones for a project").re
|
|
|
1066
1321
|
handleApiError(err);
|
|
1067
1322
|
}
|
|
1068
1323
|
});
|
|
1069
|
-
milestoneCommand.command("create").description("Create a new milestone").
|
|
1324
|
+
milestoneCommand.command("create").description("Create a new milestone").option("--project <id>", "Project ID").requiredOption("--name <name>", "Milestone name").option("--date <YYYY-MM-DD>", "Due date").option("--description <desc>", "Description").action(async (opts) => {
|
|
1325
|
+
const projectId = resolveProjectOrExit(opts.project);
|
|
1070
1326
|
const client = new AtollClient();
|
|
1071
1327
|
try {
|
|
1072
1328
|
const orgId = await resolveOrgId(client);
|
|
1073
1329
|
const data = await client.post(
|
|
1074
|
-
`/api/orgs/${orgId}/projects/${
|
|
1330
|
+
`/api/orgs/${orgId}/projects/${projectId}/milestones`,
|
|
1075
1331
|
{
|
|
1076
1332
|
name: opts.name,
|
|
1077
1333
|
description: opts.description ?? null,
|
|
@@ -1105,11 +1361,12 @@ function sourceForField(cfg, profileName, field, envVar) {
|
|
|
1105
1361
|
function withSource(value, source) {
|
|
1106
1362
|
return source ? `${bold(value)} ${dim(`from ${source}`)}` : bold(value);
|
|
1107
1363
|
}
|
|
1108
|
-
var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, API key)").addHelpText("after", `
|
|
1364
|
+
var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, project, API key)").addHelpText("after", `
|
|
1109
1365
|
Examples:
|
|
1110
1366
|
$ atoll config show
|
|
1111
1367
|
$ atoll config set-org my-org
|
|
1112
1368
|
$ atoll config set-team team-abc123
|
|
1369
|
+
$ atoll config set-project project-abc123
|
|
1113
1370
|
$ atoll config set-base-url https://atollhq.com`);
|
|
1114
1371
|
configCommand.command("show").description("Display current configuration").option("--profile <name>", "Profile name to show").action((opts) => {
|
|
1115
1372
|
const cfg = readConfig();
|
|
@@ -1120,6 +1377,7 @@ configCommand.command("show").description("Display current configuration").optio
|
|
|
1120
1377
|
apiKey: sourceForField(cfg, resolved.profile, "apiKey", "ATOLL_API_KEY"),
|
|
1121
1378
|
orgSlug: sourceForField(cfg, resolved.profile, "orgSlug", "ATOLL_ORG"),
|
|
1122
1379
|
defaultTeam: sourceForField(cfg, resolved.profile, "defaultTeam", "ATOLL_TEAM"),
|
|
1380
|
+
defaultProject: sourceForField(cfg, resolved.profile, "defaultProject", "ATOLL_PROJECT"),
|
|
1123
1381
|
baseUrl: sourceForField(cfg, resolved.profile, "baseUrl", "ATOLL_BASE_URL")
|
|
1124
1382
|
};
|
|
1125
1383
|
if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
|
|
@@ -1129,6 +1387,7 @@ configCommand.command("show").description("Display current configuration").optio
|
|
|
1129
1387
|
selectedProfile: resolved.profile ?? null,
|
|
1130
1388
|
orgSlug: resolved.orgSlug ?? null,
|
|
1131
1389
|
defaultTeam: resolved.defaultTeam ?? null,
|
|
1390
|
+
defaultProject: resolved.defaultProject ?? null,
|
|
1132
1391
|
baseUrl: resolved.baseUrl ?? null,
|
|
1133
1392
|
apiKeySet,
|
|
1134
1393
|
sources,
|
|
@@ -1143,6 +1402,7 @@ configCommand.command("show").description("Display current configuration").optio
|
|
|
1143
1402
|
console.log(` ${dim("Selected profile:")} ${resolved.profile ? bold(resolved.profile) : gray("(legacy config)")}`);
|
|
1144
1403
|
console.log(` ${dim("Org slug:")} ${resolved.orgSlug ? withSource(resolved.orgSlug, sources.orgSlug) : gray("(not set)")}`);
|
|
1145
1404
|
console.log(` ${dim("Default team:")} ${resolved.defaultTeam ? withSource(resolved.defaultTeam, sources.defaultTeam) : gray("(not set)")}`);
|
|
1405
|
+
console.log(` ${dim("Default proj:")} ${resolved.defaultProject ? withSource(resolved.defaultProject, sources.defaultProject) : gray("(not set)")}`);
|
|
1146
1406
|
console.log(` ${dim("Base URL:")} ${resolved.baseUrl ? withSource(resolved.baseUrl, sources.baseUrl) : gray("(default)")}`);
|
|
1147
1407
|
console.log(` ${dim("API key:")} ${apiKeySet ? withSource("set", sources.apiKey) : gray("not set \u2014 run `atoll auth login`")}`);
|
|
1148
1408
|
});
|
|
@@ -1174,6 +1434,34 @@ configCommand.command("set-team <team>").description("Set the default team slug
|
|
|
1174
1434
|
profileName ? success(`Default team for profile "${profileName}" set to "${team}"`) : success(`Default team set to "${team}"`)
|
|
1175
1435
|
);
|
|
1176
1436
|
});
|
|
1437
|
+
configCommand.command("set-project <project>").description("Set the default project ID").option("--profile <name>", "Profile name to update").action((project, opts) => {
|
|
1438
|
+
const cfg = readConfig();
|
|
1439
|
+
const profileName = opts.profile || process.env.ATOLL_PROFILE || cfg.activeProfile;
|
|
1440
|
+
if (profileName) {
|
|
1441
|
+
ensureProfile(cfg, profileName).defaultProject = project;
|
|
1442
|
+
} else {
|
|
1443
|
+
cfg.defaultProject = project;
|
|
1444
|
+
}
|
|
1445
|
+
writeConfig(cfg);
|
|
1446
|
+
output(
|
|
1447
|
+
{ defaultProject: project, profile: profileName ?? null },
|
|
1448
|
+
profileName ? success(`Default project for profile "${profileName}" set to "${project}"`) : success(`Default project set to "${project}"`)
|
|
1449
|
+
);
|
|
1450
|
+
});
|
|
1451
|
+
configCommand.command("clear-project").description("Clear the default project ID").option("--profile <name>", "Profile name to update").action((opts) => {
|
|
1452
|
+
const cfg = readConfig();
|
|
1453
|
+
const profileName = opts.profile || process.env.ATOLL_PROFILE || cfg.activeProfile;
|
|
1454
|
+
if (profileName) {
|
|
1455
|
+
delete ensureProfile(cfg, profileName).defaultProject;
|
|
1456
|
+
} else {
|
|
1457
|
+
delete cfg.defaultProject;
|
|
1458
|
+
}
|
|
1459
|
+
writeConfig(cfg);
|
|
1460
|
+
output(
|
|
1461
|
+
{ defaultProject: null, profile: profileName ?? null },
|
|
1462
|
+
profileName ? success(`Default project for profile "${profileName}" cleared`) : success("Default project cleared")
|
|
1463
|
+
);
|
|
1464
|
+
});
|
|
1177
1465
|
configCommand.command("set-base-url <url>").description("Set the default API base URL").option("--profile <name>", "Profile name to update").action((url, opts) => {
|
|
1178
1466
|
const cfg = readConfig();
|
|
1179
1467
|
const profileName = opts.profile || process.env.ATOLL_PROFILE || cfg.activeProfile;
|
|
@@ -1444,6 +1732,7 @@ var agentContextCommand = new import_commander9.Command("agent-context").descrip
|
|
|
1444
1732
|
apiKeySet: Boolean(profile.apiKey),
|
|
1445
1733
|
orgSlug: profile.orgSlug ?? null,
|
|
1446
1734
|
defaultTeam: profile.defaultTeam ?? null,
|
|
1735
|
+
defaultProject: profile.defaultProject ?? null,
|
|
1447
1736
|
baseUrl: profile.baseUrl ?? null
|
|
1448
1737
|
}));
|
|
1449
1738
|
outputJson({
|
|
@@ -1462,6 +1751,7 @@ var agentContextCommand = new import_commander9.Command("agent-context").descrip
|
|
|
1462
1751
|
selectedProfile: resolved.profile ?? null,
|
|
1463
1752
|
orgSlug: resolved.orgSlug ?? null,
|
|
1464
1753
|
defaultTeam: resolved.defaultTeam ?? null,
|
|
1754
|
+
defaultProject: resolved.defaultProject ?? null,
|
|
1465
1755
|
baseUrl: resolved.baseUrl ?? null,
|
|
1466
1756
|
apiKeySet: Boolean(resolved.apiKey)
|
|
1467
1757
|
},
|
|
@@ -1485,6 +1775,15 @@ function buildCommandContext() {
|
|
|
1485
1775
|
},
|
|
1486
1776
|
signal_types: SIGNAL_TYPES
|
|
1487
1777
|
},
|
|
1778
|
+
auth: {
|
|
1779
|
+
subcommands: {
|
|
1780
|
+
setup: { description: "Interactive profile setup", flags: {} },
|
|
1781
|
+
manage: { description: "Interactive profile manager for listing, adding/updating, switching, and deleting profiles", flags: {} },
|
|
1782
|
+
profiles: { description: "List saved profiles", flags: { "--json": { type: "boolean" } } },
|
|
1783
|
+
use: { args: ["profile"], flags: { "--json": { type: "boolean" } } },
|
|
1784
|
+
"delete-profile": { args: ["profile"], flags: { "--force": { type: "boolean" }, "--json": { type: "boolean" } } }
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1488
1787
|
issue: {
|
|
1489
1788
|
subcommands: {
|
|
1490
1789
|
list: {
|
|
@@ -1492,6 +1791,7 @@ function buildCommandContext() {
|
|
|
1492
1791
|
"--status": { type: "enum", values: ISSUE_STATUSES },
|
|
1493
1792
|
"--assignee": { type: "string" },
|
|
1494
1793
|
"--priority": { type: "enum", values: PRIORITIES },
|
|
1794
|
+
"--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject" },
|
|
1495
1795
|
"--limit": { type: "integer", min: 1, max: 100, default: 25 },
|
|
1496
1796
|
"--offset": { type: "integer", min: 0, default: 0 },
|
|
1497
1797
|
"--json": { type: "boolean", default: false }
|
|
@@ -1505,6 +1805,7 @@ function buildCommandContext() {
|
|
|
1505
1805
|
"--description": { type: "string" },
|
|
1506
1806
|
"--status": { type: "enum", values: ISSUE_STATUSES },
|
|
1507
1807
|
"--priority": { type: "enum", values: PRIORITIES },
|
|
1808
|
+
"--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject" },
|
|
1508
1809
|
"--json": { type: "boolean", default: false }
|
|
1509
1810
|
}
|
|
1510
1811
|
},
|
|
@@ -1531,6 +1832,26 @@ function buildCommandContext() {
|
|
|
1531
1832
|
unassign: { args: ["identifier"], flags: { "--json": { type: "boolean" } } }
|
|
1532
1833
|
}
|
|
1533
1834
|
},
|
|
1835
|
+
milestone: {
|
|
1836
|
+
subcommands: {
|
|
1837
|
+
list: {
|
|
1838
|
+
flags: {
|
|
1839
|
+
"--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject", required_without_default: true },
|
|
1840
|
+
"--limit": { type: "integer", min: 1, max: 100, default: 25 },
|
|
1841
|
+
"--json": { type: "boolean" }
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
create: {
|
|
1845
|
+
flags: {
|
|
1846
|
+
"--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject", required_without_default: true },
|
|
1847
|
+
"--name": { type: "string", required: true },
|
|
1848
|
+
"--date": { type: "string" },
|
|
1849
|
+
"--description": { type: "string" },
|
|
1850
|
+
"--json": { type: "boolean" }
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1534
1855
|
project: {
|
|
1535
1856
|
subcommands: {
|
|
1536
1857
|
list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
|
|
@@ -1603,7 +1924,7 @@ init_config();
|
|
|
1603
1924
|
var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".atoll");
|
|
1604
1925
|
var FEEDBACK_PATH = (0, import_node_path3.join)(CONFIG_DIR2, "feedback.jsonl");
|
|
1605
1926
|
var VALID_TYPES = ["bug", "feature"];
|
|
1606
|
-
var
|
|
1927
|
+
var DEFAULT_BASE_URL3 = "https://atollhq.com";
|
|
1607
1928
|
var feedbackCommand = new import_commander10.Command("feedback").description("Record CLI or platform feedback").argument("[text...]", "Feedback text").option("--type <type>", `Feedback type (${VALID_TYPES.join(", ")})`, "bug").option("--url <url>", "Related page or endpoint URL").option("--send", "Also submit to the configured Atoll feedback endpoint").action(async (text, opts) => {
|
|
1608
1929
|
if (text.length === 0) {
|
|
1609
1930
|
outputError('Feedback text is required. Usage: atoll feedback "what went wrong"');
|
|
@@ -1656,7 +1977,7 @@ async function recordFeedback(description, opts) {
|
|
|
1656
1977
|
upstream_status: null,
|
|
1657
1978
|
upstream_error: null
|
|
1658
1979
|
};
|
|
1659
|
-
const endpoint = process.env.ATOLL_FEEDBACK_ENDPOINT || (opts.send ? `${resolveConfig().baseUrl ||
|
|
1980
|
+
const endpoint = process.env.ATOLL_FEEDBACK_ENDPOINT || (opts.send ? `${resolveConfig().baseUrl || DEFAULT_BASE_URL3}/api/feedback` : null);
|
|
1660
1981
|
if (endpoint) {
|
|
1661
1982
|
try {
|
|
1662
1983
|
const response = await fetch(endpoint, {
|
|
@@ -1715,6 +2036,7 @@ Examples:
|
|
|
1715
2036
|
$ atoll heartbeat
|
|
1716
2037
|
$ atoll project list
|
|
1717
2038
|
$ atoll milestone list --project <id>
|
|
2039
|
+
$ atoll auth setup
|
|
1718
2040
|
$ atoll config show`).option("--profile <name>", "Use a saved auth profile (env: ATOLL_PROFILE)").option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").option("--json", "Emit machine-readable JSON").hook("preAction", (_thisCommand, actionCommand) => {
|
|
1719
2041
|
const opts = actionCommand.optsWithGlobals();
|
|
1720
2042
|
if (opts.json) {
|
|
@@ -1768,8 +2090,8 @@ _atoll_completions() {
|
|
|
1768
2090
|
local issue_cmds="list view get create update archive unarchive delete assign unassign"
|
|
1769
2091
|
local project_cmds="list create view get"
|
|
1770
2092
|
local milestone_cmds="list create"
|
|
1771
|
-
local config_cmds="show set-org set-team set-base-url"
|
|
1772
|
-
local auth_cmds="login logout status profiles use"
|
|
2093
|
+
local config_cmds="show set-org set-team set-project clear-project set-base-url"
|
|
2094
|
+
local auth_cmds="login setup manage logout status profiles use delete-profile"
|
|
1773
2095
|
local webhook_cmds="list create delete"
|
|
1774
2096
|
local feedback_cmds="add list"
|
|
1775
2097
|
local completion_cmds="bash zsh"
|
|
@@ -1886,15 +2208,20 @@ _atoll() {
|
|
|
1886
2208
|
'show[Show configuration]' \\
|
|
1887
2209
|
'set-org[Set default org slug]' \\
|
|
1888
2210
|
'set-team[Set default team]' \\
|
|
2211
|
+
'set-project[Set default project]' \\
|
|
2212
|
+
'clear-project[Clear default project]' \\
|
|
1889
2213
|
'set-base-url[Set default API base URL]'
|
|
1890
2214
|
;;
|
|
1891
2215
|
auth)
|
|
1892
2216
|
_values 'subcommand' \\
|
|
1893
2217
|
'login[Log in]' \\
|
|
2218
|
+
'setup[Interactive profile setup]' \\
|
|
2219
|
+
'manage[Interactive profile manager]' \\
|
|
1894
2220
|
'logout[Log out]' \\
|
|
1895
2221
|
'status[Show auth status]' \\
|
|
1896
2222
|
'profiles[List auth profiles]' \\
|
|
1897
|
-
'use[Set active auth profile]'
|
|
2223
|
+
'use[Set active auth profile]' \\
|
|
2224
|
+
'delete-profile[Delete auth profile]'
|
|
1898
2225
|
;;
|
|
1899
2226
|
webhook)
|
|
1900
2227
|
_values 'subcommand' \\
|