alchemy 0.27.0 → 0.29.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 (63) hide show
  1. package/README.md +1 -1
  2. package/lib/alchemy.d.ts +21 -1
  3. package/lib/alchemy.d.ts.map +1 -1
  4. package/lib/alchemy.js +42 -8
  5. package/lib/alchemy.js.map +1 -1
  6. package/lib/apply.js +3 -12
  7. package/lib/apply.js.map +1 -1
  8. package/lib/cloudflare/do-state-store/internal.d.ts +1 -1
  9. package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
  10. package/lib/cloudflare/do-state-store/internal.js +2 -2
  11. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  12. package/lib/cloudflare/do-state-store/store.d.ts.map +1 -1
  13. package/lib/cloudflare/do-state-store/store.js +19 -4
  14. package/lib/cloudflare/do-state-store/store.js.map +1 -1
  15. package/lib/cloudflare/secrets-store.d.ts.map +1 -1
  16. package/lib/cloudflare/secrets-store.js +21 -17
  17. package/lib/cloudflare/secrets-store.js.map +1 -1
  18. package/lib/cloudflare/worker-assets.d.ts.map +1 -1
  19. package/lib/cloudflare/worker-assets.js +9 -0
  20. package/lib/cloudflare/worker-assets.js.map +1 -1
  21. package/lib/destroy.d.ts.map +1 -1
  22. package/lib/destroy.js +3 -6
  23. package/lib/destroy.js.map +1 -1
  24. package/lib/github/comment.d.ts +156 -0
  25. package/lib/github/comment.d.ts.map +1 -0
  26. package/lib/github/comment.js +192 -0
  27. package/lib/github/comment.js.map +1 -0
  28. package/lib/github/index.d.ts +1 -0
  29. package/lib/github/index.d.ts.map +1 -1
  30. package/lib/github/index.js +1 -0
  31. package/lib/github/index.js.map +1 -1
  32. package/lib/index.d.ts +1 -1
  33. package/lib/index.d.ts.map +1 -1
  34. package/lib/index.js +1 -0
  35. package/lib/index.js.map +1 -1
  36. package/lib/scope.d.ts +2 -1
  37. package/lib/scope.d.ts.map +1 -1
  38. package/lib/scope.js +1 -1
  39. package/lib/scope.js.map +1 -1
  40. package/lib/util/cli.d.ts +2 -13
  41. package/lib/util/cli.d.ts.map +1 -1
  42. package/lib/util/cli.js +46 -60
  43. package/lib/util/cli.js.map +1 -1
  44. package/lib/util/logger.d.ts +1 -1
  45. package/lib/util/logger.d.ts.map +1 -1
  46. package/lib/util/telemetry/client.js +1 -1
  47. package/lib/util/telemetry/client.js.map +1 -1
  48. package/package.json +2 -5
  49. package/src/alchemy.ts +71 -9
  50. package/src/apply.ts +4 -17
  51. package/src/cloudflare/do-state-store/internal.ts +2 -1
  52. package/src/cloudflare/do-state-store/store.ts +26 -4
  53. package/src/cloudflare/secrets-store.ts +22 -21
  54. package/src/cloudflare/worker-assets.ts +16 -0
  55. package/src/destroy.ts +4 -7
  56. package/src/github/comment.ts +278 -0
  57. package/src/github/index.ts +1 -0
  58. package/src/index.ts +1 -1
  59. package/src/scope.ts +10 -6
  60. package/src/util/cli.ts +117 -0
  61. package/src/util/logger.ts +1 -1
  62. package/src/util/telemetry/client.ts +1 -1
  63. package/src/util/cli.tsx +0 -221
@@ -0,0 +1,278 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import type { Secret } from "../secret.ts";
4
+ import { logger } from "../util/logger.ts";
5
+ import { createGitHubClient, verifyGitHubAuth } from "./client.ts";
6
+
7
+ /**
8
+ * Properties for creating or updating a GitHub Comment
9
+ */
10
+ export interface GitHubCommentProps {
11
+ /**
12
+ * Repository owner (user or organization)
13
+ */
14
+ owner: string;
15
+
16
+ /**
17
+ * Repository name
18
+ */
19
+ repository: string;
20
+
21
+ /**
22
+ * Issue or Pull Request number to comment on
23
+ */
24
+ issueNumber: number;
25
+
26
+ /**
27
+ * Comment body (supports GitHub Markdown)
28
+ */
29
+ body: string;
30
+
31
+ /**
32
+ * Whether to allow deletion of the comment
33
+ * By default, comments are never deleted to preserve discussion history
34
+ * @default false
35
+ */
36
+ allowDelete?: boolean;
37
+
38
+ /**
39
+ * Optional GitHub API token (overrides environment variable)
40
+ * If not provided, will use GITHUB_TOKEN or GITHUB_ACCESS_TOKEN environment variables
41
+ * Token must have 'repo' scope for private repositories
42
+ * or 'public_repo' scope for public repositories
43
+ */
44
+ token?: Secret;
45
+ }
46
+
47
+ /**
48
+ * Output returned after Comment creation/update
49
+ */
50
+ export interface GitHubComment
51
+ extends Resource<"github::Comment">,
52
+ Omit<GitHubCommentProps, "token"> {
53
+ /**
54
+ * The ID of the resource
55
+ */
56
+ id: string;
57
+
58
+ /**
59
+ * The numeric ID of the comment in GitHub
60
+ */
61
+ commentId: number;
62
+
63
+ /**
64
+ * URL to view the comment
65
+ */
66
+ htmlUrl: string;
67
+
68
+ /**
69
+ * Time at which the comment was created/updated
70
+ */
71
+ updatedAt: string;
72
+ }
73
+
74
+ /**
75
+ * Resource for managing GitHub issue and pull request comments
76
+ *
77
+ * By default, comments are never deleted to preserve discussion history.
78
+ * Set `allowDelete: true` to enable deletion when the resource is destroyed.
79
+ *
80
+ * Authentication is handled in the following order:
81
+ * 1. `token` parameter in the resource props (if provided)
82
+ * 2. `GITHUB_ACCESS_TOKEN` environment variable (for actions with admin permissions)
83
+ * 3. `GITHUB_TOKEN` environment variable
84
+ * 4. GitHub CLI token (if gh is installed and authenticated)
85
+ *
86
+ * The token must have the following permissions:
87
+ * - 'repo' scope for private repositories
88
+ * - 'public_repo' scope for public repositories
89
+ *
90
+ * @example
91
+ * ## Create a comment on an issue
92
+ *
93
+ * Create a comment on issue #123 using the default GITHUB_TOKEN
94
+ *
95
+ * ```ts
96
+ * const comment = await GitHubComment("issue-comment", {
97
+ * owner: "my-org",
98
+ * repository: "my-repo",
99
+ * issueNumber: 123,
100
+ * body: "This is a comment created by Alchemy!"
101
+ * });
102
+ * ```
103
+ *
104
+ * @example
105
+ * ## Create a comment on a pull request
106
+ *
107
+ * Comments work the same way for pull requests
108
+ *
109
+ * ```ts
110
+ * const prComment = await GitHubComment("pr-comment", {
111
+ * owner: "my-org",
112
+ * repository: "my-repo",
113
+ * issueNumber: 456, // PR number
114
+ * body: "## Deployment Status\n\n✅ Successfully deployed to staging!"
115
+ * });
116
+ * ```
117
+ *
118
+ * @example
119
+ * ## Update comment content
120
+ *
121
+ * Comments can be updated by changing the body content
122
+ *
123
+ * ```ts
124
+ * const comment = await GitHubComment("status-comment", {
125
+ * owner: "my-org",
126
+ * repository: "my-repo",
127
+ * issueNumber: 789,
128
+ * body: "🔄 Deployment in progress..."
129
+ * });
130
+ *
131
+ * // Later, update the comment
132
+ * await GitHubComment("status-comment", {
133
+ * owner: "my-org",
134
+ * repository: "my-repo",
135
+ * issueNumber: 789,
136
+ * body: "✅ Deployment completed successfully!"
137
+ * });
138
+ * ```
139
+ *
140
+ * @example
141
+ * ## Allow comment deletion
142
+ *
143
+ * By default comments are preserved, but you can opt-in to deletion
144
+ *
145
+ * ```ts
146
+ * const comment = await GitHubComment("temp-comment", {
147
+ * owner: "my-org",
148
+ * repository: "my-repo",
149
+ * issueNumber: 123,
150
+ * body: "This comment can be deleted",
151
+ * allowDelete: true
152
+ * });
153
+ * ```
154
+ *
155
+ * @example
156
+ * ## Use custom authentication token
157
+ *
158
+ * Pass a custom GitHub token for authentication
159
+ *
160
+ * ```ts
161
+ * const comment = await GitHubComment("authenticated-comment", {
162
+ * owner: "my-org",
163
+ * repository: "my-repo",
164
+ * issueNumber: 123,
165
+ * body: "Comment with custom token",
166
+ * token: alchemy.secret(process.env.CUSTOM_GITHUB_TOKEN)
167
+ * });
168
+ * ```
169
+ */
170
+ export const GitHubComment = Resource(
171
+ "github::Comment",
172
+ async function (
173
+ this: Context<GitHubComment>,
174
+ _id: string,
175
+ props: GitHubCommentProps,
176
+ ): Promise<GitHubComment> {
177
+ // Create authenticated Octokit client - will automatically handle token resolution
178
+ const octokit = await createGitHubClient({
179
+ token: props.token?.unencrypted,
180
+ });
181
+
182
+ // Verify authentication and permissions
183
+ if (!this.quiet) {
184
+ await verifyGitHubAuth(octokit, props.owner, props.repository);
185
+ }
186
+
187
+ if (this.phase === "delete") {
188
+ if (this.output?.commentId && props.allowDelete) {
189
+ try {
190
+ // Delete the comment
191
+ await octokit.rest.issues.deleteComment({
192
+ owner: props.owner,
193
+ repo: props.repository,
194
+ comment_id: this.output.commentId,
195
+ });
196
+ } catch (error: any) {
197
+ // Ignore 404 errors (comment already deleted)
198
+ if (error.status === 404) {
199
+ logger.log("Comment doesn't exist, ignoring");
200
+ } else {
201
+ throw error;
202
+ }
203
+ }
204
+ } else if (this.output?.commentId && !props.allowDelete) {
205
+ logger.log(
206
+ "Comment deletion skipped - allowDelete is false (default behavior to preserve discussion history)",
207
+ );
208
+ }
209
+
210
+ // Return void (a deleted resource has no content)
211
+ return this.destroy();
212
+ }
213
+
214
+ try {
215
+ if (this.phase === "update" && this.output?.commentId) {
216
+ // Update existing comment
217
+ const { data: updatedComment } =
218
+ await octokit.rest.issues.updateComment({
219
+ owner: props.owner,
220
+ repo: props.repository,
221
+ comment_id: this.output.commentId,
222
+ body: props.body,
223
+ });
224
+
225
+ return this({
226
+ id: `${props.owner}/${props.repository}/issues/${props.issueNumber}/comments/${updatedComment.id}`,
227
+ commentId: updatedComment.id,
228
+ owner: props.owner,
229
+ repository: props.repository,
230
+ issueNumber: props.issueNumber,
231
+ body: props.body,
232
+ allowDelete: props.allowDelete,
233
+ htmlUrl: updatedComment.html_url,
234
+ updatedAt: updatedComment.updated_at,
235
+ });
236
+ } else {
237
+ // Create new comment
238
+ const { data: newComment } = await octokit.rest.issues.createComment({
239
+ owner: props.owner,
240
+ repo: props.repository,
241
+ issue_number: props.issueNumber,
242
+ body: props.body,
243
+ });
244
+
245
+ return this({
246
+ id: `${props.owner}/${props.repository}/issues/${props.issueNumber}/comments/${newComment.id}`,
247
+ commentId: newComment.id,
248
+ owner: props.owner,
249
+ repository: props.repository,
250
+ issueNumber: props.issueNumber,
251
+ body: props.body,
252
+ allowDelete: props.allowDelete,
253
+ htmlUrl: newComment.html_url,
254
+ updatedAt: newComment.updated_at,
255
+ });
256
+ }
257
+ } catch (error: any) {
258
+ if (error.status === 403) {
259
+ logger.error(
260
+ "\n⚠️ Error creating/updating GitHub comment: Insufficient permissions.",
261
+ );
262
+ logger.error(
263
+ "Make sure your GitHub token has the required permissions (repo scope for private repos).\n",
264
+ );
265
+ } else if (error.status === 404) {
266
+ logger.error(
267
+ `\n⚠️ Issue or Pull Request #${props.issueNumber} not found in ${props.owner}/${props.repository}`,
268
+ );
269
+ logger.error(
270
+ "Make sure the issue/PR exists and you have access to it\n",
271
+ );
272
+ } else {
273
+ logger.error("Error creating/updating GitHub comment:", error.message);
274
+ }
275
+ throw error;
276
+ }
277
+ },
278
+ );
@@ -1,2 +1,3 @@
1
+ export * from "./comment.ts";
1
2
  export * from "./repository-environment.ts";
2
3
  export * from "./secret.ts";
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { AlchemyOptions, Phase } from "./alchemy.ts";
2
2
  export type * from "./context.ts";
3
3
  export * from "./resource.ts";
4
- export type * from "./scope.ts";
4
+ export * from "./scope.ts";
5
5
  export * from "./secret.ts";
6
6
  export * from "./serde.ts";
7
7
  export * from "./state.ts";
package/src/scope.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createDummyLogger,
9
9
  createLoggerInstance,
10
10
  type LoggerApi,
11
- } from "./util/cli.tsx";
11
+ } from "./util/cli.ts";
12
12
  import type { ITelemetryClient } from "./util/telemetry/client.ts";
13
13
 
14
14
  export interface ScopeOptions {
@@ -21,6 +21,7 @@ export interface ScopeOptions {
21
21
  quiet?: boolean;
22
22
  phase?: Phase;
23
23
  telemetryClient?: ITelemetryClient;
24
+ logger?: LoggerApi;
24
25
  }
25
26
 
26
27
  // TODO: support browser
@@ -97,11 +98,14 @@ export class Scope {
97
98
 
98
99
  this.logger = this.quiet
99
100
  ? createDummyLogger()
100
- : createLoggerInstance({
101
- phase: this.phase,
102
- stage: this.stage,
103
- appName: this.appName ?? "",
104
- });
101
+ : createLoggerInstance(
102
+ {
103
+ phase: this.phase,
104
+ stage: this.stage,
105
+ appName: this.appName ?? "",
106
+ },
107
+ options.logger,
108
+ );
105
109
 
106
110
  this.stateStore =
107
111
  options.stateStore ??
@@ -0,0 +1,117 @@
1
+ import packageJson from "../../package.json" with { type: "json" };
2
+ import type { Phase } from "../alchemy.ts";
3
+ import { dedent } from "./dedent.ts";
4
+
5
+ // ANSI color codes
6
+ const colors = {
7
+ reset: "\x1b[0m",
8
+ cyanBright: "\x1b[96m",
9
+ yellowBright: "\x1b[93m",
10
+ magenta: "\x1b[35m",
11
+ greenBright: "\x1b[92m",
12
+ redBright: "\x1b[91m",
13
+ gray: "\x1b[90m",
14
+ } as const;
15
+
16
+ type ColorName = keyof typeof colors;
17
+
18
+ // Check if colors should be disabled
19
+ const shouldDisableColors = (): boolean => {
20
+ return Boolean(
21
+ process.env.CI || process.env.NO_COLOR || !process.stdout.isTTY,
22
+ );
23
+ };
24
+
25
+ // Apply color if colors are enabled
26
+ const colorize = (text: string, color: ColorName): string => {
27
+ if (shouldDisableColors()) {
28
+ return text;
29
+ }
30
+ return `${colors[color]}${text}${colors.reset}`;
31
+ };
32
+
33
+ export type Task = {
34
+ prefix?: string;
35
+ prefixColor?: string;
36
+ resource?: string;
37
+ message: string;
38
+ status?: "pending" | "success" | "failure";
39
+ };
40
+
41
+ export type LoggerApi = {
42
+ log: (...args: unknown[]) => void;
43
+ warn: (...args: unknown[]) => void;
44
+ error: (...args: unknown[]) => void;
45
+ task: (id: string, data: Task) => void;
46
+ exit: () => void;
47
+ };
48
+
49
+ type AlchemyInfo = {
50
+ phase: Phase;
51
+ stage: string;
52
+ appName: string;
53
+ };
54
+
55
+ let loggerApi: LoggerApi | null = null;
56
+ export const createLoggerInstance = (
57
+ alchemyInfo: AlchemyInfo,
58
+ customLogger?: LoggerApi,
59
+ ) => {
60
+ if (loggerApi) return loggerApi;
61
+
62
+ // Use custom logger if provided, otherwise use the basic fallback logger
63
+ loggerApi = customLogger ?? createFallbackLogger(alchemyInfo);
64
+ return loggerApi;
65
+ };
66
+
67
+ export const createDummyLogger = (): LoggerApi => {
68
+ return {
69
+ log: () => {},
70
+ error: () => {},
71
+ warn: () => {},
72
+ task: () => {},
73
+ exit: () => {},
74
+ };
75
+ };
76
+
77
+ export const createFallbackLogger = (alchemyInfo: AlchemyInfo): LoggerApi => {
78
+ console.log(dedent`
79
+ ${colorize("Alchemy", "cyanBright")} (v${packageJson.version})
80
+ App: ${alchemyInfo.appName}
81
+ Phase: ${alchemyInfo.phase}
82
+ Stage: ${alchemyInfo.stage}
83
+
84
+ `);
85
+
86
+ return {
87
+ log: console.log,
88
+ error: (...args: unknown[]) =>
89
+ console.error(colorize("ERROR", "redBright"), ...args),
90
+ warn: (...args: unknown[]) =>
91
+ console.warn(colorize("WARN", "yellowBright"), ...args),
92
+ task: (_id: string, data: Task) => {
93
+ const prefix = data.prefix ? `[${data.prefix}]` : "";
94
+
95
+ // Pad the prefix to ensure consistent alignment (12 characters total including brackets)
96
+ const paddedPrefix = prefix.padEnd(12);
97
+ const prefixWithColor =
98
+ data.prefixColor && prefix
99
+ ? colorize(paddedPrefix, data.prefixColor as ColorName)
100
+ : paddedPrefix;
101
+
102
+ const resource = data.resource ? colorize(data.resource, "gray") : "";
103
+ const message = data.message;
104
+
105
+ if (prefixWithColor && resource) {
106
+ console.log(`${prefixWithColor}${resource} ${message}`);
107
+ } else if (prefixWithColor) {
108
+ console.log(`${prefixWithColor}${message}`);
109
+ } else {
110
+ console.log(`${message}`);
111
+ }
112
+ },
113
+ exit: () => {},
114
+ };
115
+ };
116
+
117
+ export const formatFQN = (fqn: string) => fqn.split("/").slice(2).join(" > ");
@@ -1,5 +1,5 @@
1
1
  import { Scope } from "../scope.ts";
2
- import type { LoggerApi } from "./cli.tsx";
2
+ import type { LoggerApi } from "./cli.ts";
3
3
 
4
4
  export const logger = new Proxy({} as LoggerApi, {
5
5
  get: (_, prop: keyof LoggerApi) => {
@@ -51,7 +51,7 @@ export class TelemetryClient implements ITelemetryClient {
51
51
 
52
52
  record(event: Telemetry.EventInput, timestamp = Date.now()) {
53
53
  if (!this.context) {
54
- throw new Error("Context not initialized");
54
+ return;
55
55
  }
56
56
  const payload = {
57
57
  ...event,
package/src/util/cli.tsx DELETED
@@ -1,221 +0,0 @@
1
- import { Box, render, Static, Text, useApp } from "ink";
2
- import Spinner from "ink-spinner";
3
- import { format } from "node:util";
4
- import { useCallback, useEffect, useRef, useState } from "react";
5
- import packageJson from "../../package.json" with { type: "json" };
6
- import type { Phase } from "../alchemy.ts";
7
- import { dedent } from "./dedent.ts";
8
-
9
- export type Color = Parameters<typeof Text>[0]["color"];
10
- export type Task = {
11
- prefix?: string;
12
- prefixColor?: Color;
13
- resource?: string;
14
- message: string;
15
- status?: "pending" | "success" | "failure";
16
- };
17
-
18
- export type LogMessage = {
19
- id: number;
20
- text: string;
21
- };
22
-
23
- export type LoggerApi = {
24
- log: (...args: unknown[]) => void;
25
- warn: (...args: unknown[]) => void;
26
- error: (...args: unknown[]) => void;
27
- task: (id: string, data: Task) => void;
28
- exit: () => void;
29
- };
30
-
31
- type AlchemyInfo = {
32
- phase: Phase;
33
- stage: string;
34
- appName: string;
35
- };
36
-
37
- type LoggerCLIProps = {
38
- alchemyInfo: AlchemyInfo;
39
- setLogger: (logger: LoggerApi) => void;
40
- };
41
-
42
- export function LoggerCLI({ alchemyInfo, setLogger }: LoggerCLIProps) {
43
- const [logs, setLogs] = useState<LogMessage[]>([{ id: 0, text: "#heading" }]);
44
- const [tasks, setTasks] = useState<Map<string, Task>>(new Map());
45
- const { exit } = useApp();
46
- let nextId = useRef(1);
47
-
48
- const log = useCallback(
49
- (...args: unknown[]) =>
50
- setLogs((prev) => [
51
- ...prev,
52
- { id: nextId.current++, text: format(...args) },
53
- ]),
54
- [],
55
- );
56
-
57
- const task = useCallback((id: string, data: Task) => {
58
- setTasks((prev) => {
59
- prev.set(id, data);
60
- return prev;
61
- });
62
- }, []);
63
-
64
- useEffect(() => {
65
- setLogger({
66
- log: log,
67
- warn: log,
68
- error: log,
69
- task: task,
70
- exit: () => exit(),
71
- });
72
-
73
- return () => {
74
- setTasks(new Map());
75
- setLogs([]);
76
- };
77
- }, [exit, log, setLogger, task]);
78
-
79
- return (
80
- <>
81
- <Static items={logs}>
82
- {(log) => {
83
- if (log.id === 0) {
84
- return (
85
- <Box flexDirection="column" key={log.text}>
86
- <Box flexDirection="column" borderStyle="round" padding={1}>
87
- <Text color="green" bold>
88
- Alchemy (v{packageJson.version})
89
- </Text>
90
- <Text color="gray" bold>
91
- App:{" "}
92
- <Text color="magenta" bold={false}>
93
- {alchemyInfo.appName}
94
- </Text>
95
- </Text>
96
- <Text color="gray" bold>
97
- Phase:{" "}
98
- <Text color="magenta" bold={false}>
99
- {alchemyInfo.phase}
100
- </Text>
101
- </Text>
102
- <Text color="gray" bold>
103
- Stage:{" "}
104
- <Text color="magenta" bold={false}>
105
- {alchemyInfo.stage}
106
- </Text>
107
- </Text>
108
- </Box>
109
- <Text color="green" bold>
110
- Logs
111
- </Text>
112
- </Box>
113
- );
114
- }
115
-
116
- return (
117
- <Text color="gray" key={log.id} wrap="wrap">
118
- {log.text}
119
- </Text>
120
- );
121
- }}
122
- </Static>
123
-
124
- <Box flexDirection="column" marginTop={1}>
125
- <Text color="green" bold>
126
- Tasks
127
- </Text>
128
- <Box flexDirection="column" paddingLeft={1}>
129
- {Array.from(tasks.entries()).map(([id, task]) => (
130
- <Box key={id} flexDirection="row" gap={1}>
131
- {!task.status || task.status === "pending" ? (
132
- <Spinner />
133
- ) : task.status === "success" ? (
134
- <Text color="green">✓</Text>
135
- ) : (
136
- <Text color="red">✗</Text>
137
- )}
138
-
139
- {task.prefix && (
140
- <Text color={task.prefixColor ?? "white"} bold>
141
- {task.prefix.padStart(9)}
142
- </Text>
143
- )}
144
-
145
- {task.resource && (
146
- <Text color="gray" bold>
147
- [{task.resource}]
148
- </Text>
149
- )}
150
-
151
- <Text bold>{task.message}</Text>
152
- </Box>
153
- ))}
154
- </Box>
155
- </Box>
156
- </>
157
- );
158
- }
159
-
160
- let loggerApi: LoggerApi | null = null;
161
- export const createLoggerInstance = (alchemyInfo: AlchemyInfo) => {
162
- if (loggerApi) return loggerApi;
163
-
164
- if (
165
- process.env.CI ||
166
- process.env.USE_FALLBACK_LOGGER ||
167
- !process.stdin.isTTY
168
- ) {
169
- loggerApi = createFallbackLogger(alchemyInfo);
170
- return loggerApi;
171
- }
172
-
173
- render(
174
- <LoggerCLI
175
- alchemyInfo={alchemyInfo}
176
- setLogger={(logger) => {
177
- loggerApi = logger;
178
- }}
179
- />,
180
- {
181
- exitOnCtrlC: true,
182
- },
183
- );
184
-
185
- process.on("SIGINT", () => {
186
- loggerApi?.exit();
187
- process.exit(0);
188
- });
189
-
190
- return loggerApi!;
191
- };
192
-
193
- export const createDummyLogger = (): LoggerApi => {
194
- return {
195
- log: () => {},
196
- error: () => {},
197
- warn: () => {},
198
- task: () => {},
199
- exit: () => {},
200
- };
201
- };
202
-
203
- export const createFallbackLogger = (alchemyInfo: AlchemyInfo): LoggerApi => {
204
- console.log(dedent`
205
- Alchemy (v${packageJson.version})
206
- App: ${alchemyInfo.appName}
207
- Phase: ${alchemyInfo.phase}
208
- Stage: ${alchemyInfo.stage}
209
-
210
- `);
211
-
212
- return {
213
- log: console.log,
214
- error: console.error,
215
- warn: console.warn,
216
- task: () => {},
217
- exit: () => {},
218
- };
219
- };
220
-
221
- export const formatFQN = (fqn: string) => fqn.split("/").slice(2).join(" > ");