bktide 1.0.1755266193 → 1.0.1755547716

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.
Files changed (75) hide show
  1. package/README.md +107 -1
  2. package/WORKFLOW_README.md +1 -1
  3. package/completions/bktide-dynamic.fish +171 -0
  4. package/completions/bktide.bash +124 -0
  5. package/completions/bktide.fish +107 -0
  6. package/completions/bktide.zsh +139 -0
  7. package/dist/commands/BaseCommand.js +7 -7
  8. package/dist/commands/BaseCommand.js.map +1 -1
  9. package/dist/commands/GenerateCompletions.js +238 -0
  10. package/dist/commands/GenerateCompletions.js.map +1 -0
  11. package/dist/commands/ListAnnotations.js +7 -0
  12. package/dist/commands/ListAnnotations.js.map +1 -1
  13. package/dist/commands/ListBuilds.js +67 -3
  14. package/dist/commands/ListBuilds.js.map +1 -1
  15. package/dist/commands/ListOrganizations.js +6 -0
  16. package/dist/commands/ListOrganizations.js.map +1 -1
  17. package/dist/commands/ListPipelines.js +87 -12
  18. package/dist/commands/ListPipelines.js.map +1 -1
  19. package/dist/commands/ManageToken.js +32 -9
  20. package/dist/commands/ManageToken.js.map +1 -1
  21. package/dist/commands/ShowViewer.js +7 -1
  22. package/dist/commands/ShowViewer.js.map +1 -1
  23. package/dist/commands/index.js +1 -0
  24. package/dist/commands/index.js.map +1 -1
  25. package/dist/formatters/annotations/PlainTextFormatter.js +37 -9
  26. package/dist/formatters/annotations/PlainTextFormatter.js.map +1 -1
  27. package/dist/formatters/builds/PlainTextFormatter.js +82 -60
  28. package/dist/formatters/builds/PlainTextFormatter.js.map +1 -1
  29. package/dist/formatters/errors/AlfredFormatter.js +20 -0
  30. package/dist/formatters/errors/AlfredFormatter.js.map +1 -1
  31. package/dist/formatters/errors/PlainTextFormatter.js +121 -23
  32. package/dist/formatters/errors/PlainTextFormatter.js.map +1 -1
  33. package/dist/formatters/organizations/PlainTextFormatter.js +37 -6
  34. package/dist/formatters/organizations/PlainTextFormatter.js.map +1 -1
  35. package/dist/formatters/pipelines/AlfredFormatter.js.map +1 -1
  36. package/dist/formatters/pipelines/Formatter.js.map +1 -1
  37. package/dist/formatters/pipelines/JsonFormatter.js.map +1 -1
  38. package/dist/formatters/pipelines/PlainTextFormatter.js +165 -19
  39. package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -1
  40. package/dist/formatters/token/AlfredFormatter.js +15 -2
  41. package/dist/formatters/token/AlfredFormatter.js.map +1 -1
  42. package/dist/formatters/token/PlainTextFormatter.js +56 -18
  43. package/dist/formatters/token/PlainTextFormatter.js.map +1 -1
  44. package/dist/formatters/viewer/PlainTextFormatter.js +8 -7
  45. package/dist/formatters/viewer/PlainTextFormatter.js.map +1 -1
  46. package/dist/index.js +47 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/services/CredentialManager.js +80 -10
  49. package/dist/services/CredentialManager.js.map +1 -1
  50. package/dist/ui/help.js +69 -0
  51. package/dist/ui/help.js.map +1 -0
  52. package/dist/ui/progress.js +356 -0
  53. package/dist/ui/progress.js.map +1 -0
  54. package/dist/ui/reporter.js +111 -0
  55. package/dist/ui/reporter.js.map +1 -0
  56. package/dist/ui/responsive-table.js +183 -0
  57. package/dist/ui/responsive-table.js.map +1 -0
  58. package/dist/ui/spinner.js +20 -0
  59. package/dist/ui/spinner.js.map +1 -0
  60. package/dist/ui/symbols.js +46 -0
  61. package/dist/ui/symbols.js.map +1 -0
  62. package/dist/ui/table.js +32 -0
  63. package/dist/ui/table.js.map +1 -0
  64. package/dist/ui/theme.js +280 -0
  65. package/dist/ui/theme.js.map +1 -0
  66. package/dist/ui/width.js +111 -0
  67. package/dist/ui/width.js.map +1 -0
  68. package/dist/utils/alfred.js +6 -0
  69. package/dist/utils/alfred.js.map +1 -0
  70. package/dist/utils/cli-error-handler.js +35 -20
  71. package/dist/utils/cli-error-handler.js.map +1 -1
  72. package/dist/utils/pagination.js +92 -0
  73. package/dist/utils/pagination.js.map +1 -0
  74. package/info.plist +51 -218
  75. package/package.json +23 -5
@@ -1,7 +1,8 @@
1
- import { Entry } from '@napi-rs/keyring';
2
1
  import { logger } from './logger.js';
3
2
  import { BuildkiteClient } from './BuildkiteClient.js';
4
3
  import { BuildkiteRestClient } from './BuildkiteRestClient.js';
4
+ import { isRunningInAlfred } from '../utils/alfred.js';
5
+ import { Progress } from '../ui/progress.js';
5
6
  const SERVICE_NAME = 'bktide';
6
7
  const ACCOUNT_KEY = 'default';
7
8
  /**
@@ -10,7 +11,28 @@ const ACCOUNT_KEY = 'default';
10
11
  export class CredentialManager {
11
12
  entry;
12
13
  constructor(serviceName = SERVICE_NAME, accountName = ACCOUNT_KEY) {
13
- this.entry = new Entry(serviceName, accountName);
14
+ // Do not load keyring when running under Alfred
15
+ if (!isRunningInAlfred()) {
16
+ // Lazy init via dynamic import to avoid resolving native module in Alfred context
17
+ // Note: constructor remains sync; actual instantiation is deferred in ensureEntry
18
+ void this.ensureEntry(serviceName, accountName);
19
+ }
20
+ }
21
+ async ensureEntry(serviceName = SERVICE_NAME, accountName = ACCOUNT_KEY) {
22
+ if (this.entry)
23
+ return this.entry;
24
+ if (isRunningInAlfred())
25
+ return undefined;
26
+ try {
27
+ const keyring = await import('@napi-rs/keyring');
28
+ this.entry = new keyring.Entry(serviceName, accountName);
29
+ return this.entry;
30
+ }
31
+ catch (error) {
32
+ logger.debug('Failed to initialize keyring Entry, continuing without keychain', error);
33
+ this.entry = undefined;
34
+ return undefined;
35
+ }
14
36
  }
15
37
  /**
16
38
  * Stores a token in the system keychain
@@ -18,8 +40,15 @@ export class CredentialManager {
18
40
  * @returns true if token was successfully stored
19
41
  */
20
42
  async saveToken(token) {
43
+ if (isRunningInAlfred()) {
44
+ // In Alfred path, we do not persist tokens programmatically
45
+ throw new Error('In Alfred, set token via Workflow Configuration (User Configuration).');
46
+ }
21
47
  try {
22
- await this.entry.setPassword(token);
48
+ const entry = await this.ensureEntry();
49
+ if (!entry)
50
+ throw new Error('Keyring unavailable');
51
+ await entry.setPassword(token);
23
52
  logger.debug('Token saved to system keychain');
24
53
  return true;
25
54
  }
@@ -33,11 +62,18 @@ export class CredentialManager {
33
62
  * @returns The stored token or undefined if not found
34
63
  */
35
64
  async getToken() {
65
+ // Alfred: use env var only
66
+ if (isRunningInAlfred()) {
67
+ return process.env.BUILDKITE_API_TOKEN || process.env.BK_TOKEN || undefined;
68
+ }
36
69
  try {
37
- const token = this.entry.getPassword();
70
+ const entry = await this.ensureEntry();
71
+ if (!entry)
72
+ return undefined;
73
+ const token = entry.getPassword();
38
74
  return token || undefined;
39
75
  }
40
- catch (error) {
76
+ catch {
41
77
  return undefined;
42
78
  }
43
79
  }
@@ -46,8 +82,15 @@ export class CredentialManager {
46
82
  * @returns true if token was successfully deleted
47
83
  */
48
84
  async deleteToken() {
85
+ if (isRunningInAlfred()) {
86
+ // Nothing to delete in keyring under Alfred
87
+ return true;
88
+ }
49
89
  try {
50
- await this.entry.deletePassword();
90
+ const entry = await this.ensureEntry();
91
+ if (!entry)
92
+ return false;
93
+ await entry.deletePassword();
51
94
  logger.debug('Token deleted from system keychain');
52
95
  return true;
53
96
  }
@@ -67,9 +110,10 @@ export class CredentialManager {
67
110
  /**
68
111
  * Validates if a token is valid by making test API calls to both GraphQL and REST APIs
69
112
  * @param token Optional token to validate. If not provided, will use the stored token.
113
+ * @param options Optional configuration for progress display
70
114
  * @returns Object containing validation status for both GraphQL and REST APIs
71
115
  */
72
- async validateToken(token) {
116
+ async validateToken(token, options) {
73
117
  try {
74
118
  // If no token provided, try to get the stored one
75
119
  const tokenToValidate = token || await this.getToken();
@@ -103,6 +147,16 @@ export class CredentialManager {
103
147
  }
104
148
  const organizations = {};
105
149
  let allValid = true;
150
+ // Determine if we should show progress
151
+ const showProgress = options?.showProgress !== false &&
152
+ !isRunningInAlfred() &&
153
+ orgSlugs.length > 0;
154
+ const progress = showProgress ? Progress.bar({
155
+ total: orgSlugs.length * 3,
156
+ label: 'Validating token access',
157
+ format: options?.format
158
+ }) : null;
159
+ let checkCount = 0;
106
160
  // Validate each organization
107
161
  for (const orgSlug of orgSlugs) {
108
162
  const orgStatus = {
@@ -110,8 +164,11 @@ export class CredentialManager {
110
164
  builds: false,
111
165
  organizations: false
112
166
  };
167
+ // Check GraphQL access
168
+ if (progress) {
169
+ progress.update(checkCount++, `Checking GraphQL access for ${orgSlug}`);
170
+ }
113
171
  try {
114
- // Check GraphQL access
115
172
  await graphqlClient.getViewer();
116
173
  orgStatus.graphql = true;
117
174
  }
@@ -119,8 +176,11 @@ export class CredentialManager {
119
176
  logger.debug(`GraphQL validation failed for organization ${orgSlug}`, error);
120
177
  allValid = false;
121
178
  }
179
+ // Check build access
180
+ if (progress) {
181
+ progress.update(checkCount++, `Checking build access for ${orgSlug}`);
182
+ }
122
183
  try {
123
- // Check build access
124
184
  await restClient.hasBuildAccess(orgSlug);
125
185
  orgStatus.builds = true;
126
186
  }
@@ -128,8 +188,11 @@ export class CredentialManager {
128
188
  logger.debug(`Build access validation failed for organization ${orgSlug}`, error);
129
189
  allValid = false;
130
190
  }
191
+ // Check organization access
192
+ if (progress) {
193
+ progress.update(checkCount++, `Checking organization access for ${orgSlug}`);
194
+ }
131
195
  try {
132
- // Check organization access
133
196
  await restClient.hasOrganizationAccess(orgSlug);
134
197
  orgStatus.organizations = true;
135
198
  }
@@ -139,6 +202,13 @@ export class CredentialManager {
139
202
  }
140
203
  organizations[orgSlug] = orgStatus;
141
204
  }
205
+ // Complete the progress bar
206
+ if (progress) {
207
+ const successCount = Object.values(organizations)
208
+ .filter(org => org.graphql && org.builds && org.organizations)
209
+ .length;
210
+ progress.complete(`✓ Validated ${orgSlugs.length} organizations (${successCount} fully accessible)`);
211
+ }
142
212
  return {
143
213
  valid: allValid,
144
214
  canListOrganizations: true,
@@ -1 +1 @@
1
- {"version":3,"file":"CredentialManager.js","sourceRoot":"/","sources":["services/CredentialManager.ts"],"names":[],"mappings":"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","sourcesContent":["import { Entry } from '@napi-rs/keyring';\nimport { logger } from './logger.js';\nimport { BuildkiteClient } from './BuildkiteClient.js';\nimport { BuildkiteRestClient } from './BuildkiteRestClient.js';\nimport { TokenValidationStatus, OrganizationValidationStatus } from '../types/credentials.js';\n\nconst SERVICE_NAME = 'bktide';\nconst ACCOUNT_KEY = 'default';\n\n/**\n * Manages secure storage of credentials using the system's keychain\n */\nexport class CredentialManager {\n private entry: Entry;\n\n constructor(serviceName = SERVICE_NAME, accountName = ACCOUNT_KEY) {\n this.entry = new Entry(serviceName, accountName);\n }\n\n /**\n * Stores a token in the system keychain\n * @param token The Buildkite API token to store\n * @returns true if token was successfully stored\n */\n async saveToken(token: string): Promise<boolean> {\n try {\n await this.entry.setPassword(token);\n logger.debug('Token saved to system keychain');\n return true;\n } catch (error) {\n logger.error('Failed to save token to system keychain', error);\n return false;\n }\n }\n\n /**\n * Retrieves the stored token from the system keychain\n * @returns The stored token or undefined if not found\n */\n async getToken(): Promise<string | undefined> {\n try {\n const token = this.entry.getPassword();\n return token || undefined;\n } catch (error) {\n return undefined;\n }\n }\n\n /**\n * Deletes the stored token from the system keychain\n * @returns true if token was successfully deleted\n */\n async deleteToken(): Promise<boolean> {\n try {\n await this.entry.deletePassword();\n logger.debug('Token deleted from system keychain');\n return true;\n } catch (error) {\n logger.error('Failed to delete token from system keychain', error);\n return false;\n }\n }\n\n /**\n * Checks if a token exists in the system keychain\n * @returns true if a token exists\n */\n async hasToken(): Promise<boolean> {\n const token = await this.getToken();\n return !!token;\n }\n\n /**\n * Validates if a token is valid by making test API calls to both GraphQL and REST APIs\n * @param token Optional token to validate. If not provided, will use the stored token.\n * @returns Object containing validation status for both GraphQL and REST APIs\n */\n async validateToken(token?: string): Promise<TokenValidationStatus> {\n try {\n // If no token provided, try to get the stored one\n const tokenToValidate = token || await this.getToken();\n if (!tokenToValidate) {\n logger.debug('No token provided for validation');\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n\n // Create clients with the token\n const graphqlClient = new BuildkiteClient(tokenToValidate, { debug: false, caching: false });\n const restClient = new BuildkiteRestClient(tokenToValidate, { debug: false });\n \n // First check if we can list organizations\n let orgSlugs: string[] = [];\n try {\n orgSlugs = await graphqlClient.getOrganizations().then(orgs => orgs.map(org => org.slug));\n logger.debug('Successfully retrieved organization slugs');\n } catch (error) {\n logger.debug('Failed to retrieve organization slugs', {\n error: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error && error.cause ? error.cause : undefined\n });\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n\n const organizations: Record<string, OrganizationValidationStatus> = {};\n let allValid = true;\n\n // Validate each organization\n for (const orgSlug of orgSlugs) {\n const orgStatus: OrganizationValidationStatus = {\n graphql: false,\n builds: false,\n organizations: false\n };\n\n try {\n // Check GraphQL access\n await graphqlClient.getViewer();\n orgStatus.graphql = true;\n } catch (error) {\n logger.debug(`GraphQL validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n try {\n // Check build access\n await restClient.hasBuildAccess(orgSlug);\n orgStatus.builds = true;\n } catch (error) {\n logger.debug(`Build access validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n try {\n // Check organization access\n await restClient.hasOrganizationAccess(orgSlug);\n orgStatus.organizations = true;\n } catch (error) {\n logger.debug(`Organization access validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n organizations[orgSlug] = orgStatus;\n }\n\n return {\n valid: allValid,\n canListOrganizations: true,\n organizations\n };\n } catch (error) {\n logger.debug('Token validation failed', error);\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n }\n}"]}
1
+ {"version":3,"file":"CredentialManager.js","sourceRoot":"/","sources":["services/CredentialManager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,MAAM,YAAY,GAAG,QAAQ,CAAC;AAC9B,MAAM,WAAW,GAAG,SAAS,CAAC;AAE9B;;GAEG;AACH,MAAM,OAAO,iBAAiB;IACpB,KAAK,CAAoB;IAEjC,YAAY,WAAW,GAAG,YAAY,EAAE,WAAW,GAAG,WAAW;QAC/D,gDAAgD;QAChD,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YACzB,kFAAkF;YAClF,kFAAkF;YAClF,KAAK,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,WAAW,GAAG,YAAY,EAAE,WAAW,GAAG,WAAW;QAC7E,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC;QAClC,IAAI,iBAAiB,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,WAAW,CAAU,CAAC;YAClE,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,iEAAiE,EAAE,KAAK,CAAC,CAAC;YACvF,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC,KAAa;QAC3B,IAAI,iBAAiB,EAAE,EAAE,CAAC;YACxB,4DAA4D;YAC5D,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;YACnD,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC/B,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,yCAAyC,EAAE,KAAK,CAAC,CAAC;YAC/D,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACZ,2BAA2B;QAC3B,IAAI,iBAAiB,EAAE,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,SAAS,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YAClC,OAAO,KAAK,IAAI,SAAS,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW;QACf,IAAI,iBAAiB,EAAE,EAAE,CAAC;YACxB,4CAA4C;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK;gBAAE,OAAO,KAAK,CAAC;YACzB,MAAM,KAAK,CAAC,cAAc,EAAE,CAAC;YAC7B,MAAM,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,6CAA6C,EAAE,KAAK,CAAC,CAAC;YACnE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACZ,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,aAAa,CAAC,KAAc,EAAE,OAAqD;QACvF,IAAI,CAAC;YACH,kDAAkD;YAClD,MAAM,eAAe,GAAG,KAAK,IAAI,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;YACvD,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,MAAM,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;gBACjD,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,oBAAoB,EAAE,KAAK;oBAC3B,aAAa,EAAE,EAAE;iBAClB,CAAC;YACJ,CAAC;YAED,gCAAgC;YAChC,MAAM,aAAa,GAAG,IAAI,eAAe,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7F,MAAM,UAAU,GAAG,IAAI,mBAAmB,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YAE9E,2CAA2C;YAC3C,IAAI,QAAQ,GAAa,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC1F,MAAM,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;YAC5D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,uCAAuC,EAAE;oBACpD,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;oBAC7D,KAAK,EAAE,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;iBACvE,CAAC,CAAC;gBACH,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,oBAAoB,EAAE,KAAK;oBAC3B,aAAa,EAAE,EAAE;iBAClB,CAAC;YACJ,CAAC;YAED,MAAM,aAAa,GAAiD,EAAE,CAAC;YACvE,IAAI,QAAQ,GAAG,IAAI,CAAC;YAEpB,uCAAuC;YACvC,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,KAAK,KAAK;gBAChC,CAAC,iBAAiB,EAAE;gBACpB,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAExC,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAC3C,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAC1B,KAAK,EAAE,yBAAyB;gBAChC,MAAM,EAAE,OAAO,EAAE,MAAM;aACxB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAEV,IAAI,UAAU,GAAG,CAAC,CAAC;YAEnB,6BAA6B;YAC7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,MAAM,SAAS,GAAiC;oBAC9C,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE,KAAK;oBACb,aAAa,EAAE,KAAK;iBACrB,CAAC;gBAEF,uBAAuB;gBACvB,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,+BAA+B,OAAO,EAAE,CAAC,CAAC;gBAC1E,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC;oBAChC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;gBAC3B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,CAAC,KAAK,CAAC,8CAA8C,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;oBAC7E,QAAQ,GAAG,KAAK,CAAC;gBACnB,CAAC;gBAED,qBAAqB;gBACrB,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,6BAA6B,OAAO,EAAE,CAAC,CAAC;gBACxE,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,UAAU,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;oBACzC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC;gBAC1B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,CAAC,KAAK,CAAC,mDAAmD,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;oBAClF,QAAQ,GAAG,KAAK,CAAC;gBACnB,CAAC;gBAED,4BAA4B;gBAC5B,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,oCAAoC,OAAO,EAAE,CAAC,CAAC;gBAC/E,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,UAAU,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;oBAChD,SAAS,CAAC,aAAa,GAAG,IAAI,CAAC;gBACjC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,CAAC,KAAK,CAAC,0DAA0D,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;oBACzF,QAAQ,GAAG,KAAK,CAAC;gBACnB,CAAC;gBAED,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;YACrC,CAAC;YAED,4BAA4B;YAC5B,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC;qBAC9C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,aAAa,CAAC;qBAC7D,MAAM,CAAC;gBACV,QAAQ,CAAC,QAAQ,CAAC,eAAe,QAAQ,CAAC,MAAM,mBAAmB,YAAY,oBAAoB,CAAC,CAAC;YACvG,CAAC;YAED,OAAO;gBACL,KAAK,EAAE,QAAQ;gBACf,oBAAoB,EAAE,IAAI;gBAC1B,aAAa;aACd,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;YAC/C,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,oBAAoB,EAAE,KAAK;gBAC3B,aAAa,EAAE,EAAE;aAClB,CAAC;QACJ,CAAC;IACH,CAAC;CACF","sourcesContent":["// Avoid importing native module eagerly; import lazily when needed\nimport type { Entry } from '@napi-rs/keyring';\nimport { logger } from './logger.js';\nimport { BuildkiteClient } from './BuildkiteClient.js';\nimport { BuildkiteRestClient } from './BuildkiteRestClient.js';\nimport { TokenValidationStatus, OrganizationValidationStatus } from '../types/credentials.js';\nimport { isRunningInAlfred } from '../utils/alfred.js';\nimport { Progress } from '../ui/progress.js';\n\nconst SERVICE_NAME = 'bktide';\nconst ACCOUNT_KEY = 'default';\n\n/**\n * Manages secure storage of credentials using the system's keychain\n */\nexport class CredentialManager {\n private entry: Entry | undefined;\n\n constructor(serviceName = SERVICE_NAME, accountName = ACCOUNT_KEY) {\n // Do not load keyring when running under Alfred\n if (!isRunningInAlfred()) {\n // Lazy init via dynamic import to avoid resolving native module in Alfred context\n // Note: constructor remains sync; actual instantiation is deferred in ensureEntry\n void this.ensureEntry(serviceName, accountName);\n }\n }\n\n private async ensureEntry(serviceName = SERVICE_NAME, accountName = ACCOUNT_KEY): Promise<Entry | undefined> {\n if (this.entry) return this.entry;\n if (isRunningInAlfred()) return undefined;\n try {\n const keyring = await import('@napi-rs/keyring');\n this.entry = new keyring.Entry(serviceName, accountName) as Entry;\n return this.entry;\n } catch (error) {\n logger.debug('Failed to initialize keyring Entry, continuing without keychain', error);\n this.entry = undefined;\n return undefined;\n }\n }\n\n /**\n * Stores a token in the system keychain\n * @param token The Buildkite API token to store\n * @returns true if token was successfully stored\n */\n async saveToken(token: string): Promise<boolean> {\n if (isRunningInAlfred()) {\n // In Alfred path, we do not persist tokens programmatically\n throw new Error('In Alfred, set token via Workflow Configuration (User Configuration).');\n }\n try {\n const entry = await this.ensureEntry();\n if (!entry) throw new Error('Keyring unavailable');\n await entry.setPassword(token);\n logger.debug('Token saved to system keychain');\n return true;\n } catch (error) {\n logger.error('Failed to save token to system keychain', error);\n return false;\n }\n }\n\n /**\n * Retrieves the stored token from the system keychain\n * @returns The stored token or undefined if not found\n */\n async getToken(): Promise<string | undefined> {\n // Alfred: use env var only\n if (isRunningInAlfred()) {\n return process.env.BUILDKITE_API_TOKEN || process.env.BK_TOKEN || undefined;\n }\n try {\n const entry = await this.ensureEntry();\n if (!entry) return undefined;\n const token = entry.getPassword();\n return token || undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Deletes the stored token from the system keychain\n * @returns true if token was successfully deleted\n */\n async deleteToken(): Promise<boolean> {\n if (isRunningInAlfred()) {\n // Nothing to delete in keyring under Alfred\n return true;\n }\n try {\n const entry = await this.ensureEntry();\n if (!entry) return false;\n await entry.deletePassword();\n logger.debug('Token deleted from system keychain');\n return true;\n } catch (error) {\n logger.error('Failed to delete token from system keychain', error);\n return false;\n }\n }\n\n /**\n * Checks if a token exists in the system keychain\n * @returns true if a token exists\n */\n async hasToken(): Promise<boolean> {\n const token = await this.getToken();\n return !!token;\n }\n\n /**\n * Validates if a token is valid by making test API calls to both GraphQL and REST APIs\n * @param token Optional token to validate. If not provided, will use the stored token.\n * @param options Optional configuration for progress display\n * @returns Object containing validation status for both GraphQL and REST APIs\n */\n async validateToken(token?: string, options?: { format?: string; showProgress?: boolean }): Promise<TokenValidationStatus> {\n try {\n // If no token provided, try to get the stored one\n const tokenToValidate = token || await this.getToken();\n if (!tokenToValidate) {\n logger.debug('No token provided for validation');\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n\n // Create clients with the token\n const graphqlClient = new BuildkiteClient(tokenToValidate, { debug: false, caching: false });\n const restClient = new BuildkiteRestClient(tokenToValidate, { debug: false });\n \n // First check if we can list organizations\n let orgSlugs: string[] = [];\n try {\n orgSlugs = await graphqlClient.getOrganizations().then(orgs => orgs.map(org => org.slug));\n logger.debug('Successfully retrieved organization slugs');\n } catch (error) {\n logger.debug('Failed to retrieve organization slugs', {\n error: error instanceof Error ? error.message : String(error),\n cause: error instanceof Error && error.cause ? error.cause : undefined\n });\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n\n const organizations: Record<string, OrganizationValidationStatus> = {};\n let allValid = true;\n\n // Determine if we should show progress\n const showProgress = options?.showProgress !== false && \n !isRunningInAlfred() && \n orgSlugs.length > 0;\n \n const progress = showProgress ? Progress.bar({\n total: orgSlugs.length * 3,\n label: 'Validating token access',\n format: options?.format\n }) : null;\n\n let checkCount = 0;\n\n // Validate each organization\n for (const orgSlug of orgSlugs) {\n const orgStatus: OrganizationValidationStatus = {\n graphql: false,\n builds: false,\n organizations: false\n };\n\n // Check GraphQL access\n if (progress) {\n progress.update(checkCount++, `Checking GraphQL access for ${orgSlug}`);\n }\n try {\n await graphqlClient.getViewer();\n orgStatus.graphql = true;\n } catch (error) {\n logger.debug(`GraphQL validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n // Check build access\n if (progress) {\n progress.update(checkCount++, `Checking build access for ${orgSlug}`);\n }\n try {\n await restClient.hasBuildAccess(orgSlug);\n orgStatus.builds = true;\n } catch (error) {\n logger.debug(`Build access validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n // Check organization access\n if (progress) {\n progress.update(checkCount++, `Checking organization access for ${orgSlug}`);\n }\n try {\n await restClient.hasOrganizationAccess(orgSlug);\n orgStatus.organizations = true;\n } catch (error) {\n logger.debug(`Organization access validation failed for organization ${orgSlug}`, error);\n allValid = false;\n }\n\n organizations[orgSlug] = orgStatus;\n }\n\n // Complete the progress bar\n if (progress) {\n const successCount = Object.values(organizations)\n .filter(org => org.graphql && org.builds && org.organizations)\n .length;\n progress.complete(`✓ Validated ${orgSlugs.length} organizations (${successCount} fully accessible)`);\n }\n\n return {\n valid: allValid,\n canListOrganizations: true,\n organizations\n };\n } catch (error) {\n logger.debug('Token validation failed', error);\n return { \n valid: false, \n canListOrganizations: false,\n organizations: {} \n };\n }\n }\n}"]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Custom Help formatter for Commander with width-aware text wrapping
3
+ */
4
+ import { Help } from 'commander';
5
+ import { termWidth, wrapText } from './width.js';
6
+ export class WidthAwareHelp extends Help {
7
+ /**
8
+ * Get the terminal width with a max limit for readability
9
+ */
10
+ getHelpWidth() {
11
+ // Cap at 100 chars for readability, even on ultra-wide terminals
12
+ return Math.min(termWidth(), 100);
13
+ }
14
+ /**
15
+ * Wrap text to fit terminal width
16
+ */
17
+ wrapLine(text, indent = 0) {
18
+ const width = this.getHelpWidth();
19
+ const effectiveWidth = Math.max(40, width - indent);
20
+ const lines = wrapText(text, effectiveWidth);
21
+ const indentStr = ' '.repeat(indent);
22
+ return lines.map((line, i) => i === 0 ? line : indentStr + line).join('\n');
23
+ }
24
+ /**
25
+ * Override formatHelp to apply width-aware formatting
26
+ */
27
+ formatHelp(cmd, helper) {
28
+ const width = this.getHelpWidth();
29
+ // Get the default help text
30
+ let helpText = super.formatHelp(cmd, helper);
31
+ // Process each line to apply wrapping
32
+ const lines = helpText.split('\n');
33
+ const processedLines = [];
34
+ for (const line of lines) {
35
+ // Skip empty lines
36
+ if (line.trim() === '') {
37
+ processedLines.push(line);
38
+ continue;
39
+ }
40
+ // Detect option/command lines (they start with spaces and contain two or more spaces for alignment)
41
+ const optionMatch = line.match(/^(\s+)(\S+)\s{2,}(.*)$/);
42
+ if (optionMatch) {
43
+ const [, indent, option, description] = optionMatch;
44
+ const optionIndent = indent.length;
45
+ const descIndent = optionIndent + option.length + 2;
46
+ // If the line is too long, wrap the description
47
+ if (line.length > width) {
48
+ const wrappedDesc = this.wrapLine(description, descIndent);
49
+ processedLines.push(`${indent}${option} ${wrappedDesc}`);
50
+ }
51
+ else {
52
+ processedLines.push(line);
53
+ }
54
+ }
55
+ else if (line.length > width) {
56
+ // For other long lines (like descriptions), wrap them
57
+ const leadingSpaces = line.match(/^(\s*)/)?.[1] || '';
58
+ const content = line.trim();
59
+ const wrapped = this.wrapLine(content, leadingSpaces.length);
60
+ processedLines.push(leadingSpaces + wrapped);
61
+ }
62
+ else {
63
+ processedLines.push(line);
64
+ }
65
+ }
66
+ return processedLines.join('\n');
67
+ }
68
+ }
69
+ //# sourceMappingURL=help.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.js","sourceRoot":"/","sources":["ui/help.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom Help formatter for Commander with width-aware text wrapping\n */\n\nimport { Help } from 'commander';\nimport { termWidth, wrapText } from './width.js';\n\nexport class WidthAwareHelp extends Help {\n /**\n * Get the terminal width with a max limit for readability\n */\n private getHelpWidth(): number {\n // Cap at 100 chars for readability, even on ultra-wide terminals\n return Math.min(termWidth(), 100);\n }\n\n /**\n * Wrap text to fit terminal width\n */\n private wrapLine(text: string, indent = 0): string {\n const width = this.getHelpWidth();\n const effectiveWidth = Math.max(40, width - indent);\n const lines = wrapText(text, effectiveWidth);\n const indentStr = ' '.repeat(indent);\n return lines.map((line, i) => i === 0 ? line : indentStr + line).join('\\n');\n }\n\n /**\n * Override formatHelp to apply width-aware formatting\n */\n formatHelp(cmd: any, helper: Help): string {\n const width = this.getHelpWidth();\n \n // Get the default help text\n let helpText = super.formatHelp(cmd, helper);\n \n // Process each line to apply wrapping\n const lines = helpText.split('\\n');\n const processedLines: string[] = [];\n \n for (const line of lines) {\n // Skip empty lines\n if (line.trim() === '') {\n processedLines.push(line);\n continue;\n }\n \n // Detect option/command lines (they start with spaces and contain two or more spaces for alignment)\n const optionMatch = line.match(/^(\\s+)(\\S+)\\s{2,}(.*)$/);\n if (optionMatch) {\n const [, indent, option, description] = optionMatch;\n const optionIndent = indent.length;\n const descIndent = optionIndent + option.length + 2;\n \n // If the line is too long, wrap the description\n if (line.length > width) {\n const wrappedDesc = this.wrapLine(description, descIndent);\n processedLines.push(`${indent}${option} ${wrappedDesc}`);\n } else {\n processedLines.push(line);\n }\n } else if (line.length > width) {\n // For other long lines (like descriptions), wrap them\n const leadingSpaces = line.match(/^(\\s*)/)?.[1] || '';\n const content = line.trim();\n const wrapped = this.wrapLine(content, leadingSpaces.length);\n processedLines.push(leadingSpaces + wrapped);\n } else {\n processedLines.push(line);\n }\n }\n \n return processedLines.join('\\n');\n }\n\n\n}\n"]}
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Unified progress indicator system
3
+ * Provides both determinate (bar) and indeterminate (spinner) progress indicators
4
+ */
5
+ import { COLORS, SYMBOLS } from './theme.js';
6
+ import { termWidth, truncate } from './width.js';
7
+ /**
8
+ * Check if output format is machine-readable
9
+ */
10
+ function isMachineFormat(format) {
11
+ const f = (format || '').toLowerCase();
12
+ return f === 'json' || f === 'alfred';
13
+ }
14
+ /**
15
+ * Check if we should show progress indicators
16
+ */
17
+ function shouldShowProgress(format) {
18
+ // Don't show in non-TTY environments
19
+ if (!process.stderr.isTTY)
20
+ return false;
21
+ // Don't show for machine formats
22
+ if (format && isMachineFormat(format))
23
+ return false;
24
+ // Don't show in CI environments
25
+ if (process.env.CI)
26
+ return false;
27
+ // Don't show if NO_COLOR is set (indicates non-interactive)
28
+ if (process.env.NO_COLOR)
29
+ return false;
30
+ return true;
31
+ }
32
+ /**
33
+ * Spinner for indeterminate progress
34
+ * Shows animated spinner with updating label
35
+ */
36
+ class Spinner {
37
+ format;
38
+ interval;
39
+ frame = 0;
40
+ frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
41
+ label = '';
42
+ lastLineLength = 0;
43
+ isActive = false;
44
+ stream = process.stderr;
45
+ constructor(label, format) {
46
+ this.format = format;
47
+ if (label)
48
+ this.label = label;
49
+ }
50
+ shouldShow() {
51
+ return shouldShowProgress(this.format);
52
+ }
53
+ start() {
54
+ if (!this.shouldShow() || this.isActive)
55
+ return;
56
+ this.isActive = true;
57
+ this.interval = setInterval(() => {
58
+ this.render();
59
+ this.frame = (this.frame + 1) % this.frames.length;
60
+ }, 80);
61
+ }
62
+ update(_value, label) {
63
+ // For spinner, we only care about label updates
64
+ if (label !== undefined) {
65
+ this.label = label;
66
+ }
67
+ if (!this.isActive) {
68
+ this.start();
69
+ }
70
+ }
71
+ stop() {
72
+ if (!this.isActive)
73
+ return;
74
+ if (this.interval) {
75
+ clearInterval(this.interval);
76
+ this.interval = undefined;
77
+ }
78
+ this.clear();
79
+ this.isActive = false;
80
+ }
81
+ complete(message) {
82
+ this.stop();
83
+ if (message && this.shouldShow()) {
84
+ this.stream.write(COLORS.success(`${SYMBOLS.success} ${message}\n`));
85
+ }
86
+ }
87
+ fail(message) {
88
+ this.stop();
89
+ if (message && this.shouldShow()) {
90
+ this.stream.write(COLORS.error(`${SYMBOLS.error} ${message}\n`));
91
+ }
92
+ }
93
+ clear() {
94
+ if (!this.stream.isTTY)
95
+ return;
96
+ this.stream.write('\r' + ' '.repeat(this.lastLineLength) + '\r');
97
+ }
98
+ render() {
99
+ if (!this.shouldShow() || !this.isActive)
100
+ return;
101
+ const spinner = COLORS.info(this.frames[this.frame]);
102
+ const line = this.label ? `${spinner} ${this.label}` : spinner;
103
+ this.clear();
104
+ this.stream.write(line);
105
+ this.lastLineLength = line.length;
106
+ }
107
+ }
108
+ /**
109
+ * Progress bar for determinate progress
110
+ * Shows percentage and optional counts
111
+ */
112
+ class Bar {
113
+ current = 0;
114
+ total;
115
+ barWidth = 30;
116
+ label;
117
+ lastLineLength = 0;
118
+ isActive = false;
119
+ stream = process.stderr;
120
+ format;
121
+ constructor(options) {
122
+ this.total = options.total || 100;
123
+ this.label = options.label;
124
+ this.barWidth = options.barWidth || 30;
125
+ this.format = options.format;
126
+ }
127
+ shouldShow() {
128
+ return shouldShowProgress(this.format);
129
+ }
130
+ start() {
131
+ if (!this.shouldShow() || this.isActive)
132
+ return;
133
+ this.isActive = true;
134
+ this.render();
135
+ }
136
+ update(value, label) {
137
+ if (!this.isActive) {
138
+ this.start();
139
+ }
140
+ // For bar, value should be a number
141
+ if (typeof value === 'number') {
142
+ this.current = Math.min(value, this.total);
143
+ }
144
+ if (label !== undefined) {
145
+ this.label = label;
146
+ }
147
+ if (this.shouldShow() && this.isActive) {
148
+ this.render();
149
+ }
150
+ }
151
+ stop() {
152
+ if (!this.isActive)
153
+ return;
154
+ this.clear();
155
+ this.isActive = false;
156
+ }
157
+ complete(message) {
158
+ if (!this.shouldShow())
159
+ return;
160
+ this.current = this.total;
161
+ this.render();
162
+ this.clear();
163
+ if (message) {
164
+ this.stream.write(COLORS.success(`${SYMBOLS.success} ${message}\n`));
165
+ }
166
+ this.isActive = false;
167
+ }
168
+ fail(message) {
169
+ this.stop();
170
+ if (message && this.shouldShow()) {
171
+ this.stream.write(COLORS.error(`${SYMBOLS.error} ${message}\n`));
172
+ }
173
+ }
174
+ clear() {
175
+ if (!this.stream.isTTY)
176
+ return;
177
+ this.stream.write('\r' + ' '.repeat(this.lastLineLength) + '\r');
178
+ }
179
+ render() {
180
+ if (!this.shouldShow() || !this.isActive)
181
+ return;
182
+ const percentage = Math.round((this.current / this.total) * 100);
183
+ const filledLength = Math.round((this.current / this.total) * this.barWidth);
184
+ const emptyLength = this.barWidth - filledLength;
185
+ const filled = '█'.repeat(filledLength);
186
+ const empty = '░'.repeat(emptyLength);
187
+ const bar = `[${filled}${empty}]`;
188
+ const parts = [];
189
+ if (this.label) {
190
+ const maxLabelWidth = Math.max(20, termWidth() - this.barWidth - 20);
191
+ parts.push(truncate(this.label, maxLabelWidth));
192
+ }
193
+ parts.push(bar);
194
+ parts.push(`${percentage}%`);
195
+ parts.push(`(${this.current}/${this.total})`);
196
+ const line = parts.join(' ');
197
+ this.clear();
198
+ this.stream.write(line);
199
+ this.lastLineLength = line.length;
200
+ }
201
+ }
202
+ /**
203
+ * No-op progress for non-interactive environments
204
+ */
205
+ class NoOpProgress {
206
+ update() { }
207
+ stop() { }
208
+ complete() { }
209
+ fail() { }
210
+ }
211
+ /**
212
+ * Main Progress API - factory methods for creating progress indicators
213
+ */
214
+ export class Progress {
215
+ /**
216
+ * Create a spinner (indeterminate progress)
217
+ * Use for operations of unknown duration
218
+ */
219
+ static spinner(label, options) {
220
+ if (!shouldShowProgress(options?.format)) {
221
+ return new NoOpProgress();
222
+ }
223
+ const spinner = new Spinner(label, options?.format);
224
+ if (label) {
225
+ spinner.start();
226
+ }
227
+ return spinner;
228
+ }
229
+ /**
230
+ * Create a progress bar (determinate progress)
231
+ * Use when you know the total number of items
232
+ */
233
+ static bar(options) {
234
+ if (!shouldShowProgress(options.format)) {
235
+ return new NoOpProgress();
236
+ }
237
+ const bar = new Bar(options);
238
+ bar.start();
239
+ return bar;
240
+ }
241
+ /**
242
+ * Smart factory that creates appropriate progress type
243
+ * Creates bar if total is provided, spinner otherwise
244
+ */
245
+ static create(options) {
246
+ if (!options) {
247
+ return Progress.spinner();
248
+ }
249
+ if (options.total !== undefined && options.total > 0) {
250
+ return Progress.bar(options);
251
+ }
252
+ return Progress.spinner(options.label, options);
253
+ }
254
+ }
255
+ /**
256
+ * Helper for async operations with progress tracking
257
+ */
258
+ export async function withProgress(operation, options) {
259
+ const progress = Progress.create(options);
260
+ try {
261
+ const result = await operation(progress);
262
+ progress.complete(options?.successMessage);
263
+ return result;
264
+ }
265
+ catch (error) {
266
+ progress.fail(error instanceof Error ? error.message : 'Operation failed');
267
+ throw error;
268
+ }
269
+ }
270
+ // ============================================================================
271
+ // Legacy API - for backward compatibility during migration
272
+ // ============================================================================
273
+ /**
274
+ * @deprecated Use Progress.bar() instead
275
+ */
276
+ export class ProgressBar {
277
+ progress;
278
+ constructor(options) {
279
+ this.progress = Progress.bar({
280
+ total: options.total,
281
+ label: options.label,
282
+ format: options.format
283
+ });
284
+ }
285
+ start() {
286
+ // Already started in constructor
287
+ }
288
+ update(value, label) {
289
+ this.progress.update(value, label);
290
+ }
291
+ stop() {
292
+ this.progress.stop();
293
+ }
294
+ complete(message) {
295
+ this.progress.complete(message);
296
+ }
297
+ }
298
+ /**
299
+ * @deprecated Use Progress.spinner() instead
300
+ */
301
+ export class IndeterminateProgress {
302
+ progress;
303
+ constructor(label, format) {
304
+ this.progress = Progress.spinner(label, { format });
305
+ }
306
+ start() {
307
+ // Already started if label provided
308
+ }
309
+ updateLabel(label) {
310
+ this.progress.update(label, label);
311
+ }
312
+ stop() {
313
+ this.progress.stop();
314
+ }
315
+ complete(message) {
316
+ this.progress.complete(message);
317
+ }
318
+ }
319
+ /**
320
+ * @deprecated Use withProgress() instead
321
+ */
322
+ export async function withCountedProgress(items, operation, options = {}) {
323
+ if (!items || items.length === 0) {
324
+ return;
325
+ }
326
+ const progress = Progress.bar({
327
+ total: items.length,
328
+ label: options.label || 'Processing',
329
+ format: options.format
330
+ });
331
+ try {
332
+ for (let i = 0; i < items.length; i++) {
333
+ const label = options.itemLabel ?
334
+ options.itemLabel(items[i], i) :
335
+ `Processing item ${i + 1}/${items.length}`;
336
+ progress.update(i, label);
337
+ await operation(items[i], i);
338
+ }
339
+ progress.update(items.length, 'Complete');
340
+ const completeMessage = options.onComplete ?
341
+ options.onComplete(items.length) :
342
+ `Processed ${items.length} items`;
343
+ progress.complete(completeMessage);
344
+ }
345
+ catch (error) {
346
+ progress.stop();
347
+ throw error;
348
+ }
349
+ }
350
+ /**
351
+ * @deprecated Use withProgress() instead
352
+ */
353
+ export async function withIndeterminateProgress(operation, label, format, successMessage) {
354
+ return withProgress(operation, { label, format, successMessage });
355
+ }
356
+ //# sourceMappingURL=progress.js.map