@heroku/sdk 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE.txt +207 -0
  2. package/README.md +94 -0
  3. package/dist/compositions/add-on.d.ts +7 -0
  4. package/dist/compositions/add-on.js +6 -0
  5. package/dist/compositions/app.d.ts +8 -0
  6. package/dist/compositions/app.js +11 -0
  7. package/dist/compositions/dyno.d.ts +17 -0
  8. package/dist/compositions/dyno.js +23 -0
  9. package/dist/compositions/pg.d.ts +15 -0
  10. package/dist/compositions/pg.js +56 -0
  11. package/dist/compositions/pipeline.d.ts +33 -0
  12. package/dist/compositions/pipeline.js +146 -0
  13. package/dist/core/create-client.d.ts +32 -0
  14. package/dist/core/create-client.js +48 -0
  15. package/dist/core/dispatcher.d.ts +3 -0
  16. package/dist/core/dispatcher.js +80 -0
  17. package/dist/core/extend-resource.d.ts +24 -0
  18. package/dist/core/extend-resource.js +3 -0
  19. package/dist/core/extensions-proxy.d.ts +2 -0
  20. package/dist/core/extensions-proxy.js +56 -0
  21. package/dist/core/heroku-sdk.d.ts +14 -0
  22. package/dist/core/heroku-sdk.js +54 -0
  23. package/dist/core/interpolate-path.d.ts +3 -0
  24. package/dist/core/interpolate-path.js +13 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +2 -0
  27. package/dist/resources/data/backup.d.ts +9 -0
  28. package/dist/resources/data/backup.js +10 -0
  29. package/dist/resources/data/database.d.ts +54 -0
  30. package/dist/resources/data/database.js +44 -0
  31. package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
  32. package/dist/resources/data/internal/resolve-addon-id.js +13 -0
  33. package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
  34. package/dist/resources/data/internal/resolve-pg-database.js +52 -0
  35. package/dist/resources/data/maintenance.d.ts +9 -0
  36. package/dist/resources/data/maintenance.js +10 -0
  37. package/dist/resources/data/postgres-credential.d.ts +11 -0
  38. package/dist/resources/data/postgres-credential.js +16 -0
  39. package/dist/resources/data/postgres-database.d.ts +32 -0
  40. package/dist/resources/data/postgres-database.js +14 -0
  41. package/dist/resources/data/postgres.d.ts +9 -0
  42. package/dist/resources/data/postgres.js +10 -0
  43. package/dist/resources/data/transfer-schedule.d.ts +9 -0
  44. package/dist/resources/data/transfer-schedule.js +10 -0
  45. package/dist/resources/data/transfer.d.ts +49 -0
  46. package/dist/resources/data/transfer.js +14 -0
  47. package/dist/resources/extensions/data.d.ts +4 -0
  48. package/dist/resources/extensions/data.js +4 -0
  49. package/dist/resources/extensions/platform.d.ts +8 -0
  50. package/dist/resources/extensions/platform.js +8 -0
  51. package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
  52. package/dist/resources/platform/add-on/create-and-wait.js +63 -0
  53. package/dist/resources/platform/add-on/debug.d.ts +2 -0
  54. package/dist/resources/platform/add-on/debug.js +2 -0
  55. package/dist/resources/platform/add-on/describe.d.ts +20 -0
  56. package/dist/resources/platform/add-on/describe.js +51 -0
  57. package/dist/resources/platform/add-on/errors.d.ts +51 -0
  58. package/dist/resources/platform/add-on/errors.js +61 -0
  59. package/dist/resources/platform/add-on/index.d.ts +23 -0
  60. package/dist/resources/platform/add-on/index.js +25 -0
  61. package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
  62. package/dist/resources/platform/add-on/list-plans.js +53 -0
  63. package/dist/resources/platform/add-on/pricing.d.ts +72 -0
  64. package/dist/resources/platform/add-on/pricing.js +100 -0
  65. package/dist/resources/platform/add-on/resolve.d.ts +43 -0
  66. package/dist/resources/platform/add-on/resolve.js +103 -0
  67. package/dist/resources/platform/add-on/types.d.ts +49 -0
  68. package/dist/resources/platform/add-on/types.js +1 -0
  69. package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
  70. package/dist/resources/platform/add-on/upgrade.js +36 -0
  71. package/dist/resources/platform/add-on.d.ts +211 -0
  72. package/dist/resources/platform/add-on.js +354 -0
  73. package/dist/resources/platform/app/index.d.ts +58 -0
  74. package/dist/resources/platform/app/index.js +53 -0
  75. package/dist/resources/platform/app/info.d.ts +30 -0
  76. package/dist/resources/platform/app/info.js +27 -0
  77. package/dist/resources/platform/app.d.ts +56 -0
  78. package/dist/resources/platform/app.js +50 -0
  79. package/dist/resources/platform/dyno/index.d.ts +45 -0
  80. package/dist/resources/platform/dyno/index.js +26 -0
  81. package/dist/resources/platform/dyno.d.ts +45 -0
  82. package/dist/resources/platform/dyno.js +26 -0
  83. package/dist/resources/platform/log-session/index.d.ts +102 -0
  84. package/dist/resources/platform/log-session/index.js +223 -0
  85. package/dist/resources/platform/log-session/parser.d.ts +74 -0
  86. package/dist/resources/platform/log-session/parser.js +71 -0
  87. package/dist/resources/platform/log-session.d.ts +81 -0
  88. package/dist/resources/platform/log-session.js +167 -0
  89. package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
  90. package/dist/resources/platform/pipeline/coupling.js +55 -0
  91. package/dist/resources/platform/pipeline/index.d.ts +53 -0
  92. package/dist/resources/platform/pipeline/index.js +75 -0
  93. package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
  94. package/dist/resources/platform/pipeline/promotion.js +109 -0
  95. package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
  96. package/dist/resources/platform/pipeline-coupling.js +55 -0
  97. package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
  98. package/dist/resources/platform/pipeline-promotion.js +128 -0
  99. package/dist/resources/platform/pipeline.d.ts +53 -0
  100. package/dist/resources/platform/pipeline.js +75 -0
  101. package/dist/resources/platform/shield.d.ts +2 -0
  102. package/dist/resources/platform/shield.js +6 -0
  103. package/dist/services/data.d.ts +5 -0
  104. package/dist/services/data.js +5 -0
  105. package/dist/services/platform.d.ts +5 -0
  106. package/dist/services/platform.js +5 -0
  107. package/dist/utils/wait.d.ts +16 -0
  108. package/dist/utils/wait.js +35 -0
  109. package/package.json +54 -0
@@ -0,0 +1,128 @@
1
+ import { HerokuApiClient } from '@heroku/heroku-fetch';
2
+ import createDebug from 'debug';
3
+ import { extendResource } from '../../core/extend-resource.js';
4
+ const debug = createDebug('heroku:sdk:resources:pipeline-promotion');
5
+ const DEFAULT_INTERVAL_MS = 1000;
6
+ const DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS = 100;
7
+ export async function promotePipeline(ctx, body, options = {}) {
8
+ const { intervalMs = DEFAULT_INTERVAL_MS, onReleaseStream, releaseStreamMaxAttempts = DEFAULT_RELEASE_STREAM_MAX_ATTEMPTS, signal, timeoutMs, } = options;
9
+ debug('promote source=%s targets=%d', body.source?.app?.id ?? '<unknown>', body.targets?.length ?? 0);
10
+ const promotion = await ctx.platform.pipelinePromotion.create(body);
11
+ if (!promotion.id) {
12
+ throw new Error('Pipeline promotion response did not include an id');
13
+ }
14
+ debug('promote created promotion=%s intervalMs=%d timeoutMs=%s', promotion.id, intervalMs, timeoutMs ?? '<none>');
15
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
16
+ const streamRunner = onReleaseStream
17
+ ? createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
18
+ : undefined;
19
+ let poll = 0;
20
+ while (true) {
21
+ signal?.throwIfAborted();
22
+ poll++;
23
+ // eslint-disable-next-line no-await-in-loop
24
+ const targets = await ctx.platform.pipelinePromotionTarget.list(promotion.id);
25
+ debug('promote poll=%d promotion=%s statuses=%o', poll, promotion.id, targets.map(target => target.status));
26
+ if (targets.every(target => target.status !== 'pending')) {
27
+ debug('promote terminal promotion=%s polls=%d', promotion.id, poll);
28
+ return { promotion, targets };
29
+ }
30
+ // eslint-disable-next-line no-await-in-loop
31
+ await streamRunner?.(targets);
32
+ if (deadline !== undefined && Date.now() >= deadline) {
33
+ throw new Error(`Pipeline promotion ${promotion.id} did not reach a terminal state within ${timeoutMs}ms`);
34
+ }
35
+ // eslint-disable-next-line no-await-in-loop
36
+ await wait(intervalMs, signal);
37
+ }
38
+ }
39
+ /**
40
+ * Returns a function that, on first call where `targets` is a single
41
+ * promotion target with `release.id` and `app.id`, fetches the release's
42
+ * output stream and hands it to `onReleaseStream`. Subsequent calls are
43
+ * no-ops; the stream is one-shot per promotion.
44
+ */
45
+ function createReleaseStreamRunner(ctx, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
46
+ let handled = false;
47
+ return async (targets) => {
48
+ if (handled || targets.length !== 1)
49
+ return;
50
+ const target = targets[0];
51
+ if (!target.release?.id || !target.app?.id)
52
+ return;
53
+ handled = true;
54
+ debug('promote release-stream lookup app=%s release=%s', target.app.id, target.release.id);
55
+ const release = await ctx.platform.release.info(target.app.id, target.release.id);
56
+ if (!release.output_stream_url) {
57
+ debug('promote release-stream skipped reason=no-output-stream-url');
58
+ return;
59
+ }
60
+ debug('promote release-stream fetching url=%s maxAttempts=%d', release.output_stream_url, releaseStreamMaxAttempts);
61
+ const stream = await fetchReleaseOutput(release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
62
+ await onReleaseStream({ stream, target });
63
+ };
64
+ }
65
+ export const pipelinePromotionExtensions = extendResource('platform', 'pipelinePromotion', ctx => ({
66
+ promote: (body, options) => promotePipeline(ctx, body, options),
67
+ }));
68
+ async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
69
+ const parsed = new URL(url);
70
+ const path = `${parsed.pathname}${parsed.search}`;
71
+ // The release output URL points at a third-party stream host (e.g. busl).
72
+ // Use a custom-service client so the platform prefixUrl and bearer token
73
+ // don't leak across origins.
74
+ const buslClient = new HerokuApiClient({
75
+ baseUrl: parsed.origin,
76
+ service: 'custom',
77
+ token: '',
78
+ });
79
+ let attempt = 0;
80
+ while (true) {
81
+ signal?.throwIfAborted();
82
+ attempt++;
83
+ let response;
84
+ try {
85
+ // eslint-disable-next-line no-await-in-loop
86
+ response = await buslClient.stream(path);
87
+ }
88
+ catch {
89
+ // Treat HTTP errors thrown by the api-client as a retryable miss.
90
+ response = undefined;
91
+ }
92
+ if (response?.ok && response.body) {
93
+ debug('release-stream attempt=%d status=%d ok stream=true', attempt, response.status);
94
+ return response.body;
95
+ }
96
+ debug('release-stream attempt=%d status=%s ok=false', attempt, response?.status ?? '<error>');
97
+ if (response?.body) {
98
+ // Drain so the connection can be reused.
99
+ // eslint-disable-next-line no-await-in-loop
100
+ await response.body.cancel().catch(() => { });
101
+ }
102
+ if (attempt >= maxAttempts) {
103
+ throw new Error('stream release output not available');
104
+ }
105
+ // eslint-disable-next-line no-await-in-loop
106
+ await wait(intervalMs, signal);
107
+ }
108
+ }
109
+ function wait(ms, signal) {
110
+ return new Promise((resolve, reject) => {
111
+ function onAbort() {
112
+ clearTimeout(timer);
113
+ reject(signal.reason ?? new Error('Aborted'));
114
+ }
115
+ const timer = setTimeout(() => {
116
+ signal?.removeEventListener('abort', onAbort);
117
+ resolve();
118
+ }, ms);
119
+ if (signal) {
120
+ if (signal.aborted) {
121
+ clearTimeout(timer);
122
+ reject(signal.reason ?? new Error('Aborted'));
123
+ return;
124
+ }
125
+ signal.addEventListener('abort', onAbort, { once: true });
126
+ }
127
+ });
128
+ }
@@ -0,0 +1,53 @@
1
+ import type { Pipeline } from '@heroku/types/3.sdk';
2
+ import { type HerokuApiClientOptions } from '@heroku/heroku-fetch';
3
+ import type { ResourceCtx } from '../../core/extend-resource.js';
4
+ export type ResolvePipelineOptions = {
5
+ clientOptions?: HerokuApiClientOptions;
6
+ signal?: AbortSignal;
7
+ };
8
+ export declare class PipelineNotFoundError extends Error {
9
+ readonly identity: string;
10
+ readonly id = "not_found";
11
+ readonly statusCode = 404;
12
+ constructor(identity: string);
13
+ get body(): {
14
+ id: string;
15
+ message: string;
16
+ resource: string;
17
+ };
18
+ }
19
+ export declare class PipelineAmbiguousError extends Error {
20
+ readonly identity: string;
21
+ readonly matches: Pipeline[];
22
+ readonly id = "multiple_matches";
23
+ readonly statusCode = 422;
24
+ constructor(identity: string, matches: Pipeline[]);
25
+ get body(): {
26
+ id: string;
27
+ message: string;
28
+ };
29
+ }
30
+ /**
31
+ * Resolve a Pipeline by identity.
32
+ *
33
+ * The identity may be:
34
+ * - a UUID — fetched directly via `pipeline.info`.
35
+ * - a name — looked up via `GET /pipelines?eq[name]=<name>`. Pipeline
36
+ * names are not unique platform-wide, so a name lookup may return
37
+ * zero, one, or many matches.
38
+ *
39
+ * The name path uses a raw `HerokuApiClient` because the route
40
+ * registry's `pipeline.list` doesn't accept query params, and we don't
41
+ * want to bake those params into the routes-generated client (where
42
+ * they would leak across unrelated calls).
43
+ *
44
+ * Errors:
45
+ * - throws `PipelineNotFoundError` when no pipeline matches the name.
46
+ * - throws `PipelineAmbiguousError` when more than one does. Callers
47
+ * that want to prompt the user should catch this and inspect
48
+ * `error.matches`.
49
+ */
50
+ export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
51
+ export declare const pipelineExtensions: import("../../index.js").ResourceExtension<"platform", "pipeline", {
52
+ resolve: (identity: string, options?: ResolvePipelineOptions) => Promise<Pipeline>;
53
+ }>;
@@ -0,0 +1,75 @@
1
+ import { HerokuApiClient } from '@heroku/heroku-fetch';
2
+ import { extendResource } from '../../core/extend-resource.js';
3
+ export class PipelineNotFoundError extends Error {
4
+ identity;
5
+ id = 'not_found';
6
+ statusCode = 404;
7
+ constructor(identity) {
8
+ super(`Couldn't find a pipeline matching '${identity}'.`);
9
+ this.identity = identity;
10
+ this.name = 'PipelineNotFoundError';
11
+ }
12
+ get body() {
13
+ return { id: this.id, message: this.message, resource: 'pipeline' };
14
+ }
15
+ }
16
+ export class PipelineAmbiguousError extends Error {
17
+ identity;
18
+ matches;
19
+ id = 'multiple_matches';
20
+ statusCode = 422;
21
+ constructor(identity, matches) {
22
+ super(`Ambiguous identifier; multiple pipelines named '${identity}' found (${matches.length} matches).`);
23
+ this.identity = identity;
24
+ this.matches = matches;
25
+ this.name = 'PipelineAmbiguousError';
26
+ }
27
+ get body() {
28
+ return { id: this.id, message: this.message };
29
+ }
30
+ }
31
+ const UUID_RE = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
32
+ /**
33
+ * Resolve a Pipeline by identity.
34
+ *
35
+ * The identity may be:
36
+ * - a UUID — fetched directly via `pipeline.info`.
37
+ * - a name — looked up via `GET /pipelines?eq[name]=<name>`. Pipeline
38
+ * names are not unique platform-wide, so a name lookup may return
39
+ * zero, one, or many matches.
40
+ *
41
+ * The name path uses a raw `HerokuApiClient` because the route
42
+ * registry's `pipeline.list` doesn't accept query params, and we don't
43
+ * want to bake those params into the routes-generated client (where
44
+ * they would leak across unrelated calls).
45
+ *
46
+ * Errors:
47
+ * - throws `PipelineNotFoundError` when no pipeline matches the name.
48
+ * - throws `PipelineAmbiguousError` when more than one does. Callers
49
+ * that want to prompt the user should catch this and inspect
50
+ * `error.matches`.
51
+ */
52
+ export async function resolvePipeline(ctx, identity, options = {}) {
53
+ options.signal?.throwIfAborted();
54
+ if (UUID_RE.test(identity)) {
55
+ return ctx.platform.pipeline.info(identity);
56
+ }
57
+ const apiClient = new HerokuApiClient({
58
+ ...options.clientOptions,
59
+ service: 'platform',
60
+ });
61
+ const response = await apiClient.get('/pipelines', {
62
+ searchParams: { 'eq[name]': identity },
63
+ });
64
+ const matches = (await response.json());
65
+ if (matches.length === 0) {
66
+ throw new PipelineNotFoundError(identity);
67
+ }
68
+ if (matches.length > 1) {
69
+ throw new PipelineAmbiguousError(identity, matches);
70
+ }
71
+ return matches[0];
72
+ }
73
+ export const pipelineExtensions = extendResource('platform', 'pipeline', ctx => ({
74
+ resolve: (identity, options) => resolvePipeline(ctx, identity, options),
75
+ }));
@@ -0,0 +1,2 @@
1
+ export declare function shieldToPrivate(size: string): string;
2
+ export declare function privateToShield(size: string): string;
@@ -0,0 +1,6 @@
1
+ export function shieldToPrivate(size) {
2
+ return size.replace(/^Shield-/, 'Private-');
3
+ }
4
+ export function privateToShield(size) {
5
+ return size.replace(/^Private-/, 'Shield-');
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { HerokuClient } from '@heroku/types/data';
3
+ import type { RoutesClientExtras } from '../core/create-client.js';
4
+ export type DataClient = HerokuClient & RoutesClientExtras<HerokuClient>;
5
+ export declare function createDataClient(options?: HerokuApiClientOptions): DataClient;
@@ -0,0 +1,5 @@
1
+ import * as routes from '@heroku/types/data/routes';
2
+ import { createClient } from '../core/create-client.js';
3
+ export function createDataClient(options = {}) {
4
+ return createClient(routes, { service: 'data', ...options });
5
+ }
@@ -0,0 +1,5 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { HerokuClient } from '@heroku/types/3.sdk';
3
+ import type { RoutesClientExtras } from '../core/create-client.js';
4
+ export type PlatformClient = HerokuClient & RoutesClientExtras<HerokuClient>;
5
+ export declare function createPlatformClient(options?: HerokuApiClientOptions): PlatformClient;
@@ -0,0 +1,5 @@
1
+ import * as routes from '@heroku/types/3.sdk/routes';
2
+ import { createClient } from '../core/create-client.js';
3
+ export function createPlatformClient(options = {}) {
4
+ return createClient(routes, { service: 'platform', ...options });
5
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Promise-based delay that respects an optional AbortSignal.
3
+ *
4
+ * Resolves after `ms` milliseconds. If `signal` is provided and is
5
+ * (or becomes) aborted, the promise rejects with `signal.reason`
6
+ * (or `new Error('Aborted')` if `reason` is unset) and clears the
7
+ * pending timer.
8
+ *
9
+ * Used by polling loops in resource extensions (e.g. `addOn.createAndWait`,
10
+ * `pipelinePromotion.promote`) to space out poll requests while staying
11
+ * responsive to cancellation.
12
+ *
13
+ * @param ms Milliseconds to wait before resolving.
14
+ * @param signal Optional AbortSignal to cancel the wait early.
15
+ */
16
+ export declare function wait(ms: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Promise-based delay that respects an optional AbortSignal.
3
+ *
4
+ * Resolves after `ms` milliseconds. If `signal` is provided and is
5
+ * (or becomes) aborted, the promise rejects with `signal.reason`
6
+ * (or `new Error('Aborted')` if `reason` is unset) and clears the
7
+ * pending timer.
8
+ *
9
+ * Used by polling loops in resource extensions (e.g. `addOn.createAndWait`,
10
+ * `pipelinePromotion.promote`) to space out poll requests while staying
11
+ * responsive to cancellation.
12
+ *
13
+ * @param ms Milliseconds to wait before resolving.
14
+ * @param signal Optional AbortSignal to cancel the wait early.
15
+ */
16
+ export function wait(ms, signal) {
17
+ return new Promise((resolve, reject) => {
18
+ function onAbort() {
19
+ clearTimeout(timer);
20
+ reject(signal.reason ?? new Error('Aborted'));
21
+ }
22
+ const timer = setTimeout(() => {
23
+ signal?.removeEventListener('abort', onAbort);
24
+ resolve();
25
+ }, ms);
26
+ if (signal) {
27
+ if (signal.aborted) {
28
+ clearTimeout(timer);
29
+ reject(signal.reason ?? new Error('Aborted'));
30
+ return;
31
+ }
32
+ signal.addEventListener('abort', onAbort, { once: true });
33
+ }
34
+ });
35
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@heroku/sdk",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Heroku SDK",
5
+ "author": "Heroku",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./platform": "./dist/services/platform.js",
11
+ "./data": "./dist/services/data.js",
12
+ "./extensions/platform": "./dist/resources/extensions/platform.js",
13
+ "./extensions/data": "./dist/resources/extensions/data.js",
14
+ "./resources/*": "./dist/resources/*.js"
15
+ },
16
+ "sideEffects": false,
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/heroku/heroku-sdk"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "coverage": "vitest --run --coverage",
24
+ "example": "tsx",
25
+ "lint": "eslint",
26
+ "lint:fix": "eslint --fix",
27
+ "prepare": "npm run build",
28
+ "test": "vitest --run"
29
+ },
30
+ "license": "Apache-2.0",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "devDependencies": {
38
+ "@heroku-cli/test-utils": "^0.3.0",
39
+ "@types/debug": "^4.1.12",
40
+ "@types/node": "^25.6.2",
41
+ "@vitest/coverage-v8": "^4.1.8",
42
+ "eslint": "^9.39.4",
43
+ "eslint-config-oclif": "^6.0.167",
44
+ "eslint-plugin-vitest": "0.5.4",
45
+ "tsx": "^4.22.4",
46
+ "typescript": "^6.0.2",
47
+ "vitest": "^4.1.4"
48
+ },
49
+ "dependencies": {
50
+ "@heroku/heroku-fetch": "^0.1.1-beta",
51
+ "@heroku/types": "^3.0.0-beta",
52
+ "debug": "^4.4.0"
53
+ }
54
+ }