@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/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 = Object.entries(config.profiles ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
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
- const marker = profile.active ? "*" : " ";
272
- const parts = [
273
- `${marker} ${profile.name}`,
274
- profile.apiKeySet ? "key=set" : "key=not set",
275
- profile.orgSlug ? `org=${profile.orgSlug}` : null,
276
- profile.defaultTeam ? `team=${profile.defaultTeam}` : null,
277
- profile.baseUrl ? `baseUrl=${profile.baseUrl}` : null
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").requiredOption("--project <id>", "Project ID").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
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/${opts.project}/milestones`
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").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Milestone name").option("--date <YYYY-MM-DD>", "Due date").option("--description <desc>", "Description").action(async (opts) => {
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/${opts.project}/milestones`,
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 DEFAULT_BASE_URL2 = "https://atollhq.com";
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 || DEFAULT_BASE_URL2}/api/feedback` : null);
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' \\