@magland/mochi 0.3.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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
@@ -0,0 +1,447 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerIssues = registerIssues;
37
+ const crypto = __importStar(require("crypto"));
38
+ const avatar_1 = require("./avatar");
39
+ const icons_1 = require("./icons");
40
+ const issues = __importStar(require("./issues"));
41
+ const html_1 = require("./html");
42
+ const markdown_1 = require("./markdown");
43
+ const ops_1 = require("./ops");
44
+ const render_1 = require("./render");
45
+ const session_1 = require("./session");
46
+ const views_1 = require("./views");
47
+ const web_1 = require("./web");
48
+ // The issue pages, over the store in issues.ts.
49
+ //
50
+ // Who may do what follows the vault's model rather than GitHub's: reading is
51
+ // anonymous like every other read, opening an issue and commenting need a
52
+ // session (a vault's users are its users; there is no public sign-up to
53
+ // abuse), and closing, reopening, or editing needs the write role on the
54
+ // repository or being the person who wrote the thing.
55
+ // Issue bodies and comments are the long fields here, and people paste logs
56
+ // into them.
57
+ const form = (0, web_1.urlencodedForm)('3mb');
58
+ function issuesUrl(ctx) {
59
+ return `${(0, views_1.repoUrl)(ctx)}/issues`;
60
+ }
61
+ /** Issue bodies are markdown, rendered by the same sanitizing pipeline as a
62
+ * README: this is text one user writes and another reads. Relative links
63
+ * resolve against the default branch, since an issue has no file of its own. */
64
+ function body(ctx, text) {
65
+ const base = (0, views_1.repoUrl)(ctx);
66
+ const ref = (0, views_1.encPath)(ctx.defaultBranch || ctx.ref || 'HEAD');
67
+ return (0, html_1.raw)((0, markdown_1.renderMarkdown)(text, {
68
+ rawBase: `${base}/raw/${ref}`,
69
+ blobBase: `${base}/blob/${ref}`,
70
+ issueBase: `${base}/issues`,
71
+ commitBase: `${base}/commit`,
72
+ }));
73
+ }
74
+ function stateBadge(state) {
75
+ return state === 'open'
76
+ ? (0, html_1.html) `<span class="state-badge open">${(0, icons_1.icon)('issue-opened')}<span>Open</span></span>`
77
+ : (0, html_1.html) `<span class="state-badge closed">${(0, icons_1.icon)('issue-closed')}<span>Closed</span></span>`;
78
+ }
79
+ /** The same filter with one field changed, as a query string. */
80
+ function filterUrl(base, filter, change) {
81
+ const next = { ...filter, ...change };
82
+ const params = new URLSearchParams();
83
+ if (next.state !== 'open')
84
+ params.set('state', next.state);
85
+ if (next.label)
86
+ params.set('label', next.label);
87
+ if (next.author)
88
+ params.set('author', next.author);
89
+ if (next.query)
90
+ params.set('q', next.query);
91
+ if (next.sort !== 'newest')
92
+ params.set('sort', next.sort);
93
+ const q = params.toString();
94
+ return q === '' ? base : `${base}?${q}`;
95
+ }
96
+ /**
97
+ * A label's colour is derived from its name, the way an identicon is: a vault
98
+ * keeps no label registry, so the set of labels is whatever the issues carry,
99
+ * and a colour that comes from the name is stable without one being stored.
100
+ */
101
+ function labelStyle(label) {
102
+ const hash = crypto.createHash('sha256').update(label.trim().toLowerCase()).digest();
103
+ return `background: hsl(${Math.round((hash[0] / 256) * 360)} 58% 46%); color: #fff;`;
104
+ }
105
+ function labelChips(labels, base, filter) {
106
+ return (0, html_1.joinHtml)(labels.map((l) => base && filter
107
+ ? (0, html_1.html) `<a class="chip label" style="${(0, html_1.raw)(labelStyle(l))}" href="${filterUrl(base, filter, {
108
+ label: l,
109
+ })}">${l}</a>`
110
+ : (0, html_1.html) `<span class="chip label" style="${(0, html_1.raw)(labelStyle(l))}">${l}</span>`));
111
+ }
112
+ function listPage(ctx, list, filter, counts, labels, authors) {
113
+ const base = issuesUrl(ctx);
114
+ const tab = (id, label, n, glyph) => (0, html_1.html) `<a class="state-tab${filter.state === id ? ' current' : ''}" href="${filterUrl(base, filter, {
115
+ state: id,
116
+ })}">${(0, icons_1.icon)(glyph)}<span>${n} ${label}</span></a>`;
117
+ const rows = list.map((i) => (0, html_1.html) `<tr>
118
+ <td class="issue-cell">${(0, icons_1.icon)(i.state === 'open' ? 'issue-opened' : 'issue-closed', i.state === 'open' ? 'issue-open' : 'issue-closed')}<span>
119
+ <a class="issue-link" href="${base}/${i.number}">${i.title}</a>${labelChips(i.labels, base, filter)}
120
+ <div class="muted small">#${i.number} opened ${(0, render_1.timeTag)(i.created)} by ${i.author}${i.state === 'closed' && i.closedAt ? (0, html_1.html) ` &middot; closed ${(0, render_1.timeTag)(i.closedAt)}` : ''}</div></span></td>
121
+ <td class="right muted small">${i.comments > 0
122
+ ? (0, html_1.html) `<a class="issue-comments" href="${base}/${i.number}" title="${i.comments} comment${i.comments === 1 ? '' : 's'}">${(0, icons_1.icon)('comment')}<span>${i.comments}</span></a>`
123
+ : ''}</td>
124
+ </tr>`);
125
+ const newBtn = ctx.viewer
126
+ ? (0, html_1.html) `<a class="btn btn-primary" href="${base}/new">${(0, icons_1.icon)('plus')}<span>New issue</span></a>`
127
+ : (0, html_1.html) `<a class="btn" href="/login?next=${encodeURIComponent(`${base}/new`)}">${(0, icons_1.icon)('plus')}<span>New issue</span></a>`;
128
+ // The three menus and the search box. Each menu is a list of links carrying
129
+ // the rest of the filter, so narrowing never loses what was already chosen
130
+ // and nothing here needs script.
131
+ const menu = (label, current, glyph, items) => (0, html_1.html) `<details class="dropdown">
132
+ <summary class="btn">${(0, icons_1.icon)(glyph)}<span>${current || label}</span>${(0, icons_1.icon)('chevron-down', 'caret')}</summary>
133
+ <div class="dropdown-menu"><div class="dd-scroll">${items}</div></div>
134
+ </details>`;
135
+ const labelMenu = menu('Labels', filter.label, 'tag', [
136
+ (0, html_1.html) `<a class="dd-item${filter.label === '' ? ' current' : ''}" href="${filterUrl(base, filter, {
137
+ label: '',
138
+ })}"><span class="dd-check"></span><span class="dd-label">Any label</span></a>`,
139
+ ...labels.map((l) => (0, html_1.html) `<a class="dd-item${filter.label === l.label ? ' current' : ''}" href="${filterUrl(base, filter, {
140
+ label: l.label,
141
+ })}">${filter.label === l.label ? (0, icons_1.icon)('check', 'dd-check') : (0, html_1.raw)('<span class="dd-check"></span>')}<span class="dd-label">${l.label}</span><span class="muted small">${l.count}</span></a>`),
142
+ ]);
143
+ const authorMenu = menu('Author', filter.author, 'person', [
144
+ (0, html_1.html) `<a class="dd-item${filter.author === '' ? ' current' : ''}" href="${filterUrl(base, filter, {
145
+ author: '',
146
+ })}"><span class="dd-check"></span><span class="dd-label">Anyone</span></a>`,
147
+ ...authors.map((a) => (0, html_1.html) `<a class="dd-item${filter.author === a.author ? ' current' : ''}" href="${filterUrl(base, filter, {
148
+ author: a.author,
149
+ })}">${filter.author === a.author ? (0, icons_1.icon)('check', 'dd-check') : (0, html_1.raw)('<span class="dd-check"></span>')}<span class="dd-label">${a.author}</span><span class="muted small">${a.count}</span></a>`),
150
+ ]);
151
+ const sortMenu = menu('Sort', issues.ISSUE_SORTS.find((x) => x.key === filter.sort)?.label ?? 'Newest', 'filter', issues.ISSUE_SORTS.map((x) => (0, html_1.html) `<a class="dd-item${filter.sort === x.key ? ' current' : ''}" href="${filterUrl(base, filter, {
152
+ sort: x.key,
153
+ })}">${filter.sort === x.key ? (0, icons_1.icon)('check', 'dd-check') : (0, html_1.raw)('<span class="dd-check"></span>')}<span class="dd-label">${x.label}</span></a>`));
154
+ const hidden = [
155
+ filter.state === 'open' ? '' : (0, html_1.html) `<input type="hidden" name="state" value="${filter.state}">`,
156
+ filter.label === '' ? '' : (0, html_1.html) `<input type="hidden" name="label" value="${filter.label}">`,
157
+ filter.author === '' ? '' : (0, html_1.html) `<input type="hidden" name="author" value="${filter.author}">`,
158
+ filter.sort === 'newest' ? '' : (0, html_1.html) `<input type="hidden" name="sort" value="${filter.sort}">`,
159
+ ];
160
+ const search = (0, html_1.html) `<form class="issue-search" method="get" action="${base}" role="search">${hidden}
161
+ <input class="search-input" type="search" name="q" value="${filter.query}" placeholder="Search issues" aria-label="Search issues">${(0, icons_1.icon)('search', 'search-glyph')}</form>`;
162
+ const narrowed = filter.label !== '' || filter.author !== '' || filter.query !== '';
163
+ const clear = narrowed
164
+ ? (0, html_1.html) `<a class="btn" href="${filterUrl(base, filter, { label: '', author: '', query: '' })}">${(0, icons_1.icon)('x')}<span>Clear</span></a>`
165
+ : '';
166
+ const empty = narrowed
167
+ ? 'No issues match that.'
168
+ : filter.state === 'closed'
169
+ ? 'No closed issues.'
170
+ : filter.state === 'open'
171
+ ? 'No open issues. Anything that needs doing here can be written down.'
172
+ : 'No issues yet.';
173
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'issues')}
174
+ <div class="page-head">
175
+ <span class="state-filter">${tab('open', 'Open', counts.open, 'issue-opened')}${tab('closed', 'Closed', counts.closed, 'issue-closed')}</span>
176
+ ${newBtn}
177
+ </div>
178
+ <div class="issue-filters">${search}<span class="right-group">${labelMenu}${authorMenu}${sortMenu}${clear}</span></div>
179
+ ${rows.length
180
+ ? (0, html_1.html) `<table class="listing issues"><tbody>${rows}</tbody></table>`
181
+ : (0, html_1.html) `<div class="empty-state">${empty}</div>`}`;
182
+ return (0, views_1.layout)(`Issues - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, filterUrl(base, filter, {})));
183
+ }
184
+ function commentCard(author, when, rendered, note = '') {
185
+ return (0, html_1.html) `<article class="issue-comment">
186
+ <div class="issue-comment-head">${(0, avatar_1.avatar)(author, 24)}<b>${author}</b><span class="muted small">${note || 'commented'} ${(0, render_1.timeTag)(when)}</span></div>
187
+ <div class="issue-comment-body markdown-body">${rendered}</div>
188
+ </article>`;
189
+ }
190
+ function issuePage(ctx, issue, canWrite) {
191
+ const base = issuesUrl(ctx);
192
+ const viewer = ctx.viewer;
193
+ const mine = viewer !== null && viewer.auth.username === issue.author;
194
+ const canClose = canWrite || mine;
195
+ const comments = issue.commentList.map((c) => commentCard(c.author, c.created, body(ctx, c.body)));
196
+ const closedNote = issue.state === 'closed' && issue.closedAt
197
+ ? (0, html_1.html) `<div class="issue-event">${(0, icons_1.icon)('issue-closed', 'issue-closed')}<span><b>${issue.closedBy ?? 'someone'}</b> closed this ${(0, render_1.timeTag)(issue.closedAt)}</span></div>`
198
+ : '';
199
+ let replyBox;
200
+ if (viewer) {
201
+ const toggle = canClose
202
+ ? (0, html_1.html) `<button class="btn" type="submit" name="state" value="${issue.state === 'open' ? 'closed' : 'open'}" formaction="${base}/${issue.number}/state">${(0, icons_1.icon)(issue.state === 'open' ? 'issue-closed' : 'issue-opened')}<span>${issue.state === 'open' ? 'Close issue' : 'Reopen issue'}</span></button>`
203
+ : '';
204
+ replyBox = (0, html_1.html) `<form class="issue-reply" method="post" action="${base}/${issue.number}/comment">${(0, views_1.csrfField)(viewer)}
205
+ <div class="issue-comment-head">${(0, avatar_1.avatar)(viewer.auth.username, 24)}<b>${viewer.auth.username}</b></div>
206
+ <textarea class="code-editor" name="body" rows="6" placeholder="Leave a comment"></textarea>
207
+ <div class="actions">${toggle}<button class="btn btn-primary" type="submit">Comment</button></div>
208
+ </form>`;
209
+ }
210
+ else {
211
+ replyBox = (0, html_1.html) `<p class="muted"><a href="/login?next=${encodeURIComponent(`${base}/${issue.number}`)}">Sign in</a> to comment.</p>`;
212
+ }
213
+ const edit = canClose
214
+ ? (0, html_1.html) `<a class="btn" href="${base}/${issue.number}/edit">${(0, icons_1.icon)('pencil')}<span>Edit</span></a>`
215
+ : '';
216
+ const description = body(ctx, issue.body);
217
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'issues')}
218
+ <div class="issue-head">
219
+ <h1 class="issue-title">${issue.title} <span class="muted">#${issue.number}</span></h1>
220
+ <span class="right-group">${edit}<a class="btn" href="${base}">Back to issues</a></span>
221
+ </div>
222
+ <div class="issue-sub">${stateBadge(issue.state)}<span class="muted"><b>${issue.author}</b> opened this ${(0, render_1.timeTag)(issue.created)} &middot; ${issue.commentList.length} comment${issue.commentList.length === 1 ? '' : 's'}</span>${labelChips(issue.labels)}</div>
223
+ <div class="issue-thread">
224
+ ${commentCard(issue.author, issue.created, description.text ? description : (0, html_1.raw)('<p class="muted">No description.</p>'), 'opened this')}
225
+ ${comments}
226
+ ${closedNote}
227
+ ${replyBox}
228
+ </div>`;
229
+ return (0, views_1.layout)(`${issue.title} · #${issue.number} - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, `${base}/${issue.number}`));
230
+ }
231
+ function editPage(ctx, viewer, mode, values, error) {
232
+ const base = issuesUrl(ctx);
233
+ const action = mode === 'new' ? `${base}/new` : `${base}/${values.number}/edit`;
234
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'issues')}
235
+ <div class="form-box wide">
236
+ <h1>${mode === 'new' ? 'New issue' : `Edit issue #${values.number}`}</h1>
237
+ ${error ? (0, html_1.html) `<div class="form-error">${error}</div>` : ''}
238
+ <form method="post" action="${action}">${(0, views_1.csrfField)(viewer)}
239
+ <div class="field"><label for="title">Title</label>
240
+ <input type="text" id="title" name="title" value="${values.title}" maxlength="${issues.MAX_TITLE}" required autofocus></div>
241
+ <div class="field"><label for="body">Description</label>
242
+ <textarea class="code-editor" id="body" name="body" rows="12" placeholder="Markdown is welcome">${values.body}</textarea></div>
243
+ ${ctx.canPush
244
+ ? (0, html_1.html) `<div class="field"><label for="labels">Labels</label>
245
+ <input type="text" id="labels" name="labels" value="${values.labels.join(', ')}" placeholder="bug, ui">
246
+ <p class="muted small">Separated by commas. Labels are for people with push access.</p></div>`
247
+ : ''}
248
+ <div class="actions"><button class="btn btn-primary" type="submit">${mode === 'new' ? 'Open issue' : 'Save changes'}</button><a class="btn" href="${mode === 'new' ? base : `${base}/${values.number}`}">Cancel</a></div>
249
+ </form>
250
+ </div>`;
251
+ return (0, views_1.layout)(mode === 'new' ? `New issue - ${ctx.collection}/${ctx.repo}` : `Edit #${values.number}`, content, (0, views_1.repoOpts)(ctx, action));
252
+ }
253
+ function registerIssues(app, root) {
254
+ /** The repository context plus the issue named in the URL. */
255
+ async function withIssue(req, res, viewer) {
256
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
257
+ if (!loaded)
258
+ return null;
259
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
260
+ const n = parseInt(req.params.n ?? '', 10);
261
+ if (!/^[1-9][0-9]*$/.test(req.params.n ?? '')) {
262
+ (0, web_1.send404)(res, 'No such issue', viewer);
263
+ return null;
264
+ }
265
+ const issue = issues.readIssue(root, ctx.collection, ctx.repo, n);
266
+ if (!issue) {
267
+ (0, web_1.send404)(res, `Issue #${n} does not exist in ${ctx.collection}/${ctx.repo}`, viewer);
268
+ return null;
269
+ }
270
+ return { ctx, issue, n };
271
+ }
272
+ app.get('/:collection/:repo/issues', (0, web_1.ah)(async (req, res) => {
273
+ const viewer = (0, session_1.getViewer)(req, root);
274
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
275
+ if (!loaded)
276
+ return;
277
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
278
+ const asked = String(req.query.state ?? 'open');
279
+ const sortAsked = String(req.query.sort ?? 'newest');
280
+ const filter = {
281
+ state: asked === 'closed' ? 'closed' : asked === 'all' ? 'all' : 'open',
282
+ label: String(req.query.label ?? '').slice(0, 60),
283
+ author: String(req.query.author ?? '').slice(0, 60),
284
+ // A query is a reader's typing and becomes a substring match, not a
285
+ // pattern, so its only bound is a sane length.
286
+ query: String(req.query.q ?? '').slice(0, 200),
287
+ sort: (issues.ISSUE_SORTS.find((x) => x.key === sortAsked)?.key ?? 'newest'),
288
+ };
289
+ // Counts on the state tabs are of the whole repository, as GitHub's are,
290
+ // so they do not move as the other filters narrow the list.
291
+ const { all, matched } = issues.listIssuesAndMatches(root, ctx.collection, ctx.repo, filter.query);
292
+ const counts = {
293
+ open: all.filter((i) => i.state === 'open').length,
294
+ closed: all.filter((i) => i.state === 'closed').length,
295
+ };
296
+ const list = issues.selectIssues(matched, filter);
297
+ res
298
+ .type('html')
299
+ .send(listPage(ctx, list, filter, counts, issues.labelsInUse(all), issues.authorsInUse(all)));
300
+ }));
301
+ app.get('/:collection/:repo/issues/new', (0, web_1.ah)(async (req, res) => {
302
+ const viewer = (0, web_1.requireViewerPage)(root, req, res);
303
+ if (!viewer)
304
+ return;
305
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
306
+ if (!loaded)
307
+ return;
308
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
309
+ res.type('html').send(editPage(ctx, viewer, 'new', { title: '', body: '', labels: [] }));
310
+ }));
311
+ app.post('/:collection/:repo/issues/new', form, (0, web_1.ah)(async (req, res) => {
312
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
313
+ if (!viewer)
314
+ return;
315
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
316
+ if (!loaded)
317
+ return;
318
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
319
+ const title = (0, web_1.field)(req, 'title');
320
+ const text = (0, web_1.field)(req, 'body');
321
+ // Labels are a push-scope decision, so a form that carries them from
322
+ // someone without it is simply read without them.
323
+ const labels = ctx.canPush ? (0, web_1.field)(req, 'labels').split(',') : [];
324
+ try {
325
+ const created = issues.createIssue(root, ctx.collection, ctx.repo, {
326
+ title,
327
+ body: text,
328
+ author: viewer.auth.username,
329
+ labels,
330
+ });
331
+ res.redirect(`${issuesUrl(ctx)}/${created.number}`);
332
+ }
333
+ catch (e) {
334
+ const message = e instanceof ops_1.OpError ? e.message : 'Could not open the issue.';
335
+ res
336
+ .status(400)
337
+ .type('html')
338
+ .send(editPage(ctx, viewer, 'new', { title, body: text, labels: labels.map((l) => l.trim()).filter(Boolean) }, message));
339
+ }
340
+ }));
341
+ app.get('/:collection/:repo/issues/:n', (0, web_1.ah)(async (req, res) => {
342
+ const viewer = (0, session_1.getViewer)(req, root);
343
+ const found = await withIssue(req, res, viewer);
344
+ if (!found)
345
+ return;
346
+ res.type('html').send(issuePage(found.ctx, found.issue, found.ctx.canPush));
347
+ }));
348
+ app.get('/:collection/:repo/issues/:n/edit', (0, web_1.ah)(async (req, res) => {
349
+ const viewer = (0, web_1.requireViewerPage)(root, req, res);
350
+ if (!viewer)
351
+ return;
352
+ const found = await withIssue(req, res, viewer);
353
+ if (!found)
354
+ return;
355
+ const { ctx, issue } = found;
356
+ if (!ctx.canPush && viewer.auth.username !== issue.author) {
357
+ (0, web_1.fail)(res, 403, 'Only the author or someone with push access can edit this issue.', viewer, `${issuesUrl(ctx)}/${issue.number}`);
358
+ return;
359
+ }
360
+ res
361
+ .type('html')
362
+ .send(editPage(ctx, viewer, 'edit', { number: issue.number, title: issue.title, body: issue.body, labels: issue.labels }));
363
+ }));
364
+ app.post('/:collection/:repo/issues/:n/edit', form, (0, web_1.ah)(async (req, res) => {
365
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
366
+ if (!viewer)
367
+ return;
368
+ const found = await withIssue(req, res, viewer);
369
+ if (!found)
370
+ return;
371
+ const { ctx, issue } = found;
372
+ if (!ctx.canPush && viewer.auth.username !== issue.author) {
373
+ (0, web_1.fail)(res, 403, 'Only the author or someone with push access can edit this issue.', viewer, `${issuesUrl(ctx)}/${issue.number}`);
374
+ return;
375
+ }
376
+ const title = (0, web_1.field)(req, 'title');
377
+ const text = (0, web_1.field)(req, 'body');
378
+ try {
379
+ issues.editIssue(root, ctx.collection, ctx.repo, issue.number, {
380
+ title,
381
+ body: text,
382
+ labels: ctx.canPush ? (0, web_1.field)(req, 'labels').split(',') : undefined,
383
+ });
384
+ res.redirect(`${issuesUrl(ctx)}/${issue.number}`);
385
+ }
386
+ catch (e) {
387
+ const message = e instanceof ops_1.OpError ? e.message : 'Could not save the issue.';
388
+ res
389
+ .status(400)
390
+ .type('html')
391
+ .send(editPage(ctx, viewer, 'edit', { number: issue.number, title, body: text, labels: issue.labels }, message));
392
+ }
393
+ }));
394
+ app.post('/:collection/:repo/issues/:n/comment', form, (0, web_1.ah)(async (req, res) => {
395
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
396
+ if (!viewer)
397
+ return;
398
+ const found = await withIssue(req, res, viewer);
399
+ if (!found)
400
+ return;
401
+ const { ctx, issue } = found;
402
+ try {
403
+ issues.addComment(root, ctx.collection, ctx.repo, issue.number, {
404
+ author: viewer.auth.username,
405
+ body: (0, web_1.field)(req, 'body'),
406
+ });
407
+ }
408
+ catch (e) {
409
+ const message = e instanceof ops_1.OpError ? e.message : 'Could not add the comment.';
410
+ (0, web_1.fail)(res, 400, message, viewer, `${issuesUrl(ctx)}/${issue.number}`);
411
+ return;
412
+ }
413
+ res.redirect(`${issuesUrl(ctx)}/${issue.number}`);
414
+ }));
415
+ app.post('/:collection/:repo/issues/:n/state', form, (0, web_1.ah)(async (req, res) => {
416
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
417
+ if (!viewer)
418
+ return;
419
+ const found = await withIssue(req, res, viewer);
420
+ if (!found)
421
+ return;
422
+ const { ctx, issue } = found;
423
+ if (!ctx.canPush && viewer.auth.username !== issue.author) {
424
+ (0, web_1.fail)(res, 403, 'Only the author or someone with push access can close this issue.', viewer, `${issuesUrl(ctx)}/${issue.number}`);
425
+ return;
426
+ }
427
+ const state = (0, web_1.field)(req, 'state') === 'closed' ? 'closed' : 'open';
428
+ // A comment posted with the button is kept, so "close with a comment"
429
+ // is one action rather than two.
430
+ const text = (0, web_1.field)(req, 'body').trim();
431
+ if (text !== '') {
432
+ try {
433
+ issues.addComment(root, ctx.collection, ctx.repo, issue.number, { author: viewer.auth.username, body: text });
434
+ }
435
+ catch (e) {
436
+ // A comment too long to store must not stop the state change, which
437
+ // is the reason this is swallowed rather than reported. Only the
438
+ // validation failures are: a disk that is full or a directory that
439
+ // cannot be written is not something to close an issue quietly over.
440
+ if (!(e instanceof ops_1.OpError))
441
+ throw e;
442
+ }
443
+ }
444
+ issues.setIssueState(root, ctx.collection, ctx.repo, issue.number, state, viewer.auth.username);
445
+ res.redirect(`${issuesUrl(ctx)}/${issue.number}`);
446
+ }));
447
+ }
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.chooseEngine = chooseEngine;
37
+ exports.jobRunCmd = jobRunCmd;
38
+ const readline = __importStar(require("readline"));
39
+ const docker_1 = require("./runner/docker");
40
+ const manual_run_1 = require("./runner/manual-run");
41
+ function parseArgs(args, usage) {
42
+ const out = {
43
+ url: null,
44
+ token: null,
45
+ job: null,
46
+ yes: false,
47
+ engine: null,
48
+ images: {},
49
+ workDir: null,
50
+ network: null,
51
+ cacheDir: null,
52
+ actionsUrl: null,
53
+ actionCache: true,
54
+ };
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (a === '-h' || a === '--help')
58
+ usage();
59
+ else if (a === '--job')
60
+ out.job = args[++i] ?? null;
61
+ else if (a === '--yes' || a === '-y')
62
+ out.yes = true;
63
+ else if (a === '--engine') {
64
+ const e = args[++i] ?? '';
65
+ if (e !== 'docker' && e !== 'podman') {
66
+ console.error(`--engine takes docker or podman, got: ${e}`);
67
+ process.exit(1);
68
+ }
69
+ out.engine = e;
70
+ }
71
+ else if (a === '--image') {
72
+ const spec = args[++i] ?? '';
73
+ const eq = spec.indexOf('=');
74
+ if (eq === -1) {
75
+ console.error(`--image needs <label>=<image>, got: ${spec}`);
76
+ process.exit(1);
77
+ }
78
+ out.images[spec.slice(0, eq)] = spec.slice(eq + 1);
79
+ }
80
+ else if (a === '--work-dir')
81
+ out.workDir = args[++i] ?? null;
82
+ else if (a === '--network')
83
+ out.network = args[++i] ?? null;
84
+ else if (a === '--cache-dir')
85
+ out.cacheDir = args[++i] ?? null;
86
+ else if (a === '--actions-url')
87
+ out.actionsUrl = args[++i] ?? null;
88
+ else if (a === '--no-action-cache')
89
+ out.actionCache = false;
90
+ else if (a.startsWith('-')) {
91
+ console.error(`Unknown option: ${a}`);
92
+ process.exit(1);
93
+ }
94
+ else if (!out.url)
95
+ out.url = a;
96
+ else if (!out.token)
97
+ out.token = a;
98
+ else {
99
+ console.error(`Unexpected argument: ${a}`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ function ask(question) {
106
+ return new Promise((resolve) => {
107
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
108
+ // A closed stdin (Ctrl-D, or the terminal going away) answers nothing,
109
+ // and nothing must never read as consent. The flag matters: rl.close()
110
+ // below emits 'close' synchronously, and without it the empty answer
111
+ // would win the race against the real one.
112
+ let answered = false;
113
+ rl.on('close', () => {
114
+ if (!answered)
115
+ resolve('');
116
+ });
117
+ rl.question(question, (answer) => {
118
+ answered = true;
119
+ rl.close();
120
+ resolve(answer);
121
+ });
122
+ });
123
+ }
124
+ /**
125
+ * Which container engine to use: the flag if given, whatever is present if
126
+ * only one is, and a question with docker as the default when both are. The
127
+ * probe demands a working daemon, not just a binary on the path, since a
128
+ * docker command with nothing behind it runs nothing.
129
+ */
130
+ async function chooseEngine(flag, interactive) {
131
+ if (flag) {
132
+ const v = await (0, docker_1.probeEngine)(flag);
133
+ if (!v) {
134
+ console.error(`--engine ${flag}: ${flag} is not available here, or its daemon is not running.`);
135
+ process.exit(1);
136
+ }
137
+ return flag;
138
+ }
139
+ const [docker, podman] = await Promise.all([(0, docker_1.probeEngine)('docker'), (0, docker_1.probeEngine)('podman')]);
140
+ if (docker && !podman)
141
+ return 'docker';
142
+ if (podman && !docker) {
143
+ console.log(`Using podman ${podman} (no working docker here).`);
144
+ return 'podman';
145
+ }
146
+ if (!docker && !podman) {
147
+ console.error('Neither docker nor podman works here. Jobs run in containers, so one of the two is needed.');
148
+ process.exit(1);
149
+ }
150
+ if (!interactive)
151
+ return 'docker';
152
+ const answer = (await ask(`Both docker (${docker}) and podman (${podman}) work here. Use which? [docker] `)).trim().toLowerCase();
153
+ if (answer === '' || answer === 'docker' || answer === 'd')
154
+ return 'docker';
155
+ if (answer === 'podman' || answer === 'p')
156
+ return 'podman';
157
+ console.error(`That is neither; pass --engine docker or --engine podman.`);
158
+ process.exit(1);
159
+ }
160
+ async function jobRunCmd(args, usage) {
161
+ const a = parseArgs(args, usage);
162
+ if (!a.url || !a.token) {
163
+ console.error('Usage: mochi job run <vault-url> <token> (both come from the run page, minted together)');
164
+ process.exit(1);
165
+ }
166
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
167
+ if (!a.yes && !interactive) {
168
+ console.error('There is no terminal to confirm the steps on. Pass --yes to run without confirmation, knowing that this executes whatever the workflow contains.');
169
+ process.exit(1);
170
+ }
171
+ const engine = await chooseEngine(a.engine, interactive);
172
+ (0, docker_1.setContainerEngine)(engine);
173
+ const session = new manual_run_1.ManualSession({
174
+ host: a.url.replace(/\/+$/, ''),
175
+ token: a.token,
176
+ jobFilter: a.job,
177
+ yes: a.yes,
178
+ images: a.images,
179
+ workDir: a.workDir ?? undefined,
180
+ network: a.network ?? undefined,
181
+ cacheDir: a.cacheDir ?? undefined,
182
+ actionsUrl: a.actionsUrl ?? undefined,
183
+ actionCache: a.actionCache,
184
+ confirm: async (q) => {
185
+ const answer = (await ask(q)).trim().toLowerCase();
186
+ return answer === 'y' || answer === 'yes';
187
+ },
188
+ });
189
+ const stop = () => {
190
+ console.log('\nStopping after the current job. Press Ctrl-C again to quit now, which fails a job in progress.');
191
+ session.stop();
192
+ process.once('SIGINT', () => process.exit(130));
193
+ };
194
+ process.once('SIGINT', stop);
195
+ process.once('SIGTERM', stop);
196
+ process.exit(await session.run());
197
+ }