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.
- package/README.md +1 -1
- package/lib/alchemy.d.ts +21 -1
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +42 -8
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.js +3 -12
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/do-state-store/internal.d.ts +1 -1
- package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
- package/lib/cloudflare/do-state-store/internal.js +2 -2
- package/lib/cloudflare/do-state-store/internal.js.map +1 -1
- package/lib/cloudflare/do-state-store/store.d.ts.map +1 -1
- package/lib/cloudflare/do-state-store/store.js +19 -4
- package/lib/cloudflare/do-state-store/store.js.map +1 -1
- package/lib/cloudflare/secrets-store.d.ts.map +1 -1
- package/lib/cloudflare/secrets-store.js +21 -17
- package/lib/cloudflare/secrets-store.js.map +1 -1
- package/lib/cloudflare/worker-assets.d.ts.map +1 -1
- package/lib/cloudflare/worker-assets.js +9 -0
- package/lib/cloudflare/worker-assets.js.map +1 -1
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +3 -6
- package/lib/destroy.js.map +1 -1
- package/lib/github/comment.d.ts +156 -0
- package/lib/github/comment.d.ts.map +1 -0
- package/lib/github/comment.js +192 -0
- package/lib/github/comment.js.map +1 -0
- package/lib/github/index.d.ts +1 -0
- package/lib/github/index.d.ts.map +1 -1
- package/lib/github/index.js +1 -0
- package/lib/github/index.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/scope.d.ts +2 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +1 -1
- package/lib/scope.js.map +1 -1
- package/lib/util/cli.d.ts +2 -13
- package/lib/util/cli.d.ts.map +1 -1
- package/lib/util/cli.js +46 -60
- package/lib/util/cli.js.map +1 -1
- package/lib/util/logger.d.ts +1 -1
- package/lib/util/logger.d.ts.map +1 -1
- package/lib/util/telemetry/client.js +1 -1
- package/lib/util/telemetry/client.js.map +1 -1
- package/package.json +2 -5
- package/src/alchemy.ts +71 -9
- package/src/apply.ts +4 -17
- package/src/cloudflare/do-state-store/internal.ts +2 -1
- package/src/cloudflare/do-state-store/store.ts +26 -4
- package/src/cloudflare/secrets-store.ts +22 -21
- package/src/cloudflare/worker-assets.ts +16 -0
- package/src/destroy.ts +4 -7
- package/src/github/comment.ts +278 -0
- package/src/github/index.ts +1 -0
- package/src/index.ts +1 -1
- package/src/scope.ts +10 -6
- package/src/util/cli.ts +117 -0
- package/src/util/logger.ts +1 -1
- package/src/util/telemetry/client.ts +1 -1
- 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
|
+
);
|
package/src/github/index.ts
CHANGED
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
|
|
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.
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 ??
|
package/src/util/cli.ts
ADDED
|
@@ -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(" > ");
|
package/src/util/logger.ts
CHANGED
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(" > ");
|