saturndocs 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/LICENSE +21 -0
- package/dist/auth-FRCFGNYT.js +14 -0
- package/dist/brokenLinks-TTOBP4P7.js +48 -0
- package/dist/build-F5PWGP57.js +39 -0
- package/dist/chunk-3P2OFTXR.js +71 -0
- package/dist/chunk-3V753MRI.js +100 -0
- package/dist/chunk-5KX4ZK5E.js +104 -0
- package/dist/chunk-CMNX5DLQ.js +119 -0
- package/dist/chunk-GYRVGWDM.js +3499 -0
- package/dist/chunk-I6U4V7B4.js +1552 -0
- package/dist/chunk-J6P3VGGE.js +21 -0
- package/dist/chunk-KGJESHJC.js +37 -0
- package/dist/chunk-M2BZEDAS.js +57 -0
- package/dist/chunk-N7QPZMLP.js +82 -0
- package/dist/chunk-OEAEX5YW.js +51 -0
- package/dist/chunk-OZWTXMO3.js +584 -0
- package/dist/chunk-QLFODLVN.js +46 -0
- package/dist/chunk-R3X2DPFF.js +568 -0
- package/dist/chunk-S45UWCML.js +588 -0
- package/dist/chunk-SABK3R2P.js +2452 -0
- package/dist/chunk-STCCGFKC.js +246 -0
- package/dist/chunk-TKISMN3P.js +39 -0
- package/dist/chunk-U7PKMSB3.js +465 -0
- package/dist/chunk-VNYDYHXM.js +22 -0
- package/dist/chunk-VXEUNRVA.js +26 -0
- package/dist/chunk-XO4CUC7V.js +99 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +95 -0
- package/dist/deploy-QU7DCKWU.js +11 -0
- package/dist/dev-VDSDCBO2.js +43 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +191 -0
- package/dist/init-7T5TEPBH.js +14 -0
- package/dist/manage-MKH27U3B.js +12 -0
- package/dist/openapiCheck-KGBQJTIQ.js +66 -0
- package/dist/pages-HIPXRLSY.js +15 -0
- package/dist/read-DMSJUEDA.js +160 -0
- package/dist/read-ZEVUZNNB.js +146 -0
- package/dist/requests-H5MIGBIS.js +13 -0
- package/dist/schema-XJUEZSIW.js +13 -0
- package/dist/sites-I4WI2MPG.js +14 -0
- package/dist/status-7R3NOI5H.js +14 -0
- package/dist/suggestions-4WFQ3APX.js +739 -0
- package/dist/validate-VXL4PL42.js +30 -0
- package/package.json +68 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readTextInput
|
|
3
|
+
} from "./chunk-N7QPZMLP.js";
|
|
4
|
+
import {
|
|
5
|
+
asCliError,
|
|
6
|
+
clientFor
|
|
7
|
+
} from "./chunk-TKISMN3P.js";
|
|
8
|
+
import {
|
|
9
|
+
interruptSignal
|
|
10
|
+
} from "./chunk-KGJESHJC.js";
|
|
11
|
+
import {
|
|
12
|
+
applyCommandHelp
|
|
13
|
+
} from "./chunk-M2BZEDAS.js";
|
|
14
|
+
import {
|
|
15
|
+
CliError,
|
|
16
|
+
DEFAULT_REQUEST_WAIT_MS,
|
|
17
|
+
REQUEST_TEXT_LIMIT,
|
|
18
|
+
checkedRequestText,
|
|
19
|
+
checkedSubmissionId,
|
|
20
|
+
commandMetadata,
|
|
21
|
+
createRequest,
|
|
22
|
+
createResultWriter,
|
|
23
|
+
failureResult,
|
|
24
|
+
invalidInput,
|
|
25
|
+
listRequestThreads,
|
|
26
|
+
readRequestThread,
|
|
27
|
+
replyToRequest,
|
|
28
|
+
requestWaitErrorCode,
|
|
29
|
+
resolveGlobalOptions,
|
|
30
|
+
resolveTarget,
|
|
31
|
+
retryRequest,
|
|
32
|
+
successResult,
|
|
33
|
+
waitForRequest
|
|
34
|
+
} from "./chunk-GYRVGWDM.js";
|
|
35
|
+
|
|
36
|
+
// src/commands/requests/create.ts
|
|
37
|
+
var REQUESTS_CREATE_METADATA = Object.freeze([
|
|
38
|
+
commandMetadata("requests create", {
|
|
39
|
+
description: "Submit one documentation request and return the queued job it opened, without waiting for the draft. The job that comes back is the root of the conversation, so it is the id every later read, answer, and retry names. --submission-id is a UUID you mint and keep: replaying it with the same words recovers that same job in a fresh process, and sending different words under it is refused as a conflict rather than replacing what is already running.",
|
|
40
|
+
target: { arguments: [] },
|
|
41
|
+
effect: "Queues one documentation request on the named site. Nothing is published.",
|
|
42
|
+
idempotency: "submission_id",
|
|
43
|
+
output: { fields: ["job_id", "state"], ids: ["submission_id", "thread_id", "job_id"], limitations: [] },
|
|
44
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "interrupted"],
|
|
45
|
+
examples: [
|
|
46
|
+
{ command: 'saturndocs requests create --site acme --submission-id 5f80c9a2-012b-4dee-8de4-97d44cd28b60 --text "Document the export option" --page pages/export.mdx', description: "One short request about one page." },
|
|
47
|
+
{ command: "saturndocs requests create --site acme --submission-id 5f80c9a2-012b-4dee-8de4-97d44cd28b60 --file request.md", description: "The request read from a file, which is also how a lost answer is replayed." },
|
|
48
|
+
{ command: "saturndocs requests create --site acme --submission-id 5f80c9a2-012b-4dee-8de4-97d44cd28b60 --file - --json", description: "The request read from stdin, as one result object." }
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
]);
|
|
52
|
+
function quoted(value) {
|
|
53
|
+
return /^[\w.\-/@]+$/u.test(value) ? value : JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
function recoveryFor(siteId, flags, submissionId) {
|
|
56
|
+
const stdin = flags.file === "-";
|
|
57
|
+
const source = flags.file === void 0 || stdin ? null : `--file ${quoted(flags.file)}`;
|
|
58
|
+
const line = source === null ? null : ["saturndocs requests create", `--site ${quoted(siteId)}`, `--submission-id ${submissionId}`, flags.page === void 0 ? null : `--page ${quoted(flags.page)}`, source].filter((part) => part !== null).join(" ");
|
|
59
|
+
const replaySource = stdin ? "the same text on stdin" : source === null ? "the same --text" : `the same ${source}`;
|
|
60
|
+
return {
|
|
61
|
+
command: line,
|
|
62
|
+
replay: { submission_id: submissionId, stdin },
|
|
63
|
+
message: `It is not known whether the request was queued. Nothing was sent again. Read the conversations on ${siteId}, or send it once more with --submission-id ${submissionId}, ${replaySource}${flags.page === void 0 ? "" : ` and --page ${flags.page}`}: the same id with the same words recovers the one job, and with different words it is refused.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function registerRequestsCreate(group, environment = {}) {
|
|
67
|
+
const metadata = REQUESTS_CREATE_METADATA[0];
|
|
68
|
+
const command = group.command("create").action(async (_options, self) => {
|
|
69
|
+
const globals = resolveGlobalOptions(self);
|
|
70
|
+
const writer = createResultWriter({ json: globals.json });
|
|
71
|
+
const flags = self.opts();
|
|
72
|
+
const context = { command: "requests create", siteId: globals.site };
|
|
73
|
+
let recovery = null;
|
|
74
|
+
try {
|
|
75
|
+
const submissionId = checkedSubmissionId(flags.submissionId ?? "");
|
|
76
|
+
const text = await readTextInput(flags, {
|
|
77
|
+
command: "requests create",
|
|
78
|
+
requirement: metadata.text.requirement,
|
|
79
|
+
limit: metadata.text.limit ?? REQUEST_TEXT_LIMIT,
|
|
80
|
+
interactive: globals.input,
|
|
81
|
+
stdin: environment.stdin,
|
|
82
|
+
readFileImpl: environment.readFileImpl
|
|
83
|
+
});
|
|
84
|
+
const request = checkedRequestText(text ?? "");
|
|
85
|
+
const client = clientFor(globals, environment);
|
|
86
|
+
const target = await resolveTarget(client, { site: globals.site ?? void 0, mutation: true });
|
|
87
|
+
context.siteId = target.site_id;
|
|
88
|
+
recovery = recoveryFor(target.site_id, flags, submissionId);
|
|
89
|
+
const queued = await createRequest(client, {
|
|
90
|
+
siteId: target.site_id,
|
|
91
|
+
submissionId,
|
|
92
|
+
request,
|
|
93
|
+
page: flags.page ?? null
|
|
94
|
+
});
|
|
95
|
+
const resolved = {
|
|
96
|
+
...context,
|
|
97
|
+
ids: { submission_id: submissionId, thread_id: queued.job_id, job_id: queued.job_id }
|
|
98
|
+
};
|
|
99
|
+
process.exitCode = writer.write(successResult(resolved, queued), {
|
|
100
|
+
text: `${queued.job_id} ${queued.state}; thread ${queued.job_id}`
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const failure = asCliError(error);
|
|
104
|
+
if (failure instanceof CliError && failure.mutation === "unknown" && recovery !== null) context.recovery = recovery;
|
|
105
|
+
process.exitCode = writer.write(failureResult(failure, context));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
applyCommandHelp(command, metadata);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/commands/requests/read.ts
|
|
112
|
+
var REQUESTS_READ_METADATA = Object.freeze([
|
|
113
|
+
commandMetadata("requests list", {
|
|
114
|
+
description: "One page of conversations, newest original request first. The listing is always read as threads, so each entry carries the request that started it, the newest job and its state, whether it is waiting on an answer, and the most recent turns. Paging is the server's: one page per invocation, and next_cursor is the only way to the next one.",
|
|
115
|
+
target: { arguments: [] },
|
|
116
|
+
effect: "Nothing. This reads one page.",
|
|
117
|
+
output: {
|
|
118
|
+
fields: ["threads", "page_cursor", "next_cursor", "has_newer", "failed", "waiting"],
|
|
119
|
+
ids: [],
|
|
120
|
+
limitations: ["page_bounded"]
|
|
121
|
+
},
|
|
122
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
|
|
123
|
+
examples: [
|
|
124
|
+
{ command: "saturndocs requests list --site acme", description: "The newest page of conversations." },
|
|
125
|
+
{ command: "saturndocs requests list --site acme --limit 50 --json", description: "A larger page as one result object." },
|
|
126
|
+
{ command: "saturndocs requests list --site acme --cursor eyJ...", description: "The page after the one whose next_cursor is given." }
|
|
127
|
+
]
|
|
128
|
+
}),
|
|
129
|
+
commandMetadata("requests get", {
|
|
130
|
+
description: "One conversation bounded to its newest 100 turns: the request that started it, its latest job, whether it is waiting on an answer, and the returned turns oldest first. The server supplies no total or cursor, so older turns may not be present. Any job of the thread names it, so a reply's job id reads the same conversation as its root. A thread whose latest job failed is a successful read; the failure is in the result, not in the exit code.",
|
|
131
|
+
target: { arguments: [{ name: "job", required: true, description: "Any job of the conversation, including a reply." }] },
|
|
132
|
+
effect: "Nothing. This reads one conversation.",
|
|
133
|
+
output: {
|
|
134
|
+
fields: ["root", "thread_id", "site_id", "open", "latest_job", "entries"],
|
|
135
|
+
ids: ["thread_id", "job_id", "attempt", "suggestion_id"],
|
|
136
|
+
limitations: ["page_bounded"]
|
|
137
|
+
},
|
|
138
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
|
|
139
|
+
examples: [
|
|
140
|
+
{ command: "saturndocs requests get job-2f1c --site acme", description: "The newest 100 turns behind one request." },
|
|
141
|
+
{ command: "saturndocs requests get job-reply-9 --json", description: "A reply's job id, which resolves to its root thread." }
|
|
142
|
+
]
|
|
143
|
+
})
|
|
144
|
+
]);
|
|
145
|
+
async function prepare(globals, environment) {
|
|
146
|
+
const client = clientFor(globals, environment);
|
|
147
|
+
const target = await resolveTarget(client, globals.site === null ? {} : { site: globals.site });
|
|
148
|
+
return { client, siteId: target.site_id };
|
|
149
|
+
}
|
|
150
|
+
function integerFlag(value, name) {
|
|
151
|
+
if (value === void 0) return void 0;
|
|
152
|
+
const parsed = Number(value);
|
|
153
|
+
if (!Number.isInteger(parsed)) throw invalidInput(`${name} takes a whole number.`);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
function registerList(group, environment) {
|
|
157
|
+
const metadata = REQUESTS_READ_METADATA[0];
|
|
158
|
+
const command = group.command("list").action(async (_options, self) => {
|
|
159
|
+
const globals = resolveGlobalOptions(self);
|
|
160
|
+
const writer = createResultWriter({ json: globals.json });
|
|
161
|
+
const context = { command: "requests list", siteId: null };
|
|
162
|
+
try {
|
|
163
|
+
const flags = self.opts();
|
|
164
|
+
const limit = integerFlag(flags.limit, "--limit");
|
|
165
|
+
const run = await prepare(globals, environment);
|
|
166
|
+
context.siteId = run.siteId;
|
|
167
|
+
const page = await listRequestThreads(run.client, {
|
|
168
|
+
siteId: run.siteId,
|
|
169
|
+
cursor: flags.cursor,
|
|
170
|
+
limit
|
|
171
|
+
});
|
|
172
|
+
const bounded = {
|
|
173
|
+
...context,
|
|
174
|
+
siteId: run.siteId,
|
|
175
|
+
limitations: [{
|
|
176
|
+
code: "page_bounded",
|
|
177
|
+
message: page.next_cursor === null ? "One page was read. The server reported no page after it." : "One page was read. Pass its next_cursor to read the page after it.",
|
|
178
|
+
at: "threads"
|
|
179
|
+
}]
|
|
180
|
+
};
|
|
181
|
+
process.exitCode = writer.write(successResult(bounded, page), {
|
|
182
|
+
text: `${page.threads.length} conversation(s)${page.next_cursor === null ? "" : "; more follow"}`
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
process.exitCode = writer.write(failureResult(asCliError(error), context));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
applyCommandHelp(command, metadata);
|
|
189
|
+
}
|
|
190
|
+
function idsOf(thread) {
|
|
191
|
+
const ids = {
|
|
192
|
+
thread_id: thread.thread_id,
|
|
193
|
+
job_id: thread.latest_job.job_id,
|
|
194
|
+
attempt: thread.latest_job.attempt
|
|
195
|
+
};
|
|
196
|
+
if (thread.latest_job.suggestion_id !== null) ids.suggestion_id = thread.latest_job.suggestion_id;
|
|
197
|
+
if (thread.latest_job.pr_number !== null) ids.pr_number = thread.latest_job.pr_number;
|
|
198
|
+
return ids;
|
|
199
|
+
}
|
|
200
|
+
function registerGet(group, environment) {
|
|
201
|
+
const metadata = REQUESTS_READ_METADATA[1];
|
|
202
|
+
const command = group.command("get").argument("<job>", "Any job of the conversation, including a reply.").action(async (job, _options, self) => {
|
|
203
|
+
const globals = resolveGlobalOptions(self);
|
|
204
|
+
const writer = createResultWriter({ json: globals.json });
|
|
205
|
+
const context = { command: "requests get", siteId: null };
|
|
206
|
+
try {
|
|
207
|
+
const run = await prepare(globals, environment);
|
|
208
|
+
context.siteId = run.siteId;
|
|
209
|
+
const thread = await readRequestThread(run.client, {
|
|
210
|
+
siteId: run.siteId,
|
|
211
|
+
jobId: job
|
|
212
|
+
});
|
|
213
|
+
context.siteId = thread.site_id;
|
|
214
|
+
context.ids = idsOf(thread);
|
|
215
|
+
context.limitations = [{
|
|
216
|
+
code: "page_bounded",
|
|
217
|
+
message: "The server bounds this read to the newest 100 entries and supplies no total or cursor, so older entries may not be present.",
|
|
218
|
+
at: "entries"
|
|
219
|
+
}];
|
|
220
|
+
process.exitCode = writer.write(successResult(context, thread), {
|
|
221
|
+
text: `${thread.thread_id}: ${thread.latest_job.state}, ${thread.entries.length} turn(s)${thread.open ? ", waiting on an answer" : ""}`
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
process.exitCode = writer.write(failureResult(asCliError(error), context));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
applyCommandHelp(command, metadata);
|
|
228
|
+
}
|
|
229
|
+
function registerRequestsRead(group, environment = {}) {
|
|
230
|
+
registerList(group, environment);
|
|
231
|
+
registerGet(group, environment);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/commands/requests/reply.ts
|
|
235
|
+
var REQUESTS_REPLY_METADATA = Object.freeze([
|
|
236
|
+
commandMetadata("requests reply", {
|
|
237
|
+
description: "Answer a clarification on a conversation that is waiting on one. The conversation is named by any of its jobs, as the positional argument or as --thread, and the root it resolves to is the thread the answer joins; the answer opens a new job of its own and the root thread id does not change. The page and the reader evidence the original request carried are the conversation's, so this command never sends them again and never replaces them. --submission-id is a UUID you mint and keep: replaying it with the same words recovers the job the answer already opened, even once the conversation has stopped waiting.",
|
|
238
|
+
target: { arguments: [{ name: "job", required: false, description: "Any job of the conversation to answer, including a reply. --thread takes the same value." }] },
|
|
239
|
+
effect: "Adds one answer to an existing conversation on the named site and queues the job it opens. Nothing is published.",
|
|
240
|
+
idempotency: "submission_id",
|
|
241
|
+
output: { fields: ["job_id", "state"], ids: ["submission_id", "thread_id", "job_id"], limitations: [] },
|
|
242
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "interrupted"],
|
|
243
|
+
examples: [
|
|
244
|
+
{ command: 'saturndocs requests reply job-2f1c --site acme --submission-id 6f80c9a2-012b-4dee-8de4-97d44cd28b60 --text "Use the public option name"', description: "One answer to the question the conversation is waiting on." },
|
|
245
|
+
{ command: "saturndocs requests reply --thread job-2f1c --site acme --submission-id 6f80c9a2-012b-4dee-8de4-97d44cd28b60 --file answer.md", description: "The same, with the conversation named by the contract's own flag and the answer read from a file." },
|
|
246
|
+
{ command: "saturndocs requests reply job-reply-9 --site acme --submission-id 6f80c9a2-012b-4dee-8de4-97d44cd28b60 --file - --json", description: "A reply's job id, which resolves to its root, with the answer on stdin." }
|
|
247
|
+
]
|
|
248
|
+
})
|
|
249
|
+
]);
|
|
250
|
+
function quoted2(value) {
|
|
251
|
+
return /^[\w.\-/@]+$/u.test(value) ? value : JSON.stringify(value);
|
|
252
|
+
}
|
|
253
|
+
function recoveryFor2(siteId, job, flags, submissionId) {
|
|
254
|
+
const stdin = flags.file === "-";
|
|
255
|
+
const source = flags.file === void 0 || stdin ? null : `--file ${quoted2(flags.file)}`;
|
|
256
|
+
const line = source === null ? null : ["saturndocs requests reply", quoted2(job), `--site ${quoted2(siteId)}`, `--submission-id ${submissionId}`, source].join(" ");
|
|
257
|
+
const replaySource = stdin ? "the same text on stdin" : source === null ? "the same --text" : `the same ${source}`;
|
|
258
|
+
return {
|
|
259
|
+
command: line,
|
|
260
|
+
replay: { submission_id: submissionId, stdin },
|
|
261
|
+
message: `It is not known whether the answer was added to ${job}. Nothing was sent again. Read the conversation with saturndocs requests get ${job}, or send it once more with --submission-id ${submissionId} and ${replaySource}: the same id with the same words recovers the one job even after the conversation stops waiting, and with different words it is refused.`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function threadArgument(job, flags) {
|
|
265
|
+
const named = job ?? flags.thread;
|
|
266
|
+
if (named === void 0 || named.trim() === "") {
|
|
267
|
+
throw invalidInput("requests reply needs the conversation to answer. Pass the job as the argument, or --thread <job>.");
|
|
268
|
+
}
|
|
269
|
+
if (job !== void 0 && flags.thread !== void 0 && job !== flags.thread) {
|
|
270
|
+
throw invalidInput(`requests reply was given two conversations, ${job} and --thread ${flags.thread}. Name one.`);
|
|
271
|
+
}
|
|
272
|
+
return named;
|
|
273
|
+
}
|
|
274
|
+
function registerRequestsReply(group, environment = {}) {
|
|
275
|
+
const metadata = REQUESTS_REPLY_METADATA[0];
|
|
276
|
+
const command = group.command("reply").argument("[job]", "Any job of the conversation to answer, including a reply.").option("--thread <thread>", "The conversation to answer, when it is not given as the argument.").action(async (job, _options, self) => {
|
|
277
|
+
const globals = resolveGlobalOptions(self);
|
|
278
|
+
const writer = createResultWriter({ json: globals.json });
|
|
279
|
+
const flags = self.opts();
|
|
280
|
+
const context = { command: "requests reply", siteId: globals.site };
|
|
281
|
+
let recovery = null;
|
|
282
|
+
try {
|
|
283
|
+
const named = threadArgument(job, flags);
|
|
284
|
+
const submissionId = checkedSubmissionId(flags.submissionId ?? "");
|
|
285
|
+
const text = await readTextInput(flags, {
|
|
286
|
+
command: "requests reply",
|
|
287
|
+
requirement: metadata.text.requirement,
|
|
288
|
+
limit: metadata.text.limit ?? REQUEST_TEXT_LIMIT,
|
|
289
|
+
interactive: globals.input,
|
|
290
|
+
stdin: environment.stdin,
|
|
291
|
+
readFileImpl: environment.readFileImpl
|
|
292
|
+
});
|
|
293
|
+
const request = checkedRequestText(text ?? "");
|
|
294
|
+
const client = clientFor(globals, environment);
|
|
295
|
+
const target = await resolveTarget(client, { site: globals.site ?? void 0, mutation: true });
|
|
296
|
+
context.siteId = target.site_id;
|
|
297
|
+
recovery = recoveryFor2(target.site_id, named, flags, submissionId);
|
|
298
|
+
const reply = await replyToRequest(client, {
|
|
299
|
+
siteId: target.site_id,
|
|
300
|
+
jobId: named,
|
|
301
|
+
submissionId,
|
|
302
|
+
request
|
|
303
|
+
});
|
|
304
|
+
const resolved = {
|
|
305
|
+
...context,
|
|
306
|
+
ids: { submission_id: submissionId, thread_id: reply.threadId, job_id: reply.queued.job_id }
|
|
307
|
+
};
|
|
308
|
+
process.exitCode = writer.write(successResult(resolved, reply.queued), {
|
|
309
|
+
text: `${reply.queued.job_id} ${reply.queued.state}; thread ${reply.threadId}`
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
const failure = asCliError(error);
|
|
313
|
+
if (failure instanceof CliError && failure.mutation === "unknown" && recovery !== null) context.recovery = recovery;
|
|
314
|
+
process.exitCode = writer.write(failureResult(failure, context));
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
applyCommandHelp(command, metadata);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/commands/requests/retry.ts
|
|
321
|
+
var REQUESTS_RETRY_METADATA = Object.freeze([
|
|
322
|
+
commandMetadata("requests retry", {
|
|
323
|
+
description: "Send one failed request again. The job keeps its own id and the conversation keeps its history, so the attempt is the only thing telling the new run from the one that failed. The attempt the server returns is reported as it is, including the null it sends when the operations Worker named none: nothing here fills one in. A job that is not failed is the server's own refusal, with its sentence saying which state it is in, on exit 4.",
|
|
324
|
+
target: { arguments: [{ name: "job", required: true, description: "The failed job to send again." }] },
|
|
325
|
+
effect: "Queues one more run of a failed request on the named site. Nothing is published.",
|
|
326
|
+
output: { fields: ["job_id", "state", "attempt"], ids: ["job_id", "attempt"], limitations: [] },
|
|
327
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "conflict", "rate_limited", "service_unavailable", "transport_failure", "unknown_outcome", "interrupted"],
|
|
328
|
+
examples: [
|
|
329
|
+
{ command: "saturndocs requests retry job-2f1c --site acme", description: "Send a failed request through the agent again." },
|
|
330
|
+
{ command: "saturndocs requests retry job-2f1c --site acme --json", description: "The same, as one result object carrying the returned attempt." }
|
|
331
|
+
]
|
|
332
|
+
})
|
|
333
|
+
]);
|
|
334
|
+
function registerRequestsRetry(group, environment = {}) {
|
|
335
|
+
const metadata = REQUESTS_RETRY_METADATA[0];
|
|
336
|
+
const command = group.command("retry").argument("<job>", "The failed job to send again.").action(async (job, _options, self) => {
|
|
337
|
+
const globals = resolveGlobalOptions(self);
|
|
338
|
+
const writer = createResultWriter({ json: globals.json });
|
|
339
|
+
const context = { command: "requests retry", siteId: globals.site };
|
|
340
|
+
try {
|
|
341
|
+
const client = clientFor(globals, environment);
|
|
342
|
+
const target = await resolveTarget(client, { site: globals.site ?? void 0, mutation: true });
|
|
343
|
+
context.siteId = target.site_id;
|
|
344
|
+
const retried = await retryRequest(client, { siteId: target.site_id, jobId: job });
|
|
345
|
+
const resolved = { ...context, ids: { job_id: retried.job_id, attempt: retried.attempt } };
|
|
346
|
+
process.exitCode = writer.write(successResult(resolved, retried), {
|
|
347
|
+
text: `${retried.job_id} ${retried.state}; attempt ${retried.attempt === null ? "unknown" : retried.attempt}`
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
process.exitCode = writer.write(failureResult(asCliError(error), context));
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
applyCommandHelp(command, metadata);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/requests/wait.ts
|
|
357
|
+
var REQUESTS_WAIT_METADATA = Object.freeze([
|
|
358
|
+
commandMetadata("requests wait", {
|
|
359
|
+
description: "Follow one conversation until its latest job says what came of it: a draft with the suggestion it produced, a clarification question, a conclusion that nothing needed changing, a failure, or a cancellation. Only the latest job's current attempt counts, so a retry is never reported as the failure the previous attempt wrote. A job that ends without writing any turn is reported as an outcome that could not be read, never as a guess. The deadline covers the waiting and the reads in flight alike; reaching it exits 6 with the last state that was verified and the command to resume with, and the remote job keeps running. Interrupting cancels nothing remote.",
|
|
360
|
+
target: { arguments: [{ name: "job", required: true, description: "Any job of the conversation to follow." }] },
|
|
361
|
+
effect: "Nothing. This reads one conversation until it concludes or the deadline passes.",
|
|
362
|
+
output: {
|
|
363
|
+
fields: ["outcome", "state", "job_id", "attempt", "suggestion_id", "thread", "polls", "elapsed_ms"],
|
|
364
|
+
ids: ["thread_id", "job_id", "attempt", "suggestion_id"],
|
|
365
|
+
limitations: []
|
|
366
|
+
},
|
|
367
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "remote_failure", "wait_timeout", "interrupted"],
|
|
368
|
+
examples: [
|
|
369
|
+
{ command: "saturndocs requests wait job-2f1c --site acme", description: "Follow a request to its draft, its question, or its failure." },
|
|
370
|
+
{ command: "saturndocs requests wait job-2f1c --site acme --timeout 300 --json", description: "The same, bounded to five minutes, as one result object." }
|
|
371
|
+
]
|
|
372
|
+
})
|
|
373
|
+
]);
|
|
374
|
+
function idsOf2(result) {
|
|
375
|
+
const ids = { attempt: result.attempt };
|
|
376
|
+
if (result.thread !== null) ids.thread_id = result.thread.thread_id;
|
|
377
|
+
if (result.job_id !== null) ids.job_id = result.job_id;
|
|
378
|
+
if (result.suggestion_id !== null) ids.suggestion_id = result.suggestion_id;
|
|
379
|
+
return ids;
|
|
380
|
+
}
|
|
381
|
+
function resumeCommand(verb, result, job, siteId) {
|
|
382
|
+
return `saturndocs requests ${verb} ${result.job_id ?? job} --site ${siteId}`;
|
|
383
|
+
}
|
|
384
|
+
function recoveryFor3(result, job, siteId) {
|
|
385
|
+
const state = result.state ?? "unknown";
|
|
386
|
+
if (result.outcome === "timed_out") {
|
|
387
|
+
return {
|
|
388
|
+
command: resumeCommand("wait", result, job, siteId),
|
|
389
|
+
replay: null,
|
|
390
|
+
message: `The job was ${state} on ${siteId} when the deadline passed. It is still running; this resumes the wait.`
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (result.outcome === "failed") {
|
|
394
|
+
return { command: resumeCommand("retry", result, job, siteId), replay: null, message: `The job failed on ${siteId}. This sends it again under the same job id.` };
|
|
395
|
+
}
|
|
396
|
+
if (result.outcome === "cancelled" || result.outcome === "outcome_unknown") {
|
|
397
|
+
return { command: resumeCommand("get", result, job, siteId), replay: null, message: `Read the conversation on ${siteId} before asking again; it was ${state} and carried no result to act on.` };
|
|
398
|
+
}
|
|
399
|
+
if (result.outcome === "question" && result.thread !== null) {
|
|
400
|
+
return {
|
|
401
|
+
command: `saturndocs requests reply --thread ${result.thread.thread_id} --site ${siteId}`,
|
|
402
|
+
replay: null,
|
|
403
|
+
message: "Answering is a reply on the root thread, not a new request."
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function textFor(result) {
|
|
409
|
+
const job = result.job_id ?? "the request";
|
|
410
|
+
const suffix = result.suggestion_id === null ? "" : ` (${result.suggestion_id})`;
|
|
411
|
+
return `${job} ${result.outcome}${suffix}: ${result.note}`;
|
|
412
|
+
}
|
|
413
|
+
function registerRequestsWait(group, environment = {}) {
|
|
414
|
+
const metadata = REQUESTS_WAIT_METADATA[0];
|
|
415
|
+
const command = group.command("wait").argument("<job>", "Any job of the conversation to follow.").action(async (job, _options, self) => {
|
|
416
|
+
const globals = resolveGlobalOptions(self);
|
|
417
|
+
const writer = createResultWriter({ json: globals.json });
|
|
418
|
+
const context = { command: "requests wait", siteId: globals.site };
|
|
419
|
+
try {
|
|
420
|
+
const signal = environment.signal ?? interruptSignal() ?? void 0;
|
|
421
|
+
const client = clientFor(globals, environment);
|
|
422
|
+
const target = await resolveTarget(client, { site: globals.site ?? void 0, signal });
|
|
423
|
+
context.siteId = target.site_id;
|
|
424
|
+
const result = await waitForRequest(client, {
|
|
425
|
+
siteId: target.site_id,
|
|
426
|
+
jobId: job,
|
|
427
|
+
deadlineMs: globals.timeout === null ? DEFAULT_REQUEST_WAIT_MS : globals.timeout * 1e3,
|
|
428
|
+
signal
|
|
429
|
+
});
|
|
430
|
+
const resolved = {
|
|
431
|
+
...context,
|
|
432
|
+
ids: idsOf2(result),
|
|
433
|
+
recovery: recoveryFor3(result, job, target.site_id)
|
|
434
|
+
};
|
|
435
|
+
const code = requestWaitErrorCode(result.outcome);
|
|
436
|
+
if (code === null) {
|
|
437
|
+
process.exitCode = writer.write(successResult(resolved, result), { text: textFor(result) });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const failed = new CliError(code, textFor(result), { recovery: resolved.recovery, data: result });
|
|
441
|
+
process.exitCode = writer.write(failureResult(failed, resolved));
|
|
442
|
+
} catch (error) {
|
|
443
|
+
process.exitCode = writer.write(failureResult(asCliError(error), context));
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
applyCommandHelp(command, metadata);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/commands/requests/index.ts
|
|
450
|
+
var REQUESTS_METADATA = Object.freeze([...REQUESTS_READ_METADATA, ...REQUESTS_CREATE_METADATA, ...REQUESTS_REPLY_METADATA, ...REQUESTS_RETRY_METADATA, ...REQUESTS_WAIT_METADATA]);
|
|
451
|
+
function registerRequests(program, _cliDir, environment = {}) {
|
|
452
|
+
const group = program.command("requests").description("Documentation requests and the conversations behind them");
|
|
453
|
+
registerRequestsRead(group, environment);
|
|
454
|
+
registerRequestsCreate(group, environment);
|
|
455
|
+
registerRequestsReply(group, environment);
|
|
456
|
+
registerRequestsRetry(group, environment);
|
|
457
|
+
registerRequestsWait(group, environment);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export {
|
|
461
|
+
REQUESTS_CREATE_METADATA,
|
|
462
|
+
registerRequestsCreate,
|
|
463
|
+
REQUESTS_METADATA,
|
|
464
|
+
registerRequests
|
|
465
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/commands/common.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
function resolveDocsDir(dir) {
|
|
6
|
+
const docsDir = resolve(dir ?? process.cwd());
|
|
7
|
+
if (!existsSync(resolve(docsDir, "docs.json"))) {
|
|
8
|
+
console.error(
|
|
9
|
+
pc.red(`error: no docs.json found in ${docsDir}. Pass a docs directory.`)
|
|
10
|
+
);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return docsDir;
|
|
14
|
+
}
|
|
15
|
+
function messageOf(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
resolveDocsDir,
|
|
21
|
+
messageOf
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/lib/workspace.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
function findWorkspaceRoot(startDir) {
|
|
5
|
+
let dir = startDir;
|
|
6
|
+
for (; ; ) {
|
|
7
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) && existsSync(join(dir, "apps", "web"))) {
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
const parent = dirname(dir);
|
|
11
|
+
if (parent === dir) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Could not locate the SaturnDocs workspace root (a directory with pnpm-workspace.yaml and apps/web) at or above ${startDir}.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function findWebDir(startDir) {
|
|
20
|
+
return join(findWorkspaceRoot(startDir), "apps", "web");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
findWorkspaceRoot,
|
|
25
|
+
findWebDir
|
|
26
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requestedSite
|
|
3
|
+
} from "./chunk-CMNX5DLQ.js";
|
|
4
|
+
import {
|
|
5
|
+
asCliError,
|
|
6
|
+
clientFor
|
|
7
|
+
} from "./chunk-TKISMN3P.js";
|
|
8
|
+
import {
|
|
9
|
+
applyCommandHelp
|
|
10
|
+
} from "./chunk-M2BZEDAS.js";
|
|
11
|
+
import {
|
|
12
|
+
PAGE_TRAFFIC_WINDOWS,
|
|
13
|
+
commandMetadata,
|
|
14
|
+
createResultWriter,
|
|
15
|
+
failureResult,
|
|
16
|
+
listPages,
|
|
17
|
+
pagesUnavailable,
|
|
18
|
+
resolveGlobalOptions,
|
|
19
|
+
resolveTarget,
|
|
20
|
+
successResult
|
|
21
|
+
} from "./chunk-GYRVGWDM.js";
|
|
22
|
+
|
|
23
|
+
// src/commands/pages.ts
|
|
24
|
+
import { InvalidArgumentError } from "commander";
|
|
25
|
+
var PAGES_METADATA = Object.freeze([
|
|
26
|
+
commandMetadata("pages list", {
|
|
27
|
+
description: "Read the pages of the release readers are seeing, with where each one is read, where its source is, and how often it was read in the traffic window. A site with no live release answers unavailable: no_release and carries no page list at all, which this command reports as such rather than as a published site with nothing in it.",
|
|
28
|
+
target: { arguments: [{ name: "site-id", required: false, description: "The site to read. Defaults to the site the credential resolves to." }] },
|
|
29
|
+
effect: "Nothing. One read of the published page listing.",
|
|
30
|
+
output: { fields: ["site_id", "days", "release_id", "traffic", "pages", "unavailable"], ids: ["release_id"], limitations: ["page_bounded", "section_unavailable"] },
|
|
31
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
|
|
32
|
+
examples: [
|
|
33
|
+
{ command: "saturndocs pages list", description: "Every published page of the live release, over the server's default window." },
|
|
34
|
+
{ command: "saturndocs pages list --days 30", description: "The same list with a 30-day traffic window." },
|
|
35
|
+
{ command: "saturndocs pages list --json", description: "The listing as one result object, with the resolved site_id in the envelope." }
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
]);
|
|
39
|
+
function trafficWindow(value) {
|
|
40
|
+
const days = Number(value);
|
|
41
|
+
if (!PAGE_TRAFFIC_WINDOWS.includes(days)) {
|
|
42
|
+
throw new InvalidArgumentError(`The server keeps ${PAGE_TRAFFIC_WINDOWS.join(", ")} day windows and no others.`);
|
|
43
|
+
}
|
|
44
|
+
return days;
|
|
45
|
+
}
|
|
46
|
+
function pagesLimitations(result) {
|
|
47
|
+
if (pagesUnavailable(result)) {
|
|
48
|
+
return [{
|
|
49
|
+
code: "section_unavailable",
|
|
50
|
+
message: "This site has no live release, so there is no published page list to read. That is not a site whose published list is empty.",
|
|
51
|
+
at: "pages"
|
|
52
|
+
}];
|
|
53
|
+
}
|
|
54
|
+
if (result.traffic === "partial") {
|
|
55
|
+
return [{ code: "page_bounded", message: "The edge returned as many address counts as one answer holds, so some reads are missing from the view and agent-read figures.", at: "pages" }];
|
|
56
|
+
}
|
|
57
|
+
if (result.traffic === "not_configured" || result.traffic === "failed") {
|
|
58
|
+
return [{
|
|
59
|
+
code: "section_unavailable",
|
|
60
|
+
message: `Traffic figures are ${result.traffic}: every views and agent_reads figure below is 0 because nothing was counted, not because nobody read the page.`,
|
|
61
|
+
at: "pages"
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function listText(result) {
|
|
67
|
+
if (pagesUnavailable(result)) {
|
|
68
|
+
return `${result.site_id}: no live release, so there is no published page list. Publish a release before reading pages.`;
|
|
69
|
+
}
|
|
70
|
+
const head = `${result.site_id}: release ${result.release_id}, ${result.pages.length} published pages, traffic ${result.traffic} over ${result.days} days`;
|
|
71
|
+
return [head, ...result.pages.map((page) => `${page.url} views ${page.views} agent reads ${page.agent_reads} ${page.path}`)].join("\n");
|
|
72
|
+
}
|
|
73
|
+
function registerPages(program, _cliDir, environment = {}) {
|
|
74
|
+
const pages = program.command("pages").description("Read the published pages of the release readers are seeing");
|
|
75
|
+
const list = pages.command("list").argument("[site-id]", "The site to read. Defaults to the site the credential resolves to.").option("--days <n>", `Traffic window: ${PAGE_TRAFFIC_WINDOWS.join(", ")}.`, trafficWindow).action(async (siteId, options, self) => {
|
|
76
|
+
const globals = resolveGlobalOptions(self);
|
|
77
|
+
const writer = createResultWriter({ json: globals.json });
|
|
78
|
+
const context = { command: "pages list", siteId: null };
|
|
79
|
+
try {
|
|
80
|
+
const client = clientFor(globals, environment);
|
|
81
|
+
const named = requestedSite(siteId, globals.site);
|
|
82
|
+
const target = await resolveTarget(client, named === void 0 ? {} : { site: named });
|
|
83
|
+
context.siteId = target.site_id;
|
|
84
|
+
const result = await listPages(client, target.site_id, options.days === void 0 ? {} : { days: options.days });
|
|
85
|
+
context.limitations = pagesLimitations(result);
|
|
86
|
+
if (!pagesUnavailable(result)) context.ids = { release_id: result.release_id };
|
|
87
|
+
process.exitCode = writer.write(successResult(context, result), { text: listText(result) });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
process.exitCode = writer.write(failureResult(asCliError(error), context));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
applyCommandHelp(list, PAGES_METADATA[0]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
PAGES_METADATA,
|
|
97
|
+
pagesLimitations,
|
|
98
|
+
registerPages
|
|
99
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|