@myna-sh/cli 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/bin/myna.mjs +7 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +2181 -0
- package/dist/main.js.map +1 -0
- package/package.json +42 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2181 @@
|
|
|
1
|
+
// src/main.ts
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
|
|
4
|
+
// ../sdk/dist/chunk-VAS24QPJ.js
|
|
5
|
+
var MynaApiError = class _MynaApiError extends Error {
|
|
6
|
+
/** HTTP status code. */
|
|
7
|
+
status;
|
|
8
|
+
/** Stable machine-readable error code (e.g. `VALIDATION_FAILED`). */
|
|
9
|
+
code;
|
|
10
|
+
/** RFC 9457 `type` URI. */
|
|
11
|
+
type;
|
|
12
|
+
/** Human-facing title. */
|
|
13
|
+
title;
|
|
14
|
+
/** Longer human-facing detail, when provided. */
|
|
15
|
+
detail;
|
|
16
|
+
/** Server request id for support/correlation. */
|
|
17
|
+
requestId;
|
|
18
|
+
/** Field-level validation errors, when present. */
|
|
19
|
+
fields;
|
|
20
|
+
/** The full parsed problem document (or a synthesized one). */
|
|
21
|
+
problem;
|
|
22
|
+
constructor(problem) {
|
|
23
|
+
super(problem.detail ?? problem.title ?? `Request failed (${problem.status})`);
|
|
24
|
+
this.name = "MynaApiError";
|
|
25
|
+
this.status = problem.status;
|
|
26
|
+
this.code = problem.code;
|
|
27
|
+
this.type = problem.type;
|
|
28
|
+
this.title = problem.title;
|
|
29
|
+
this.detail = problem.detail;
|
|
30
|
+
this.requestId = problem.requestId;
|
|
31
|
+
this.fields = problem.fields;
|
|
32
|
+
this.problem = problem;
|
|
33
|
+
Object.setPrototypeOf(this, _MynaApiError.prototype);
|
|
34
|
+
}
|
|
35
|
+
static is(value) {
|
|
36
|
+
return value instanceof _MynaApiError;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function isMynaApiError(value) {
|
|
40
|
+
return value instanceof MynaApiError;
|
|
41
|
+
}
|
|
42
|
+
function problemFromResponse(status, body, fallbackTitle) {
|
|
43
|
+
if (body && typeof body === "object" && "code" in body && "status" in body) {
|
|
44
|
+
return new MynaApiError(body);
|
|
45
|
+
}
|
|
46
|
+
const detail = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : typeof body === "string" && body ? body : void 0;
|
|
47
|
+
return new MynaApiError({
|
|
48
|
+
type: "about:blank",
|
|
49
|
+
title: fallbackTitle,
|
|
50
|
+
status,
|
|
51
|
+
code: status >= 500 ? "INTERNAL" : "REQUEST_FAILED",
|
|
52
|
+
detail
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
56
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
57
|
+
function resolveFetch(custom) {
|
|
58
|
+
if (custom) return custom;
|
|
59
|
+
if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
|
|
60
|
+
throw new Error("No fetch implementation available. Pass `fetch` to the Myna client.");
|
|
61
|
+
}
|
|
62
|
+
function buildQuery(query2) {
|
|
63
|
+
if (!query2) return "";
|
|
64
|
+
const params = new URLSearchParams();
|
|
65
|
+
const append = (key, value) => {
|
|
66
|
+
if (value === void 0 || value === null) return;
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
params.set(key, value.join(","));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === "object") {
|
|
72
|
+
for (const [op, v] of Object.entries(value)) {
|
|
73
|
+
append(`${key}[${op}]`, v);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
params.set(key, String(value));
|
|
78
|
+
};
|
|
79
|
+
for (const [key, value] of Object.entries(query2)) append(key, value);
|
|
80
|
+
const qs = params.toString();
|
|
81
|
+
return qs ? `?${qs}` : "";
|
|
82
|
+
}
|
|
83
|
+
function sleep(ms, signal) {
|
|
84
|
+
return new Promise((resolve3, reject) => {
|
|
85
|
+
if (signal?.aborted) return reject(signal.reason ?? new Error("Aborted"));
|
|
86
|
+
const timer = setTimeout(resolve3, ms);
|
|
87
|
+
signal?.addEventListener(
|
|
88
|
+
"abort",
|
|
89
|
+
() => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
92
|
+
},
|
|
93
|
+
{ once: true }
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
var HttpClient = class {
|
|
98
|
+
baseUrl;
|
|
99
|
+
token;
|
|
100
|
+
fetchImpl;
|
|
101
|
+
defaultHeaders;
|
|
102
|
+
retry;
|
|
103
|
+
constructor(options) {
|
|
104
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
105
|
+
this.token = options.token;
|
|
106
|
+
this.fetchImpl = resolveFetch(options.fetch);
|
|
107
|
+
this.defaultHeaders = options.headers ?? {};
|
|
108
|
+
this.retry = {
|
|
109
|
+
maxRetries: options.retry?.maxRetries ?? 3,
|
|
110
|
+
baseDelayMs: options.retry?.baseDelayMs ?? 200,
|
|
111
|
+
maxDelayMs: options.retry?.maxDelayMs ?? 5e3
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Perform a request and unwrap the `{ data }` envelope. */
|
|
115
|
+
async request(method, path, options = {}) {
|
|
116
|
+
const raw = await this.requestRaw(method, path, options);
|
|
117
|
+
if (raw.status === 204 || raw.status === 304) return void 0;
|
|
118
|
+
const parsed = await raw.json().catch(() => void 0);
|
|
119
|
+
return parsed && "data" in parsed ? parsed.data : parsed;
|
|
120
|
+
}
|
|
121
|
+
/** Perform a request and return the full parsed body (envelope included). */
|
|
122
|
+
async requestEnvelope(method, path, options = {}) {
|
|
123
|
+
const raw = await this.requestRaw(method, path, options);
|
|
124
|
+
return await raw.json().catch(() => ({}));
|
|
125
|
+
}
|
|
126
|
+
/** Low-level request with retries; throws `MynaApiError` on non-2xx. */
|
|
127
|
+
async requestRaw(method, path, options = {}) {
|
|
128
|
+
const url = `${this.baseUrl}${path}${buildQuery(options.query)}`;
|
|
129
|
+
const headers = {
|
|
130
|
+
accept: "application/json",
|
|
131
|
+
...this.defaultHeaders,
|
|
132
|
+
...options.headers
|
|
133
|
+
};
|
|
134
|
+
const token = options.token ?? this.token;
|
|
135
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
136
|
+
let payload;
|
|
137
|
+
if (options.body !== void 0) {
|
|
138
|
+
if (options.body instanceof Uint8Array || typeof options.body === "string") {
|
|
139
|
+
payload = options.body;
|
|
140
|
+
} else {
|
|
141
|
+
headers["content-type"] = "application/json";
|
|
142
|
+
payload = JSON.stringify(options.body);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
146
|
+
const idempotent = SAFE_METHODS.has(method.toUpperCase()) || Boolean(options.idempotencyKey);
|
|
147
|
+
const init = { method, headers };
|
|
148
|
+
if (payload !== void 0) init.body = payload;
|
|
149
|
+
if (options.signal) init.signal = options.signal;
|
|
150
|
+
let attempt = 0;
|
|
151
|
+
for (; ; ) {
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
response = await this.fetchImpl(url, init);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (idempotent && attempt < this.retry.maxRetries && !isAbort(error)) {
|
|
157
|
+
await sleep(this.backoff(attempt), options.signal);
|
|
158
|
+
attempt++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (response.ok) return response;
|
|
164
|
+
if (idempotent && RETRYABLE_STATUS.has(response.status) && attempt < this.retry.maxRetries) {
|
|
165
|
+
const retryAfter = retryAfterMs(response);
|
|
166
|
+
await sleep(retryAfter ?? this.backoff(attempt), options.signal);
|
|
167
|
+
attempt++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const body = await response.json().catch(() => void 0);
|
|
171
|
+
throw problemFromResponse(response.status, body, response.statusText || "Request failed");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
backoff(attempt) {
|
|
175
|
+
const base = Math.min(this.retry.baseDelayMs * 2 ** attempt, this.retry.maxDelayMs);
|
|
176
|
+
return Math.round(base * (0.5 + Math.random() * 0.5));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function retryAfterMs(response) {
|
|
180
|
+
const header = response.headers.get("retry-after");
|
|
181
|
+
if (!header) return void 0;
|
|
182
|
+
const seconds = Number(header);
|
|
183
|
+
return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
|
|
184
|
+
}
|
|
185
|
+
function isAbort(error) {
|
|
186
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ../sdk/dist/management.js
|
|
190
|
+
import { createHash, randomUUID } from "crypto";
|
|
191
|
+
import { readFile } from "fs/promises";
|
|
192
|
+
import { basename } from "path";
|
|
193
|
+
function generateTypes(collections) {
|
|
194
|
+
const sorted = [...collections].sort((a, b) => a.name.localeCompare(b.name));
|
|
195
|
+
const blocks = [];
|
|
196
|
+
for (const collection of sorted) {
|
|
197
|
+
blocks.push(renderInterface(collection));
|
|
198
|
+
}
|
|
199
|
+
const registry = sorted.map((c) => ` ${JSON.stringify(c.name)}: ${pascal(c.name)};`).join("\n");
|
|
200
|
+
const header = [
|
|
201
|
+
"// Generated by `myna types generate`. Do not edit by hand.",
|
|
202
|
+
"// This file is safe to commit and regenerate.",
|
|
203
|
+
""
|
|
204
|
+
].join("\n");
|
|
205
|
+
return `${header}${blocks.join("\n\n")}
|
|
206
|
+
|
|
207
|
+
export interface MynaCollections {
|
|
208
|
+
${registry}
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
function renderInterface(collection) {
|
|
213
|
+
const name = pascal(collection.name);
|
|
214
|
+
const lines = collection.fields.map((f) => renderField(f, 1));
|
|
215
|
+
const label = collection.label ? ` ${collection.label}` : "";
|
|
216
|
+
return `/**${label} (${collection.kind}) */
|
|
217
|
+
export interface ${name} {
|
|
218
|
+
${lines.join("\n")}
|
|
219
|
+
}`;
|
|
220
|
+
}
|
|
221
|
+
function renderField(field, depth) {
|
|
222
|
+
const indent = " ".repeat(depth);
|
|
223
|
+
const optional = field.required ? "" : "?";
|
|
224
|
+
const doc = field.description ? `${indent}/** ${escapeComment(field.description)} */
|
|
225
|
+
` : "";
|
|
226
|
+
return `${doc}${indent}${safeKey(field.key)}${optional}: ${fieldType(field, depth)};`;
|
|
227
|
+
}
|
|
228
|
+
function fieldType(field, depth) {
|
|
229
|
+
switch (field.type) {
|
|
230
|
+
case "text":
|
|
231
|
+
return field.enum && field.enum.length > 0 ? field.enum.map((v) => JSON.stringify(v)).join(" | ") : "string";
|
|
232
|
+
case "slug":
|
|
233
|
+
return "string";
|
|
234
|
+
case "markdown":
|
|
235
|
+
return "string";
|
|
236
|
+
case "number":
|
|
237
|
+
return "number";
|
|
238
|
+
case "boolean":
|
|
239
|
+
return "boolean";
|
|
240
|
+
case "date":
|
|
241
|
+
case "datetime":
|
|
242
|
+
return "string";
|
|
243
|
+
case "json":
|
|
244
|
+
return "JsonValue";
|
|
245
|
+
case "asset":
|
|
246
|
+
return field.multiple ? "string[]" : "string";
|
|
247
|
+
case "reference":
|
|
248
|
+
return field.multiple ? "string[]" : "string";
|
|
249
|
+
case "object":
|
|
250
|
+
return renderObject(field, depth);
|
|
251
|
+
case "list":
|
|
252
|
+
return `${listItemType(field.item, depth)}[]`;
|
|
253
|
+
default:
|
|
254
|
+
return "unknown";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function renderObject(field, depth) {
|
|
258
|
+
const lines = field.fields.map((f) => renderField(f, depth + 1));
|
|
259
|
+
const closingIndent = " ".repeat(depth);
|
|
260
|
+
return `{
|
|
261
|
+
${lines.join("\n")}
|
|
262
|
+
${closingIndent}}`;
|
|
263
|
+
}
|
|
264
|
+
function listItemType(item, depth) {
|
|
265
|
+
switch (item.kind) {
|
|
266
|
+
case "text":
|
|
267
|
+
return item.enum && item.enum.length > 0 ? `(${item.enum.map((v) => JSON.stringify(v)).join(" | ")})` : "string";
|
|
268
|
+
case "number":
|
|
269
|
+
return "number";
|
|
270
|
+
case "boolean":
|
|
271
|
+
return "boolean";
|
|
272
|
+
case "date":
|
|
273
|
+
case "datetime":
|
|
274
|
+
return "string";
|
|
275
|
+
case "markdown":
|
|
276
|
+
return "string";
|
|
277
|
+
case "json":
|
|
278
|
+
return "JsonValue";
|
|
279
|
+
case "reference":
|
|
280
|
+
return "string";
|
|
281
|
+
case "asset":
|
|
282
|
+
return "string";
|
|
283
|
+
case "object": {
|
|
284
|
+
const lines = item.fields.map((f) => renderField(f, depth + 1));
|
|
285
|
+
const closingIndent = " ".repeat(depth);
|
|
286
|
+
return `{
|
|
287
|
+
${lines.join("\n")}
|
|
288
|
+
${closingIndent}}`;
|
|
289
|
+
}
|
|
290
|
+
default:
|
|
291
|
+
return "unknown";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
var JSON_VALUE = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
|
|
295
|
+
function generateTypesModule(collections) {
|
|
296
|
+
return `${JSON_VALUE}
|
|
297
|
+
|
|
298
|
+
${generateTypes(collections)}`;
|
|
299
|
+
}
|
|
300
|
+
function pascal(key) {
|
|
301
|
+
return key.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
302
|
+
}
|
|
303
|
+
function safeKey(key) {
|
|
304
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
305
|
+
}
|
|
306
|
+
function escapeComment(text) {
|
|
307
|
+
return text.replace(/\*\//g, "*\\/");
|
|
308
|
+
}
|
|
309
|
+
var DEFAULT_API_URL = "https://api.myna.sh";
|
|
310
|
+
var ManagementClient = class {
|
|
311
|
+
http;
|
|
312
|
+
newIdempotencyKey;
|
|
313
|
+
fetchImpl;
|
|
314
|
+
constructor(options) {
|
|
315
|
+
if (!options.token) throw new Error("createManagementClient: `token` is required.");
|
|
316
|
+
this.newIdempotencyKey = options.idempotencyKey ?? (() => randomUUID());
|
|
317
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
318
|
+
this.http = new HttpClient({
|
|
319
|
+
baseUrl: `${(options.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "")}/v1`,
|
|
320
|
+
token: options.token,
|
|
321
|
+
fetch: options.fetch,
|
|
322
|
+
retry: options.retry
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// Convenience: idempotency key for a mutation.
|
|
326
|
+
idem() {
|
|
327
|
+
return this.newIdempotencyKey();
|
|
328
|
+
}
|
|
329
|
+
get(path, query2, signal) {
|
|
330
|
+
return this.http.request("GET", path, { query: query2, signal });
|
|
331
|
+
}
|
|
332
|
+
async page(path, opts = {}) {
|
|
333
|
+
const body = await this.http.requestEnvelope(
|
|
334
|
+
"GET",
|
|
335
|
+
path,
|
|
336
|
+
{ query: { limit: opts.limit, cursor: opts.cursor }, signal: opts.signal }
|
|
337
|
+
);
|
|
338
|
+
return { data: body.data, nextCursor: body.pagination?.nextCursor ?? null };
|
|
339
|
+
}
|
|
340
|
+
mutate(method, path, body, query2) {
|
|
341
|
+
return this.http.request(method, path, { body, query: query2, idempotencyKey: this.idem() });
|
|
342
|
+
}
|
|
343
|
+
// --- Organizations --------------------------------------------------------
|
|
344
|
+
organizations = {
|
|
345
|
+
list: (signal) => this.get("/organizations", void 0, signal),
|
|
346
|
+
create: (body) => this.mutate("POST", "/organizations", body),
|
|
347
|
+
get: (organization, signal) => this.get(`/organizations/${enc(organization)}`, void 0, signal),
|
|
348
|
+
update: (organization, body) => this.mutate("PATCH", `/organizations/${enc(organization)}`, body),
|
|
349
|
+
delete: (organization) => this.mutate("DELETE", `/organizations/${enc(organization)}`),
|
|
350
|
+
export: (organization) => this.mutate("POST", `/organizations/${enc(organization)}/export`),
|
|
351
|
+
usage: (organization, signal) => this.get(`/organizations/${enc(organization)}/usage`, void 0, signal)
|
|
352
|
+
};
|
|
353
|
+
// --- Members & invitations ------------------------------------------------
|
|
354
|
+
members = {
|
|
355
|
+
list: (organization, signal) => this.get(`/organizations/${enc(organization)}/members`, void 0, signal),
|
|
356
|
+
update: (organization, user, body) => this.mutate("PATCH", `/organizations/${enc(organization)}/members/${enc(user)}`, body),
|
|
357
|
+
remove: (organization, user) => this.mutate("DELETE", `/organizations/${enc(organization)}/members/${enc(user)}`)
|
|
358
|
+
};
|
|
359
|
+
invitations = {
|
|
360
|
+
list: (organization, signal) => this.get(`/organizations/${enc(organization)}/invitations`, void 0, signal),
|
|
361
|
+
create: (organization, body) => this.mutate("POST", `/organizations/${enc(organization)}/invitations`, body),
|
|
362
|
+
revoke: (organization, invitation) => this.mutate("DELETE", `/organizations/${enc(organization)}/invitations/${enc(invitation)}`),
|
|
363
|
+
accept: (token) => this.mutate("POST", `/invitations/${enc(token)}/accept`)
|
|
364
|
+
};
|
|
365
|
+
// --- Projects -------------------------------------------------------------
|
|
366
|
+
projects = {
|
|
367
|
+
list: (organization, signal) => this.get(`/organizations/${enc(organization)}/projects`, void 0, signal),
|
|
368
|
+
create: (organization, body) => this.mutate("POST", `/organizations/${enc(organization)}/projects`, body),
|
|
369
|
+
get: (project, signal) => this.get(`/projects/${enc(project)}`, void 0, signal),
|
|
370
|
+
update: (project, body) => this.mutate("PATCH", `/projects/${enc(project)}`, body),
|
|
371
|
+
archive: (project) => this.mutate("POST", `/projects/${enc(project)}/archive`)
|
|
372
|
+
};
|
|
373
|
+
// --- Schema ---------------------------------------------------------------
|
|
374
|
+
schema = {
|
|
375
|
+
collections: (project, signal) => this.get(`/projects/${enc(project)}/collections`, void 0, signal),
|
|
376
|
+
collection: (project, key, signal) => this.get(`/projects/${enc(project)}/collections/${enc(key)}`, void 0, signal),
|
|
377
|
+
versions: (project, key, signal) => this.get(`/projects/${enc(project)}/collections/${enc(key)}/versions`, void 0, signal),
|
|
378
|
+
diff: (project, collections) => this.mutate("POST", `/projects/${enc(project)}/schema/diff`, { collections }),
|
|
379
|
+
push: (project, collections, opts = {}) => this.mutate("POST", `/projects/${enc(project)}/schema/push`, {
|
|
380
|
+
collections,
|
|
381
|
+
allowDestructive: opts.allowDestructive ?? false,
|
|
382
|
+
changeSummary: opts.changeSummary
|
|
383
|
+
})
|
|
384
|
+
};
|
|
385
|
+
// --- Entries & revisions --------------------------------------------------
|
|
386
|
+
entries = {
|
|
387
|
+
list: (project, opts = {}) => this.page(
|
|
388
|
+
`/projects/${enc(project)}/entries` + query({ collection: opts.collection, status: opts.status }),
|
|
389
|
+
opts
|
|
390
|
+
),
|
|
391
|
+
create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/entries`, body),
|
|
392
|
+
get: (project, entry, signal) => this.get(`/projects/${enc(project)}/entries/${enc(entry)}`, void 0, signal),
|
|
393
|
+
update: (project, entry, body) => this.mutate("PATCH", `/projects/${enc(project)}/entries/${enc(entry)}`, body),
|
|
394
|
+
delete: (project, entry, changeSetId) => this.mutate("DELETE", `/projects/${enc(project)}/entries/${enc(entry)}`, void 0, { changeSetId }),
|
|
395
|
+
unpublish: (project, entry, changeSetId) => this.mutate("POST", `/projects/${enc(project)}/entries/${enc(entry)}/unpublish`, void 0, { changeSetId }),
|
|
396
|
+
restore: (project, entry) => this.mutate("POST", `/projects/${enc(project)}/entries/${enc(entry)}/restore`),
|
|
397
|
+
revisions: (project, entry, signal) => this.get(`/projects/${enc(project)}/entries/${enc(entry)}/revisions`, void 0, signal),
|
|
398
|
+
revision: (project, entry, revision, signal) => this.get(`/projects/${enc(project)}/entries/${enc(entry)}/revisions/${enc(revision)}`, void 0, signal),
|
|
399
|
+
restoreRevision: (project, entry, revision) => this.mutate(
|
|
400
|
+
"POST",
|
|
401
|
+
`/projects/${enc(project)}/entries/${enc(entry)}/revisions/${enc(revision)}/restore`
|
|
402
|
+
)
|
|
403
|
+
};
|
|
404
|
+
// --- Change sets ----------------------------------------------------------
|
|
405
|
+
changeSets = {
|
|
406
|
+
list: (project, opts = {}) => this.page(`/projects/${enc(project)}/change-sets` + query({ status: opts.status }), opts),
|
|
407
|
+
create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/change-sets`, body),
|
|
408
|
+
get: (project, changeSet, signal) => this.get(`/projects/${enc(project)}/change-sets/${enc(changeSet)}`, void 0, signal),
|
|
409
|
+
update: (project, changeSet, body) => this.mutate("PATCH", `/projects/${enc(project)}/change-sets/${enc(changeSet)}`, body),
|
|
410
|
+
validate: (project, changeSet) => this.mutate("POST", `/projects/${enc(project)}/change-sets/${enc(changeSet)}/validate`),
|
|
411
|
+
publish: (project, changeSet) => this.mutate("POST", `/projects/${enc(project)}/change-sets/${enc(changeSet)}/publish`, { confirm: true }),
|
|
412
|
+
close: (project, changeSet) => this.mutate("POST", `/projects/${enc(project)}/change-sets/${enc(changeSet)}/close`)
|
|
413
|
+
};
|
|
414
|
+
// --- Previews -------------------------------------------------------------
|
|
415
|
+
previews = {
|
|
416
|
+
create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/previews`, body),
|
|
417
|
+
list: (project, signal) => this.get(`/projects/${enc(project)}/previews`, void 0, signal),
|
|
418
|
+
revoke: (project, preview) => this.mutate("DELETE", `/projects/${enc(project)}/previews/${enc(preview)}`)
|
|
419
|
+
};
|
|
420
|
+
// --- Assets ---------------------------------------------------------------
|
|
421
|
+
assets = {
|
|
422
|
+
createUpload: (project, body) => this.mutate("POST", `/projects/${enc(project)}/assets/uploads`, body),
|
|
423
|
+
completeUpload: (project, upload) => this.mutate("POST", `/projects/${enc(project)}/assets/uploads/${enc(upload)}/complete`).then(
|
|
424
|
+
(r) => r.asset
|
|
425
|
+
),
|
|
426
|
+
list: (project, opts = {}) => this.page(`/projects/${enc(project)}/assets`, opts),
|
|
427
|
+
get: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, void 0, signal),
|
|
428
|
+
usage: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, { usage: "true" }, signal),
|
|
429
|
+
update: (project, asset, body) => this.mutate("PATCH", `/projects/${enc(project)}/assets/${enc(asset)}`, body),
|
|
430
|
+
delete: (project, asset) => this.mutate("DELETE", `/projects/${enc(project)}/assets/${enc(asset)}`),
|
|
431
|
+
/** Full presigned upload flow: create → PUT bytes → complete. */
|
|
432
|
+
upload: (project, input, meta = {}) => this.uploadAsset(project, input, meta)
|
|
433
|
+
};
|
|
434
|
+
// --- API keys -------------------------------------------------------------
|
|
435
|
+
apiKeys = {
|
|
436
|
+
listForOrganization: (organization, signal) => this.get(`/organizations/${enc(organization)}/api-keys`, void 0, signal),
|
|
437
|
+
createForOrganization: (organization, body) => this.mutate("POST", `/organizations/${enc(organization)}/api-keys`, body),
|
|
438
|
+
revokeForOrganization: (organization, key) => this.mutate("DELETE", `/organizations/${enc(organization)}/api-keys/${enc(key)}`),
|
|
439
|
+
listForProject: (project, signal) => this.get(`/projects/${enc(project)}/api-keys`, void 0, signal),
|
|
440
|
+
createForProject: (project, body) => this.mutate("POST", `/projects/${enc(project)}/api-keys`, body),
|
|
441
|
+
revokeForProject: (project, key) => this.mutate("DELETE", `/projects/${enc(project)}/api-keys/${enc(key)}`)
|
|
442
|
+
};
|
|
443
|
+
// --- Webhooks -------------------------------------------------------------
|
|
444
|
+
webhooks = {
|
|
445
|
+
list: (project, signal) => this.get(`/projects/${enc(project)}/webhooks`, void 0, signal),
|
|
446
|
+
create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/webhooks`, body),
|
|
447
|
+
update: (project, webhook, body) => this.mutate("PATCH", `/projects/${enc(project)}/webhooks/${enc(webhook)}`, body),
|
|
448
|
+
delete: (project, webhook) => this.mutate("DELETE", `/projects/${enc(project)}/webhooks/${enc(webhook)}`),
|
|
449
|
+
deliveries: (project, webhook, signal) => this.get(`/projects/${enc(project)}/webhooks/${enc(webhook)}/deliveries`, void 0, signal),
|
|
450
|
+
retry: (project, webhook, delivery) => this.mutate("POST", `/projects/${enc(project)}/webhooks/${enc(webhook)}/deliveries/${enc(delivery)}/retry`)
|
|
451
|
+
};
|
|
452
|
+
// --- Activity -------------------------------------------------------------
|
|
453
|
+
activity(project, opts = {}) {
|
|
454
|
+
return this.page(
|
|
455
|
+
`/projects/${enc(project)}/activity` + query({
|
|
456
|
+
actorType: opts.actorType,
|
|
457
|
+
action: opts.action,
|
|
458
|
+
targetType: opts.targetType,
|
|
459
|
+
from: opts.from,
|
|
460
|
+
to: opts.to
|
|
461
|
+
}),
|
|
462
|
+
opts
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
// --- Billing --------------------------------------------------------------
|
|
466
|
+
billing = {
|
|
467
|
+
status: (organization, signal) => this.get(`/organizations/${enc(organization)}/billing`, void 0, signal),
|
|
468
|
+
checkout: (organization, body) => this.mutate("POST", `/organizations/${enc(organization)}/billing/checkout`, body),
|
|
469
|
+
portal: (organization) => this.mutate("POST", `/organizations/${enc(organization)}/billing/portal`)
|
|
470
|
+
};
|
|
471
|
+
// --- Upload helper --------------------------------------------------------
|
|
472
|
+
async uploadAsset(project, input, meta) {
|
|
473
|
+
const { bytes, filename } = await readInput(input, meta.filename);
|
|
474
|
+
const contentType = meta.contentType ?? guessContentType(filename);
|
|
475
|
+
const checksum = createHash("md5").update(bytes).digest("hex");
|
|
476
|
+
const upload = await this.assets.createUpload(project, {
|
|
477
|
+
filename,
|
|
478
|
+
contentType,
|
|
479
|
+
byteSize: meta.byteSize ?? bytes.byteLength,
|
|
480
|
+
checksum
|
|
481
|
+
});
|
|
482
|
+
const put = await this.fetchImpl(upload.url, {
|
|
483
|
+
method: upload.method,
|
|
484
|
+
headers: upload.headers,
|
|
485
|
+
body: bytes
|
|
486
|
+
});
|
|
487
|
+
if (!put.ok) {
|
|
488
|
+
throw new Error(`Asset upload PUT failed with status ${put.status}.`);
|
|
489
|
+
}
|
|
490
|
+
return this.assets.completeUpload(project, upload.uploadId);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
async function readInput(input, filenameOverride) {
|
|
494
|
+
if (typeof input === "string") {
|
|
495
|
+
const bytes2 = await readFile(input);
|
|
496
|
+
return { bytes: new Uint8Array(bytes2), filename: filenameOverride ?? basename(input) };
|
|
497
|
+
}
|
|
498
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
499
|
+
return { bytes, filename: filenameOverride ?? "upload.bin" };
|
|
500
|
+
}
|
|
501
|
+
var CONTENT_TYPES = {
|
|
502
|
+
png: "image/png",
|
|
503
|
+
jpg: "image/jpeg",
|
|
504
|
+
jpeg: "image/jpeg",
|
|
505
|
+
gif: "image/gif",
|
|
506
|
+
webp: "image/webp",
|
|
507
|
+
avif: "image/avif",
|
|
508
|
+
pdf: "application/pdf",
|
|
509
|
+
json: "application/json",
|
|
510
|
+
txt: "text/plain",
|
|
511
|
+
md: "text/markdown",
|
|
512
|
+
csv: "text/csv",
|
|
513
|
+
mp4: "video/mp4",
|
|
514
|
+
webm: "video/webm",
|
|
515
|
+
mp3: "audio/mpeg",
|
|
516
|
+
wav: "audio/wav"
|
|
517
|
+
};
|
|
518
|
+
function guessContentType(filename) {
|
|
519
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
520
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
521
|
+
}
|
|
522
|
+
function enc(segment) {
|
|
523
|
+
return encodeURIComponent(segment);
|
|
524
|
+
}
|
|
525
|
+
function query(params) {
|
|
526
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== "");
|
|
527
|
+
if (entries.length === 0) return "";
|
|
528
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
529
|
+
}
|
|
530
|
+
function createManagementClient(options) {
|
|
531
|
+
return new ManagementClient(options);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/config.ts
|
|
535
|
+
import { execFileSync } from "child_process";
|
|
536
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, rmSync } from "fs";
|
|
537
|
+
import { homedir, platform } from "os";
|
|
538
|
+
import { dirname, join, resolve } from "path";
|
|
539
|
+
var DEFAULT_API_URL2 = "https://api.myna.sh";
|
|
540
|
+
var KEYCHAIN_SERVICE = "sh.myna.cli";
|
|
541
|
+
function configDir() {
|
|
542
|
+
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
543
|
+
return join(base, "myna");
|
|
544
|
+
}
|
|
545
|
+
function configFile() {
|
|
546
|
+
return join(configDir(), "config.json");
|
|
547
|
+
}
|
|
548
|
+
function credentialsFile() {
|
|
549
|
+
return join(configDir(), "credentials.json");
|
|
550
|
+
}
|
|
551
|
+
function readUserConfig() {
|
|
552
|
+
try {
|
|
553
|
+
return JSON.parse(readFileSync(configFile(), "utf8"));
|
|
554
|
+
} catch {
|
|
555
|
+
return {};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function writeUserConfig(patch) {
|
|
559
|
+
const current = readUserConfig();
|
|
560
|
+
const next = { ...current, ...patch };
|
|
561
|
+
ensureDir(configDir());
|
|
562
|
+
writeFileSync(configFile(), JSON.stringify(next, null, 2) + "\n");
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
function ensureDir(dir) {
|
|
566
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
|
|
567
|
+
}
|
|
568
|
+
function findLinkedProject(startDir = process.cwd()) {
|
|
569
|
+
let dir = resolve(startDir);
|
|
570
|
+
for (; ; ) {
|
|
571
|
+
const file = join(dir, ".myna", "project.json");
|
|
572
|
+
if (existsSync(file)) {
|
|
573
|
+
try {
|
|
574
|
+
const linked = JSON.parse(readFileSync(file, "utf8"));
|
|
575
|
+
return { linked, root: dir };
|
|
576
|
+
} catch {
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const parent = dirname(dir);
|
|
581
|
+
if (parent === dir) return void 0;
|
|
582
|
+
dir = parent;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function writeLinkedProject(root, linked) {
|
|
586
|
+
const dir = join(root, ".myna");
|
|
587
|
+
ensureDir(dir);
|
|
588
|
+
writeFileSync(join(dir, "project.json"), JSON.stringify(linked, null, 2) + "\n");
|
|
589
|
+
}
|
|
590
|
+
function removeLinkedProject(root) {
|
|
591
|
+
const file = join(root, ".myna", "project.json");
|
|
592
|
+
if (existsSync(file)) {
|
|
593
|
+
rmSync(file);
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
function isMac() {
|
|
599
|
+
return platform() === "darwin";
|
|
600
|
+
}
|
|
601
|
+
function keychainAccount(apiUrl) {
|
|
602
|
+
return apiUrl.replace(/\/+$/, "");
|
|
603
|
+
}
|
|
604
|
+
function storeToken(apiUrl, token) {
|
|
605
|
+
const account = keychainAccount(apiUrl);
|
|
606
|
+
if (isMac()) {
|
|
607
|
+
try {
|
|
608
|
+
execFileSync(
|
|
609
|
+
"security",
|
|
610
|
+
["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", token],
|
|
611
|
+
{ stdio: "ignore" }
|
|
612
|
+
);
|
|
613
|
+
return "keychain";
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const file = credentialsFile();
|
|
618
|
+
const store = readCredentialFile();
|
|
619
|
+
store[account] = token;
|
|
620
|
+
ensureDir(configDir());
|
|
621
|
+
writeFileSync(file, JSON.stringify(store, null, 2) + "\n");
|
|
622
|
+
chmodSync(file, 384);
|
|
623
|
+
return "file";
|
|
624
|
+
}
|
|
625
|
+
function loadToken(apiUrl) {
|
|
626
|
+
const account = keychainAccount(apiUrl);
|
|
627
|
+
if (isMac()) {
|
|
628
|
+
try {
|
|
629
|
+
const out = execFileSync(
|
|
630
|
+
"security",
|
|
631
|
+
["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"],
|
|
632
|
+
{ stdio: ["ignore", "pipe", "ignore"] }
|
|
633
|
+
);
|
|
634
|
+
const token = out.toString("utf8").trim();
|
|
635
|
+
if (token) return token;
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return readCredentialFile()[account];
|
|
640
|
+
}
|
|
641
|
+
function clearToken(apiUrl) {
|
|
642
|
+
const account = keychainAccount(apiUrl);
|
|
643
|
+
if (isMac()) {
|
|
644
|
+
try {
|
|
645
|
+
execFileSync("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account], {
|
|
646
|
+
stdio: "ignore"
|
|
647
|
+
});
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const store = readCredentialFile();
|
|
652
|
+
if (account in store) {
|
|
653
|
+
delete store[account];
|
|
654
|
+
writeFileSync(credentialsFile(), JSON.stringify(store, null, 2) + "\n");
|
|
655
|
+
chmodSync(credentialsFile(), 384);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function readCredentialFile() {
|
|
659
|
+
try {
|
|
660
|
+
return JSON.parse(readFileSync(credentialsFile(), "utf8"));
|
|
661
|
+
} catch {
|
|
662
|
+
return {};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function resolveContext(flags) {
|
|
666
|
+
const link = findLinkedProject();
|
|
667
|
+
const user = readUserConfig();
|
|
668
|
+
const apiUrl = flags.apiUrl ?? process.env.MYNA_API_URL ?? link?.linked.apiUrl ?? user.apiUrl ?? DEFAULT_API_URL2;
|
|
669
|
+
const token = flags.token ?? process.env.MYNA_TOKEN ?? loadToken(apiUrl);
|
|
670
|
+
const project = flags.project ?? process.env.MYNA_PROJECT ?? link?.linked.project ?? user.defaultProject;
|
|
671
|
+
const organization = flags.organization ?? process.env.MYNA_ORGANIZATION ?? link?.linked.organization ?? user.defaultOrganization;
|
|
672
|
+
return {
|
|
673
|
+
apiUrl: apiUrl.replace(/\/+$/, ""),
|
|
674
|
+
token,
|
|
675
|
+
project,
|
|
676
|
+
organization,
|
|
677
|
+
json: Boolean(flags.json),
|
|
678
|
+
interactive: flags.interactive !== false && process.stdout.isTTY === true,
|
|
679
|
+
linked: link?.linked,
|
|
680
|
+
linkedRoot: link?.root
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/output.ts
|
|
685
|
+
var CLI_JSON_VERSION = 1;
|
|
686
|
+
var UsageError = class extends Error {
|
|
687
|
+
};
|
|
688
|
+
var CliError = class extends Error {
|
|
689
|
+
};
|
|
690
|
+
var mode = { json: false };
|
|
691
|
+
function setOutputMode(next) {
|
|
692
|
+
mode = next;
|
|
693
|
+
}
|
|
694
|
+
function diag(message) {
|
|
695
|
+
if (!mode.json) process.stderr.write(message + "\n");
|
|
696
|
+
}
|
|
697
|
+
function emit(value, human) {
|
|
698
|
+
if (mode.json) {
|
|
699
|
+
process.stdout.write(JSON.stringify({ version: CLI_JSON_VERSION, data: value }) + "\n");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (human) {
|
|
703
|
+
human();
|
|
704
|
+
} else {
|
|
705
|
+
process.stdout.write(prettyValue(value) + "\n");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function reportError(error) {
|
|
709
|
+
if (isMynaApiError(error)) {
|
|
710
|
+
const problem = {
|
|
711
|
+
code: error.code,
|
|
712
|
+
status: error.status,
|
|
713
|
+
title: error.title,
|
|
714
|
+
detail: error.detail,
|
|
715
|
+
requestId: error.requestId,
|
|
716
|
+
fields: error.fields
|
|
717
|
+
};
|
|
718
|
+
if (mode.json) {
|
|
719
|
+
process.stdout.write(JSON.stringify({ version: CLI_JSON_VERSION, error: problem }) + "\n");
|
|
720
|
+
} else {
|
|
721
|
+
process.stderr.write(`error: ${error.code} \u2014 ${error.detail ?? error.title}
|
|
722
|
+
`);
|
|
723
|
+
for (const f of error.fields ?? []) {
|
|
724
|
+
process.stderr.write(` \xB7 ${f.path}: ${f.message}
|
|
725
|
+
`);
|
|
726
|
+
}
|
|
727
|
+
if (error.requestId) process.stderr.write(` requestId: ${error.requestId}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
const usage = error instanceof UsageError;
|
|
733
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
734
|
+
if (mode.json) {
|
|
735
|
+
process.stdout.write(
|
|
736
|
+
JSON.stringify({ version: CLI_JSON_VERSION, error: { code: usage ? "USAGE" : "ERROR", detail: message } }) + "\n"
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
process.stderr.write(`error: ${message}
|
|
740
|
+
`);
|
|
741
|
+
}
|
|
742
|
+
return usage ? 2 : 1;
|
|
743
|
+
}
|
|
744
|
+
function table(rows, columns) {
|
|
745
|
+
if (rows.length === 0) {
|
|
746
|
+
process.stdout.write("(none)\n");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const headers = columns.map((c) => c.header);
|
|
750
|
+
const cells = rows.map((r) => columns.map((c) => c.value(r) ?? ""));
|
|
751
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((row) => stripLen(row[i] ?? ""))));
|
|
752
|
+
const line = (values) => values.map((v, i) => pad(v, widths[i] ?? v.length)).join(" ").replace(/\s+$/, "");
|
|
753
|
+
process.stdout.write(line(headers) + "\n");
|
|
754
|
+
process.stdout.write(widths.map((w) => "\u2500".repeat(w)).join(" ") + "\n");
|
|
755
|
+
for (const row of cells) process.stdout.write(line(row) + "\n");
|
|
756
|
+
}
|
|
757
|
+
function keyValues(pairs) {
|
|
758
|
+
const width = Math.max(...pairs.map(([k]) => k.length));
|
|
759
|
+
for (const [k, v] of pairs) {
|
|
760
|
+
process.stdout.write(`${pad(k + ":", width + 1)} ${v}
|
|
761
|
+
`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function pad(value, width) {
|
|
765
|
+
const len = stripLen(value);
|
|
766
|
+
return len >= width ? value : value + " ".repeat(width - len);
|
|
767
|
+
}
|
|
768
|
+
function stripLen(value) {
|
|
769
|
+
return value.length;
|
|
770
|
+
}
|
|
771
|
+
function prettyValue(value) {
|
|
772
|
+
if (typeof value === "string") return value;
|
|
773
|
+
return JSON.stringify(value, null, 2);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/client.ts
|
|
777
|
+
function buildContext(flags) {
|
|
778
|
+
const resolved = resolveContext(flags);
|
|
779
|
+
let mgmt;
|
|
780
|
+
return {
|
|
781
|
+
...resolved,
|
|
782
|
+
management() {
|
|
783
|
+
if (!resolved.token) {
|
|
784
|
+
throw new CliError("Not authenticated. Run `myna login` or pass --token / set MYNA_TOKEN.");
|
|
785
|
+
}
|
|
786
|
+
mgmt ??= createManagementClient({ token: resolved.token, apiUrl: resolved.apiUrl });
|
|
787
|
+
return mgmt;
|
|
788
|
+
},
|
|
789
|
+
requireProject() {
|
|
790
|
+
if (!resolved.project) {
|
|
791
|
+
throw new UsageError("No project selected. Pass --project, set MYNA_PROJECT, or run `myna link`.");
|
|
792
|
+
}
|
|
793
|
+
return resolved.project;
|
|
794
|
+
},
|
|
795
|
+
requireOrganization() {
|
|
796
|
+
if (!resolved.organization) {
|
|
797
|
+
throw new UsageError("No organization selected. Pass --organization or set a default with `myna organizations use`.");
|
|
798
|
+
}
|
|
799
|
+
return resolved.organization;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/runtime.ts
|
|
805
|
+
function handle(fn) {
|
|
806
|
+
return async (...cbArgs) => {
|
|
807
|
+
const command = cbArgs[cbArgs.length - 1];
|
|
808
|
+
const ctx = buildContext(command.optsWithGlobals());
|
|
809
|
+
setOutputMode({ json: ctx.json });
|
|
810
|
+
try {
|
|
811
|
+
await fn(ctx, command.processedArgs, command.opts());
|
|
812
|
+
} catch (error) {
|
|
813
|
+
process.exitCode = reportError(error);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/util.ts
|
|
819
|
+
import { execFile } from "child_process";
|
|
820
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
821
|
+
import { platform as platform2 } from "os";
|
|
822
|
+
function parseData(input) {
|
|
823
|
+
if (!input) return {};
|
|
824
|
+
const raw = input.startsWith("@") ? readFileSync2(input.slice(1), "utf8") : input;
|
|
825
|
+
let parsed;
|
|
826
|
+
try {
|
|
827
|
+
parsed = JSON.parse(raw);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
throw new UsageError(`Invalid JSON for --data: ${error instanceof Error ? error.message : String(error)}`);
|
|
830
|
+
}
|
|
831
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
832
|
+
throw new UsageError("--data must be a JSON object.");
|
|
833
|
+
}
|
|
834
|
+
return parsed;
|
|
835
|
+
}
|
|
836
|
+
function applySets(target, sets) {
|
|
837
|
+
if (!sets) return target;
|
|
838
|
+
for (const assignment of sets) {
|
|
839
|
+
const eq = assignment.indexOf("=");
|
|
840
|
+
if (eq === -1) throw new UsageError(`Invalid --set "${assignment}". Expected path=value.`);
|
|
841
|
+
const path = assignment.slice(0, eq);
|
|
842
|
+
const rawValue = assignment.slice(eq + 1);
|
|
843
|
+
let value;
|
|
844
|
+
try {
|
|
845
|
+
value = JSON.parse(rawValue);
|
|
846
|
+
} catch {
|
|
847
|
+
value = rawValue;
|
|
848
|
+
}
|
|
849
|
+
setPath(target, path.split("."), value);
|
|
850
|
+
}
|
|
851
|
+
return target;
|
|
852
|
+
}
|
|
853
|
+
function setPath(obj, parts, value) {
|
|
854
|
+
let cursor = obj;
|
|
855
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
856
|
+
const key = parts[i];
|
|
857
|
+
if (typeof cursor[key] !== "object" || cursor[key] === null) cursor[key] = {};
|
|
858
|
+
cursor = cursor[key];
|
|
859
|
+
}
|
|
860
|
+
cursor[parts[parts.length - 1]] = value;
|
|
861
|
+
}
|
|
862
|
+
function openBrowser(url) {
|
|
863
|
+
const cmd = platform2() === "darwin" ? "open" : platform2() === "win32" ? "cmd" : "xdg-open";
|
|
864
|
+
const args = platform2() === "win32" ? ["/c", "start", "", url] : [url];
|
|
865
|
+
execFile(cmd, args, () => void 0);
|
|
866
|
+
}
|
|
867
|
+
function parseEntryRef(ref) {
|
|
868
|
+
if (ref.startsWith("ent_") && !ref.includes("/")) return { slugOrId: ref };
|
|
869
|
+
const slash = ref.indexOf("/");
|
|
870
|
+
if (slash === -1) return { slugOrId: ref };
|
|
871
|
+
return { collection: ref.slice(0, slash), slugOrId: ref.slice(slash + 1) };
|
|
872
|
+
}
|
|
873
|
+
async function resolveEntryId(mgmt, project, ref) {
|
|
874
|
+
const parsed = parseEntryRef(ref);
|
|
875
|
+
if (parsed.slugOrId.startsWith("ent_")) return parsed.slugOrId;
|
|
876
|
+
if (!parsed.collection) {
|
|
877
|
+
throw new UsageError(`Provide the entry as <collection>/<slug> or an ent_ id (got "${ref}").`);
|
|
878
|
+
}
|
|
879
|
+
let cursor;
|
|
880
|
+
for (let guard = 0; guard < 50; guard++) {
|
|
881
|
+
const page = await mgmt.entries.list(project, { collection: parsed.collection, limit: 100, cursor });
|
|
882
|
+
const match = page.data.find((e) => e.slug === parsed.slugOrId || e.id === parsed.slugOrId);
|
|
883
|
+
if (match) return match.id;
|
|
884
|
+
if (!page.nextCursor) break;
|
|
885
|
+
cursor = page.nextCursor;
|
|
886
|
+
}
|
|
887
|
+
throw new CliError(`Entry not found: ${ref}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/commands/auth.ts
|
|
891
|
+
async function apiPost(apiUrl, path, body) {
|
|
892
|
+
const res = await fetch(`${apiUrl}/v1${path}`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
895
|
+
body: JSON.stringify(body)
|
|
896
|
+
});
|
|
897
|
+
const json = await res.json().catch(() => ({}));
|
|
898
|
+
if (!res.ok) throw new CliError(json.detail ?? `Request failed (${res.status}).`);
|
|
899
|
+
return json.data;
|
|
900
|
+
}
|
|
901
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
902
|
+
function registerAuth(program) {
|
|
903
|
+
program.command("login").description("Authenticate via the browser device-authorization flow").action(
|
|
904
|
+
handle(async (ctx) => {
|
|
905
|
+
const start = await apiPost(ctx.apiUrl, "/auth/device", {});
|
|
906
|
+
diag(`
|
|
907
|
+
To authenticate, open:
|
|
908
|
+
${start.verificationUri}
|
|
909
|
+
and enter the code: ${start.userCode}
|
|
910
|
+
`);
|
|
911
|
+
if (ctx.interactive) openBrowser(start.verificationUri);
|
|
912
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
913
|
+
let token;
|
|
914
|
+
while (Date.now() < deadline) {
|
|
915
|
+
await sleep2(Math.max(1, start.interval) * 1e3);
|
|
916
|
+
const poll = await apiPost(ctx.apiUrl, `/auth/device/${start.deviceCode}/token`, {
|
|
917
|
+
deviceCode: start.deviceCode
|
|
918
|
+
});
|
|
919
|
+
if (poll.status === "approved" && poll.token) {
|
|
920
|
+
token = poll.token;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
if (poll.status === "denied" || poll.status === "expired") {
|
|
924
|
+
throw new CliError(`Authorization ${poll.status}.`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (!token) throw new CliError("Device authorization timed out.");
|
|
928
|
+
const location = storeToken(ctx.apiUrl, token);
|
|
929
|
+
diag(` Stored credential (${location}).`);
|
|
930
|
+
emit(
|
|
931
|
+
{ ok: true, apiUrl: ctx.apiUrl, storage: location },
|
|
932
|
+
() => process.stdout.write(`Logged in to ${ctx.apiUrl}
|
|
933
|
+
`)
|
|
934
|
+
);
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
program.command("logout").description("Remove the stored credential for the current API URL").action(
|
|
938
|
+
handle(async (ctx) => {
|
|
939
|
+
clearToken(ctx.apiUrl);
|
|
940
|
+
emit({ ok: true }, () => process.stdout.write(`Logged out of ${ctx.apiUrl}
|
|
941
|
+
`));
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
program.command("whoami").description("Show the authenticated identity").action(
|
|
945
|
+
handle(async (ctx) => {
|
|
946
|
+
const token = ctx.token ?? loadToken(ctx.apiUrl);
|
|
947
|
+
if (!token) throw new CliError("Not authenticated. Run `myna login`.");
|
|
948
|
+
const account = await fetch(`${ctx.apiUrl}/v1/account`, {
|
|
949
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
950
|
+
}).then(async (r) => r.ok ? (await r.json()).data.user : void 0).catch(() => void 0);
|
|
951
|
+
let organization;
|
|
952
|
+
if (ctx.organization) {
|
|
953
|
+
organization = await ctx.management().organizations.get(ctx.organization).then((o) => ({ name: o.name, role: o.role, slug: o.slug })).catch((e) => {
|
|
954
|
+
if (isMynaApiError(e)) return void 0;
|
|
955
|
+
throw e;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
emit({ apiUrl: ctx.apiUrl, authenticated: true, account: account ?? null, organization: organization ?? null }, () => {
|
|
959
|
+
const pairs = [["API", ctx.apiUrl], ["Authenticated", "yes"]];
|
|
960
|
+
if (account && typeof account === "object" && "username" in account) {
|
|
961
|
+
pairs.push(["User", String(account.username)]);
|
|
962
|
+
} else {
|
|
963
|
+
pairs.push(["Credential", "API token"]);
|
|
964
|
+
}
|
|
965
|
+
if (organization) pairs.push(["Organization", `${organization.name} (${organization.role})`]);
|
|
966
|
+
keyValues(pairs);
|
|
967
|
+
});
|
|
968
|
+
})
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/commands/workspace.ts
|
|
973
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
974
|
+
import { join as join2 } from "path";
|
|
975
|
+
var EXAMPLE_SCHEMA = `import { collection, field } from "@myna-sh/schema";
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Example collection. Edit freely, then run:
|
|
979
|
+
* myna schema diff
|
|
980
|
+
* myna schema push
|
|
981
|
+
*/
|
|
982
|
+
export const posts = collection({
|
|
983
|
+
name: "posts",
|
|
984
|
+
label: "Blog posts",
|
|
985
|
+
visibility: "public",
|
|
986
|
+
titleField: "title",
|
|
987
|
+
path: "/blog/{slug}",
|
|
988
|
+
fields: {
|
|
989
|
+
title: field.text({ required: true, maxLength: 120 }),
|
|
990
|
+
slug: field.slug({ from: "title", required: true }),
|
|
991
|
+
description: field.text({ multiline: true }),
|
|
992
|
+
body: field.markdown({ required: true }),
|
|
993
|
+
cover: field.asset({ allowed: ["image/*"] }),
|
|
994
|
+
publishedAt: field.datetime(),
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
`;
|
|
998
|
+
function appUrlFor(apiUrl) {
|
|
999
|
+
try {
|
|
1000
|
+
const url = new URL(apiUrl);
|
|
1001
|
+
if (url.hostname.startsWith("api.")) {
|
|
1002
|
+
url.hostname = "app." + url.hostname.slice(4);
|
|
1003
|
+
return url.origin;
|
|
1004
|
+
}
|
|
1005
|
+
return apiUrl;
|
|
1006
|
+
} catch {
|
|
1007
|
+
return apiUrl;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function registerWorkspace(program) {
|
|
1011
|
+
program.command("init").description("Scaffold a local `myna/` schema directory (and link if a project is set)").option("--schema-dir <dir>", "schema directory", "myna").action(
|
|
1012
|
+
handle(async (ctx, _args, opts) => {
|
|
1013
|
+
const dir = opts.schemaDir ?? "myna";
|
|
1014
|
+
mkdirSync2(dir, { recursive: true });
|
|
1015
|
+
const file = join2(dir, "posts.ts");
|
|
1016
|
+
const created = [];
|
|
1017
|
+
if (!existsSync2(file)) {
|
|
1018
|
+
writeFileSync2(file, EXAMPLE_SCHEMA);
|
|
1019
|
+
created.push(file);
|
|
1020
|
+
}
|
|
1021
|
+
let linked = false;
|
|
1022
|
+
if (ctx.project) {
|
|
1023
|
+
writeLinkedProject(process.cwd(), {
|
|
1024
|
+
project: ctx.project,
|
|
1025
|
+
organization: ctx.organization,
|
|
1026
|
+
...ctx.apiUrl !== DEFAULT_API_URL2 ? { apiUrl: ctx.apiUrl } : {}
|
|
1027
|
+
});
|
|
1028
|
+
created.push(".myna/project.json");
|
|
1029
|
+
linked = true;
|
|
1030
|
+
}
|
|
1031
|
+
emit({ schemaDir: dir, created, linked }, () => {
|
|
1032
|
+
diag(`Initialized ${dir}/`);
|
|
1033
|
+
for (const c of created) process.stdout.write(` created ${c}
|
|
1034
|
+
`);
|
|
1035
|
+
if (!linked) process.stdout.write(` run \`myna link --project <ref>\` to link a project
|
|
1036
|
+
`);
|
|
1037
|
+
});
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
1040
|
+
program.command("link").description("Link the current directory to a project (writes .myna/project.json, no secrets)").argument("[project]", "project id or slug").action(
|
|
1041
|
+
handle(async (ctx, args) => {
|
|
1042
|
+
const project = args[0] ?? ctx.project;
|
|
1043
|
+
if (!project) throw new UsageError("Provide a project: `myna link <project>` or --project.");
|
|
1044
|
+
const resolved = await ctx.management().projects.get(project);
|
|
1045
|
+
writeLinkedProject(process.cwd(), {
|
|
1046
|
+
project,
|
|
1047
|
+
organization: ctx.organization ?? resolved.organizationId,
|
|
1048
|
+
...ctx.apiUrl !== DEFAULT_API_URL2 ? { apiUrl: ctx.apiUrl } : {}
|
|
1049
|
+
});
|
|
1050
|
+
emit(
|
|
1051
|
+
{ linked: true, project: resolved.slug, organizationId: resolved.organizationId },
|
|
1052
|
+
() => process.stdout.write(`Linked to ${resolved.name} (${resolved.slug})
|
|
1053
|
+
`)
|
|
1054
|
+
);
|
|
1055
|
+
})
|
|
1056
|
+
);
|
|
1057
|
+
program.command("unlink").description("Remove the project link from the current directory tree").action(
|
|
1058
|
+
handle(async () => {
|
|
1059
|
+
const found = findLinkedProject();
|
|
1060
|
+
if (!found) throw new CliError("No linked project found.");
|
|
1061
|
+
removeLinkedProject(found.root);
|
|
1062
|
+
emit({ unlinked: true }, () => process.stdout.write(`Unlinked ${found.linked.project}
|
|
1063
|
+
`));
|
|
1064
|
+
})
|
|
1065
|
+
);
|
|
1066
|
+
program.command("status").description("Show the resolved configuration and project link").action(
|
|
1067
|
+
handle(async (ctx) => {
|
|
1068
|
+
const hasToken = Boolean(ctx.token ?? loadToken(ctx.apiUrl));
|
|
1069
|
+
emit(
|
|
1070
|
+
{
|
|
1071
|
+
apiUrl: ctx.apiUrl,
|
|
1072
|
+
authenticated: hasToken,
|
|
1073
|
+
project: ctx.project ?? null,
|
|
1074
|
+
organization: ctx.organization ?? null,
|
|
1075
|
+
linkedRoot: ctx.linkedRoot ?? null
|
|
1076
|
+
},
|
|
1077
|
+
() => keyValues([
|
|
1078
|
+
["API URL", ctx.apiUrl],
|
|
1079
|
+
["Authenticated", hasToken ? "yes" : "no"],
|
|
1080
|
+
["Project", ctx.project ?? "(none)"],
|
|
1081
|
+
["Organization", ctx.organization ?? "(none)"],
|
|
1082
|
+
["Linked at", ctx.linkedRoot ?? "(none)"]
|
|
1083
|
+
])
|
|
1084
|
+
);
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
program.command("open").description("Open the current project in the dashboard").action(
|
|
1088
|
+
handle(async (ctx) => {
|
|
1089
|
+
const app = appUrlFor(ctx.apiUrl);
|
|
1090
|
+
const path = ctx.organization && ctx.project ? `/${ctx.organization}/${ctx.project}` : "";
|
|
1091
|
+
const url = `${app}${path}`;
|
|
1092
|
+
if (ctx.interactive) openBrowser(url);
|
|
1093
|
+
emit({ url }, () => process.stdout.write(`${url}
|
|
1094
|
+
`));
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/commands/schema.ts
|
|
1100
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1101
|
+
import { join as join4 } from "path";
|
|
1102
|
+
|
|
1103
|
+
// src/schema-loader.ts
|
|
1104
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
1105
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1106
|
+
import { build } from "esbuild";
|
|
1107
|
+
|
|
1108
|
+
// ../schema/dist/canonical.js
|
|
1109
|
+
function canonicalStringify(value) {
|
|
1110
|
+
return JSON.stringify(sortValue(value));
|
|
1111
|
+
}
|
|
1112
|
+
function sortValue(value) {
|
|
1113
|
+
if (Array.isArray(value))
|
|
1114
|
+
return value.map(sortValue);
|
|
1115
|
+
if (value && typeof value === "object") {
|
|
1116
|
+
const out = {};
|
|
1117
|
+
for (const key of Object.keys(value).sort()) {
|
|
1118
|
+
const v = value[key];
|
|
1119
|
+
if (v === void 0)
|
|
1120
|
+
continue;
|
|
1121
|
+
out[key] = sortValue(v);
|
|
1122
|
+
}
|
|
1123
|
+
return out;
|
|
1124
|
+
}
|
|
1125
|
+
return value;
|
|
1126
|
+
}
|
|
1127
|
+
function toCanonical(schema) {
|
|
1128
|
+
return JSON.parse(canonicalStringify(schema));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/schema-loader.ts
|
|
1132
|
+
var DEFAULT_SCHEMA_DIR = "myna";
|
|
1133
|
+
function looksLikeCollection(value) {
|
|
1134
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && Array.isArray(value.fields);
|
|
1135
|
+
}
|
|
1136
|
+
function collectFromModule(mod, out) {
|
|
1137
|
+
const consider = (value) => {
|
|
1138
|
+
if (Array.isArray(value)) {
|
|
1139
|
+
for (const v of value) consider(v);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (looksLikeCollection(value)) out.set(value.name, value);
|
|
1143
|
+
};
|
|
1144
|
+
for (const value of Object.values(mod)) consider(value);
|
|
1145
|
+
}
|
|
1146
|
+
function listSchemaFiles(dir) {
|
|
1147
|
+
const files = [];
|
|
1148
|
+
for (const entry of readdirSync(dir)) {
|
|
1149
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") continue;
|
|
1150
|
+
const full = join3(dir, entry);
|
|
1151
|
+
const st = statSync(full);
|
|
1152
|
+
if (st.isDirectory()) {
|
|
1153
|
+
files.push(...listSchemaFiles(full));
|
|
1154
|
+
} else if (/\.(ts|mts|js|mjs)$/.test(entry) && !/\.d\.ts$/.test(entry)) {
|
|
1155
|
+
files.push(full);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return files.sort();
|
|
1159
|
+
}
|
|
1160
|
+
async function loadLocalSchemas(schemaDir = DEFAULT_SCHEMA_DIR) {
|
|
1161
|
+
const dir = resolve2(schemaDir);
|
|
1162
|
+
if (!existsSync3(dir)) {
|
|
1163
|
+
throw new CliError(`Schema directory not found: ${schemaDir}. Run \`myna init\` to scaffold it.`);
|
|
1164
|
+
}
|
|
1165
|
+
const files = listSchemaFiles(dir);
|
|
1166
|
+
if (files.length === 0) {
|
|
1167
|
+
throw new CliError(`No schema files found in ${schemaDir}.`);
|
|
1168
|
+
}
|
|
1169
|
+
const collections = /* @__PURE__ */ new Map();
|
|
1170
|
+
for (const file of files) {
|
|
1171
|
+
const result = await build({
|
|
1172
|
+
entryPoints: [file],
|
|
1173
|
+
bundle: true,
|
|
1174
|
+
write: false,
|
|
1175
|
+
format: "esm",
|
|
1176
|
+
platform: "node",
|
|
1177
|
+
target: "node20",
|
|
1178
|
+
logLevel: "silent",
|
|
1179
|
+
absWorkingDir: process.cwd()
|
|
1180
|
+
}).catch((error) => {
|
|
1181
|
+
throw new CliError(`Failed to compile ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1182
|
+
});
|
|
1183
|
+
const code = result.outputFiles[0]?.text ?? "";
|
|
1184
|
+
const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
1185
|
+
const mod = await import(dataUrl);
|
|
1186
|
+
collectFromModule(mod, collections);
|
|
1187
|
+
}
|
|
1188
|
+
if (collections.size === 0) {
|
|
1189
|
+
throw new CliError("No collections were exported from the schema directory.");
|
|
1190
|
+
}
|
|
1191
|
+
return [...collections.values()].map((c) => toCanonical(c)).sort((a, b) => a.name.localeCompare(b.name));
|
|
1192
|
+
}
|
|
1193
|
+
function canonicalJson(schemas) {
|
|
1194
|
+
return schemas.map((s) => s);
|
|
1195
|
+
}
|
|
1196
|
+
function schemaDirFor(root, override) {
|
|
1197
|
+
if (override) return override;
|
|
1198
|
+
if (root) {
|
|
1199
|
+
const candidate = join3(root, DEFAULT_SCHEMA_DIR);
|
|
1200
|
+
if (existsSync3(candidate)) return candidate;
|
|
1201
|
+
}
|
|
1202
|
+
return DEFAULT_SCHEMA_DIR;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/dsl-emit.ts
|
|
1206
|
+
function schemaToDsl(schema) {
|
|
1207
|
+
const opts = [
|
|
1208
|
+
`name: ${str(schema.name)}`,
|
|
1209
|
+
`label: ${str(schema.label)}`
|
|
1210
|
+
];
|
|
1211
|
+
if (schema.kind !== "collection") opts.push(`kind: ${str(schema.kind)}`);
|
|
1212
|
+
opts.push(`visibility: ${str(schema.visibility)}`);
|
|
1213
|
+
if (schema.titleField) opts.push(`titleField: ${str(schema.titleField)}`);
|
|
1214
|
+
if (schema.path) opts.push(`path: ${str(schema.path)}`);
|
|
1215
|
+
const fieldLines = schema.fields.map((f) => ` ${ident(f.key)}: ${emitField(f)},`).join("\n");
|
|
1216
|
+
return [
|
|
1217
|
+
`import { collection, field, item } from "@myna-sh/schema";`,
|
|
1218
|
+
``,
|
|
1219
|
+
`export const ${varName(schema.name)} = collection({`,
|
|
1220
|
+
` ${opts.join(",\n ")},`,
|
|
1221
|
+
` fields: {`,
|
|
1222
|
+
fieldLines,
|
|
1223
|
+
` },`,
|
|
1224
|
+
`});`,
|
|
1225
|
+
``
|
|
1226
|
+
].join("\n");
|
|
1227
|
+
}
|
|
1228
|
+
function emitField(field) {
|
|
1229
|
+
const o = {};
|
|
1230
|
+
if (field.label && field.label !== field.key) o.label = field.label;
|
|
1231
|
+
if (field.description) o.description = field.description;
|
|
1232
|
+
if (field.required) o.required = true;
|
|
1233
|
+
switch (field.type) {
|
|
1234
|
+
case "text":
|
|
1235
|
+
assign(o, { multiline: field.multiline || void 0, minLength: field.minLength, maxLength: field.maxLength, pattern: field.pattern, enum: field.enum, default: field.default });
|
|
1236
|
+
return call("text", o);
|
|
1237
|
+
case "slug":
|
|
1238
|
+
assign(o, { from: field.from });
|
|
1239
|
+
return call("slug", o);
|
|
1240
|
+
case "markdown":
|
|
1241
|
+
assign(o, { minLength: field.minLength, maxLength: field.maxLength, default: field.default });
|
|
1242
|
+
return call("markdown", o);
|
|
1243
|
+
case "number":
|
|
1244
|
+
assign(o, { integer: field.integer || void 0, min: field.min, max: field.max, default: field.default });
|
|
1245
|
+
return call("number", o);
|
|
1246
|
+
case "boolean":
|
|
1247
|
+
assign(o, { default: field.default });
|
|
1248
|
+
return call("boolean", o);
|
|
1249
|
+
case "date":
|
|
1250
|
+
case "datetime":
|
|
1251
|
+
assign(o, { min: field.min, max: field.max, default: field.default });
|
|
1252
|
+
return call(field.type, o);
|
|
1253
|
+
case "json":
|
|
1254
|
+
assign(o, { default: field.default });
|
|
1255
|
+
return call("json", o);
|
|
1256
|
+
case "asset":
|
|
1257
|
+
assign(o, { allowed: field.allowed, multiple: field.multiple || void 0 });
|
|
1258
|
+
return call("asset", o);
|
|
1259
|
+
case "reference":
|
|
1260
|
+
assign(o, { to: field.target, multiple: field.multiple || void 0 });
|
|
1261
|
+
return call("reference", o);
|
|
1262
|
+
case "object": {
|
|
1263
|
+
const inner = field.fields.map((f) => ` ${ident(f.key)}: ${emitField(f)}`).join(",\n");
|
|
1264
|
+
o.fields = `__RAW__{
|
|
1265
|
+
${inner},
|
|
1266
|
+
}`;
|
|
1267
|
+
return call("object", o);
|
|
1268
|
+
}
|
|
1269
|
+
case "list":
|
|
1270
|
+
o.of = `__RAW__${emitItem(field.item)}`;
|
|
1271
|
+
assign(o, { minItems: field.minItems, maxItems: field.maxItems });
|
|
1272
|
+
return call("list", o);
|
|
1273
|
+
default:
|
|
1274
|
+
return `field.json({})`;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function emitItem(item) {
|
|
1278
|
+
switch (item.kind) {
|
|
1279
|
+
case "reference":
|
|
1280
|
+
return `item.reference({ to: ${str(item.target)} })`;
|
|
1281
|
+
case "asset":
|
|
1282
|
+
return `item.asset({ allowed: ${JSON.stringify(item.allowed)} })`;
|
|
1283
|
+
case "object": {
|
|
1284
|
+
const inner = item.fields.map((f) => ` ${ident(f.key)}: ${emitField(f)}`).join(",\n");
|
|
1285
|
+
return `item.object({ fields: {
|
|
1286
|
+
${inner},
|
|
1287
|
+
} })`;
|
|
1288
|
+
}
|
|
1289
|
+
default:
|
|
1290
|
+
return `item.${item.kind}()`;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function call(name, opts) {
|
|
1294
|
+
const entries = Object.entries(opts);
|
|
1295
|
+
if (entries.length === 0) return `field.${name}()`;
|
|
1296
|
+
const body = entries.map(([k, v]) => `${ident(k)}: ${literal(v)}`).join(", ");
|
|
1297
|
+
return `field.${name}({ ${body} })`;
|
|
1298
|
+
}
|
|
1299
|
+
function assign(target, values) {
|
|
1300
|
+
for (const [k, v] of Object.entries(values)) {
|
|
1301
|
+
if (v !== void 0) target[k] = v;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function literal(value) {
|
|
1305
|
+
if (typeof value === "string" && value.startsWith("__RAW__")) return value.slice("__RAW__".length);
|
|
1306
|
+
return JSON.stringify(value);
|
|
1307
|
+
}
|
|
1308
|
+
function str(value) {
|
|
1309
|
+
return JSON.stringify(value);
|
|
1310
|
+
}
|
|
1311
|
+
function ident(key) {
|
|
1312
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
1313
|
+
}
|
|
1314
|
+
function varName(name) {
|
|
1315
|
+
const camel = name.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase());
|
|
1316
|
+
return /^[A-Za-z_$]/.test(camel) ? camel : `_${camel}`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/commands/schema.ts
|
|
1320
|
+
async function deployedSchemas(ctx, project) {
|
|
1321
|
+
const mgmt = ctx.management();
|
|
1322
|
+
const collections = await mgmt.schema.collections(project);
|
|
1323
|
+
const out = [];
|
|
1324
|
+
for (const c of collections) {
|
|
1325
|
+
const versions = await mgmt.schema.versions(project, c.key);
|
|
1326
|
+
const latest = versions[0];
|
|
1327
|
+
if (latest) out.push(latest.schemaJson);
|
|
1328
|
+
}
|
|
1329
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1330
|
+
}
|
|
1331
|
+
function registerSchema(program) {
|
|
1332
|
+
const schema = program.command("schema").description("Inspect and deploy code-owned schemas");
|
|
1333
|
+
schema.command("inspect").description("Show deployed collections, or one collection's fields").argument("[collection]", "collection key").action(
|
|
1334
|
+
handle(async (ctx, args) => {
|
|
1335
|
+
const project = ctx.requireProject();
|
|
1336
|
+
const key = args[0];
|
|
1337
|
+
if (!key) {
|
|
1338
|
+
const collections = await ctx.management().schema.collections(project);
|
|
1339
|
+
emit(
|
|
1340
|
+
collections,
|
|
1341
|
+
() => table(collections, [
|
|
1342
|
+
{ header: "KEY", value: (c) => c.key },
|
|
1343
|
+
{ header: "NAME", value: (c) => c.displayName },
|
|
1344
|
+
{ header: "KIND", value: (c) => c.kind },
|
|
1345
|
+
{ header: "VISIBILITY", value: (c) => c.visibility }
|
|
1346
|
+
])
|
|
1347
|
+
);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const versions = await ctx.management().schema.versions(project, key);
|
|
1351
|
+
const latest = versions[0];
|
|
1352
|
+
if (!latest) throw new CliError(`Collection ${key} has no versions.`);
|
|
1353
|
+
const s = latest.schemaJson;
|
|
1354
|
+
emit({ collection: key, version: latest.version, schema: s }, () => {
|
|
1355
|
+
diag(`${key} \u2014 v${latest.version} (${s.kind}, ${s.visibility})`);
|
|
1356
|
+
table(s.fields, [
|
|
1357
|
+
{ header: "FIELD", value: (f) => f.key },
|
|
1358
|
+
{ header: "TYPE", value: (f) => f.type },
|
|
1359
|
+
{ header: "REQUIRED", value: (f) => f.required ? "yes" : "" }
|
|
1360
|
+
]);
|
|
1361
|
+
});
|
|
1362
|
+
})
|
|
1363
|
+
);
|
|
1364
|
+
schema.command("pull").description("Write deployed schemas to local DSL files").option("--schema-dir <dir>", "target schema directory").action(
|
|
1365
|
+
handle(async (ctx, _args, opts) => {
|
|
1366
|
+
const project = ctx.requireProject();
|
|
1367
|
+
const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
|
|
1368
|
+
const schemas = await deployedSchemas(ctx, project);
|
|
1369
|
+
mkdirSync3(dir, { recursive: true });
|
|
1370
|
+
const written = [];
|
|
1371
|
+
for (const s of schemas) {
|
|
1372
|
+
const file = join4(dir, `${s.name}.ts`);
|
|
1373
|
+
writeFileSync3(file, schemaToDsl(s));
|
|
1374
|
+
written.push(file);
|
|
1375
|
+
}
|
|
1376
|
+
emit({ pulled: schemas.length, files: written }, () => {
|
|
1377
|
+
for (const f of written) process.stdout.write(` wrote ${f}
|
|
1378
|
+
`);
|
|
1379
|
+
});
|
|
1380
|
+
})
|
|
1381
|
+
);
|
|
1382
|
+
schema.command("diff").description("Diff local schemas against the deployed schemas").option("--schema-dir <dir>", "schema directory").action(
|
|
1383
|
+
handle(async (ctx, _args, opts) => {
|
|
1384
|
+
const project = ctx.requireProject();
|
|
1385
|
+
const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
|
|
1386
|
+
const local = await loadLocalSchemas(dir);
|
|
1387
|
+
const result = await ctx.management().schema.diff(project, canonicalJson(local));
|
|
1388
|
+
emit(result, () => renderDiff(result));
|
|
1389
|
+
})
|
|
1390
|
+
);
|
|
1391
|
+
schema.command("push").description("Apply local schemas, creating immutable versions").option("--schema-dir <dir>", "schema directory").option("--allow-destructive", "permit destructive schema changes", false).option("--summary <text>", "change summary").action(
|
|
1392
|
+
handle(async (ctx, _args, opts) => {
|
|
1393
|
+
const project = ctx.requireProject();
|
|
1394
|
+
const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
|
|
1395
|
+
const local = await loadLocalSchemas(dir);
|
|
1396
|
+
const collections = canonicalJson(local);
|
|
1397
|
+
const allowDestructive = Boolean(opts.allowDestructive);
|
|
1398
|
+
const plan = await ctx.management().schema.diff(project, collections);
|
|
1399
|
+
if (!ctx.json) renderDiff(plan);
|
|
1400
|
+
if (plan.hasDestructive && !allowDestructive) {
|
|
1401
|
+
throw new UsageError(
|
|
1402
|
+
"This change is destructive. Re-run with --allow-destructive to proceed."
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
if (plan.ops.length === 0) {
|
|
1406
|
+
emit({ applied: false, reason: "no-changes", diff: plan }, () => diag("No schema changes to apply."));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const pushed = await ctx.management().schema.push(project, collections, { allowDestructive, changeSummary: opts.summary }).catch((error) => {
|
|
1410
|
+
if (isMynaApiError(error) && error.code === "SCHEMA_CHANGE_DESTRUCTIVE") {
|
|
1411
|
+
throw new UsageError("Destructive schema change rejected. Pass --allow-destructive.");
|
|
1412
|
+
}
|
|
1413
|
+
throw error;
|
|
1414
|
+
});
|
|
1415
|
+
emit(
|
|
1416
|
+
pushed,
|
|
1417
|
+
() => diag(`Applied ${pushed.versions.length} schema version(s) (${pushed.diff.classification}).`)
|
|
1418
|
+
);
|
|
1419
|
+
})
|
|
1420
|
+
);
|
|
1421
|
+
const types = program.command("types").description("Generate content types");
|
|
1422
|
+
types.command("generate").description("Generate deterministic TypeScript types from schemas").option("--schema-dir <dir>", "schema directory").option("--deployed", "use deployed schemas instead of local files", false).option("--out <file>", "write to a file instead of stdout").action(
|
|
1423
|
+
handle(async (ctx, _args, opts) => {
|
|
1424
|
+
let schemas;
|
|
1425
|
+
if (opts.deployed) {
|
|
1426
|
+
schemas = await deployedSchemas(ctx, ctx.requireProject());
|
|
1427
|
+
} else {
|
|
1428
|
+
const dir = schemaDirFor(ctx.linkedRoot, opts.schemaDir);
|
|
1429
|
+
schemas = await loadLocalSchemas(dir);
|
|
1430
|
+
}
|
|
1431
|
+
const code = generateTypesModule(schemas);
|
|
1432
|
+
const out = opts.out;
|
|
1433
|
+
if (out) {
|
|
1434
|
+
writeFileSync3(out, code);
|
|
1435
|
+
emit({ written: out, collections: schemas.length }, () => diag(`Wrote ${out}`));
|
|
1436
|
+
} else if (ctx.json) {
|
|
1437
|
+
emit({ code, collections: schemas.length });
|
|
1438
|
+
} else {
|
|
1439
|
+
process.stdout.write(code);
|
|
1440
|
+
}
|
|
1441
|
+
})
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
function renderDiff(diff) {
|
|
1445
|
+
if (diff.ops.length === 0) {
|
|
1446
|
+
diag("No changes.");
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
for (const op of diff.ops) {
|
|
1450
|
+
const marker = op.classification === "destructive" ? "!" : op.classification === "conditionally_destructive" ? "~" : "+";
|
|
1451
|
+
const target = op.field ? `${op.collection}.${op.field}` : op.collection;
|
|
1452
|
+
process.stdout.write(` ${marker} ${op.kind} ${target} \u2014 ${op.detail}
|
|
1453
|
+
`);
|
|
1454
|
+
}
|
|
1455
|
+
process.stdout.write(` classification: ${diff.classification}
|
|
1456
|
+
`);
|
|
1457
|
+
for (const err of diff.contentValidationErrors) {
|
|
1458
|
+
process.stderr.write(` content error: ${err.collection} ${err.path} \u2014 ${err.message}
|
|
1459
|
+
`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/commands/entries.ts
|
|
1464
|
+
function registerEntries(program) {
|
|
1465
|
+
const entries = program.command("entries").description("Create, read, update, and manage entries");
|
|
1466
|
+
entries.command("list").description("List entries in a collection").argument("<collection>", "collection key").option("--status <status>", "filter by status").option("--limit <n>", "page size", "25").option("--cursor <cursor>", "pagination cursor").action(
|
|
1467
|
+
handle(async (ctx, args, opts) => {
|
|
1468
|
+
const project = ctx.requireProject();
|
|
1469
|
+
const page = await ctx.management().entries.list(project, {
|
|
1470
|
+
collection: args[0],
|
|
1471
|
+
status: opts.status,
|
|
1472
|
+
limit: Number(opts.limit),
|
|
1473
|
+
cursor: opts.cursor
|
|
1474
|
+
});
|
|
1475
|
+
emit(
|
|
1476
|
+
{ data: page.data, nextCursor: page.nextCursor },
|
|
1477
|
+
() => table(page.data, [
|
|
1478
|
+
{ header: "ID", value: (e) => e.id },
|
|
1479
|
+
{ header: "SLUG", value: (e) => e.slug ?? "" },
|
|
1480
|
+
{ header: "STATUS", value: (e) => e.status },
|
|
1481
|
+
{ header: "UPDATED", value: (e) => e.updatedAt }
|
|
1482
|
+
])
|
|
1483
|
+
);
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
entries.command("get").description("Get one entry by <collection>/<slug-or-id>").argument("<ref>", "collection/slug or entry id").action(
|
|
1487
|
+
handle(async (ctx, args) => {
|
|
1488
|
+
const project = ctx.requireProject();
|
|
1489
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1490
|
+
const entry = await ctx.management().entries.get(project, id);
|
|
1491
|
+
emit(entry);
|
|
1492
|
+
})
|
|
1493
|
+
);
|
|
1494
|
+
entries.command("create").description("Create an entry").argument("<collection>", "collection key").option("--data <json-or-@file>", "entry data as JSON or @file").option("--set <path=value...>", "set individual fields", collect, []).option("--slug <slug>", "explicit slug").option("--change-set <id>", "attach to a change set").option("--summary <text>", "change summary").action(
|
|
1495
|
+
handle(async (ctx, args, opts) => {
|
|
1496
|
+
const project = ctx.requireProject();
|
|
1497
|
+
const data = applySets(parseData(opts.data), opts.set);
|
|
1498
|
+
const entry = await ctx.management().entries.create(project, {
|
|
1499
|
+
collection: args[0],
|
|
1500
|
+
data,
|
|
1501
|
+
slug: opts.slug,
|
|
1502
|
+
changeSetId: opts.changeSet,
|
|
1503
|
+
changeSummary: opts.summary
|
|
1504
|
+
});
|
|
1505
|
+
emit(entry, () => diag(`Created ${entry.id} (${entry.status}).`));
|
|
1506
|
+
})
|
|
1507
|
+
);
|
|
1508
|
+
entries.command("update").description("Update an entry").argument("<ref>", "collection/slug or entry id").option("--data <json-or-@file>", "replacement data as JSON or @file").option("--set <path=value...>", "set individual fields", collect, []).option("--slug <slug>", "new slug").option("--expected-revision <id>", "optimistic concurrency guard").option("--change-set <id>", "attach to a change set").option("--summary <text>", "change summary").action(
|
|
1509
|
+
handle(async (ctx, args, opts) => {
|
|
1510
|
+
const project = ctx.requireProject();
|
|
1511
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1512
|
+
const sets = opts.set;
|
|
1513
|
+
let data;
|
|
1514
|
+
if (opts.data !== void 0 || sets.length > 0) {
|
|
1515
|
+
const base = opts.data !== void 0 ? parseData(opts.data) : (await ctx.management().entries.get(project, id)).data;
|
|
1516
|
+
data = applySets(base, sets);
|
|
1517
|
+
}
|
|
1518
|
+
if (!data && !opts.slug) throw new UsageError("Nothing to update. Pass --data, --set, or --slug.");
|
|
1519
|
+
const entry = await ctx.management().entries.update(project, id, {
|
|
1520
|
+
data,
|
|
1521
|
+
slug: opts.slug,
|
|
1522
|
+
expectedRevisionId: opts.expectedRevision,
|
|
1523
|
+
changeSetId: opts.changeSet,
|
|
1524
|
+
changeSummary: opts.summary
|
|
1525
|
+
});
|
|
1526
|
+
emit(entry, () => diag(`Updated ${entry.id} (${entry.status}).`));
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
entries.command("delete").description("Stage an entry deletion on a change set").argument("<ref>", "collection/slug or entry id").option("--confirm-delete", "required consequence flag", false).option("--change-set <id>", "attach to a change set").action(
|
|
1530
|
+
handle(async (ctx, args, opts) => {
|
|
1531
|
+
if (!opts.confirmDelete) throw new UsageError("Deletion requires --confirm-delete.");
|
|
1532
|
+
const project = ctx.requireProject();
|
|
1533
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1534
|
+
const result = await ctx.management().entries.delete(project, id, opts.changeSet);
|
|
1535
|
+
emit(result, () => diag(`Staged delete of ${id} on change set ${result.changeSetId}.`));
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1538
|
+
entries.command("unpublish").description("Stage an entry unpublish on a change set").argument("<ref>", "collection/slug or entry id").option("--change-set <id>", "attach to a change set").action(
|
|
1539
|
+
handle(async (ctx, args, opts) => {
|
|
1540
|
+
const project = ctx.requireProject();
|
|
1541
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1542
|
+
const result = await ctx.management().entries.unpublish(project, id, opts.changeSet);
|
|
1543
|
+
emit(result, () => diag(`Staged unpublish of ${id} on change set ${result.changeSetId}.`));
|
|
1544
|
+
})
|
|
1545
|
+
);
|
|
1546
|
+
entries.command("revisions").description("List an entry's revisions").argument("<ref>", "collection/slug or entry id").action(
|
|
1547
|
+
handle(async (ctx, args) => {
|
|
1548
|
+
const project = ctx.requireProject();
|
|
1549
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1550
|
+
const revisions = await ctx.management().entries.revisions(project, id);
|
|
1551
|
+
emit(
|
|
1552
|
+
revisions,
|
|
1553
|
+
() => table(revisions, [
|
|
1554
|
+
{ header: "ID", value: (r) => r.id },
|
|
1555
|
+
{ header: "#", value: (r) => String(r.revisionNumber) },
|
|
1556
|
+
{ header: "BY", value: (r) => r.createdByType },
|
|
1557
|
+
{ header: "CREATED", value: (r) => r.createdAt }
|
|
1558
|
+
])
|
|
1559
|
+
);
|
|
1560
|
+
})
|
|
1561
|
+
);
|
|
1562
|
+
entries.command("restore").description("Restore a historical revision into a new draft").argument("<ref>", "collection/slug or entry id").requiredOption("--revision <id>", "revision id to restore").action(
|
|
1563
|
+
handle(async (ctx, args, opts) => {
|
|
1564
|
+
const project = ctx.requireProject();
|
|
1565
|
+
const id = await resolveEntryId(ctx.management(), project, args[0]);
|
|
1566
|
+
const result = await ctx.management().entries.restoreRevision(project, id, opts.revision);
|
|
1567
|
+
emit(result, () => diag(`Restored revision ${opts.revision} into ${result.revision.id}.`));
|
|
1568
|
+
})
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
function collect(value, previous) {
|
|
1572
|
+
return [...previous, value];
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/commands/changes.ts
|
|
1576
|
+
function registerChanges(program) {
|
|
1577
|
+
const changes = program.command("changes").description("Manage change sets: validate, preview, publish");
|
|
1578
|
+
changes.command("list").description("List change sets").option("--status <status>", "filter by status").option("--limit <n>", "page size", "25").action(
|
|
1579
|
+
handle(async (ctx, _args, opts) => {
|
|
1580
|
+
const project = ctx.requireProject();
|
|
1581
|
+
const page = await ctx.management().changeSets.list(project, {
|
|
1582
|
+
status: opts.status,
|
|
1583
|
+
limit: Number(opts.limit)
|
|
1584
|
+
});
|
|
1585
|
+
emit(
|
|
1586
|
+
{ data: page.data, nextCursor: page.nextCursor },
|
|
1587
|
+
() => table(page.data, [
|
|
1588
|
+
{ header: "ID", value: (c) => c.id },
|
|
1589
|
+
{ header: "TITLE", value: (c) => c.title },
|
|
1590
|
+
{ header: "STATUS", value: (c) => c.status },
|
|
1591
|
+
{ header: "ITEMS", value: (c) => String(c.items.length) }
|
|
1592
|
+
])
|
|
1593
|
+
);
|
|
1594
|
+
})
|
|
1595
|
+
);
|
|
1596
|
+
changes.command("create").description("Create a change set").requiredOption("--title <title>", "change set title").option("--description <text>", "change set description").action(
|
|
1597
|
+
handle(async (ctx, _args, opts) => {
|
|
1598
|
+
const project = ctx.requireProject();
|
|
1599
|
+
const cs = await ctx.management().changeSets.create(project, {
|
|
1600
|
+
title: opts.title,
|
|
1601
|
+
description: opts.description
|
|
1602
|
+
});
|
|
1603
|
+
emit(cs, () => diag(`Created change set ${cs.id}.`));
|
|
1604
|
+
})
|
|
1605
|
+
);
|
|
1606
|
+
changes.command("inspect").description("Show a change set and its items").argument("<id>", "change set id").action(
|
|
1607
|
+
handle(async (ctx, args) => {
|
|
1608
|
+
const project = ctx.requireProject();
|
|
1609
|
+
const cs = await ctx.management().changeSets.get(project, args[0]);
|
|
1610
|
+
emit(cs, () => {
|
|
1611
|
+
diag(`${cs.title} \u2014 ${cs.status}`);
|
|
1612
|
+
table(cs.items, [
|
|
1613
|
+
{ header: "TYPE", value: (i) => i.resourceType },
|
|
1614
|
+
{ header: "RESOURCE", value: (i) => i.resourceId },
|
|
1615
|
+
{ header: "OP", value: (i) => i.operation }
|
|
1616
|
+
]);
|
|
1617
|
+
});
|
|
1618
|
+
})
|
|
1619
|
+
);
|
|
1620
|
+
changes.command("validate").description("Validate a change set against deployed schemas").argument("<id>", "change set id").action(
|
|
1621
|
+
handle(async (ctx, args) => {
|
|
1622
|
+
const project = ctx.requireProject();
|
|
1623
|
+
const result = await ctx.management().changeSets.validate(project, args[0]);
|
|
1624
|
+
emit(result, () => {
|
|
1625
|
+
if (result.valid) {
|
|
1626
|
+
diag("valid");
|
|
1627
|
+
} else {
|
|
1628
|
+
for (const e of result.errors) {
|
|
1629
|
+
process.stderr.write(` ${e.resourceId} ${e.path} \u2014 ${e.message}
|
|
1630
|
+
`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
if (!result.valid) process.exitCode = 1;
|
|
1635
|
+
})
|
|
1636
|
+
);
|
|
1637
|
+
changes.command("preview").description("Create a preview for a change set").argument("<id>", "change set id").action(
|
|
1638
|
+
handle(async (ctx, args) => {
|
|
1639
|
+
const project = ctx.requireProject();
|
|
1640
|
+
const preview = await ctx.management().previews.create(project, { changeSetId: args[0] });
|
|
1641
|
+
emit(preview, () => {
|
|
1642
|
+
process.stdout.write(`${preview.url}
|
|
1643
|
+
`);
|
|
1644
|
+
diag(`fallback: ${preview.fallbackUrl}`);
|
|
1645
|
+
});
|
|
1646
|
+
})
|
|
1647
|
+
);
|
|
1648
|
+
changes.command("publish").description("Atomically publish a change set").argument("<id>", "change set id").option("--confirm-publish", "required consequence flag", false).action(
|
|
1649
|
+
handle(async (ctx, args, opts) => {
|
|
1650
|
+
if (!opts.confirmPublish) throw new UsageError("Publishing requires --confirm-publish.");
|
|
1651
|
+
const project = ctx.requireProject();
|
|
1652
|
+
const result = await ctx.management().changeSets.publish(project, args[0]);
|
|
1653
|
+
emit(
|
|
1654
|
+
result,
|
|
1655
|
+
() => diag(`Published ${result.publishedEntryIds.length} entr(y/ies): ${result.publishedEntryIds.join(", ")}`)
|
|
1656
|
+
);
|
|
1657
|
+
})
|
|
1658
|
+
);
|
|
1659
|
+
changes.command("close").description("Close a change set without publishing").argument("<id>", "change set id").action(
|
|
1660
|
+
handle(async (ctx, args) => {
|
|
1661
|
+
const project = ctx.requireProject();
|
|
1662
|
+
const cs = await ctx.management().changeSets.close(project, args[0]);
|
|
1663
|
+
emit(cs, () => diag(`Closed ${cs.id}.`));
|
|
1664
|
+
})
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/commands/previews.ts
|
|
1669
|
+
function previewTarget(ref) {
|
|
1670
|
+
if (ref.startsWith("chs_")) return { changeSetId: ref };
|
|
1671
|
+
if (ref.startsWith("ent_")) return { entryId: ref };
|
|
1672
|
+
throw new UsageError(`Preview target must be a change-set (chs_) or entry (ent_) id: ${ref}`);
|
|
1673
|
+
}
|
|
1674
|
+
async function create(ctx, ref) {
|
|
1675
|
+
return ctx.management().previews.create(ctx.requireProject(), previewTarget(ref));
|
|
1676
|
+
}
|
|
1677
|
+
function registerPreviews(program) {
|
|
1678
|
+
const preview = program.command("preview").description("Create and manage preview links");
|
|
1679
|
+
preview.command("create").description("Create a preview for a change set or entry").argument("<change-set-or-entry>", "chs_ or ent_ id").action(
|
|
1680
|
+
handle(async (ctx, args) => {
|
|
1681
|
+
const p = await create(ctx, args[0]);
|
|
1682
|
+
emit(p, () => {
|
|
1683
|
+
process.stdout.write(`${p.url}
|
|
1684
|
+
`);
|
|
1685
|
+
diag(`fallback: ${p.fallbackUrl}`);
|
|
1686
|
+
});
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1689
|
+
preview.command("open").description("Create a preview and open it in the browser").argument("<change-set-or-entry>", "chs_ or ent_ id").action(
|
|
1690
|
+
handle(async (ctx, args) => {
|
|
1691
|
+
const p = await create(ctx, args[0]);
|
|
1692
|
+
if (ctx.interactive) openBrowser(p.url);
|
|
1693
|
+
emit(p, () => process.stdout.write(`${p.url}
|
|
1694
|
+
`));
|
|
1695
|
+
})
|
|
1696
|
+
);
|
|
1697
|
+
preview.command("list").description("List preview tokens for the project").action(
|
|
1698
|
+
handle(async (ctx) => {
|
|
1699
|
+
const previews = await ctx.management().previews.list(ctx.requireProject());
|
|
1700
|
+
emit(
|
|
1701
|
+
previews,
|
|
1702
|
+
() => table(previews, [
|
|
1703
|
+
{ header: "ID", value: (p) => p.id },
|
|
1704
|
+
{ header: "SCOPE", value: (p) => p.changeSetId ?? p.entryId ?? "" },
|
|
1705
|
+
{ header: "EXPIRES", value: (p) => p.expiresAt },
|
|
1706
|
+
{ header: "REVOKED", value: (p) => p.revokedAt ? "yes" : "" }
|
|
1707
|
+
])
|
|
1708
|
+
);
|
|
1709
|
+
})
|
|
1710
|
+
);
|
|
1711
|
+
preview.command("revoke").description("Revoke a preview token").argument("<id>", "preview id (prv_)").action(
|
|
1712
|
+
handle(async (ctx, args) => {
|
|
1713
|
+
await ctx.management().previews.revoke(ctx.requireProject(), args[0]);
|
|
1714
|
+
emit({ ok: true }, () => diag(`Revoked ${args[0]}.`));
|
|
1715
|
+
})
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/commands/assets.ts
|
|
1720
|
+
function registerAssets(program) {
|
|
1721
|
+
const assets = program.command("assets").description("Upload and manage assets");
|
|
1722
|
+
assets.command("upload").description("Upload one or more files via the presigned flow").argument("<path...>", "file path(s)").option("--alt <text>", "default alt text applied to each asset").action(
|
|
1723
|
+
handle(async (ctx, args, opts) => {
|
|
1724
|
+
const project = ctx.requireProject();
|
|
1725
|
+
const paths = args[0];
|
|
1726
|
+
const uploaded = [];
|
|
1727
|
+
for (const path of paths) {
|
|
1728
|
+
diag(`uploading ${path} \u2026`);
|
|
1729
|
+
let asset = await ctx.management().assets.upload(project, path);
|
|
1730
|
+
if (opts.alt) asset = await ctx.management().assets.update(project, asset.id, { defaultAlt: opts.alt });
|
|
1731
|
+
uploaded.push(asset);
|
|
1732
|
+
}
|
|
1733
|
+
emit(
|
|
1734
|
+
uploaded.length === 1 ? uploaded[0] : uploaded,
|
|
1735
|
+
() => table(uploaded, [
|
|
1736
|
+
{ header: "ID", value: (a) => a.id },
|
|
1737
|
+
{ header: "FILENAME", value: (a) => a.displayFilename },
|
|
1738
|
+
{ header: "TYPE", value: (a) => a.contentType },
|
|
1739
|
+
{ header: "SIZE", value: (a) => String(a.byteSize) },
|
|
1740
|
+
{ header: "STATE", value: (a) => a.state }
|
|
1741
|
+
])
|
|
1742
|
+
);
|
|
1743
|
+
})
|
|
1744
|
+
);
|
|
1745
|
+
assets.command("list").description("List assets").option("--limit <n>", "page size", "25").option("--cursor <cursor>", "pagination cursor").action(
|
|
1746
|
+
handle(async (ctx, _args, opts) => {
|
|
1747
|
+
const project = ctx.requireProject();
|
|
1748
|
+
const page = await ctx.management().assets.list(project, {
|
|
1749
|
+
limit: Number(opts.limit),
|
|
1750
|
+
cursor: opts.cursor
|
|
1751
|
+
});
|
|
1752
|
+
emit(
|
|
1753
|
+
{ data: page.data, nextCursor: page.nextCursor },
|
|
1754
|
+
() => table(page.data, [
|
|
1755
|
+
{ header: "ID", value: (a) => a.id },
|
|
1756
|
+
{ header: "FILENAME", value: (a) => a.displayFilename },
|
|
1757
|
+
{ header: "TYPE", value: (a) => a.contentType },
|
|
1758
|
+
{ header: "STATE", value: (a) => a.state }
|
|
1759
|
+
])
|
|
1760
|
+
);
|
|
1761
|
+
})
|
|
1762
|
+
);
|
|
1763
|
+
assets.command("inspect").description("Show an asset and its usage").argument("<id>", "asset id (ast_)").action(
|
|
1764
|
+
handle(async (ctx, args) => {
|
|
1765
|
+
const project = ctx.requireProject();
|
|
1766
|
+
const result = await ctx.management().assets.usage(project, args[0]);
|
|
1767
|
+
emit(result, () => {
|
|
1768
|
+
keyValues([
|
|
1769
|
+
["ID", result.asset.id],
|
|
1770
|
+
["Filename", result.asset.displayFilename],
|
|
1771
|
+
["Type", result.asset.contentType],
|
|
1772
|
+
["Size", String(result.asset.byteSize)],
|
|
1773
|
+
["State", result.asset.state],
|
|
1774
|
+
["URL", result.asset.url],
|
|
1775
|
+
["Referenced by", result.usage.entryIds.join(", ") || "(none)"]
|
|
1776
|
+
]);
|
|
1777
|
+
});
|
|
1778
|
+
})
|
|
1779
|
+
);
|
|
1780
|
+
assets.command("delete").description("Soft-delete an asset").argument("<id>", "asset id (ast_)").option("--confirm-delete", "required consequence flag", false).action(
|
|
1781
|
+
handle(async (ctx, args, opts) => {
|
|
1782
|
+
if (!opts.confirmDelete) throw new UsageError("Deletion requires --confirm-delete.");
|
|
1783
|
+
await ctx.management().assets.delete(ctx.requireProject(), args[0]);
|
|
1784
|
+
emit({ ok: true }, () => diag(`Deleted ${args[0]}.`));
|
|
1785
|
+
})
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/commands/admin.ts
|
|
1790
|
+
function csv(value) {
|
|
1791
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1792
|
+
}
|
|
1793
|
+
function registerAdmin(program) {
|
|
1794
|
+
registerOrganizations(program);
|
|
1795
|
+
registerProjects(program);
|
|
1796
|
+
registerKeys(program);
|
|
1797
|
+
registerMembers(program);
|
|
1798
|
+
registerWebhooks(program);
|
|
1799
|
+
registerActivity(program);
|
|
1800
|
+
registerBilling(program);
|
|
1801
|
+
}
|
|
1802
|
+
function registerOrganizations(program) {
|
|
1803
|
+
const orgs = program.command("organizations").alias("orgs").description("Manage organizations");
|
|
1804
|
+
orgs.command("list").description("List organizations you belong to").action(
|
|
1805
|
+
handle(async (ctx) => {
|
|
1806
|
+
const list = await ctx.management().organizations.list();
|
|
1807
|
+
emit(
|
|
1808
|
+
list,
|
|
1809
|
+
() => table(list, [
|
|
1810
|
+
{ header: "SLUG", value: (o) => o.slug },
|
|
1811
|
+
{ header: "NAME", value: (o) => o.name },
|
|
1812
|
+
{ header: "ROLE", value: (o) => o.role },
|
|
1813
|
+
{ header: "PLAN", value: (o) => o.planKey }
|
|
1814
|
+
])
|
|
1815
|
+
);
|
|
1816
|
+
})
|
|
1817
|
+
);
|
|
1818
|
+
orgs.command("create").description("Create an organization").requiredOption("--name <name>", "organization name").option("--slug <slug>", "organization slug").action(
|
|
1819
|
+
handle(async (ctx, _args, opts) => {
|
|
1820
|
+
const org = await ctx.management().organizations.create({
|
|
1821
|
+
name: opts.name,
|
|
1822
|
+
slug: opts.slug
|
|
1823
|
+
});
|
|
1824
|
+
emit(org, () => diag(`Created ${org.slug} (${org.id}).`));
|
|
1825
|
+
})
|
|
1826
|
+
);
|
|
1827
|
+
orgs.command("use").description("Set the default organization").argument("<slug>", "organization slug or id").action(
|
|
1828
|
+
handle(async (_ctx, args) => {
|
|
1829
|
+
writeUserConfig({ defaultOrganization: args[0] });
|
|
1830
|
+
emit({ defaultOrganization: args[0] }, () => diag(`Default organization set to ${args[0]}.`));
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
function registerProjects(program) {
|
|
1835
|
+
const projects = program.command("projects").description("Manage projects");
|
|
1836
|
+
projects.command("list").description("List projects in the organization").action(
|
|
1837
|
+
handle(async (ctx) => {
|
|
1838
|
+
const list = await ctx.management().projects.list(ctx.requireOrganization());
|
|
1839
|
+
emit(
|
|
1840
|
+
list,
|
|
1841
|
+
() => table(list, [
|
|
1842
|
+
{ header: "SLUG", value: (p) => p.slug },
|
|
1843
|
+
{ header: "NAME", value: (p) => p.name },
|
|
1844
|
+
{ header: "PUBLIC API", value: (p) => p.publicApiEnabled ? "on" : "off" },
|
|
1845
|
+
{ header: "ARCHIVED", value: (p) => p.archivedAt ? "yes" : "" }
|
|
1846
|
+
])
|
|
1847
|
+
);
|
|
1848
|
+
})
|
|
1849
|
+
);
|
|
1850
|
+
projects.command("create").description("Create a project").requiredOption("--name <name>", "project name").option("--slug <slug>", "project slug").option("--timezone <tz>", "project timezone").action(
|
|
1851
|
+
handle(async (ctx, _args, opts) => {
|
|
1852
|
+
const project = await ctx.management().projects.create(ctx.requireOrganization(), {
|
|
1853
|
+
name: opts.name,
|
|
1854
|
+
slug: opts.slug,
|
|
1855
|
+
timezone: opts.timezone
|
|
1856
|
+
});
|
|
1857
|
+
emit(project, () => diag(`Created ${project.slug} (${project.id}).`));
|
|
1858
|
+
})
|
|
1859
|
+
);
|
|
1860
|
+
projects.command("inspect").description("Show project details").argument("[project]", "project id or slug").action(
|
|
1861
|
+
handle(async (ctx, args) => {
|
|
1862
|
+
const ref = args[0] ?? ctx.requireProject();
|
|
1863
|
+
const project = await ctx.management().projects.get(ref);
|
|
1864
|
+
emit(
|
|
1865
|
+
project,
|
|
1866
|
+
() => keyValues([
|
|
1867
|
+
["ID", project.id],
|
|
1868
|
+
["Name", project.name],
|
|
1869
|
+
["Slug", project.slug],
|
|
1870
|
+
["Timezone", project.timezone],
|
|
1871
|
+
["Public API", project.publicApiEnabled ? "on" : "off"],
|
|
1872
|
+
["Origins", project.origins.join(", ") || "(none)"]
|
|
1873
|
+
])
|
|
1874
|
+
);
|
|
1875
|
+
})
|
|
1876
|
+
);
|
|
1877
|
+
projects.command("archive").description("Archive a project").argument("[project]", "project id or slug").action(
|
|
1878
|
+
handle(async (ctx, args) => {
|
|
1879
|
+
const ref = args[0] ?? ctx.requireProject();
|
|
1880
|
+
const project = await ctx.management().projects.archive(ref);
|
|
1881
|
+
emit(project, () => diag(`Archived ${project.slug}.`));
|
|
1882
|
+
})
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
function registerKeys(program) {
|
|
1886
|
+
const keys = program.command("keys").description("Manage API keys");
|
|
1887
|
+
keys.command("list").description("List API keys for the project (or organization with --org-scope)").option("--org-scope", "list organization-scoped keys", false).action(
|
|
1888
|
+
handle(async (ctx, _args, opts) => {
|
|
1889
|
+
const list = opts.orgScope ? await ctx.management().apiKeys.listForOrganization(ctx.requireOrganization()) : await ctx.management().apiKeys.listForProject(ctx.requireProject());
|
|
1890
|
+
emit(
|
|
1891
|
+
list,
|
|
1892
|
+
() => table(list, [
|
|
1893
|
+
{ header: "ID", value: (k) => k.id },
|
|
1894
|
+
{ header: "NAME", value: (k) => k.name },
|
|
1895
|
+
{ header: "SCOPES", value: (k) => k.scopes.join(",") },
|
|
1896
|
+
{ header: "REVOKED", value: (k) => k.revokedAt ? "yes" : "" }
|
|
1897
|
+
])
|
|
1898
|
+
);
|
|
1899
|
+
})
|
|
1900
|
+
);
|
|
1901
|
+
keys.command("create").description("Create an API key (project-scoped by default)").requiredOption("--name <name>", "key name").requiredOption("--scopes <scopes>", "comma-separated scopes").option("--label <label>", "agent/integration label").option("--org-scope", "create an organization-scoped key", false).option("--expires-at <iso>", "expiry (ISO 8601)").action(
|
|
1902
|
+
handle(async (ctx, _args, opts) => {
|
|
1903
|
+
const body = {
|
|
1904
|
+
name: opts.name,
|
|
1905
|
+
scopes: csv(opts.scopes),
|
|
1906
|
+
label: opts.label,
|
|
1907
|
+
expiresAt: opts.expiresAt
|
|
1908
|
+
};
|
|
1909
|
+
const result = opts.orgScope ? await ctx.management().apiKeys.createForOrganization(ctx.requireOrganization(), body) : await ctx.management().apiKeys.createForProject(ctx.requireProject(), body);
|
|
1910
|
+
emit(result, () => {
|
|
1911
|
+
diag("Store this secret now \u2014 it will not be shown again.");
|
|
1912
|
+
keyValues([
|
|
1913
|
+
["ID", result.key.id],
|
|
1914
|
+
["Name", result.key.name],
|
|
1915
|
+
["Secret", result.secret]
|
|
1916
|
+
]);
|
|
1917
|
+
});
|
|
1918
|
+
})
|
|
1919
|
+
);
|
|
1920
|
+
keys.command("revoke").description("Revoke an API key").argument("<id>", "key id (key_)").option("--org-scope", "revoke an organization-scoped key", false).action(
|
|
1921
|
+
handle(async (ctx, args, opts) => {
|
|
1922
|
+
if (opts.orgScope) {
|
|
1923
|
+
await ctx.management().apiKeys.revokeForOrganization(ctx.requireOrganization(), args[0]);
|
|
1924
|
+
} else {
|
|
1925
|
+
await ctx.management().apiKeys.revokeForProject(ctx.requireProject(), args[0]);
|
|
1926
|
+
}
|
|
1927
|
+
emit({ ok: true }, () => diag(`Revoked ${args[0]}.`));
|
|
1928
|
+
})
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
function registerMembers(program) {
|
|
1932
|
+
const members = program.command("members").description("Manage organization members");
|
|
1933
|
+
members.command("list").description("List members and pending invitations").action(
|
|
1934
|
+
handle(async (ctx) => {
|
|
1935
|
+
const org = ctx.requireOrganization();
|
|
1936
|
+
const [list, invites] = await Promise.all([
|
|
1937
|
+
ctx.management().members.list(org),
|
|
1938
|
+
ctx.management().invitations.list(org).catch(() => [])
|
|
1939
|
+
]);
|
|
1940
|
+
emit({ members: list, invitations: invites }, () => {
|
|
1941
|
+
table(list, [
|
|
1942
|
+
{ header: "USER", value: (m) => m.username ?? m.userId },
|
|
1943
|
+
{ header: "EMAIL", value: (m) => m.email },
|
|
1944
|
+
{ header: "ROLE", value: (m) => m.role }
|
|
1945
|
+
]);
|
|
1946
|
+
if (invites.length > 0) {
|
|
1947
|
+
diag("\nPending invitations:");
|
|
1948
|
+
table(invites, [
|
|
1949
|
+
{ header: "EMAIL", value: (i) => i.email },
|
|
1950
|
+
{ header: "ROLE", value: (i) => i.role },
|
|
1951
|
+
{ header: "EXPIRES", value: (i) => i.expiresAt }
|
|
1952
|
+
]);
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
})
|
|
1956
|
+
);
|
|
1957
|
+
members.command("invite").description("Invite a member by email").requiredOption("--email <email>", "invitee email").requiredOption("--role <role>", "role (admin|editor|viewer)").action(
|
|
1958
|
+
handle(async (ctx, _args, opts) => {
|
|
1959
|
+
const invite = await ctx.management().invitations.create(ctx.requireOrganization(), {
|
|
1960
|
+
email: opts.email,
|
|
1961
|
+
role: opts.role
|
|
1962
|
+
});
|
|
1963
|
+
emit(invite, () => diag(`Invited ${invite.email} as ${invite.role}.`));
|
|
1964
|
+
})
|
|
1965
|
+
);
|
|
1966
|
+
members.command("update").description("Update a member's role").argument("<user>", "user id").requiredOption("--role <role>", "new role").action(
|
|
1967
|
+
handle(async (ctx, args, opts) => {
|
|
1968
|
+
await ctx.management().members.update(ctx.requireOrganization(), args[0], {
|
|
1969
|
+
role: opts.role
|
|
1970
|
+
});
|
|
1971
|
+
emit({ ok: true }, () => diag(`Updated ${args[0]} to ${opts.role}.`));
|
|
1972
|
+
})
|
|
1973
|
+
);
|
|
1974
|
+
members.command("remove").description("Remove a member").argument("<user>", "user id").action(
|
|
1975
|
+
handle(async (ctx, args) => {
|
|
1976
|
+
await ctx.management().members.remove(ctx.requireOrganization(), args[0]);
|
|
1977
|
+
emit({ ok: true }, () => diag(`Removed ${args[0]}.`));
|
|
1978
|
+
})
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
function registerWebhooks(program) {
|
|
1982
|
+
const webhooks = program.command("webhooks").description("Manage webhook endpoints");
|
|
1983
|
+
webhooks.command("list").description("List webhook endpoints").action(
|
|
1984
|
+
handle(async (ctx) => {
|
|
1985
|
+
const list = await ctx.management().webhooks.list(ctx.requireProject());
|
|
1986
|
+
emit(
|
|
1987
|
+
list,
|
|
1988
|
+
() => table(list, [
|
|
1989
|
+
{ header: "ID", value: (w) => w.id },
|
|
1990
|
+
{ header: "URL", value: (w) => w.url },
|
|
1991
|
+
{ header: "EVENTS", value: (w) => w.events.join(",") },
|
|
1992
|
+
{ header: "ENABLED", value: (w) => w.enabled ? "yes" : "no" }
|
|
1993
|
+
])
|
|
1994
|
+
);
|
|
1995
|
+
})
|
|
1996
|
+
);
|
|
1997
|
+
webhooks.command("create").description("Create a webhook endpoint").requiredOption("--url <url>", "endpoint URL").requiredOption("--events <events>", "comma-separated event types").action(
|
|
1998
|
+
handle(async (ctx, _args, opts) => {
|
|
1999
|
+
const result = await ctx.management().webhooks.create(ctx.requireProject(), {
|
|
2000
|
+
url: opts.url,
|
|
2001
|
+
events: csv(opts.events),
|
|
2002
|
+
enabled: true
|
|
2003
|
+
});
|
|
2004
|
+
emit(result, () => {
|
|
2005
|
+
diag("Store this signing secret now \u2014 it will not be shown again.");
|
|
2006
|
+
keyValues([
|
|
2007
|
+
["ID", result.endpoint.id],
|
|
2008
|
+
["URL", result.endpoint.url],
|
|
2009
|
+
["Signing secret", result.signingSecret]
|
|
2010
|
+
]);
|
|
2011
|
+
});
|
|
2012
|
+
})
|
|
2013
|
+
);
|
|
2014
|
+
webhooks.command("update").description("Update a webhook endpoint").argument("<id>", "webhook id").option("--url <url>", "new URL").option("--events <events>", "comma-separated event types").option("--enabled <bool>", "enable/disable (true|false)").action(
|
|
2015
|
+
handle(async (ctx, args, opts) => {
|
|
2016
|
+
const patch = {};
|
|
2017
|
+
if (opts.url) patch.url = opts.url;
|
|
2018
|
+
if (opts.events) patch.events = csv(opts.events);
|
|
2019
|
+
if (opts.enabled !== void 0) patch.enabled = opts.enabled === "true";
|
|
2020
|
+
const endpoint = await ctx.management().webhooks.update(ctx.requireProject(), args[0], patch);
|
|
2021
|
+
emit(endpoint, () => diag(`Updated ${endpoint.id}.`));
|
|
2022
|
+
})
|
|
2023
|
+
);
|
|
2024
|
+
webhooks.command("delete").description("Delete a webhook endpoint").argument("<id>", "webhook id").action(
|
|
2025
|
+
handle(async (ctx, args) => {
|
|
2026
|
+
await ctx.management().webhooks.delete(ctx.requireProject(), args[0]);
|
|
2027
|
+
emit({ ok: true }, () => diag(`Deleted ${args[0]}.`));
|
|
2028
|
+
})
|
|
2029
|
+
);
|
|
2030
|
+
webhooks.command("deliveries").description("List recent deliveries for a webhook").argument("<id>", "webhook id").action(
|
|
2031
|
+
handle(async (ctx, args) => {
|
|
2032
|
+
const list = await ctx.management().webhooks.deliveries(ctx.requireProject(), args[0]);
|
|
2033
|
+
emit(
|
|
2034
|
+
list,
|
|
2035
|
+
() => table(list, [
|
|
2036
|
+
{ header: "ID", value: (d) => d.id },
|
|
2037
|
+
{ header: "EVENT", value: (d) => d.eventType },
|
|
2038
|
+
{ header: "ATTEMPT", value: (d) => String(d.attempt) },
|
|
2039
|
+
{ header: "STATUS", value: (d) => d.status },
|
|
2040
|
+
{ header: "HTTP", value: (d) => d.responseStatus ? String(d.responseStatus) : "" }
|
|
2041
|
+
])
|
|
2042
|
+
);
|
|
2043
|
+
})
|
|
2044
|
+
);
|
|
2045
|
+
webhooks.command("retry").description("Retry a webhook delivery").argument("<webhook>", "webhook id").argument("<delivery>", "delivery id").action(
|
|
2046
|
+
handle(async (ctx, args) => {
|
|
2047
|
+
await ctx.management().webhooks.retry(ctx.requireProject(), args[0], args[1]);
|
|
2048
|
+
emit({ ok: true }, () => diag(`Retrying delivery ${args[1]}.`));
|
|
2049
|
+
})
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
function registerActivity(program) {
|
|
2053
|
+
program.command("activity").description("Show the project activity feed").option("--actor-type <type>", "filter by actor type").option("--action <action>", "filter by action").option("--limit <n>", "page size", "25").action(
|
|
2054
|
+
handle(async (ctx, _args, opts) => {
|
|
2055
|
+
const page = await ctx.management().activity(ctx.requireProject(), {
|
|
2056
|
+
actorType: opts.actorType,
|
|
2057
|
+
action: opts.action,
|
|
2058
|
+
limit: Number(opts.limit)
|
|
2059
|
+
});
|
|
2060
|
+
emit(
|
|
2061
|
+
{ data: page.data, nextCursor: page.nextCursor },
|
|
2062
|
+
() => table(page.data, [
|
|
2063
|
+
{ header: "WHEN", value: (a) => a.createdAt },
|
|
2064
|
+
{ header: "ACTOR", value: (a) => a.actorType },
|
|
2065
|
+
{ header: "ACTION", value: (a) => a.action },
|
|
2066
|
+
{ header: "TARGET", value: (a) => `${a.targetType ?? ""} ${a.targetId ?? ""}`.trim() }
|
|
2067
|
+
])
|
|
2068
|
+
);
|
|
2069
|
+
})
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
function registerBilling(program) {
|
|
2073
|
+
const billing = program.command("billing").description("Billing status and management");
|
|
2074
|
+
billing.command("status").description("Show the organization's plan and usage").action(
|
|
2075
|
+
handle(async (ctx) => {
|
|
2076
|
+
const org = ctx.requireOrganization();
|
|
2077
|
+
const [status, usage] = await Promise.all([
|
|
2078
|
+
ctx.management().billing.status(org),
|
|
2079
|
+
ctx.management().organizations.usage(org).catch(() => void 0)
|
|
2080
|
+
]);
|
|
2081
|
+
emit({ billing: status, usage }, () => {
|
|
2082
|
+
keyValues([
|
|
2083
|
+
["Plan", status.subscription.planKey],
|
|
2084
|
+
["Status", status.subscription.status],
|
|
2085
|
+
["Provider customer", status.hasProviderCustomer ? "yes" : "no"]
|
|
2086
|
+
]);
|
|
2087
|
+
if (usage) {
|
|
2088
|
+
diag("\nUsage:");
|
|
2089
|
+
table(usage.metrics, [
|
|
2090
|
+
{ header: "METRIC", value: (m) => m.metric },
|
|
2091
|
+
{ header: "USED", value: (m) => String(m.quantity) },
|
|
2092
|
+
{ header: "LIMIT", value: (m) => m.limit === null ? "\u221E" : String(m.limit) }
|
|
2093
|
+
]);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
})
|
|
2097
|
+
);
|
|
2098
|
+
billing.command("upgrade").description("Start a Pro checkout").option("--interval <interval>", "month|year", "month").action(
|
|
2099
|
+
handle(async (ctx, _args, opts) => {
|
|
2100
|
+
const result = await ctx.management().billing.checkout(ctx.requireOrganization(), {
|
|
2101
|
+
interval: opts.interval ?? "month"
|
|
2102
|
+
});
|
|
2103
|
+
if (ctx.interactive) openBrowser(result.checkoutUrl);
|
|
2104
|
+
emit(result, () => process.stdout.write(`${result.checkoutUrl}
|
|
2105
|
+
`));
|
|
2106
|
+
})
|
|
2107
|
+
);
|
|
2108
|
+
billing.command("portal").description("Open the billing portal").action(
|
|
2109
|
+
handle(async (ctx) => {
|
|
2110
|
+
const result = await ctx.management().billing.portal(ctx.requireOrganization());
|
|
2111
|
+
if (ctx.interactive) openBrowser(result.portalUrl);
|
|
2112
|
+
emit(result, () => process.stdout.write(`${result.portalUrl}
|
|
2113
|
+
`));
|
|
2114
|
+
})
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// src/main.ts
|
|
2119
|
+
var VERSION = "0.1.0";
|
|
2120
|
+
var GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Set(["--project", "--organization", "--token", "--api-url"]);
|
|
2121
|
+
var GLOBAL_BOOL_FLAGS = /* @__PURE__ */ new Set(["--json", "--no-interactive", "--interactive"]);
|
|
2122
|
+
function normalizeGlobals(argv) {
|
|
2123
|
+
const [node, script, ...rest] = argv;
|
|
2124
|
+
const globals = [];
|
|
2125
|
+
const remaining = [];
|
|
2126
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2127
|
+
const arg = rest[i];
|
|
2128
|
+
const eq = arg.indexOf("=");
|
|
2129
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
2130
|
+
if (GLOBAL_BOOL_FLAGS.has(name)) {
|
|
2131
|
+
globals.push(arg);
|
|
2132
|
+
} else if (GLOBAL_VALUE_FLAGS.has(name)) {
|
|
2133
|
+
if (eq === -1) {
|
|
2134
|
+
globals.push(arg);
|
|
2135
|
+
if (i + 1 < rest.length) globals.push(rest[++i]);
|
|
2136
|
+
} else {
|
|
2137
|
+
globals.push(arg);
|
|
2138
|
+
}
|
|
2139
|
+
} else {
|
|
2140
|
+
remaining.push(arg);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return [node, script, ...globals, ...remaining];
|
|
2144
|
+
}
|
|
2145
|
+
function buildProgram() {
|
|
2146
|
+
const program = new Command();
|
|
2147
|
+
program.name("myna").description("Myna \u2014 content infrastructure for developers and agents").version(VERSION, "-v, --version").option("--json", "emit a single machine-readable JSON value on stdout").option("--project <ref>", "project id or slug").option("--organization <ref>", "organization id or slug").option("--token <token>", "API token (overrides stored credentials)").option("--api-url <url>", "API base URL").option("--no-interactive", "disable prompts and browser opening").showHelpAfterError();
|
|
2148
|
+
registerAuth(program);
|
|
2149
|
+
registerWorkspace(program);
|
|
2150
|
+
registerSchema(program);
|
|
2151
|
+
registerEntries(program);
|
|
2152
|
+
registerChanges(program);
|
|
2153
|
+
registerPreviews(program);
|
|
2154
|
+
registerAssets(program);
|
|
2155
|
+
registerAdmin(program);
|
|
2156
|
+
return program;
|
|
2157
|
+
}
|
|
2158
|
+
async function main(argv = process.argv) {
|
|
2159
|
+
const program = buildProgram();
|
|
2160
|
+
program.exitOverride();
|
|
2161
|
+
try {
|
|
2162
|
+
await program.parseAsync(normalizeGlobals(argv));
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
if (error instanceof CommanderError) {
|
|
2165
|
+
if (error.code === "commander.helpDisplayed" || error.code === "commander.version" || error.code === "commander.help") {
|
|
2166
|
+
process.exitCode = 0;
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
process.exitCode = 2;
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
|
|
2173
|
+
`);
|
|
2174
|
+
process.exitCode = 1;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
export {
|
|
2178
|
+
buildProgram,
|
|
2179
|
+
main
|
|
2180
|
+
};
|
|
2181
|
+
//# sourceMappingURL=main.js.map
|