@atollhq/cli 0.1.4 → 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
@@ -13,6 +13,7 @@ var __export = (target, all) => {
13
13
  // src/lib/config.ts
14
14
  var config_exports = {};
15
15
  __export(config_exports, {
16
+ CONFIG_PATH: () => CONFIG_PATH,
16
17
  deleteConfig: () => deleteConfig,
17
18
  ensureProfile: () => ensureProfile,
18
19
  getActiveProfile: () => getActiveProfile,
@@ -20,6 +21,7 @@ __export(config_exports, {
20
21
  getProfile: () => getProfile,
21
22
  readConfig: () => readConfig,
22
23
  resolveConfig: () => resolveConfig,
24
+ resolveDefaultProject: () => resolveDefaultProject,
23
25
  writeConfig: () => writeConfig
24
26
  });
25
27
  function readConfig() {
@@ -62,9 +64,13 @@ function resolveConfig(opts) {
62
64
  apiKey: process.env.ATOLL_API_KEY || profile?.apiKey || (profileName ? void 0 : config.apiKey),
63
65
  orgSlug: process.env.ATOLL_ORG || profile?.orgSlug || (profileName ? void 0 : config.orgSlug),
64
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),
65
68
  baseUrl: process.env.ATOLL_BASE_URL || profile?.baseUrl || (profileName ? void 0 : config.baseUrl)
66
69
  };
67
70
  }
71
+ function resolveDefaultProject(explicitProject) {
72
+ return explicitProject || resolveConfig().defaultProject;
73
+ }
68
74
  function getApiKey(opts) {
69
75
  return resolveConfig(opts).apiKey;
70
76
  }
@@ -87,6 +93,8 @@ var import_node_path4 = require("path");
87
93
 
88
94
  // src/commands/auth.ts
89
95
  var import_commander = require("commander");
96
+ var import_node_process = require("process");
97
+ var import_promises = require("readline/promises");
90
98
  init_config();
91
99
 
92
100
  // src/lib/client.ts
@@ -190,24 +198,174 @@ var AtollClient = class {
190
198
  };
191
199
 
192
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
+ }
193
348
  var authCommand = new import_commander.Command("auth").description("Manage authentication");
194
- 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) => {
195
350
  const config = readConfig();
196
351
  const profileName = opts.profile || process.env.ATOLL_PROFILE || config.activeProfile;
197
352
  const orgSlug = opts.org ?? process.env.ATOLL_CLI_ORG;
198
353
  const defaultTeam = opts.team ?? process.env.ATOLL_CLI_TEAM;
354
+ const defaultProject = opts.project;
199
355
  const baseUrl = opts.baseUrl;
200
356
  if (profileName) {
201
357
  const profile = ensureProfile(config, profileName);
202
358
  profile.apiKey = opts.key;
203
359
  if (orgSlug !== void 0) profile.orgSlug = orgSlug;
204
360
  if (defaultTeam !== void 0) profile.defaultTeam = defaultTeam;
361
+ if (defaultProject !== void 0) profile.defaultProject = defaultProject;
205
362
  if (baseUrl !== void 0) profile.baseUrl = baseUrl;
206
363
  config.activeProfile = profileName;
207
364
  } else {
208
365
  config.apiKey = opts.key;
209
366
  if (orgSlug !== void 0) config.orgSlug = orgSlug;
210
367
  if (defaultTeam !== void 0) config.defaultTeam = defaultTeam;
368
+ if (defaultProject !== void 0) config.defaultProject = defaultProject;
211
369
  if (baseUrl !== void 0) config.baseUrl = baseUrl;
212
370
  }
213
371
  writeConfig(config);
@@ -216,6 +374,93 @@ authCommand.command("login").description("Save an API key to ~/.atoll/config.jso
216
374
  profileName ? `\u2713 API key saved to profile "${profileName}" in ~/.atoll/config.json` : "\u2713 API key saved to ~/.atoll/config.json"
217
375
  );
218
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
+ });
219
464
  authCommand.command("status").description("Show current auth status and user info").option("--profile <name>", "Profile name to check").action(async (opts) => {
220
465
  const resolved = resolveConfig({ profile: opts.profile });
221
466
  const apiKey = resolved.apiKey;
@@ -250,14 +495,7 @@ authCommand.command("status").description("Show current auth status and user inf
250
495
  authCommand.command("profiles").description("List saved auth profiles").action(() => {
251
496
  const config = readConfig();
252
497
  const activeProfile = config.activeProfile;
253
- const profiles = Object.entries(config.profiles ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
254
- name,
255
- active: name === activeProfile,
256
- apiKeySet: !!profile.apiKey,
257
- orgSlug: profile.orgSlug ?? null,
258
- defaultTeam: profile.defaultTeam ?? null,
259
- baseUrl: profile.baseUrl ?? null
260
- }));
498
+ const profiles = profileSummaries(config);
261
499
  if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
262
500
  output({ activeProfile: activeProfile ?? null, profiles }, "");
263
501
  return;
@@ -267,17 +505,20 @@ authCommand.command("profiles").description("List saved auth profiles").action((
267
505
  return;
268
506
  }
269
507
  for (const profile of profiles) {
270
- const marker = profile.active ? "*" : " ";
271
- const parts = [
272
- `${marker} ${profile.name}`,
273
- profile.apiKeySet ? "key=set" : "key=not set",
274
- profile.orgSlug ? `org=${profile.orgSlug}` : null,
275
- profile.defaultTeam ? `team=${profile.defaultTeam}` : null,
276
- profile.baseUrl ? `baseUrl=${profile.baseUrl}` : null
277
- ].filter(Boolean);
278
- console.log(parts.join(" "));
508
+ console.log(formatProfileLine(profile));
279
509
  }
280
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);
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
+ );
521
+ });
281
522
  authCommand.command("use <profile>").description("Set the active auth profile").action((profileName) => {
282
523
  const config = readConfig();
283
524
  if (!getProfile(config, profileName)) {
@@ -313,6 +554,7 @@ authCommand.command("logout").description("Remove stored API key").option("--pro
313
554
 
314
555
  // src/commands/issue.ts
315
556
  var import_commander2 = require("commander");
557
+ init_config();
316
558
 
317
559
  // src/lib/colors.ts
318
560
  function isTTY() {
@@ -486,12 +728,12 @@ function padEnd(str, len) {
486
728
  var issueCommand = new import_commander2.Command("issue").description("Manage issues").addHelpText("after", `
487
729
  Examples:
488
730
  $ atoll issue list
489
- $ atoll issue list --status in_progress --priority 1
731
+ $ atoll issue list --project <project-id> --status in_progress --priority 1
490
732
  $ atoll issue view ATOLL-42
491
- $ atoll issue create --title "Fix login" --priority 1 --status todo
733
+ $ atoll issue create --title "Fix login" --project <project-id> --priority 1 --status todo
492
734
  $ atoll issue update ATOLL-42 --status done
493
735
  $ atoll issue assign ATOLL-42 --to <user-id>`);
494
- 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) => {
495
737
  try {
496
738
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
497
739
  outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
@@ -505,10 +747,12 @@ issueCommand.command("list").description("List issues").option("--status <status
505
747
  const org = await resolveOrg(client);
506
748
  const limit = normalizeLimit(opts.limit);
507
749
  const offset = normalizeOffset(opts.offset);
750
+ const projectId = resolveDefaultProject(opts.project);
508
751
  const params = new URLSearchParams();
509
752
  if (opts.status) params.set("status", opts.status);
510
753
  if (opts.assignee) params.set("assigneeId", opts.assignee);
511
754
  if (opts.priority !== void 0) params.set("priority", String(opts.priority));
755
+ if (projectId) params.set("projectId", projectId);
512
756
  params.set("limit", String(limit));
513
757
  if (offset > 0) params.set("offset", String(offset));
514
758
  const qs = params.toString();
@@ -595,7 +839,7 @@ Subtasks: ${enriched.sub_tasks.length}`);
595
839
  }
596
840
  issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
597
841
  issueCommand.command("get <identifier>").description("Get issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
598
- 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) => {
599
843
  try {
600
844
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
601
845
  outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
@@ -608,9 +852,11 @@ issueCommand.command("create").description("Create a new issue").requiredOption(
608
852
  const client = new AtollClient();
609
853
  const org = await resolveOrg(client);
610
854
  const body = { title: opts.title };
855
+ const projectId = resolveDefaultProject(opts.project);
611
856
  if (opts.description !== void 0) body.description = opts.description;
612
857
  if (opts.status) body.status = opts.status;
613
858
  if (opts.priority !== void 0) body.priority = opts.priority;
859
+ if (projectId) body.projectId = projectId;
614
860
  const { issue } = await client.post(`/api/orgs/${org.id}/issues`, body);
615
861
  const projects = await fetchProjectMap(client, org.id);
616
862
  const enriched = attachIssueUrl(issue, client.baseUrl, org.slug, projects);
@@ -1012,22 +1258,32 @@ projectCommand.command("get <projectId>").description("Get project details and i
1012
1258
 
1013
1259
  // src/commands/milestone.ts
1014
1260
  var import_commander5 = require("commander");
1261
+ init_config();
1015
1262
  function progressBar2(progress, width = 20) {
1016
1263
  const filled = Math.round(progress / 100 * width);
1017
1264
  const empty = width - filled;
1018
1265
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${progress}%`;
1019
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
+ }
1020
1275
  var milestoneCommand = new import_commander5.Command("milestone").description("Manage milestones").addHelpText("after", `
1021
1276
  Examples:
1022
1277
  $ atoll milestone list --project <project-id>
1023
1278
  $ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
1024
- 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);
1025
1281
  const client = new AtollClient();
1026
1282
  try {
1027
1283
  const limit = normalizeLimit(opts.limit);
1028
1284
  const orgId = await resolveOrgId(client);
1029
1285
  const data = await client.get(
1030
- `/api/orgs/${orgId}/projects/${opts.project}/milestones`
1286
+ `/api/orgs/${orgId}/projects/${projectId}/milestones`
1031
1287
  );
1032
1288
  const allMilestones = data.milestones ?? [];
1033
1289
  const milestones = allMilestones.slice(0, limit);
@@ -1065,12 +1321,13 @@ milestoneCommand.command("list").description("List milestones for a project").re
1065
1321
  handleApiError(err);
1066
1322
  }
1067
1323
  });
1068
- 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);
1069
1326
  const client = new AtollClient();
1070
1327
  try {
1071
1328
  const orgId = await resolveOrgId(client);
1072
1329
  const data = await client.post(
1073
- `/api/orgs/${orgId}/projects/${opts.project}/milestones`,
1330
+ `/api/orgs/${orgId}/projects/${projectId}/milestones`,
1074
1331
  {
1075
1332
  name: opts.name,
1076
1333
  description: opts.description ?? null,
@@ -1092,17 +1349,37 @@ milestoneCommand.command("create").description("Create a new milestone").require
1092
1349
  // src/commands/config.ts
1093
1350
  var import_commander6 = require("commander");
1094
1351
  init_config();
1095
- var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, API key)").addHelpText("after", `
1352
+ function configLocation(profileName) {
1353
+ return profileName ? `${CONFIG_PATH} profile "${profileName}"` : CONFIG_PATH;
1354
+ }
1355
+ function sourceForField(cfg, profileName, field, envVar) {
1356
+ if (process.env[envVar]) return `$${envVar}`;
1357
+ if (profileName && cfg.profiles?.[profileName]?.[field]) return configLocation(profileName);
1358
+ if (!profileName && cfg[field]) return configLocation();
1359
+ return null;
1360
+ }
1361
+ function withSource(value, source) {
1362
+ return source ? `${bold(value)} ${dim(`from ${source}`)}` : bold(value);
1363
+ }
1364
+ var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, project, API key)").addHelpText("after", `
1096
1365
  Examples:
1097
1366
  $ atoll config show
1098
1367
  $ atoll config set-org my-org
1099
1368
  $ atoll config set-team team-abc123
1369
+ $ atoll config set-project project-abc123
1100
1370
  $ atoll config set-base-url https://atollhq.com`);
1101
1371
  configCommand.command("show").description("Display current configuration").option("--profile <name>", "Profile name to show").action((opts) => {
1102
1372
  const cfg = readConfig();
1103
1373
  const activeProfile = cfg.activeProfile;
1104
1374
  const resolved = resolveConfig({ profile: opts.profile });
1105
1375
  const apiKeySet = !!resolved.apiKey;
1376
+ const sources = {
1377
+ apiKey: sourceForField(cfg, resolved.profile, "apiKey", "ATOLL_API_KEY"),
1378
+ orgSlug: sourceForField(cfg, resolved.profile, "orgSlug", "ATOLL_ORG"),
1379
+ defaultTeam: sourceForField(cfg, resolved.profile, "defaultTeam", "ATOLL_TEAM"),
1380
+ defaultProject: sourceForField(cfg, resolved.profile, "defaultProject", "ATOLL_PROJECT"),
1381
+ baseUrl: sourceForField(cfg, resolved.profile, "baseUrl", "ATOLL_BASE_URL")
1382
+ };
1106
1383
  if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
1107
1384
  output(
1108
1385
  {
@@ -1110,8 +1387,10 @@ configCommand.command("show").description("Display current configuration").optio
1110
1387
  selectedProfile: resolved.profile ?? null,
1111
1388
  orgSlug: resolved.orgSlug ?? null,
1112
1389
  defaultTeam: resolved.defaultTeam ?? null,
1390
+ defaultProject: resolved.defaultProject ?? null,
1113
1391
  baseUrl: resolved.baseUrl ?? null,
1114
1392
  apiKeySet,
1393
+ sources,
1115
1394
  profiles: Object.keys(cfg.profiles ?? {}).sort()
1116
1395
  },
1117
1396
  ""
@@ -1121,10 +1400,11 @@ configCommand.command("show").description("Display current configuration").optio
1121
1400
  console.log(`${bold("Atoll CLI Configuration")}`);
1122
1401
  console.log(` ${dim("Active profile:")} ${activeProfile ? bold(activeProfile) : gray("(not set)")}`);
1123
1402
  console.log(` ${dim("Selected profile:")} ${resolved.profile ? bold(resolved.profile) : gray("(legacy config)")}`);
1124
- console.log(` ${dim("Org slug:")} ${resolved.orgSlug ? bold(resolved.orgSlug) : gray("(not set)")}`);
1125
- console.log(` ${dim("Default team:")} ${resolved.defaultTeam ? bold(resolved.defaultTeam) : gray("(not set)")}`);
1126
- console.log(` ${dim("Base URL:")} ${resolved.baseUrl ? bold(resolved.baseUrl) : gray("(default)")}`);
1127
- console.log(` ${dim("API key:")} ${apiKeySet ? bold("set") : gray("not set \u2014 run `atoll auth login`")}`);
1403
+ console.log(` ${dim("Org slug:")} ${resolved.orgSlug ? withSource(resolved.orgSlug, sources.orgSlug) : gray("(not set)")}`);
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)")}`);
1406
+ console.log(` ${dim("Base URL:")} ${resolved.baseUrl ? withSource(resolved.baseUrl, sources.baseUrl) : gray("(default)")}`);
1407
+ console.log(` ${dim("API key:")} ${apiKeySet ? withSource("set", sources.apiKey) : gray("not set \u2014 run `atoll auth login`")}`);
1128
1408
  });
1129
1409
  configCommand.command("set-org <slug>").description("Set the default organisation slug").option("--profile <name>", "Profile name to update").action((slug, opts) => {
1130
1410
  const cfg = readConfig();
@@ -1154,6 +1434,34 @@ configCommand.command("set-team <team>").description("Set the default team slug
1154
1434
  profileName ? success(`Default team for profile "${profileName}" set to "${team}"`) : success(`Default team set to "${team}"`)
1155
1435
  );
1156
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
+ });
1157
1465
  configCommand.command("set-base-url <url>").description("Set the default API base URL").option("--profile <name>", "Profile name to update").action((url, opts) => {
1158
1466
  const cfg = readConfig();
1159
1467
  const profileName = opts.profile || process.env.ATOLL_PROFILE || cfg.activeProfile;
@@ -1424,6 +1732,7 @@ var agentContextCommand = new import_commander9.Command("agent-context").descrip
1424
1732
  apiKeySet: Boolean(profile.apiKey),
1425
1733
  orgSlug: profile.orgSlug ?? null,
1426
1734
  defaultTeam: profile.defaultTeam ?? null,
1735
+ defaultProject: profile.defaultProject ?? null,
1427
1736
  baseUrl: profile.baseUrl ?? null
1428
1737
  }));
1429
1738
  outputJson({
@@ -1442,6 +1751,7 @@ var agentContextCommand = new import_commander9.Command("agent-context").descrip
1442
1751
  selectedProfile: resolved.profile ?? null,
1443
1752
  orgSlug: resolved.orgSlug ?? null,
1444
1753
  defaultTeam: resolved.defaultTeam ?? null,
1754
+ defaultProject: resolved.defaultProject ?? null,
1445
1755
  baseUrl: resolved.baseUrl ?? null,
1446
1756
  apiKeySet: Boolean(resolved.apiKey)
1447
1757
  },
@@ -1465,6 +1775,15 @@ function buildCommandContext() {
1465
1775
  },
1466
1776
  signal_types: SIGNAL_TYPES
1467
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
+ },
1468
1787
  issue: {
1469
1788
  subcommands: {
1470
1789
  list: {
@@ -1472,6 +1791,7 @@ function buildCommandContext() {
1472
1791
  "--status": { type: "enum", values: ISSUE_STATUSES },
1473
1792
  "--assignee": { type: "string" },
1474
1793
  "--priority": { type: "enum", values: PRIORITIES },
1794
+ "--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject" },
1475
1795
  "--limit": { type: "integer", min: 1, max: 100, default: 25 },
1476
1796
  "--offset": { type: "integer", min: 0, default: 0 },
1477
1797
  "--json": { type: "boolean", default: false }
@@ -1485,6 +1805,7 @@ function buildCommandContext() {
1485
1805
  "--description": { type: "string" },
1486
1806
  "--status": { type: "enum", values: ISSUE_STATUSES },
1487
1807
  "--priority": { type: "enum", values: PRIORITIES },
1808
+ "--project": { type: "string", default_from: "ATOLL_PROJECT or profile.defaultProject" },
1488
1809
  "--json": { type: "boolean", default: false }
1489
1810
  }
1490
1811
  },
@@ -1511,6 +1832,26 @@ function buildCommandContext() {
1511
1832
  unassign: { args: ["identifier"], flags: { "--json": { type: "boolean" } } }
1512
1833
  }
1513
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
+ },
1514
1855
  project: {
1515
1856
  subcommands: {
1516
1857
  list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
@@ -1583,7 +1924,7 @@ init_config();
1583
1924
  var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".atoll");
1584
1925
  var FEEDBACK_PATH = (0, import_node_path3.join)(CONFIG_DIR2, "feedback.jsonl");
1585
1926
  var VALID_TYPES = ["bug", "feature"];
1586
- var DEFAULT_BASE_URL2 = "https://atollhq.com";
1927
+ var DEFAULT_BASE_URL3 = "https://atollhq.com";
1587
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) => {
1588
1929
  if (text.length === 0) {
1589
1930
  outputError('Feedback text is required. Usage: atoll feedback "what went wrong"');
@@ -1636,7 +1977,7 @@ async function recordFeedback(description, opts) {
1636
1977
  upstream_status: null,
1637
1978
  upstream_error: null
1638
1979
  };
1639
- 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);
1640
1981
  if (endpoint) {
1641
1982
  try {
1642
1983
  const response = await fetch(endpoint, {
@@ -1695,6 +2036,7 @@ Examples:
1695
2036
  $ atoll heartbeat
1696
2037
  $ atoll project list
1697
2038
  $ atoll milestone list --project <id>
2039
+ $ atoll auth setup
1698
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) => {
1699
2041
  const opts = actionCommand.optsWithGlobals();
1700
2042
  if (opts.json) {
@@ -1748,8 +2090,8 @@ _atoll_completions() {
1748
2090
  local issue_cmds="list view get create update archive unarchive delete assign unassign"
1749
2091
  local project_cmds="list create view get"
1750
2092
  local milestone_cmds="list create"
1751
- local config_cmds="show set-org set-team set-base-url"
1752
- 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"
1753
2095
  local webhook_cmds="list create delete"
1754
2096
  local feedback_cmds="add list"
1755
2097
  local completion_cmds="bash zsh"
@@ -1866,15 +2208,20 @@ _atoll() {
1866
2208
  'show[Show configuration]' \\
1867
2209
  'set-org[Set default org slug]' \\
1868
2210
  'set-team[Set default team]' \\
2211
+ 'set-project[Set default project]' \\
2212
+ 'clear-project[Clear default project]' \\
1869
2213
  'set-base-url[Set default API base URL]'
1870
2214
  ;;
1871
2215
  auth)
1872
2216
  _values 'subcommand' \\
1873
2217
  'login[Log in]' \\
2218
+ 'setup[Interactive profile setup]' \\
2219
+ 'manage[Interactive profile manager]' \\
1874
2220
  'logout[Log out]' \\
1875
2221
  'status[Show auth status]' \\
1876
2222
  'profiles[List auth profiles]' \\
1877
- 'use[Set active auth profile]'
2223
+ 'use[Set active auth profile]' \\
2224
+ 'delete-profile[Delete auth profile]'
1878
2225
  ;;
1879
2226
  webhook)
1880
2227
  _values 'subcommand' \\