pageship 0.0.1 → 0.1.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 +94 -6
- package/dist/cli.mjs +1591 -6
- package/package.json +6 -8
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,1598 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
11
|
+
import { NodeServices } from "@effect/platform-node";
|
|
12
|
+
import { Cause, Config, Console, Effect, Option, Schema } from "effect";
|
|
13
|
+
import { Argument, CliConfig, CliError, CliOutput, Command, Flag, GlobalFlag } from "effect/unstable/cli";
|
|
14
|
+
//#region ../../packages/api-client/src/generated/core/bodySerializer.gen.ts
|
|
15
|
+
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
16
|
+
Object.entries({
|
|
17
|
+
$body_: "body",
|
|
18
|
+
$headers_: "headers",
|
|
19
|
+
$path_: "path",
|
|
20
|
+
$query_: "query"
|
|
21
|
+
});
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region ../../packages/api-client/src/generated/core/serverSentEvents.gen.ts
|
|
24
|
+
function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
|
|
25
|
+
let lastEventId;
|
|
26
|
+
const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
27
|
+
const createStream = async function* () {
|
|
28
|
+
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
31
|
+
while (true) {
|
|
32
|
+
if (signal.aborted) break;
|
|
33
|
+
attempt++;
|
|
34
|
+
const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
|
|
35
|
+
if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
|
|
36
|
+
try {
|
|
37
|
+
const requestInit = {
|
|
38
|
+
redirect: "follow",
|
|
39
|
+
...options,
|
|
40
|
+
body: options.serializedBody,
|
|
41
|
+
headers,
|
|
42
|
+
signal
|
|
43
|
+
};
|
|
44
|
+
let request = new Request(url, requestInit);
|
|
45
|
+
if (onRequest) request = await onRequest(url, requestInit);
|
|
46
|
+
const response = await (options.fetch ?? globalThis.fetch)(request);
|
|
47
|
+
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
|
48
|
+
if (!response.body) throw new Error("No body in SSE response");
|
|
49
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
50
|
+
let buffer = "";
|
|
51
|
+
const abortHandler = () => {
|
|
52
|
+
try {
|
|
53
|
+
reader.cancel();
|
|
54
|
+
} catch {}
|
|
55
|
+
};
|
|
56
|
+
signal.addEventListener("abort", abortHandler);
|
|
57
|
+
try {
|
|
58
|
+
while (true) {
|
|
59
|
+
const { done, value } = await reader.read();
|
|
60
|
+
if (done) break;
|
|
61
|
+
buffer += value;
|
|
62
|
+
buffer = buffer.replace(/\r\n?/g, "\n");
|
|
63
|
+
const chunks = buffer.split("\n\n");
|
|
64
|
+
buffer = chunks.pop() ?? "";
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
const lines = chunk.split("\n");
|
|
67
|
+
const dataLines = [];
|
|
68
|
+
let eventName;
|
|
69
|
+
for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
|
|
70
|
+
else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
|
|
71
|
+
else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
|
|
72
|
+
else if (line.startsWith("retry:")) {
|
|
73
|
+
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
|
74
|
+
if (!Number.isNaN(parsed)) retryDelay = parsed;
|
|
75
|
+
}
|
|
76
|
+
let data;
|
|
77
|
+
let parsedJson = false;
|
|
78
|
+
if (dataLines.length) {
|
|
79
|
+
const rawData = dataLines.join("\n");
|
|
80
|
+
try {
|
|
81
|
+
data = JSON.parse(rawData);
|
|
82
|
+
parsedJson = true;
|
|
83
|
+
} catch {
|
|
84
|
+
data = rawData;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (parsedJson) {
|
|
88
|
+
if (responseValidator) await responseValidator(data);
|
|
89
|
+
if (responseTransformer) data = await responseTransformer(data);
|
|
90
|
+
}
|
|
91
|
+
onSseEvent?.({
|
|
92
|
+
data,
|
|
93
|
+
event: eventName,
|
|
94
|
+
id: lastEventId,
|
|
95
|
+
retry: retryDelay
|
|
96
|
+
});
|
|
97
|
+
if (dataLines.length) yield data;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
signal.removeEventListener("abort", abortHandler);
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
onSseError?.(error);
|
|
107
|
+
if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
|
|
108
|
+
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
|
|
109
|
+
await sleep(backoff);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return { stream: createStream() };
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../packages/api-client/src/generated/core/pathSerializer.gen.ts
|
|
117
|
+
const separatorArrayExplode = (style) => {
|
|
118
|
+
switch (style) {
|
|
119
|
+
case "label": return ".";
|
|
120
|
+
case "matrix": return ";";
|
|
121
|
+
case "simple": return ",";
|
|
122
|
+
default: return "&";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const separatorArrayNoExplode = (style) => {
|
|
126
|
+
switch (style) {
|
|
127
|
+
case "form": return ",";
|
|
128
|
+
case "pipeDelimited": return "|";
|
|
129
|
+
case "spaceDelimited": return "%20";
|
|
130
|
+
default: return ",";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const separatorObjectExplode = (style) => {
|
|
134
|
+
switch (style) {
|
|
135
|
+
case "label": return ".";
|
|
136
|
+
case "matrix": return ";";
|
|
137
|
+
case "simple": return ",";
|
|
138
|
+
default: return "&";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
|
|
142
|
+
if (!explode) {
|
|
143
|
+
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
|
|
144
|
+
switch (style) {
|
|
145
|
+
case "label": return `.${joinedValues}`;
|
|
146
|
+
case "matrix": return `;${name}=${joinedValues}`;
|
|
147
|
+
case "simple": return joinedValues;
|
|
148
|
+
default: return `${name}=${joinedValues}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const separator = separatorArrayExplode(style);
|
|
152
|
+
const joinedValues = value.map((v) => {
|
|
153
|
+
if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
|
|
154
|
+
return serializePrimitiveParam({
|
|
155
|
+
allowReserved,
|
|
156
|
+
name,
|
|
157
|
+
value: v
|
|
158
|
+
});
|
|
159
|
+
}).join(separator);
|
|
160
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
161
|
+
};
|
|
162
|
+
const serializePrimitiveParam = ({ allowReserved, name, value }) => {
|
|
163
|
+
if (value === void 0 || value === null) return "";
|
|
164
|
+
if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
|
|
165
|
+
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
166
|
+
};
|
|
167
|
+
const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
|
|
168
|
+
if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
169
|
+
if (style !== "deepObject" && !explode) {
|
|
170
|
+
let values = [];
|
|
171
|
+
Object.entries(value).forEach(([key, v]) => {
|
|
172
|
+
values = [
|
|
173
|
+
...values,
|
|
174
|
+
key,
|
|
175
|
+
allowReserved ? v : encodeURIComponent(v)
|
|
176
|
+
];
|
|
177
|
+
});
|
|
178
|
+
const joinedValues = values.join(",");
|
|
179
|
+
switch (style) {
|
|
180
|
+
case "form": return `${name}=${joinedValues}`;
|
|
181
|
+
case "label": return `.${joinedValues}`;
|
|
182
|
+
case "matrix": return `;${name}=${joinedValues}`;
|
|
183
|
+
default: return joinedValues;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const separator = separatorObjectExplode(style);
|
|
187
|
+
const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
|
|
188
|
+
allowReserved,
|
|
189
|
+
name: style === "deepObject" ? `${name}[${key}]` : key,
|
|
190
|
+
value: v
|
|
191
|
+
})).join(separator);
|
|
192
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region ../../packages/api-client/src/generated/core/utils.gen.ts
|
|
196
|
+
const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
197
|
+
const defaultPathSerializer = ({ path, url: _url }) => {
|
|
198
|
+
let url = _url;
|
|
199
|
+
const matches = _url.match(PATH_PARAM_RE);
|
|
200
|
+
if (matches) for (const match of matches) {
|
|
201
|
+
let explode = false;
|
|
202
|
+
let name = match.substring(1, match.length - 1);
|
|
203
|
+
let style = "simple";
|
|
204
|
+
if (name.endsWith("*")) {
|
|
205
|
+
explode = true;
|
|
206
|
+
name = name.substring(0, name.length - 1);
|
|
207
|
+
}
|
|
208
|
+
if (name.startsWith(".")) {
|
|
209
|
+
name = name.substring(1);
|
|
210
|
+
style = "label";
|
|
211
|
+
} else if (name.startsWith(";")) {
|
|
212
|
+
name = name.substring(1);
|
|
213
|
+
style = "matrix";
|
|
214
|
+
}
|
|
215
|
+
const value = path[name];
|
|
216
|
+
if (value === void 0 || value === null) continue;
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
url = url.replace(match, serializeArrayParam({
|
|
219
|
+
explode,
|
|
220
|
+
name,
|
|
221
|
+
style,
|
|
222
|
+
value
|
|
223
|
+
}));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (typeof value === "object") {
|
|
227
|
+
url = url.replace(match, serializeObjectParam({
|
|
228
|
+
explode,
|
|
229
|
+
name,
|
|
230
|
+
style,
|
|
231
|
+
value,
|
|
232
|
+
valueOnly: true
|
|
233
|
+
}));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (style === "matrix") {
|
|
237
|
+
url = url.replace(match, `;${serializePrimitiveParam({
|
|
238
|
+
name,
|
|
239
|
+
value
|
|
240
|
+
})}`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
|
|
244
|
+
url = url.replace(match, replaceValue);
|
|
245
|
+
}
|
|
246
|
+
return url;
|
|
247
|
+
};
|
|
248
|
+
const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
|
|
249
|
+
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
|
250
|
+
let url = (baseUrl ?? "") + pathUrl;
|
|
251
|
+
if (path) url = defaultPathSerializer({
|
|
252
|
+
path,
|
|
253
|
+
url
|
|
254
|
+
});
|
|
255
|
+
let search = query ? querySerializer(query) : "";
|
|
256
|
+
if (search.startsWith("?")) search = search.substring(1);
|
|
257
|
+
if (search) url += `?${search}`;
|
|
258
|
+
return url;
|
|
259
|
+
};
|
|
260
|
+
function getValidRequestBody(options) {
|
|
261
|
+
const hasBody = options.body !== void 0;
|
|
262
|
+
if (hasBody && options.bodySerializer) {
|
|
263
|
+
if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
|
|
264
|
+
return options.body !== "" ? options.body : null;
|
|
265
|
+
}
|
|
266
|
+
if (hasBody) return options.body;
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region ../../packages/api-client/src/generated/core/auth.gen.ts
|
|
270
|
+
const getAuthToken = async (auth, callback) => {
|
|
271
|
+
const token = typeof callback === "function" ? await callback(auth) : callback;
|
|
272
|
+
if (!token) return;
|
|
273
|
+
if (auth.scheme === "bearer") return `Bearer ${token}`;
|
|
274
|
+
if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
|
|
275
|
+
return token;
|
|
276
|
+
};
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region ../../packages/api-client/src/generated/client/utils.gen.ts
|
|
279
|
+
const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
|
|
280
|
+
const querySerializer = (queryParams) => {
|
|
281
|
+
const search = [];
|
|
282
|
+
if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
|
|
283
|
+
const value = queryParams[name];
|
|
284
|
+
if (value === void 0 || value === null) continue;
|
|
285
|
+
const options = parameters[name] || args;
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
const serializedArray = serializeArrayParam({
|
|
288
|
+
allowReserved: options.allowReserved,
|
|
289
|
+
explode: true,
|
|
290
|
+
name,
|
|
291
|
+
style: "form",
|
|
292
|
+
value,
|
|
293
|
+
...options.array
|
|
294
|
+
});
|
|
295
|
+
if (serializedArray) search.push(serializedArray);
|
|
296
|
+
} else if (typeof value === "object") {
|
|
297
|
+
const serializedObject = serializeObjectParam({
|
|
298
|
+
allowReserved: options.allowReserved,
|
|
299
|
+
explode: true,
|
|
300
|
+
name,
|
|
301
|
+
style: "deepObject",
|
|
302
|
+
value,
|
|
303
|
+
...options.object
|
|
304
|
+
});
|
|
305
|
+
if (serializedObject) search.push(serializedObject);
|
|
306
|
+
} else {
|
|
307
|
+
const serializedPrimitive = serializePrimitiveParam({
|
|
308
|
+
allowReserved: options.allowReserved,
|
|
309
|
+
name,
|
|
310
|
+
value
|
|
311
|
+
});
|
|
312
|
+
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return search.join("&");
|
|
316
|
+
};
|
|
317
|
+
return querySerializer;
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Infers parseAs value from provided Content-Type header.
|
|
321
|
+
*/
|
|
322
|
+
const getParseAs = (contentType) => {
|
|
323
|
+
if (!contentType) return "stream";
|
|
324
|
+
const cleanContent = contentType.split(";")[0]?.trim();
|
|
325
|
+
if (!cleanContent) return;
|
|
326
|
+
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
|
|
327
|
+
if (cleanContent === "multipart/form-data") return "formData";
|
|
328
|
+
if ([
|
|
329
|
+
"application/",
|
|
330
|
+
"audio/",
|
|
331
|
+
"image/",
|
|
332
|
+
"video/"
|
|
333
|
+
].some((type) => cleanContent.startsWith(type))) return "blob";
|
|
334
|
+
if (cleanContent.startsWith("text/")) return "text";
|
|
335
|
+
};
|
|
336
|
+
const checkForExistence = (options, name) => {
|
|
337
|
+
if (!name) return false;
|
|
338
|
+
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
|
|
339
|
+
return false;
|
|
340
|
+
};
|
|
341
|
+
async function setAuthParams(options) {
|
|
342
|
+
for (const auth of options.security ?? []) {
|
|
343
|
+
if (checkForExistence(options, auth.name)) continue;
|
|
344
|
+
const token = await getAuthToken(auth, options.auth);
|
|
345
|
+
if (!token) continue;
|
|
346
|
+
const name = auth.name ?? "Authorization";
|
|
347
|
+
switch (auth.in) {
|
|
348
|
+
case "query":
|
|
349
|
+
if (!options.query) options.query = {};
|
|
350
|
+
options.query[name] = token;
|
|
351
|
+
break;
|
|
352
|
+
case "cookie":
|
|
353
|
+
options.headers.append("Cookie", `${name}=${token}`);
|
|
354
|
+
break;
|
|
355
|
+
default: options.headers.set(name, token);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const buildUrl = (options) => getUrl({
|
|
360
|
+
baseUrl: options.baseUrl,
|
|
361
|
+
path: options.path,
|
|
362
|
+
query: options.query,
|
|
363
|
+
querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
|
|
364
|
+
url: options.url
|
|
365
|
+
});
|
|
366
|
+
const mergeConfigs = (a, b) => {
|
|
367
|
+
const config = {
|
|
368
|
+
...a,
|
|
369
|
+
...b
|
|
370
|
+
};
|
|
371
|
+
if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
372
|
+
config.headers = mergeHeaders(a.headers, b.headers);
|
|
373
|
+
return config;
|
|
374
|
+
};
|
|
375
|
+
const headersEntries = (headers) => {
|
|
376
|
+
const entries = [];
|
|
377
|
+
headers.forEach((value, key) => {
|
|
378
|
+
entries.push([key, value]);
|
|
379
|
+
});
|
|
380
|
+
return entries;
|
|
381
|
+
};
|
|
382
|
+
const mergeHeaders = (...headers) => {
|
|
383
|
+
const mergedHeaders = new Headers();
|
|
384
|
+
for (const header of headers) {
|
|
385
|
+
if (!header) continue;
|
|
386
|
+
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
|
387
|
+
for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
|
|
388
|
+
else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
|
|
389
|
+
else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
|
|
390
|
+
}
|
|
391
|
+
return mergedHeaders;
|
|
392
|
+
};
|
|
393
|
+
var Interceptors = class {
|
|
394
|
+
fns = [];
|
|
395
|
+
clear() {
|
|
396
|
+
this.fns = [];
|
|
397
|
+
}
|
|
398
|
+
eject(id) {
|
|
399
|
+
const index = this.getInterceptorIndex(id);
|
|
400
|
+
if (this.fns[index]) this.fns[index] = null;
|
|
401
|
+
}
|
|
402
|
+
exists(id) {
|
|
403
|
+
const index = this.getInterceptorIndex(id);
|
|
404
|
+
return Boolean(this.fns[index]);
|
|
405
|
+
}
|
|
406
|
+
getInterceptorIndex(id) {
|
|
407
|
+
if (typeof id === "number") return this.fns[id] ? id : -1;
|
|
408
|
+
return this.fns.indexOf(id);
|
|
409
|
+
}
|
|
410
|
+
update(id, fn) {
|
|
411
|
+
const index = this.getInterceptorIndex(id);
|
|
412
|
+
if (this.fns[index]) {
|
|
413
|
+
this.fns[index] = fn;
|
|
414
|
+
return id;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
use(fn) {
|
|
419
|
+
this.fns.push(fn);
|
|
420
|
+
return this.fns.length - 1;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const createInterceptors = () => ({
|
|
424
|
+
error: new Interceptors(),
|
|
425
|
+
request: new Interceptors(),
|
|
426
|
+
response: new Interceptors()
|
|
427
|
+
});
|
|
428
|
+
const defaultQuerySerializer = createQuerySerializer({
|
|
429
|
+
allowReserved: false,
|
|
430
|
+
array: {
|
|
431
|
+
explode: true,
|
|
432
|
+
style: "form"
|
|
433
|
+
},
|
|
434
|
+
object: {
|
|
435
|
+
explode: true,
|
|
436
|
+
style: "deepObject"
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
const defaultHeaders = { "Content-Type": "application/json" };
|
|
440
|
+
const createConfig = (override = {}) => ({
|
|
441
|
+
...jsonBodySerializer,
|
|
442
|
+
headers: defaultHeaders,
|
|
443
|
+
parseAs: "auto",
|
|
444
|
+
querySerializer: defaultQuerySerializer,
|
|
445
|
+
...override
|
|
446
|
+
});
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region ../../packages/api-client/src/generated/client/client.gen.ts
|
|
449
|
+
const createClient = (config = {}) => {
|
|
450
|
+
let _config = mergeConfigs(createConfig(), config);
|
|
451
|
+
const getConfig = () => ({ ..._config });
|
|
452
|
+
const setConfig = (config) => {
|
|
453
|
+
_config = mergeConfigs(_config, config);
|
|
454
|
+
return getConfig();
|
|
455
|
+
};
|
|
456
|
+
const interceptors = createInterceptors();
|
|
457
|
+
const beforeRequest = async (options) => {
|
|
458
|
+
const opts = {
|
|
459
|
+
..._config,
|
|
460
|
+
...options,
|
|
461
|
+
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
462
|
+
headers: mergeHeaders(_config.headers, options.headers),
|
|
463
|
+
serializedBody: void 0
|
|
464
|
+
};
|
|
465
|
+
if (opts.security) await setAuthParams(opts);
|
|
466
|
+
if (opts.requestValidator) await opts.requestValidator(opts);
|
|
467
|
+
if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
|
|
468
|
+
if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
|
|
469
|
+
const resolvedOpts = opts;
|
|
470
|
+
return {
|
|
471
|
+
opts: resolvedOpts,
|
|
472
|
+
url: buildUrl(resolvedOpts)
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
const request = async (options) => {
|
|
476
|
+
const throwOnError = options.throwOnError ?? _config.throwOnError;
|
|
477
|
+
const responseStyle = options.responseStyle ?? _config.responseStyle;
|
|
478
|
+
let request;
|
|
479
|
+
let response;
|
|
480
|
+
try {
|
|
481
|
+
const { opts, url } = await beforeRequest(options);
|
|
482
|
+
const requestInit = {
|
|
483
|
+
redirect: "follow",
|
|
484
|
+
...opts,
|
|
485
|
+
body: getValidRequestBody(opts)
|
|
486
|
+
};
|
|
487
|
+
request = new Request(url, requestInit);
|
|
488
|
+
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
489
|
+
const _fetch = opts.fetch;
|
|
490
|
+
response = await _fetch(request);
|
|
491
|
+
for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
|
|
492
|
+
const result = {
|
|
493
|
+
request,
|
|
494
|
+
response
|
|
495
|
+
};
|
|
496
|
+
if (response.ok) {
|
|
497
|
+
const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
|
|
498
|
+
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
|
499
|
+
let emptyData;
|
|
500
|
+
switch (parseAs) {
|
|
501
|
+
case "arrayBuffer":
|
|
502
|
+
case "blob":
|
|
503
|
+
case "text":
|
|
504
|
+
emptyData = await response[parseAs]();
|
|
505
|
+
break;
|
|
506
|
+
case "formData":
|
|
507
|
+
emptyData = new FormData();
|
|
508
|
+
break;
|
|
509
|
+
case "stream":
|
|
510
|
+
emptyData = response.body;
|
|
511
|
+
break;
|
|
512
|
+
default: emptyData = {};
|
|
513
|
+
}
|
|
514
|
+
return opts.responseStyle === "data" ? emptyData : {
|
|
515
|
+
data: emptyData,
|
|
516
|
+
...result
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
let data;
|
|
520
|
+
switch (parseAs) {
|
|
521
|
+
case "arrayBuffer":
|
|
522
|
+
case "blob":
|
|
523
|
+
case "formData":
|
|
524
|
+
case "text":
|
|
525
|
+
data = await response[parseAs]();
|
|
526
|
+
break;
|
|
527
|
+
case "json": {
|
|
528
|
+
const text = await response.text();
|
|
529
|
+
data = text ? JSON.parse(text) : {};
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "stream": return opts.responseStyle === "data" ? response.body : {
|
|
533
|
+
data: response.body,
|
|
534
|
+
...result
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (parseAs === "json") {
|
|
538
|
+
if (opts.responseValidator) await opts.responseValidator(data);
|
|
539
|
+
if (opts.responseTransformer) data = await opts.responseTransformer(data);
|
|
540
|
+
}
|
|
541
|
+
return opts.responseStyle === "data" ? data : {
|
|
542
|
+
data,
|
|
543
|
+
...result
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const textError = await response.text();
|
|
547
|
+
let jsonError;
|
|
548
|
+
try {
|
|
549
|
+
jsonError = JSON.parse(textError);
|
|
550
|
+
} catch {}
|
|
551
|
+
throw jsonError ?? textError;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
let finalError = error;
|
|
554
|
+
for (const fn of interceptors.error.fns) if (fn) finalError = await fn(finalError, response, request, options);
|
|
555
|
+
finalError = finalError || {};
|
|
556
|
+
if (throwOnError) throw finalError;
|
|
557
|
+
return responseStyle === "data" ? void 0 : {
|
|
558
|
+
error: finalError,
|
|
559
|
+
request,
|
|
560
|
+
response
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const makeMethodFn = (method) => (options) => request({
|
|
565
|
+
...options,
|
|
566
|
+
method
|
|
567
|
+
});
|
|
568
|
+
const makeSseFn = (method) => async (options) => {
|
|
569
|
+
const { opts, url } = await beforeRequest(options);
|
|
570
|
+
return createSseClient({
|
|
571
|
+
...opts,
|
|
572
|
+
body: opts.body,
|
|
573
|
+
method,
|
|
574
|
+
onRequest: async (url, init) => {
|
|
575
|
+
let request = new Request(url, init);
|
|
576
|
+
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
577
|
+
return request;
|
|
578
|
+
},
|
|
579
|
+
serializedBody: getValidRequestBody(opts),
|
|
580
|
+
url
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
const _buildUrl = (options) => buildUrl({
|
|
584
|
+
..._config,
|
|
585
|
+
...options
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
buildUrl: _buildUrl,
|
|
589
|
+
connect: makeMethodFn("CONNECT"),
|
|
590
|
+
delete: makeMethodFn("DELETE"),
|
|
591
|
+
get: makeMethodFn("GET"),
|
|
592
|
+
getConfig,
|
|
593
|
+
head: makeMethodFn("HEAD"),
|
|
594
|
+
interceptors,
|
|
595
|
+
options: makeMethodFn("OPTIONS"),
|
|
596
|
+
patch: makeMethodFn("PATCH"),
|
|
597
|
+
post: makeMethodFn("POST"),
|
|
598
|
+
put: makeMethodFn("PUT"),
|
|
599
|
+
request,
|
|
600
|
+
setConfig,
|
|
601
|
+
sse: {
|
|
602
|
+
connect: makeSseFn("CONNECT"),
|
|
603
|
+
delete: makeSseFn("DELETE"),
|
|
604
|
+
get: makeSseFn("GET"),
|
|
605
|
+
head: makeSseFn("HEAD"),
|
|
606
|
+
options: makeSseFn("OPTIONS"),
|
|
607
|
+
patch: makeSseFn("PATCH"),
|
|
608
|
+
post: makeSseFn("POST"),
|
|
609
|
+
put: makeSseFn("PUT"),
|
|
610
|
+
trace: makeSseFn("TRACE")
|
|
611
|
+
},
|
|
612
|
+
trace: makeMethodFn("TRACE")
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region ../../packages/api-client/src/generated/client.gen.ts
|
|
617
|
+
const client = createClient(createConfig());
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region ../../packages/api-client/src/generated/sdk.gen.ts
|
|
620
|
+
const systemCompatibility = (options) => (options?.client ?? client).get({
|
|
621
|
+
url: "/api/compatibility",
|
|
622
|
+
...options
|
|
623
|
+
});
|
|
624
|
+
const personalWorkspace = (options) => (options?.client ?? client).get({
|
|
625
|
+
url: "/api/v1/workspace",
|
|
626
|
+
...options
|
|
627
|
+
});
|
|
628
|
+
const addressAllocate = (options) => (options.client ?? client).post({
|
|
629
|
+
url: "/api/v1/workspace/address",
|
|
630
|
+
...options,
|
|
631
|
+
headers: {
|
|
632
|
+
"Content-Type": "application/json",
|
|
633
|
+
...options.headers
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
const documentsCreate = (options) => (options.client ?? client).post({
|
|
637
|
+
bodySerializer: null,
|
|
638
|
+
url: "/api/v1/documents",
|
|
639
|
+
...options,
|
|
640
|
+
headers: {
|
|
641
|
+
"Content-Type": "text/html",
|
|
642
|
+
...options.headers
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
const documentsLimits = (options) => (options?.client ?? client).get({
|
|
646
|
+
url: "/api/v1/documents/limits",
|
|
647
|
+
...options
|
|
648
|
+
});
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region ../../packages/api-client/src/address.ts
|
|
651
|
+
async function updateWorkspaceAddress(baseUrl, action, options = {}) {
|
|
652
|
+
const timeout = AbortSignal.timeout(1e4);
|
|
653
|
+
const { data, response, error } = await addressAllocate({
|
|
654
|
+
baseUrl,
|
|
655
|
+
body: action,
|
|
656
|
+
credentials: options.token ? "omit" : "include",
|
|
657
|
+
headers: options.token ? { Authorization: `Bearer ${options.token}` } : void 0,
|
|
658
|
+
redirect: "error",
|
|
659
|
+
cache: "no-store",
|
|
660
|
+
throwOnError: false,
|
|
661
|
+
signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout
|
|
662
|
+
});
|
|
663
|
+
if (!response?.ok) throw new Error("Could not update your workspace address. Try again.", { cause: error });
|
|
664
|
+
if (data?.status === "signed_out" || data?.status === "uninvited" || data?.status === "reauthentication_required") return { status: data.status };
|
|
665
|
+
const validAddress = (value) => typeof value.subdomain === "string" && /^[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?$/u.test(value.subdomain) && typeof value.hostname === "string" && value.hostname === `${value.subdomain}.pageship.dev`;
|
|
666
|
+
if (data?.status === "allocated" && validAddress(data)) return data;
|
|
667
|
+
if (data?.status === "choice_required" && ["unavailable", "stale_choice"].includes(data.reason) && Array.isArray(data.choices) && data.choices.every((choice) => choice && typeof choice.choiceId === "string" && choice.choiceId.length > 0 && validAddress(choice))) return data;
|
|
668
|
+
throw new Error("Invalid workspace address response.");
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region ../../packages/api-client/src/documents.ts
|
|
672
|
+
function isDocumentId(value) {
|
|
673
|
+
return typeof value === "string" && /^[A-Za-z0-9]{6}(?![\s\S])/u.test(value);
|
|
674
|
+
}
|
|
675
|
+
function isRevisionId(value) {
|
|
676
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?![\s\S])/iu.test(value);
|
|
677
|
+
}
|
|
678
|
+
function requestOptions(baseUrl, options) {
|
|
679
|
+
const timeout = AbortSignal.timeout(3e4);
|
|
680
|
+
return {
|
|
681
|
+
baseUrl,
|
|
682
|
+
credentials: options.token ? "omit" : "include",
|
|
683
|
+
headers: options.token ? { Authorization: `Bearer ${options.token}` } : {},
|
|
684
|
+
signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
|
|
685
|
+
redirect: "error",
|
|
686
|
+
cache: "no-store",
|
|
687
|
+
throwOnError: false
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function record(value) {
|
|
691
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
692
|
+
}
|
|
693
|
+
function validTitle(value) {
|
|
694
|
+
return typeof value === "string" && value.length >= 1 && value.length <= 120 && value === value.trim() && !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/u.test(value);
|
|
695
|
+
}
|
|
696
|
+
function normalizeDocument(value) {
|
|
697
|
+
if (!record(value) || typeof value.slug !== "string") return;
|
|
698
|
+
const normalized = value.title === void 0 ? {
|
|
699
|
+
...value,
|
|
700
|
+
title: value.slug
|
|
701
|
+
} : value;
|
|
702
|
+
return validDocument(normalized) ? normalized : void 0;
|
|
703
|
+
}
|
|
704
|
+
function validDocument(value) {
|
|
705
|
+
return record(value) && isDocumentId(value.id) && typeof value.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*(?![\s\S])/u.test(value.slug) && validTitle(value.title) && typeof value.sourcePath === "string" && value.sourcePath.length <= 1024 && /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[^\x00-\x1f\x7f\\:]*\.[hH][tT][mM][lL]?(?![\s\S])/u.test(value.sourcePath) && typeof value.sourceContext === "string" && /^(?:personal|git:[a-f0-9]{64})(?![\s\S])/u.test(value.sourceContext) && (value.visibility === "private" || value.visibility === "public") && isRevisionId(value.revisionId) && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.pageship\\.dev/d/${value.id}-${value.slug}$`, "u").test(value.url);
|
|
706
|
+
}
|
|
707
|
+
var DocumentRequestError = class extends Error {
|
|
708
|
+
status;
|
|
709
|
+
httpStatus;
|
|
710
|
+
constructor(status, message, httpStatus) {
|
|
711
|
+
super(message);
|
|
712
|
+
this.status = status;
|
|
713
|
+
this.httpStatus = httpStatus;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
function failed(response, error) {
|
|
717
|
+
if (response && (response.status === 401 || response.status === 403)) throw new DocumentRequestError(response.status === 401 ? "signed_out" : "uninvited", response.status === 401 ? "Your installation is not authorized. Run login again." : "This account is not invited.", response.status);
|
|
718
|
+
if (response?.status === 413) throw new DocumentRequestError("upload_too_large", "HTML exceeds the server's per-file limit.", 413);
|
|
719
|
+
throw new Error("Could not complete the document request. Retry with the same idempotency key.", { cause: error });
|
|
720
|
+
}
|
|
721
|
+
async function getDocumentLimits(baseUrl, options = {}) {
|
|
722
|
+
const { data: received, response, error } = await documentsLimits(requestOptions(baseUrl, options));
|
|
723
|
+
if (!response?.ok) failed(response, error);
|
|
724
|
+
const data = received;
|
|
725
|
+
if (!record(data) || typeof data.maxUploadBytes !== "number" || !Number.isSafeInteger(data.maxUploadBytes) || data.maxUploadBytes < 1) throw new Error("Invalid document limits response.");
|
|
726
|
+
if (data.publicationActions !== void 0 && data.publicationActions !== null && (!Array.isArray(data.publicationActions) || !data.publicationActions.every((action) => typeof action === "string"))) throw new Error("Invalid publication capabilities response.");
|
|
727
|
+
const publicationActions = Array.isArray(data.publicationActions) ? data.publicationActions.filter((action) => action === "new" || action === "revise") : [];
|
|
728
|
+
return {
|
|
729
|
+
maxUploadBytes: data.maxUploadBytes,
|
|
730
|
+
publicationActions
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async function publishDocument(baseUrl, input, options = {}) {
|
|
734
|
+
if (input.action) {
|
|
735
|
+
if (!(await getDocumentLimits(baseUrl, options)).publicationActions.includes(input.action)) throw new DocumentRequestError("publication_action_unsupported", "This server does not support explicit publication actions. Ask the operator to update the server. No HTML was uploaded.", 400);
|
|
736
|
+
}
|
|
737
|
+
const request = requestOptions(baseUrl, options);
|
|
738
|
+
const { data: received, response, error } = await documentsCreate({
|
|
739
|
+
...request,
|
|
740
|
+
headers: {
|
|
741
|
+
...request.headers,
|
|
742
|
+
"idempotency-key": input.idempotencyKey
|
|
743
|
+
},
|
|
744
|
+
body: new Blob([new Uint8Array(input.bytes)], { type: "text/html" }),
|
|
745
|
+
query: {
|
|
746
|
+
sourcePath: input.sourcePath,
|
|
747
|
+
sourceContext: input.sourceContext,
|
|
748
|
+
action: input.action,
|
|
749
|
+
documentId: input.documentId,
|
|
750
|
+
expectedRevisionId: input.expectedRevisionId
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
if (!response?.ok) failed(response, error);
|
|
754
|
+
const data = received;
|
|
755
|
+
const published = record(data) ? normalizeDocument(data.document) : void 0;
|
|
756
|
+
if (record(data) && (data.status === "published" || data.status === "revision_conflict") && published) return {
|
|
757
|
+
status: data.status,
|
|
758
|
+
document: published
|
|
759
|
+
};
|
|
760
|
+
if (record(data) && data.status === "source_collision" && Array.isArray(data.candidates)) {
|
|
761
|
+
const candidates = data.candidates.map((document) => normalizeDocument(document));
|
|
762
|
+
if (candidates.every((document) => document !== void 0)) return {
|
|
763
|
+
status: "source_collision",
|
|
764
|
+
candidates
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (record(data) && (data.status === "idempotency_conflict" || data.status === "address_required" || data.status === "unavailable")) return { status: data.status };
|
|
768
|
+
throw new Error("Invalid publication response. Retry with the same idempotency key.");
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region ../../packages/api-client/src/index.ts
|
|
772
|
+
async function getWorkspace(baseUrl, options = {}) {
|
|
773
|
+
const timeout = AbortSignal.timeout(1e4);
|
|
774
|
+
const { data, response, error } = await personalWorkspace({
|
|
775
|
+
baseUrl,
|
|
776
|
+
credentials: options.token ? "omit" : "include",
|
|
777
|
+
headers: options.token ? { Authorization: `Bearer ${options.token}` } : void 0,
|
|
778
|
+
redirect: "error",
|
|
779
|
+
cache: "no-store",
|
|
780
|
+
signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
|
|
781
|
+
throwOnError: false
|
|
782
|
+
});
|
|
783
|
+
if (!response?.ok) throw new Error("Could not load your workspace.", { cause: error });
|
|
784
|
+
if (data?.status === "signed_out" || data?.status === "uninvited") return { status: data.status };
|
|
785
|
+
if (data?.status === "admitted" && typeof data.user?.id === "string" && data.user.id.length > 0 && typeof data.user?.name === "string" && typeof data.workspace?.id === "string" && data.workspace.id.length > 0) return data;
|
|
786
|
+
throw new Error("Invalid workspace response.");
|
|
787
|
+
}
|
|
788
|
+
function validateCompatibility(value) {
|
|
789
|
+
if (typeof value !== "object" || value === null) throw new Error("Invalid server compatibility response.");
|
|
790
|
+
const candidate = value;
|
|
791
|
+
const major = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
792
|
+
if (typeof candidate.serverVersion !== "string" || candidate.serverVersion.length === 0 || !major(candidate.currentApiMajor) || !Array.isArray(candidate.supportedApiMajors) || candidate.supportedApiMajors.length === 0 || !candidate.supportedApiMajors.every(major) || new Set(candidate.supportedApiMajors).size !== candidate.supportedApiMajors.length || !candidate.supportedApiMajors.includes(candidate.currentApiMajor) || candidate.supportedApiMajors.some((version) => version > Number(candidate.currentApiMajor))) throw new Error("Invalid server compatibility response.");
|
|
793
|
+
return candidate;
|
|
794
|
+
}
|
|
795
|
+
function evaluateCompatibility(server, requiredMajor = 1) {
|
|
796
|
+
const supported = server.supportedApiMajors;
|
|
797
|
+
const status = supported.includes(requiredMajor) ? "compatible" : requiredMajor < Math.min(...supported) ? "client_too_old" : requiredMajor > server.currentApiMajor ? "server_too_old" : "unsupported";
|
|
798
|
+
return {
|
|
799
|
+
status,
|
|
800
|
+
requiredApiMajor: requiredMajor,
|
|
801
|
+
server,
|
|
802
|
+
message: {
|
|
803
|
+
compatible: `Compatible with API major ${requiredMajor}.`,
|
|
804
|
+
client_too_old: `This client is too old for this server. Update the client to use a supported API major.`,
|
|
805
|
+
server_too_old: `This server is too old for this client. Ask the server operator to update the server.`,
|
|
806
|
+
unsupported: `This server does not support API major ${requiredMajor}. Ask the operator to restore this contract or use a compatible client.`
|
|
807
|
+
}[status]
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async function checkCompatibility(baseUrl, options = {}) {
|
|
811
|
+
let url;
|
|
812
|
+
try {
|
|
813
|
+
url = new URL(baseUrl);
|
|
814
|
+
} catch {
|
|
815
|
+
throw new Error("Server must be an absolute HTTP(S) URL.");
|
|
816
|
+
}
|
|
817
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("Server must be an HTTP(S) URL without credentials, query, or fragment.");
|
|
818
|
+
const timeout = AbortSignal.timeout(1e4);
|
|
819
|
+
const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
|
|
820
|
+
const { data, response, error } = await systemCompatibility({
|
|
821
|
+
baseUrl: url.href.replace(/\/$/u, ""),
|
|
822
|
+
signal,
|
|
823
|
+
throwOnError: false,
|
|
824
|
+
credentials: "omit",
|
|
825
|
+
cache: "no-store"
|
|
826
|
+
});
|
|
827
|
+
if (!response) throw error instanceof Error ? error : /* @__PURE__ */ new Error("Could not reach the server compatibility endpoint.");
|
|
828
|
+
if (!response.ok) throw new Error(`Server returned HTTP ${response.status} during compatibility discovery.`);
|
|
829
|
+
return evaluateCompatibility(validateCompatibility(data));
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/windows-protection.ts
|
|
833
|
+
/** DPAPI binds this ciphertext to the current Windows account on this machine. */
|
|
834
|
+
function windowsProtection(value, operation) {
|
|
835
|
+
const script = `$ErrorActionPreference='Stop'; [Console]::InputEncoding=[Text.Encoding]::UTF8; [Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Security; $value=[Console]::In.ReadToEnd(); [Console]::Out.Write(${operation === "protect" ? "[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($value),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser))" : "[Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($value),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser))"})`;
|
|
836
|
+
return new Promise((resolve, reject) => {
|
|
837
|
+
const executable = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
838
|
+
const child = execFile(executable, [
|
|
839
|
+
"-NoLogo",
|
|
840
|
+
"-NoProfile",
|
|
841
|
+
"-NonInteractive",
|
|
842
|
+
"-Command",
|
|
843
|
+
script
|
|
844
|
+
], {
|
|
845
|
+
encoding: "utf8",
|
|
846
|
+
windowsHide: true,
|
|
847
|
+
timeout: 1e4,
|
|
848
|
+
maxBuffer: 1048576
|
|
849
|
+
}, (error, stdout) => {
|
|
850
|
+
if (error) reject(/* @__PURE__ */ new Error("Windows protected storage failed. Use this Windows account on this machine and ensure Windows PowerShell and DPAPI are available."));
|
|
851
|
+
else resolve(stdout);
|
|
852
|
+
});
|
|
853
|
+
child.stdin?.on("error", () => {});
|
|
854
|
+
child.stdin?.end(value);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src/credential-store.ts
|
|
859
|
+
/** Separate files bind secrets to an exact server origin, outside project directories. */
|
|
860
|
+
async function credentialStore(server, readOnly = false) {
|
|
861
|
+
const windows = process.platform === "win32";
|
|
862
|
+
const directory = process.env.PAGESHIP_CONFIG_DIR ?? join((windows ? process.env.LOCALAPPDATA : process.env.XDG_CONFIG_HOME) ?? join(homedir(), ".config"), "pageship");
|
|
863
|
+
if (!isAbsolute(directory)) throw new Error("PAGESHIP_CONFIG_DIR must be an absolute private directory.");
|
|
864
|
+
await mkdir(directory, {
|
|
865
|
+
recursive: true,
|
|
866
|
+
mode: 448
|
|
867
|
+
});
|
|
868
|
+
const info = await lstat(directory);
|
|
869
|
+
if (!info.isDirectory() || !windows && (info.uid !== process.getuid?.() || (info.mode & 63) !== 0)) throw new Error("Credential directory must be owned by you, not a symlink, and have mode 0700. Choose a private PAGESHIP_CONFIG_DIR.");
|
|
870
|
+
const name = createHash("sha256").update(server).digest("hex");
|
|
871
|
+
const path = join(directory, `${name}.json`);
|
|
872
|
+
const lock = join(directory, `${name}.lock`);
|
|
873
|
+
try {
|
|
874
|
+
if (!readOnly) await mkdir(lock, { mode: 448 });
|
|
875
|
+
} catch {
|
|
876
|
+
throw new Error(`Another authorization command may be running. Retry after it exits. If it was killed, remove ${lock} after checking no Pageship process is running.`);
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
async read() {
|
|
880
|
+
if (windows) try {
|
|
881
|
+
if (!(await lstat(path)).isFile()) throw new Error("Credential file must not be a link.");
|
|
882
|
+
} catch (error) {
|
|
883
|
+
if (error.code !== "ENOENT") throw error;
|
|
884
|
+
}
|
|
885
|
+
let file;
|
|
886
|
+
try {
|
|
887
|
+
file = await open(path, constants.O_RDONLY | (windows ? 0 : constants.O_NOFOLLOW));
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (error.code === "ENOENT") return {};
|
|
890
|
+
throw new Error("Could not open protected credentials. Check the credential file is not a symlink.");
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const stat = await file.stat();
|
|
894
|
+
if (!stat.isFile() || !windows && (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0) || stat.nlink !== 1) throw new Error("Credential file must be owned by you and have mode 0600 with no links.");
|
|
895
|
+
const raw = await file.readFile("utf8");
|
|
896
|
+
let value;
|
|
897
|
+
try {
|
|
898
|
+
value = JSON.parse(windows ? await windowsProtection(raw, "unprotect") : raw);
|
|
899
|
+
} catch {
|
|
900
|
+
throw new Error("Could not read protected credentials. Use the original operating-system account or choose a new private PAGESHIP_CONFIG_DIR and log in again.");
|
|
901
|
+
}
|
|
902
|
+
if (!value || typeof value !== "object") throw new Error("Invalid credential file. Start again with a new private PAGESHIP_CONFIG_DIR.");
|
|
903
|
+
const candidate = value;
|
|
904
|
+
if (candidate.token !== void 0 && typeof candidate.token !== "string") throw new Error("Invalid stored credential.");
|
|
905
|
+
const p = candidate.pending;
|
|
906
|
+
if (p && (typeof p.deviceCode !== "string" || typeof p.userCode !== "string" || typeof p.verificationUri !== "string" || typeof p.verificationUriComplete !== "string" || !Number.isFinite(p.expiresAt) || !Number.isFinite(p.nextPollAt) || !Number.isFinite(p.interval) || p.interval < 1)) throw new Error("Invalid stored authorization. Start again with a new private PAGESHIP_CONFIG_DIR.");
|
|
907
|
+
return candidate;
|
|
908
|
+
} finally {
|
|
909
|
+
await file.close();
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
async write(value) {
|
|
913
|
+
if (readOnly) throw new Error("Cannot update credentials through a read-only store.");
|
|
914
|
+
const temporary = join(directory, `${name}.${randomUUID()}.tmp`);
|
|
915
|
+
try {
|
|
916
|
+
const file = await open(temporary, "wx", 384);
|
|
917
|
+
try {
|
|
918
|
+
const serialized = JSON.stringify(value);
|
|
919
|
+
await file.writeFile(windows ? await windowsProtection(serialized, "protect") : serialized);
|
|
920
|
+
await file.sync();
|
|
921
|
+
} finally {
|
|
922
|
+
await file.close();
|
|
923
|
+
}
|
|
924
|
+
await rename(temporary, path);
|
|
925
|
+
} finally {
|
|
926
|
+
await rm(temporary, { force: true });
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
close: async () => {
|
|
930
|
+
if (!readOnly) await rm(lock, {
|
|
931
|
+
recursive: true,
|
|
932
|
+
force: true
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/authorization.ts
|
|
939
|
+
function serverOrigin(value) {
|
|
940
|
+
const url = new URL(value);
|
|
941
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || url.protocol === "http:" && [
|
|
942
|
+
"localhost",
|
|
943
|
+
"127.0.0.1",
|
|
944
|
+
"[::1]"
|
|
945
|
+
].includes(url.hostname))) throw new Error("Authorization requires an HTTPS server origin, or HTTP on localhost for local development. Do not include a path, credentials, query or fragment.");
|
|
946
|
+
return url.origin;
|
|
947
|
+
}
|
|
948
|
+
async function authRequest(server, path, body, signal) {
|
|
949
|
+
const response = await fetch(`${server}/api/auth${path}`, {
|
|
950
|
+
method: "POST",
|
|
951
|
+
headers: { "Content-Type": "application/json" },
|
|
952
|
+
body: JSON.stringify(body),
|
|
953
|
+
redirect: "error",
|
|
954
|
+
credentials: "omit",
|
|
955
|
+
cache: "no-store",
|
|
956
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(1e4)])
|
|
957
|
+
});
|
|
958
|
+
if (response.status >= 500) throw new Error(`Authorization server returned HTTP ${response.status}. Retry later with login --complete if you have a pending code.`);
|
|
959
|
+
let value;
|
|
960
|
+
try {
|
|
961
|
+
value = await response.json();
|
|
962
|
+
} catch {
|
|
963
|
+
throw new Error(`Authorization server returned an unreadable response (HTTP ${response.status}). Check the server and retry.`);
|
|
964
|
+
}
|
|
965
|
+
if (!value || typeof value !== "object") throw new Error("Invalid authorization response. Check the server and retry.");
|
|
966
|
+
const data = value;
|
|
967
|
+
if (!response.ok && typeof data.error !== "string") throw new Error(`Authorization server returned HTTP ${response.status}. Retry later.`);
|
|
968
|
+
return data;
|
|
969
|
+
}
|
|
970
|
+
function pendingResult(pending, server) {
|
|
971
|
+
return {
|
|
972
|
+
status: "authorization_pending",
|
|
973
|
+
message: "Approve this installation in your browser, then run the completion command.",
|
|
974
|
+
verificationUri: pending.verificationUri,
|
|
975
|
+
verificationUriComplete: pending.verificationUriComplete,
|
|
976
|
+
userCode: pending.userCode,
|
|
977
|
+
expiresAt: new Date(pending.expiresAt).toISOString(),
|
|
978
|
+
retryAfterSeconds: Math.max(0, Math.ceil((pending.nextPollAt - Date.now()) / 1e3)),
|
|
979
|
+
next: {
|
|
980
|
+
command: "login",
|
|
981
|
+
args: [
|
|
982
|
+
"--server",
|
|
983
|
+
server,
|
|
984
|
+
"--complete"
|
|
985
|
+
]
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
async function authorize(options) {
|
|
990
|
+
const server = serverOrigin(options.server);
|
|
991
|
+
const compatibility = await checkCompatibility(server, { signal: options.signal });
|
|
992
|
+
if (compatibility.status !== "compatible") return compatibility;
|
|
993
|
+
const store = await credentialStore(server);
|
|
994
|
+
try {
|
|
995
|
+
const credentials = await store.read();
|
|
996
|
+
if (credentials.token && !options.restart && !credentials.pending) {
|
|
997
|
+
const access = await getWorkspace(server, {
|
|
998
|
+
token: credentials.token,
|
|
999
|
+
signal: options.signal
|
|
1000
|
+
});
|
|
1001
|
+
if (access.status === "admitted") return {
|
|
1002
|
+
...access,
|
|
1003
|
+
status: "authorized",
|
|
1004
|
+
message: `Authorized as ${access.user.name}.`
|
|
1005
|
+
};
|
|
1006
|
+
if (access.status === "uninvited") return {
|
|
1007
|
+
status: "uninvited",
|
|
1008
|
+
message: "This account is no longer invited. Contact the server owner or use login --restart for another account."
|
|
1009
|
+
};
|
|
1010
|
+
delete credentials.token;
|
|
1011
|
+
await store.write(credentials);
|
|
1012
|
+
}
|
|
1013
|
+
if (options.restart) delete credentials.pending;
|
|
1014
|
+
if (!credentials.pending) {
|
|
1015
|
+
if (options.complete) return {
|
|
1016
|
+
status: "invalid_attempt",
|
|
1017
|
+
message: "No pending authorization. Run login without --complete to start a new attempt."
|
|
1018
|
+
};
|
|
1019
|
+
const data = await authRequest(server, "/device/code", { client_id: "pageship-cli" }, options.signal);
|
|
1020
|
+
if (typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string" || typeof data.verification_uri_complete !== "string" || typeof data.expires_in !== "number" || data.expires_in <= 0 || typeof data.interval !== "number" || data.interval < 1) throw new Error("Invalid device authorization response. Check the server and retry.");
|
|
1021
|
+
for (const uri of [data.verification_uri, data.verification_uri_complete]) {
|
|
1022
|
+
const parsed = new URL(uri);
|
|
1023
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.protocol === "http:" && new URL(server).protocol !== "http:") throw new Error("Invalid browser verification URL.");
|
|
1024
|
+
}
|
|
1025
|
+
credentials.pending = {
|
|
1026
|
+
deviceCode: data.device_code,
|
|
1027
|
+
userCode: data.user_code,
|
|
1028
|
+
verificationUri: data.verification_uri,
|
|
1029
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
1030
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
1031
|
+
interval: data.interval,
|
|
1032
|
+
nextPollAt: 0
|
|
1033
|
+
};
|
|
1034
|
+
await store.write(credentials);
|
|
1035
|
+
}
|
|
1036
|
+
const pending = credentials.pending;
|
|
1037
|
+
if (options.interactive) options.progress(`Open ${pending.verificationUriComplete}\nCheck code ${pending.userCode}. Only approve an installation you started.\nWaiting for approval. Press Ctrl+C to stop; you can resume with login --complete.`);
|
|
1038
|
+
do {
|
|
1039
|
+
if (options.signal.aborted) return {
|
|
1040
|
+
status: "interrupted",
|
|
1041
|
+
message: "Authorization interrupted. Run login --complete to resume, or login --restart for a new code."
|
|
1042
|
+
};
|
|
1043
|
+
if (Date.now() >= pending.expiresAt) {
|
|
1044
|
+
delete credentials.pending;
|
|
1045
|
+
await store.write(credentials);
|
|
1046
|
+
return {
|
|
1047
|
+
status: "expired",
|
|
1048
|
+
message: "The authorization code expired. Run login again for a new code."
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (!options.interactive && (!options.complete || Date.now() < pending.nextPollAt)) return pendingResult(pending, server);
|
|
1052
|
+
if (Date.now() < pending.nextPollAt) await setTimeout$1(pending.nextPollAt - Date.now(), void 0, { signal: options.signal });
|
|
1053
|
+
pending.nextPollAt = Date.now() + pending.interval * 1e3;
|
|
1054
|
+
await store.write(credentials);
|
|
1055
|
+
const data = await authRequest(server, "/device/token", {
|
|
1056
|
+
client_id: "pageship-cli",
|
|
1057
|
+
device_code: pending.deviceCode,
|
|
1058
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1059
|
+
}, options.signal);
|
|
1060
|
+
if (typeof data.access_token === "string" && data.access_token.length > 0) {
|
|
1061
|
+
credentials.token = data.access_token;
|
|
1062
|
+
delete credentials.pending;
|
|
1063
|
+
await store.write(credentials);
|
|
1064
|
+
const access = await getWorkspace(server, {
|
|
1065
|
+
token: credentials.token,
|
|
1066
|
+
signal: options.signal
|
|
1067
|
+
});
|
|
1068
|
+
if (access.status !== "admitted") return {
|
|
1069
|
+
status: access.status,
|
|
1070
|
+
message: "Authorization finished, but workspace access is unavailable. Check your admission with the server owner."
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
...access,
|
|
1074
|
+
status: "authorized",
|
|
1075
|
+
message: `Authorized as ${access.user.name}. Your credential is stored privately for this server.`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
if (data.error === "slow_down") {
|
|
1079
|
+
pending.interval += 5;
|
|
1080
|
+
pending.nextPollAt = Date.now() + pending.interval * 1e3;
|
|
1081
|
+
await store.write(credentials);
|
|
1082
|
+
} else if (data.error !== "authorization_pending") {
|
|
1083
|
+
delete credentials.pending;
|
|
1084
|
+
await store.write(credentials);
|
|
1085
|
+
const status = data.error === "access_denied" ? "denied" : data.error === "expired_token" ? "expired" : "invalid_attempt";
|
|
1086
|
+
return {
|
|
1087
|
+
status,
|
|
1088
|
+
message: `Authorization ${status.replaceAll("_", " ")}. Run login again for a new code.`
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (!options.interactive) return pendingResult(pending, server);
|
|
1092
|
+
} while (!options.signal.aborted);
|
|
1093
|
+
return {
|
|
1094
|
+
status: "interrupted",
|
|
1095
|
+
message: "Authorization interrupted. Run login --complete to resume."
|
|
1096
|
+
};
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
if (options.signal.aborted) return {
|
|
1099
|
+
status: "interrupted",
|
|
1100
|
+
message: "Authorization interrupted. Run login --complete to resume or login --restart to start again."
|
|
1101
|
+
};
|
|
1102
|
+
throw error;
|
|
1103
|
+
} finally {
|
|
1104
|
+
await store.close();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function identify(serverValue, signal) {
|
|
1108
|
+
const server = serverOrigin(serverValue);
|
|
1109
|
+
const compatibility = await checkCompatibility(server, { signal });
|
|
1110
|
+
if (compatibility.status !== "compatible") return compatibility;
|
|
1111
|
+
const store = await credentialStore(server, true);
|
|
1112
|
+
try {
|
|
1113
|
+
const { token } = await store.read();
|
|
1114
|
+
if (!token) return {
|
|
1115
|
+
status: "signed_out",
|
|
1116
|
+
message: "This installation is not authorized. Run login --server with this server to begin."
|
|
1117
|
+
};
|
|
1118
|
+
const access = await getWorkspace(server, {
|
|
1119
|
+
token,
|
|
1120
|
+
signal
|
|
1121
|
+
});
|
|
1122
|
+
return {
|
|
1123
|
+
...access,
|
|
1124
|
+
message: access.status === "admitted" ? `Signed in as ${access.user.name}. Personal workspace: ${access.workspace.id}${access.installation ? `\nCLI installation: ${access.installation.id}` : ""}` : access.status === "uninvited" ? "This account is not invited. Contact the server owner." : "Your credential was revoked, expired or is invalid. Run login --server with this server to sign in again."
|
|
1125
|
+
};
|
|
1126
|
+
} finally {
|
|
1127
|
+
await store.close();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
//#endregion
|
|
1131
|
+
//#region src/publish.ts
|
|
1132
|
+
const execute = promisify(execFile);
|
|
1133
|
+
const contained = (root, path) => {
|
|
1134
|
+
const part = relative(root, path);
|
|
1135
|
+
return part !== ".." && !part.startsWith(`..${sep}`) && !isAbsolute(part);
|
|
1136
|
+
};
|
|
1137
|
+
async function sourceMetadata(file) {
|
|
1138
|
+
const path = await realpath(resolve(file));
|
|
1139
|
+
const cwd = await realpath(process.cwd());
|
|
1140
|
+
let root = contained(cwd, path) ? cwd : dirname(path);
|
|
1141
|
+
let sourceContext = "personal";
|
|
1142
|
+
try {
|
|
1143
|
+
const git = async (...args) => (await execute("git", [
|
|
1144
|
+
"-C",
|
|
1145
|
+
dirname(path),
|
|
1146
|
+
...args
|
|
1147
|
+
], {
|
|
1148
|
+
timeout: 5e3,
|
|
1149
|
+
maxBuffer: 16384
|
|
1150
|
+
})).stdout.trim();
|
|
1151
|
+
const gitRoot = await realpath(await git("rev-parse", "--show-toplevel"));
|
|
1152
|
+
if (contained(gitRoot, path)) {
|
|
1153
|
+
root = gitRoot;
|
|
1154
|
+
const remote = await git("remote", "get-url", "origin").catch(() => "");
|
|
1155
|
+
const url = remote.includes("://") ? new URL(remote) : /^[^/]+@[^:]+:.+/u.test(remote) ? new URL(`ssh://${remote.replace(":", "/")}`) : void 0;
|
|
1156
|
+
if (url && [
|
|
1157
|
+
"https:",
|
|
1158
|
+
"http:",
|
|
1159
|
+
"ssh:",
|
|
1160
|
+
"git:"
|
|
1161
|
+
].includes(url.protocol) && url.hostname) {
|
|
1162
|
+
const repository = `${url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ""}/${url.pathname.replace(/^\/+|\/+$/gu, "").replace(/\.git$/u, "")}`;
|
|
1163
|
+
sourceContext = `git:${createHash("sha256").update(repository).digest("hex")}`;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
} catch {}
|
|
1167
|
+
const sourcePath = relative(root, path).split(sep).join("/");
|
|
1168
|
+
if (!/\.html?$/iu.test(sourcePath)) throw new Error("Publish one .html or .htm file.");
|
|
1169
|
+
return {
|
|
1170
|
+
path,
|
|
1171
|
+
sourcePath,
|
|
1172
|
+
sourceContext
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
async function fileBytes(path, limit) {
|
|
1176
|
+
const file = await open(path, "r");
|
|
1177
|
+
try {
|
|
1178
|
+
const stat = await file.stat();
|
|
1179
|
+
if (!stat.isFile()) throw new Error("Publish one regular HTML file.");
|
|
1180
|
+
if (stat.size > limit) return;
|
|
1181
|
+
if (stat.size === 0) throw new Error("The HTML file is empty.");
|
|
1182
|
+
const chunks = [];
|
|
1183
|
+
let length = 0;
|
|
1184
|
+
while (length <= limit) {
|
|
1185
|
+
const chunk = Buffer.alloc(Math.min(65536, limit + 1 - length));
|
|
1186
|
+
const { bytesRead } = await file.read(chunk);
|
|
1187
|
+
if (!bytesRead) break;
|
|
1188
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
1189
|
+
length += bytesRead;
|
|
1190
|
+
}
|
|
1191
|
+
return length > limit ? void 0 : Buffer.concat(chunks);
|
|
1192
|
+
} finally {
|
|
1193
|
+
await file.close();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function invalidFile(error) {
|
|
1197
|
+
return {
|
|
1198
|
+
status: "invalid_file",
|
|
1199
|
+
message: `Could not read the HTML file. ${error instanceof Error ? error.message : "Check the file and try again."}`
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function actionArgs(action) {
|
|
1203
|
+
return action.action === "new" ? ["--new"] : action.action === "revise" ? [
|
|
1204
|
+
"--revise",
|
|
1205
|
+
action.documentId,
|
|
1206
|
+
"--expected-revision",
|
|
1207
|
+
action.expectedRevisionId
|
|
1208
|
+
] : [];
|
|
1209
|
+
}
|
|
1210
|
+
function resolutions(file, server, candidates) {
|
|
1211
|
+
const command = "publish";
|
|
1212
|
+
const args = [
|
|
1213
|
+
file,
|
|
1214
|
+
"--server",
|
|
1215
|
+
server
|
|
1216
|
+
];
|
|
1217
|
+
return {
|
|
1218
|
+
new: {
|
|
1219
|
+
command,
|
|
1220
|
+
args: [...args, "--new"]
|
|
1221
|
+
},
|
|
1222
|
+
revise: candidates.map((candidate) => ({
|
|
1223
|
+
documentId: candidate.id,
|
|
1224
|
+
url: candidate.url,
|
|
1225
|
+
command,
|
|
1226
|
+
args: [...args, ...actionArgs({
|
|
1227
|
+
action: "revise",
|
|
1228
|
+
documentId: candidate.id,
|
|
1229
|
+
expectedRevisionId: candidate.revisionId
|
|
1230
|
+
})]
|
|
1231
|
+
}))
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
async function choosePublication(candidates, signal) {
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
console.log("This source path matches published documents:");
|
|
1237
|
+
candidates.forEach((candidate, index) => console.log(`${index + 1}. ${candidate.sourcePath} (${candidate.id}) ${candidate.url}`));
|
|
1238
|
+
const input = createInterface({
|
|
1239
|
+
input: process.stdin,
|
|
1240
|
+
output: process.stdout
|
|
1241
|
+
});
|
|
1242
|
+
let selection;
|
|
1243
|
+
try {
|
|
1244
|
+
selection = (await input.question("o <number> to open, r <number> to revise, n for a separate document, or q to cancel: ", { signal })).trim();
|
|
1245
|
+
} finally {
|
|
1246
|
+
input.close();
|
|
1247
|
+
}
|
|
1248
|
+
if (selection === "q") return;
|
|
1249
|
+
if (selection === "n") return { action: "new" };
|
|
1250
|
+
const match = /^([or]) ([1-9][0-9]*)$/u.exec(selection);
|
|
1251
|
+
const candidate = match ? candidates[Number(match[2]) - 1] : void 0;
|
|
1252
|
+
if (!candidate) continue;
|
|
1253
|
+
if (match[1] === "r") return {
|
|
1254
|
+
action: "revise",
|
|
1255
|
+
documentId: candidate.id,
|
|
1256
|
+
expectedRevisionId: candidate.revisionId
|
|
1257
|
+
};
|
|
1258
|
+
try {
|
|
1259
|
+
const [program, args] = process.platform === "darwin" ? ["open", [candidate.url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", candidate.url]] : ["xdg-open", [candidate.url]];
|
|
1260
|
+
await execute(program, [...args], {
|
|
1261
|
+
timeout: 1e4,
|
|
1262
|
+
signal
|
|
1263
|
+
});
|
|
1264
|
+
console.log(`Opened ${candidate.url}. No document changed.`);
|
|
1265
|
+
} catch {
|
|
1266
|
+
if (signal.aborted) throw new Error("Inspection interrupted.");
|
|
1267
|
+
console.log(`Could not open the browser. Open ${candidate.url} to inspect it. No document changed.`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async function publish(options) {
|
|
1272
|
+
if (options.newDocument && (options.revise || options.expectedRevision) || Boolean(options.revise) !== Boolean(options.expectedRevision)) return {
|
|
1273
|
+
status: "invalid_action",
|
|
1274
|
+
message: "Use --new alone, or --revise <documentId> together with --expected-revision <revisionId>."
|
|
1275
|
+
};
|
|
1276
|
+
let action = options.newDocument ? { action: "new" } : options.revise && options.expectedRevision ? {
|
|
1277
|
+
action: "revise",
|
|
1278
|
+
documentId: options.revise,
|
|
1279
|
+
expectedRevisionId: options.expectedRevision
|
|
1280
|
+
} : {};
|
|
1281
|
+
const server = serverOrigin(options.server);
|
|
1282
|
+
const idempotencyKey = options.idempotencyKey ?? randomUUID();
|
|
1283
|
+
let source;
|
|
1284
|
+
try {
|
|
1285
|
+
source = await sourceMetadata(options.file);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
return invalidFile(error);
|
|
1288
|
+
}
|
|
1289
|
+
const compatibility = await checkCompatibility(server, { signal: options.signal });
|
|
1290
|
+
if (compatibility.status !== "compatible") return compatibility;
|
|
1291
|
+
const store = await credentialStore(server, true);
|
|
1292
|
+
try {
|
|
1293
|
+
const { token } = await store.read();
|
|
1294
|
+
if (!token) return {
|
|
1295
|
+
status: "signed_out",
|
|
1296
|
+
message: "This installation is not authorized. Run login first."
|
|
1297
|
+
};
|
|
1298
|
+
const request = {
|
|
1299
|
+
token,
|
|
1300
|
+
signal: options.signal
|
|
1301
|
+
};
|
|
1302
|
+
const { maxUploadBytes } = await getDocumentLimits(server, request);
|
|
1303
|
+
let bytes;
|
|
1304
|
+
try {
|
|
1305
|
+
bytes = await fileBytes(source.path, maxUploadBytes);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
return invalidFile(error);
|
|
1308
|
+
}
|
|
1309
|
+
if (!bytes) return {
|
|
1310
|
+
status: "upload_too_large",
|
|
1311
|
+
maxUploadBytes,
|
|
1312
|
+
message: `The HTML file exceeds the ${maxUploadBytes}-byte limit. No HTML was uploaded.`
|
|
1313
|
+
};
|
|
1314
|
+
let address = await updateWorkspaceAddress(server, options.addressChoice ? {
|
|
1315
|
+
action: "select",
|
|
1316
|
+
choiceId: options.addressChoice
|
|
1317
|
+
} : { action: options.refreshAddresses ? "refresh" : "allocate" }, request);
|
|
1318
|
+
while (address.status === "choice_required" && options.interactive) {
|
|
1319
|
+
console.log("Choose your workspace address:");
|
|
1320
|
+
address.choices.forEach((choice, index) => console.log(`${index + 1}. ${choice.hostname}`));
|
|
1321
|
+
const input = createInterface({
|
|
1322
|
+
input: process.stdin,
|
|
1323
|
+
output: process.stdout
|
|
1324
|
+
});
|
|
1325
|
+
let selection;
|
|
1326
|
+
try {
|
|
1327
|
+
selection = (await input.question("Number, r to refresh, or q to cancel: ", { signal: options.signal })).trim();
|
|
1328
|
+
} finally {
|
|
1329
|
+
input.close();
|
|
1330
|
+
}
|
|
1331
|
+
if (selection === "q") return {
|
|
1332
|
+
status: "cancelled",
|
|
1333
|
+
message: "Publication cancelled."
|
|
1334
|
+
};
|
|
1335
|
+
const choice = address.choices[Number(selection) - 1];
|
|
1336
|
+
if (selection !== "r" && !choice) continue;
|
|
1337
|
+
address = await updateWorkspaceAddress(server, selection === "r" ? { action: "refresh" } : {
|
|
1338
|
+
action: "select",
|
|
1339
|
+
choiceId: choice.choiceId
|
|
1340
|
+
}, request);
|
|
1341
|
+
}
|
|
1342
|
+
if (address.status === "choice_required") return {
|
|
1343
|
+
...address,
|
|
1344
|
+
message: `Choose a generated workspace address and repeat publish with --address-choice <choiceId>, or --refresh-addresses.\n${address.choices.map((choice) => `${choice.hostname}: ${choice.choiceId}`).join("\n")}`,
|
|
1345
|
+
next: {
|
|
1346
|
+
command: "publish",
|
|
1347
|
+
args: [
|
|
1348
|
+
options.file,
|
|
1349
|
+
"--server",
|
|
1350
|
+
server,
|
|
1351
|
+
"--address-choice",
|
|
1352
|
+
"<choiceId>"
|
|
1353
|
+
]
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
if (address.status !== "allocated") return {
|
|
1357
|
+
...address,
|
|
1358
|
+
message: "Workspace address is unavailable. Sign in again or check admission."
|
|
1359
|
+
};
|
|
1360
|
+
const send = () => publishDocument(server, {
|
|
1361
|
+
bytes,
|
|
1362
|
+
sourcePath: source.sourcePath,
|
|
1363
|
+
sourceContext: source.sourceContext,
|
|
1364
|
+
idempotencyKey,
|
|
1365
|
+
...action
|
|
1366
|
+
}, request);
|
|
1367
|
+
let result = await send();
|
|
1368
|
+
if (result.status === "source_collision" && options.interactive) {
|
|
1369
|
+
const selected = await choosePublication(result.candidates, options.signal);
|
|
1370
|
+
if (!selected) return {
|
|
1371
|
+
status: "cancelled",
|
|
1372
|
+
message: "Publication cancelled. No document changed."
|
|
1373
|
+
};
|
|
1374
|
+
action = selected;
|
|
1375
|
+
result = await send();
|
|
1376
|
+
}
|
|
1377
|
+
const message = result.status === "published" ? `Published ${result.document.visibility === "private" ? "privately" : "publicly"}: ${result.document.url}` : result.status === "source_collision" ? `Source path already published. No document changed.\n${result.candidates.map((candidate) => candidate.url).join("\n")}\nUse --new for a separate document, or --revise <documentId> --expected-revision <revisionId> from a candidate. Use --json for complete follow-up arguments.` : result.status === "revision_conflict" ? `The selected document changed. No revision was published. Inspect ${result.document.url} and select its current revision before trying again.` : result.status === "idempotency_conflict" ? "That idempotency key belongs to different content, source metadata or publication action. Reuse the original input or choose a new key." : result.status === "unavailable" ? "The selected document is unavailable for this source path. Run publish without action flags to find current candidates." : "Allocate your workspace address before publishing.";
|
|
1378
|
+
return {
|
|
1379
|
+
...result,
|
|
1380
|
+
idempotencyKey,
|
|
1381
|
+
message,
|
|
1382
|
+
...result.status === "source_collision" ? { resolutions: resolutions(options.file, server, result.candidates) } : {},
|
|
1383
|
+
...result.status === "revision_conflict" ? { resolutions: resolutions(options.file, server, [result.document]) } : {}
|
|
1384
|
+
};
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
if (options.signal.aborted) return {
|
|
1387
|
+
status: "interrupted",
|
|
1388
|
+
idempotencyKey,
|
|
1389
|
+
message: `Publication interrupted. Retry with --idempotency-key ${idempotencyKey} ${actionArgs(action).join(" ")}.`,
|
|
1390
|
+
next: {
|
|
1391
|
+
command: "publish",
|
|
1392
|
+
args: [
|
|
1393
|
+
options.file,
|
|
1394
|
+
"--server",
|
|
1395
|
+
server,
|
|
1396
|
+
"--idempotency-key",
|
|
1397
|
+
idempotencyKey,
|
|
1398
|
+
...actionArgs(action)
|
|
1399
|
+
]
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
if (error instanceof DocumentRequestError) return {
|
|
1403
|
+
status: error.status,
|
|
1404
|
+
message: error.message,
|
|
1405
|
+
idempotencyKey
|
|
1406
|
+
};
|
|
1407
|
+
return {
|
|
1408
|
+
status: "request_failed",
|
|
1409
|
+
idempotencyKey,
|
|
1410
|
+
message: `Publication did not finish. Retry with --idempotency-key ${idempotencyKey} ${actionArgs(action).join(" ")}. ${error instanceof Error ? error.message : "Check the server and file."}`,
|
|
1411
|
+
next: {
|
|
1412
|
+
command: "publish",
|
|
1413
|
+
args: [
|
|
1414
|
+
options.file,
|
|
1415
|
+
"--server",
|
|
1416
|
+
server,
|
|
1417
|
+
"--idempotency-key",
|
|
1418
|
+
idempotencyKey,
|
|
1419
|
+
...actionArgs(action)
|
|
1420
|
+
]
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
} finally {
|
|
1424
|
+
await store.close();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
//#endregion
|
|
5
1428
|
//#region package.json
|
|
6
|
-
var version = "0.0
|
|
1429
|
+
var version = "0.1.0";
|
|
7
1430
|
//#endregion
|
|
8
1431
|
//#region src/cli.ts
|
|
9
|
-
const command = Command.make("pageship").pipe(Command.withDescription("Pageship is a planned HTML publishing service.\n\nEarly development: this CLI currently provides help and version information only. Hosting, authentication, and publishing are not implemented."));
|
|
10
1432
|
const args = process.argv.slice(2);
|
|
11
|
-
|
|
1433
|
+
const separator = args.indexOf("--");
|
|
1434
|
+
const flagArgs = args.slice(0, separator === -1 ? void 0 : separator);
|
|
1435
|
+
const parseBoolean = Schema.decodeUnknownOption(Config.Boolean);
|
|
1436
|
+
const booleanValue = (value) => Option.getOrUndefined(parseBoolean(value));
|
|
1437
|
+
const json = flagArgs.some((arg, index) => arg === "--json" ? booleanValue(flagArgs[index + 1]) !== false : arg.startsWith("--json=") && booleanValue(arg.slice(7)) !== false);
|
|
1438
|
+
const jsonFlag = GlobalFlag.setting("json")({ flag: Flag.boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Emit one JSON result, including errors.")) });
|
|
1439
|
+
let outcome;
|
|
1440
|
+
const plainFormatter = CliOutput.defaultFormatter({ colors: false });
|
|
1441
|
+
const jsonFormatter = {
|
|
1442
|
+
formatCliError: plainFormatter.formatCliError,
|
|
1443
|
+
formatError: plainFormatter.formatError,
|
|
1444
|
+
formatErrors: plainFormatter.formatErrors,
|
|
1445
|
+
formatHelpDoc: (doc) => {
|
|
1446
|
+
outcome = {
|
|
1447
|
+
ok: true,
|
|
1448
|
+
command: "help",
|
|
1449
|
+
usage: doc.usage,
|
|
1450
|
+
description: doc.description,
|
|
1451
|
+
help: plainFormatter.formatHelpDoc(doc)
|
|
1452
|
+
};
|
|
1453
|
+
return "";
|
|
1454
|
+
},
|
|
1455
|
+
formatVersion: (name, version) => {
|
|
1456
|
+
outcome = {
|
|
1457
|
+
ok: true,
|
|
1458
|
+
command: "version",
|
|
1459
|
+
name,
|
|
1460
|
+
version
|
|
1461
|
+
};
|
|
1462
|
+
return "";
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const status = Command.make("status", { server: Flag.string("server").pipe(Flag.withDescription("Pageship server HTTP(S) base URL, for example http://localhost:8787.")) }, ({ server }) => Effect.gen(function* () {
|
|
1466
|
+
const result = yield* Effect.tryPromise({
|
|
1467
|
+
try: () => checkCompatibility(server),
|
|
1468
|
+
catch: (error) => error instanceof Error ? error : new Error(String(error))
|
|
1469
|
+
});
|
|
1470
|
+
outcome = {
|
|
1471
|
+
ok: result.status === "compatible",
|
|
1472
|
+
command: "status",
|
|
1473
|
+
...result
|
|
1474
|
+
};
|
|
1475
|
+
process.exitCode = result.status === "compatible" ? 0 : 2;
|
|
1476
|
+
if (!json) yield* Console.log(`${result.message}\nServer ${result.server.serverVersion}; current API major ${result.server.currentApiMajor}; supported API majors: ${result.server.supportedApiMajors.join(", ")}.\nThis CLI requires API major ${result.requiredApiMajor}.`);
|
|
1477
|
+
})).pipe(Command.withDescription("Check whether this CLI can use a Pageship server."));
|
|
1478
|
+
const serverFlag = Flag.string("server").pipe(Flag.withDescription("Pageship HTTPS origin, or localhost HTTP for development."));
|
|
1479
|
+
const login = Command.make("login", {
|
|
1480
|
+
server: serverFlag,
|
|
1481
|
+
complete: Flag.boolean("complete").pipe(Flag.withDefault(false), Flag.withDescription("Check a pending authorization once, without waiting.")),
|
|
1482
|
+
restart: Flag.boolean("restart").pipe(Flag.withDefault(false), Flag.withDescription("Start a new authorization code."))
|
|
1483
|
+
}, ({ server, complete, restart }) => authCommand("login", (signal) => authorize({
|
|
1484
|
+
server,
|
|
1485
|
+
complete,
|
|
1486
|
+
restart,
|
|
1487
|
+
interactive: !json && Boolean(process.stdin.isTTY && process.stdout.isTTY) && !complete,
|
|
1488
|
+
progress: (message) => console.log(message),
|
|
1489
|
+
signal
|
|
1490
|
+
}))).pipe(Command.withDescription("Authorize this installation through a browser."));
|
|
1491
|
+
const whoami = Command.make("whoami", { server: serverFlag }, ({ server }) => authCommand("whoami", (signal) => identify(server, signal))).pipe(Command.withDescription("Identify your personal workspace using the stored credential."));
|
|
1492
|
+
const publishCommand = Command.make("publish", {
|
|
1493
|
+
file: Argument.string("file"),
|
|
1494
|
+
server: serverFlag,
|
|
1495
|
+
idempotencyKey: Flag.string("idempotency-key").pipe(Flag.filter((value) => /^[A-Za-z0-9_-]{8,128}$/u.test(value), () => "Use an 8-128 character idempotency key containing letters, digits, underscores or hyphens."), Flag.optional),
|
|
1496
|
+
newDocument: Flag.boolean("new").pipe(Flag.withDefault(false), Flag.withDescription("Explicitly create a separate private document, even when the source matches.")),
|
|
1497
|
+
revise: Flag.string("revise").pipe(Flag.filter((value) => isDocumentId(value), () => "Use the six-character candidate document ID."), Flag.optional),
|
|
1498
|
+
expectedRevision: Flag.string("expected-revision").pipe(Flag.filter((value) => isRevisionId(value), () => "Use the candidate's current revision UUID."), Flag.optional),
|
|
1499
|
+
addressChoice: Flag.string("address-choice").pipe(Flag.optional),
|
|
1500
|
+
refreshAddresses: Flag.boolean("refresh-addresses").pipe(Flag.withDefault(false))
|
|
1501
|
+
}, ({ file, server, idempotencyKey, addressChoice, refreshAddresses, newDocument, revise, expectedRevision }) => authCommand("publish", (signal) => publish({
|
|
1502
|
+
file,
|
|
1503
|
+
server,
|
|
1504
|
+
newDocument,
|
|
1505
|
+
revise: Option.getOrUndefined(revise),
|
|
1506
|
+
expectedRevision: Option.getOrUndefined(expectedRevision),
|
|
1507
|
+
idempotencyKey: Option.getOrUndefined(idempotencyKey),
|
|
1508
|
+
addressChoice: Option.getOrUndefined(addressChoice),
|
|
1509
|
+
refreshAddresses,
|
|
1510
|
+
interactive: !json && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1511
|
+
signal
|
|
1512
|
+
}))).pipe(Command.withDescription("Publish HTML, resolve source matches, or revise a selected document."));
|
|
1513
|
+
function authCommand(name, operation) {
|
|
1514
|
+
return Effect.tryPromise({
|
|
1515
|
+
try: async () => {
|
|
1516
|
+
const controller = new AbortController();
|
|
1517
|
+
const interrupt = () => controller.abort();
|
|
1518
|
+
process.once("SIGINT", interrupt);
|
|
1519
|
+
process.once("SIGTERM", interrupt);
|
|
1520
|
+
try {
|
|
1521
|
+
const result = await operation(controller.signal).catch((error) => {
|
|
1522
|
+
if (!controller.signal.aborted) throw error;
|
|
1523
|
+
return {
|
|
1524
|
+
status: "interrupted",
|
|
1525
|
+
message: "Operation interrupted. Run the command again to retry; use login --complete to resume a pending authorization."
|
|
1526
|
+
};
|
|
1527
|
+
});
|
|
1528
|
+
const ok = [
|
|
1529
|
+
"authorized",
|
|
1530
|
+
"admitted",
|
|
1531
|
+
"published"
|
|
1532
|
+
].includes(result.status);
|
|
1533
|
+
outcome = {
|
|
1534
|
+
ok,
|
|
1535
|
+
command: name,
|
|
1536
|
+
...result
|
|
1537
|
+
};
|
|
1538
|
+
process.exitCode = ok ? 0 : result.status === "interrupted" ? 130 : [
|
|
1539
|
+
"request_failed",
|
|
1540
|
+
"invalid_file",
|
|
1541
|
+
"invalid_action"
|
|
1542
|
+
].includes(result.status) ? 1 : 2;
|
|
1543
|
+
if (!json) {
|
|
1544
|
+
console.log(result.message);
|
|
1545
|
+
const pending = result;
|
|
1546
|
+
if (typeof pending.verificationUriComplete === "string" && typeof pending.userCode === "string") console.log(`Open ${pending.verificationUriComplete}\nCheck code ${pending.userCode}.\nThen run: pageship login --server ${JSON.stringify(pending.next && pending.next.args[1])} --complete`);
|
|
1547
|
+
}
|
|
1548
|
+
} finally {
|
|
1549
|
+
process.removeListener("SIGINT", interrupt);
|
|
1550
|
+
process.removeListener("SIGTERM", interrupt);
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
catch: (error) => error instanceof Error ? error : new Error(String(error))
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
const command = Command.make("pageship").pipe(Command.withDescription("Pageship publishes private HTML documents.\n\nEarly development: check server API compatibility with the status command. Authorize this installation with login, then publish one HTML file with publish."), Command.withSubcommands([
|
|
1557
|
+
status,
|
|
1558
|
+
login,
|
|
1559
|
+
whoami,
|
|
1560
|
+
publishCommand
|
|
1561
|
+
]), Command.withGlobalFlags([jsonFlag]));
|
|
1562
|
+
const effectiveArgs = args.every((arg, index) => arg === "--json" || arg.startsWith("--json=") && booleanValue(arg.slice(7)) !== void 0 || index > 0 && args[index - 1] === "--json" && booleanValue(arg) !== void 0) ? [...args, "--help"] : args;
|
|
1563
|
+
const program = Command.runWith(command, {
|
|
1564
|
+
version,
|
|
1565
|
+
renderErrors: !json
|
|
1566
|
+
})(effectiveArgs).pipe(Effect.catch((error) => Effect.sync(() => {
|
|
1567
|
+
process.exitCode = 1;
|
|
1568
|
+
const usage = CliError.isCliError(error);
|
|
1569
|
+
const message = usage && error._tag === "ShowHelp" ? error.errors.map((entry) => entry.message).join("\n") : error.message;
|
|
1570
|
+
outcome = {
|
|
1571
|
+
ok: false,
|
|
1572
|
+
error: {
|
|
1573
|
+
code: usage ? "usage_error" : "request_failed",
|
|
1574
|
+
message
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
if (!json && !usage) console.error(`${flagArgs.includes("status") ? "Could not check server compatibility" : "Pageship request failed"}: ${message}`);
|
|
1578
|
+
if (!json && usage && error._tag !== "ShowHelp" && error._tag !== "UserError") console.error(message);
|
|
1579
|
+
})), Effect.catchCause((cause) => Effect.sync(() => {
|
|
1580
|
+
process.exitCode = 1;
|
|
1581
|
+
const message = Cause.pretty(cause);
|
|
1582
|
+
outcome = {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
error: {
|
|
1585
|
+
code: "internal_error",
|
|
1586
|
+
message
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
if (!json) console.error(message);
|
|
1590
|
+
})), Effect.provide(NodeServices.layer));
|
|
1591
|
+
await Effect.runPromise(json ? program.pipe(Effect.provideService(CliOutput.Formatter, jsonFormatter), Effect.provideService(Console.Console, {
|
|
1592
|
+
...console,
|
|
1593
|
+
log: () => {},
|
|
1594
|
+
error: () => {}
|
|
1595
|
+
}), Effect.provide(CliConfig.layer({ builtIns: [GlobalFlag.Help, GlobalFlag.Version] }))) : program);
|
|
1596
|
+
if (json) console.log(JSON.stringify(outcome));
|
|
12
1597
|
//#endregion
|
|
13
1598
|
export {};
|