spooder 4.6.2 → 5.0.1

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.
@@ -0,0 +1,303 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Directory listing - {{title}}</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ margin: 0;
11
+ padding: 40px;
12
+ background-color: #f8f9fa;
13
+ color: #333;
14
+ }
15
+
16
+ .container {
17
+ max-width: 1000px;
18
+ margin: 0 auto;
19
+ background: white;
20
+ border-radius: 8px;
21
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
22
+ overflow: hidden;
23
+ }
24
+
25
+ .header {
26
+ background: #f8f9fa;
27
+ padding: 20px 30px;
28
+ border-bottom: 1px solid #dee2e6;
29
+ }
30
+
31
+ h1 {
32
+ color: #333;
33
+ margin: 0;
34
+ font-size: 1.5rem;
35
+ font-weight: 600;
36
+ word-break: break-word;
37
+ }
38
+
39
+ .listing-table {
40
+ width: 100%;
41
+ border-collapse: collapse;
42
+ font-size: 0.9rem;
43
+ }
44
+
45
+ .listing-table th {
46
+ background: #f8f9fa;
47
+ padding: 12px 30px;
48
+ text-align: left;
49
+ font-weight: 600;
50
+ color: #495057;
51
+ border-bottom: 2px solid #dee2e6;
52
+ }
53
+
54
+ .listing-table td {
55
+ padding: 12px 30px;
56
+ border-bottom: 1px solid #f0f0f0;
57
+ vertical-align: middle;
58
+ }
59
+
60
+ .listing-table tr:hover {
61
+ background-color: #f8f9fa;
62
+ }
63
+
64
+ .entry-link {
65
+ display: flex;
66
+ align-items: center;
67
+ text-decoration: none;
68
+ color: #0066cc;
69
+ font-weight: 500;
70
+ }
71
+
72
+ .entry-link:hover {
73
+ color: #004499;
74
+ }
75
+
76
+ .entry-icon {
77
+ margin-right: 8px;
78
+ font-size: 1.1em;
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ .directory .entry-icon:before {
83
+ content: "📁";
84
+ }
85
+
86
+ .file .entry-icon:before {
87
+ content: "📄";
88
+ }
89
+
90
+ .size-col {
91
+ text-align: right;
92
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
93
+ color: #6c757d;
94
+ }
95
+
96
+ .date-col {
97
+ color: #6c757d;
98
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
99
+ }
100
+
101
+ .footer {
102
+ padding: 20px 30px;
103
+ background: #f8f9fa;
104
+ border-top: 1px solid #dee2e6;
105
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
106
+ font-size: 0.8rem;
107
+ color: #6c757d;
108
+ }
109
+
110
+ /* Mobile card layout styles - hidden by default */
111
+ .mobile-listing {
112
+ display: none;
113
+ }
114
+
115
+ .mobile-entry {
116
+ border-bottom: 1px solid #f0f0f0;
117
+ }
118
+
119
+ .mobile-entry:last-child {
120
+ border-bottom: none;
121
+ }
122
+
123
+ .mobile-entry-link {
124
+ display: block;
125
+ padding: 16px 20px;
126
+ text-decoration: none;
127
+ color: inherit;
128
+ }
129
+
130
+ .mobile-entry-link:hover,
131
+ .mobile-entry-link:active {
132
+ background-color: #f8f9fa;
133
+ }
134
+
135
+ .mobile-entry-main {
136
+ display: flex;
137
+ align-items: center;
138
+ margin-bottom: 4px;
139
+ }
140
+
141
+ .mobile-entry-icon {
142
+ margin-right: 12px;
143
+ font-size: 1.2em;
144
+ flex-shrink: 0;
145
+ }
146
+
147
+ .mobile-entry-name {
148
+ color: #0066cc;
149
+ font-weight: 500;
150
+ font-size: 1rem;
151
+ word-break: break-word;
152
+ flex: 1;
153
+ }
154
+
155
+ .mobile-entry-meta {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+ font-size: 0.8rem;
160
+ color: #6c757d;
161
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
162
+ margin-left: 32px;
163
+ }
164
+
165
+ .mobile-date {
166
+ display: none;
167
+ }
168
+
169
+ /* Responsive media queries */
170
+ @media (max-width: 768px) {
171
+ body {
172
+ padding: 20px 10px;
173
+ }
174
+
175
+ .container {
176
+ border-radius: 0;
177
+ box-shadow: none;
178
+ margin: 0 -10px;
179
+ }
180
+
181
+ .header {
182
+ padding: 16px 20px;
183
+ }
184
+
185
+ h1 {
186
+ font-size: 1.25rem;
187
+ }
188
+
189
+ .footer {
190
+ padding: 16px 20px;
191
+ }
192
+
193
+ /* Hide table layout on mobile */
194
+ .listing-table {
195
+ display: none;
196
+ }
197
+
198
+ /* Show mobile card layout */
199
+ .mobile-listing {
200
+ display: block;
201
+ }
202
+
203
+ .mobile-date {
204
+ display: inline;
205
+ }
206
+
207
+ .desktop-date {
208
+ display: none;
209
+ }
210
+ }
211
+
212
+ @media (max-width: 480px) {
213
+ body {
214
+ padding: 10px 0;
215
+ }
216
+
217
+ .container {
218
+ border-radius: 0;
219
+ }
220
+
221
+ .header {
222
+ padding: 12px 16px;
223
+ }
224
+
225
+ h1 {
226
+ font-size: 1.1rem;
227
+ }
228
+
229
+ .footer {
230
+ padding: 12px 16px;
231
+ font-size: 0.75rem;
232
+ }
233
+
234
+ .mobile-entry-link {
235
+ padding: 14px 16px;
236
+ }
237
+
238
+ .mobile-entry-icon {
239
+ margin-right: 10px;
240
+ }
241
+
242
+ .mobile-entry-name {
243
+ font-size: 0.95rem;
244
+ }
245
+
246
+ .mobile-entry-meta {
247
+ font-size: 0.75rem;
248
+ margin-left: 28px;
249
+ }
250
+ }
251
+ </style>
252
+ </head>
253
+ <body>
254
+ <div class="container">
255
+ <div class="header">
256
+ <h1>{{path}}</h1>
257
+ </div>
258
+
259
+ <!-- Desktop table layout -->
260
+ <table class="listing-table">
261
+ <thead>
262
+ <tr>
263
+ <th>Name</th>
264
+ <th>Size</th>
265
+ <th>Modified</th>
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ <t-for items="entries" as="entry"><tr>
270
+ <td>
271
+ <a href="{{base_url}}/{{entry.name}}" class="entry-link {{entry.type}}">
272
+ <span class="entry-icon"></span>
273
+ {{entry.name}}
274
+ </a>
275
+ </td>
276
+ <td class="size-col">{{entry.size}}</td>
277
+ <td class="date-col desktop-date">{{entry.modified}}</td>
278
+ </tr></t-for>
279
+ </tbody>
280
+ </table>
281
+
282
+ <!-- Mobile card layout -->
283
+ <div class="mobile-listing">
284
+ <t-for items="entries" as="entry"><div class="mobile-entry">
285
+ <a href="{{base_url}}/{{entry.name}}" class="mobile-entry-link {{entry.type}}">
286
+ <div class="mobile-entry-main">
287
+ <span class="mobile-entry-icon entry-icon"></span>
288
+ <span class="mobile-entry-name">{{entry.name}}</span>
289
+ </div>
290
+ <div class="mobile-entry-meta">
291
+ <span class="mobile-date">{{entry.modified_mobile}}</span>
292
+ <span>{{entry.size}}</span>
293
+ </div>
294
+ </a>
295
+ </div></t-for>
296
+ </div>
297
+
298
+ <div class="footer">
299
+ spooder {{version}}
300
+ </div>
301
+ </div>
302
+ </body>
303
+ </html>
package/src/github.ts DELETED
@@ -1,121 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { log } from './utils';
3
-
4
- type InstallationResponse = Array<{
5
- id: number,
6
- account: {
7
- login: string
8
- },
9
- access_tokens_url: string,
10
- repositories_url: string
11
- }>;
12
-
13
- type AccessTokenResponse = {
14
- token: string;
15
- };
16
-
17
- type RepositoryResponse = {
18
- repositories: Array<{
19
- full_name: string,
20
- name: string,
21
- url: string,
22
- owner: {
23
- login: string
24
- }
25
- }>
26
- };
27
-
28
- type IssueResponse = {
29
- number: number,
30
- url: string
31
- };
32
-
33
- type Issue = {
34
- app_id: number,
35
- private_key: string,
36
- login_name: string,
37
- repository_name: string,
38
- issue_title: string,
39
- issue_body: string
40
- issue_labels?: Array<string>
41
- };
42
-
43
- function generate_jwt(app_id: number, private_key: string): string {
44
- const encoded_header = Buffer.from(JSON.stringify({
45
- alg: 'RS256',
46
- typ: 'JWT'
47
- })).toString('base64');
48
-
49
- const encoded_payload = Buffer.from(JSON.stringify({
50
- iat: Math.floor(Date.now() / 1000),
51
- exp: Math.floor(Date.now() / 1000) + 60,
52
- iss: app_id
53
- })).toString('base64');
54
-
55
- const sign = crypto.createSign('RSA-SHA256');
56
- sign.update(encoded_header + '.' + encoded_payload);
57
-
58
- return encoded_header + '.' + encoded_payload + '.' + sign.sign(private_key, 'base64');
59
- }
60
-
61
- async function request_endpoint(url: string, bearer: string, method: string = 'GET', body?: object): Promise<Response> {
62
- return fetch(url, {
63
- method,
64
- body: body ? JSON.stringify(body) : undefined,
65
- headers: {
66
- Authorization: 'Bearer ' + bearer,
67
- Accept: 'application/vnd.github.v3+json'
68
- }
69
- });
70
- }
71
-
72
- function check_response_is_ok(res: Response, message: string): void {
73
- if (!res.ok)
74
- throw new Error(message + ' (' + res.status + ' ' + res.statusText + ')');
75
- }
76
-
77
- export async function create_github_issue(issue: Issue): Promise<void> {
78
- const jwt = generate_jwt(issue.app_id, issue.private_key);
79
- const app_res = await request_endpoint('https://api.github.com/app', jwt);
80
-
81
- check_response_is_ok(app_res, 'cannot authenticate GitHub app ' + issue.app_id);
82
-
83
- const res_installs = await request_endpoint('https://api.github.com/app/installations', jwt);
84
- check_response_is_ok(res_installs, 'cannot fetch GitHub app installations');
85
-
86
- const json_installs = await res_installs.json() as InstallationResponse;
87
-
88
- const login_name = issue.login_name.toLowerCase();
89
- const install = json_installs.find((install) => install.account.login.toLowerCase() === login_name);
90
-
91
- if (!install)
92
- throw new Error('spooder-bot is not installed on account ' + login_name);
93
-
94
- const res_access_token = await request_endpoint(install.access_tokens_url, jwt, 'POST');
95
- check_response_is_ok(res_access_token, 'cannot fetch GitHub app access token');
96
-
97
- const json_access_token = await res_access_token.json() as AccessTokenResponse;
98
- const access_token = json_access_token.token;
99
-
100
- const repositories = await request_endpoint(install.repositories_url, access_token);
101
- check_response_is_ok(repositories, 'cannot fetch GitHub app repositories');
102
-
103
- const repositories_json = await repositories.json() as RepositoryResponse;
104
-
105
- const repository_name = issue.repository_name.toLowerCase();
106
- const repository = repositories_json.repositories.find((repository) => repository.full_name.toLowerCase() === repository_name);
107
-
108
- if (!repository)
109
- throw new Error('spooder-bot is not installed on repository ' + repository_name);
110
-
111
- const issue_res = await request_endpoint(repository.url + '/issues', access_token, 'POST', {
112
- title: issue.issue_title,
113
- body: issue.issue_body,
114
- labels: issue.issue_labels
115
- });
116
-
117
- check_response_is_ok(issue_res, 'cannot create GitHub issue');
118
-
119
- const json_issue = await issue_res.json() as IssueResponse;
120
- log('[{canary}] raised issue {#%d} in {%s}: %s', json_issue.number, repository.full_name, json_issue.url);
121
- }
package/src/utils.ts DELETED
@@ -1,57 +0,0 @@
1
- import { format } from 'node:util';
2
-
3
- /** Logs a message to stdout with the prefix `[spooder] ` */
4
- export function log(message: string, ...args: unknown[]): void {
5
- let formatted_message = format('[{spooder}] ' + message, ...args);
6
-
7
- // Replace all {...} with text wrapped in ANSI color code 6.
8
- formatted_message = formatted_message.replace(/\{([^}]+)\}/g, '\x1b[38;5;6m$1\x1b[0m');
9
-
10
- process.stdout.write(formatted_message + '\n');
11
- }
12
-
13
- /** Strips ANSI color codes from a string */
14
- export function strip_color_codes(str: string): string {
15
- return str.replace(/\x1b\[[0-9;]*m/g, '');
16
- }
17
-
18
- /** Converts a command line string into an array of arguments */
19
- export function parse_command_line(command: string): string[] {
20
- const args = [];
21
- let current_arg = '';
22
- let in_quotes = false;
23
- let in_escape = false;
24
-
25
- for (let i = 0; i < command.length; i++) {
26
- const char = command[i];
27
-
28
- if (in_escape) {
29
- current_arg += char;
30
- in_escape = false;
31
- continue;
32
- }
33
-
34
- if (char === '\\') {
35
- in_escape = true;
36
- continue;
37
- }
38
-
39
- if (char === '"') {
40
- in_quotes = !in_quotes;
41
- continue;
42
- }
43
-
44
- if (char === ' ' && !in_quotes) {
45
- args.push(current_arg);
46
- current_arg = '';
47
- continue;
48
- }
49
-
50
- current_arg += char;
51
- }
52
-
53
- if (current_arg.length > 0)
54
- args.push(current_arg);
55
-
56
- return args;
57
- }