@moloodev/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/local-cli.mjs +8 -9
- package/bin/moloo.mjs +1 -1
- package/package.json +6 -6
- package/src/api-result.ts +2 -2
- package/src/cli.ts +38 -14
- package/src/editor-create.ts +31 -19
- package/src/editor-status.ts +5 -12
- package/src/editor-views.ts +5 -11
- package/src/publish.ts +26 -2
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The view server defaults to `http://127.0.0.1:4310` and opens `https://app.moloo.dev/w/<slug>/dev-editor`. Use `moloo editor dev --view-port 4451` to select another port. Click **로컬 뷰 연결** in the app and allow Chrome local network access when prompted. If a connection fails, the app displays the browser error or manifest HTTP status; check the CLI and the app's browser site permissions.
|
|
4
4
|
|
|
5
|
-
`moloo editor publish` always targets a workspace you administer. For `moloo.*` manifests it defaults to workspace `moloo`; `--workspace` overrides that default. Other publishers use the explicit flag, saved workspace or membership selection. Publisher ownership is verified on the server. Builtin means publisher `moloo`, managed by the admin-only `moloo` workspace; these submissions publish immediately after checks pass (422 with finding details on failure). Other publishers are claimed by the submitting workspace on first submission and receive 202 pending review in `https://admin.moloo.dev`. Run `moloo editor status` to inspect revision status, check findings, and rejection reasons.
|
|
5
|
+
`moloo editor publish` always targets a workspace you administer. For `moloo.*` manifests it defaults to workspace `moloo`; `--workspace` overrides that default. Other publishers use the explicit flag, saved workspace or membership selection. Publisher ownership is verified on the server. Builtin means publisher `moloo`, managed by the admin-only `moloo` workspace; these submissions publish immediately after checks pass (422 with finding details on failure). Other publishers are claimed by the submitting workspace on first submission and receive 202 pending review in `https://admin.moloo.dev`. Run `moloo editor status` to inspect revision status, which tracks point at each revision, check findings, and rejection reasons.
|
|
6
|
+
|
|
7
|
+
Editors have two release tracks, `stable` (the default) and `beta`. Each track points at one revision. `moloo editor publish --track beta` releases to beta as soon as checks pass, with no platform review; workspaces that opt into beta in their editor settings get it, and all others stay on stable. `moloo editor promote beta stable` moves stable to the current beta revision: immediately for `moloo.*` editors and for revisions already reviewed, otherwise after platform review. A stable release also moves beta when beta is older, so beta never trails stable.
|
|
6
8
|
|
|
7
9
|
Create a standalone editor with `moloo editor create <publisher>.<name>` (for example, `moloo editor create acme.notes`), or use `moloo editor create acme.notes --here` in an empty directory. The CLI downloads template tag `v1` from `moloodev/editor-template`, then rewrites its package, manifest, and README title. It never overwrites an existing destination. In the generated directory, run `pnpm install`, `moloo login`, `moloo workspace use <slug>`, then `moloo editor dev`. When ready, run `moloo editor build`, `moloo editor publish`, and `moloo editor status`.
|
|
8
10
|
|
package/bin/local-cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
export const LOCAL_CLI_ENV = "MOLOO_LOCAL_CLI";
|
|
@@ -8,17 +8,16 @@ export function findLocalCli(cwd, currentBin, env = process.env) {
|
|
|
8
8
|
if (env[LOCAL_CLI_ENV]) return null;
|
|
9
9
|
for (let directory = resolve(cwd); ; directory = dirname(directory)) {
|
|
10
10
|
const packageRoot = join(directory, "node_modules/@moloodev/cli");
|
|
11
|
-
let pkg;
|
|
12
11
|
try {
|
|
13
|
-
pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
}
|
|
17
|
-
if (pkg) {
|
|
18
|
-
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.moloo;
|
|
19
|
-
if (typeof bin !== "string") throw new Error("Local @moloodev/cli has no moloo bin");
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
13
|
+
const bin = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.moloo;
|
|
14
|
+
if (typeof bin !== "string" || !bin) throw new Error("Local @moloodev/cli has no moloo bin");
|
|
20
15
|
const local = realpathSync(resolve(packageRoot, bin));
|
|
21
16
|
return local === realpathSync(currentBin) ? null : local;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error.code !== "ENOENT" || lstatSync(packageRoot, { throwIfNoEntry: false })) {
|
|
19
|
+
throw new Error(`Broken local @moloodev/cli at ${packageRoot}: ${error.message}. ${LOCAL_CLI_ENV}=1 skips the handoff.`, { cause: error });
|
|
20
|
+
}
|
|
22
21
|
}
|
|
23
22
|
if (dirname(directory) === directory) return null;
|
|
24
23
|
}
|
package/bin/moloo.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const { runCli } = await import("../src/cli.ts");
|
|
|
20
20
|
runCli(process.argv.slice(2)).then(
|
|
21
21
|
(code) => process.exit(code),
|
|
22
22
|
(err) => {
|
|
23
|
-
console.error(err);
|
|
23
|
+
console.error(err instanceof Error && process.env.MOLOO_DEBUG !== "1" ? `Error: ${err.message}` : err);
|
|
24
24
|
process.exit(1);
|
|
25
25
|
},
|
|
26
26
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moloodev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"moloo": "./bin/moloo.mjs"
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"tw-animate-css": "^1.4.0",
|
|
25
25
|
"vite": "^8.2.2",
|
|
26
26
|
"zod": "^4.5.4",
|
|
27
|
-
"@moloodev/editor-sdk": "0.
|
|
28
|
-
"@moloodev/
|
|
29
|
-
"@moloodev/
|
|
30
|
-
"@moloodev/ui": "0.
|
|
27
|
+
"@moloodev/editor-sdk": "0.7.0",
|
|
28
|
+
"@moloodev/platform": "0.7.0",
|
|
29
|
+
"@moloodev/editor-store": "0.7.0",
|
|
30
|
+
"@moloodev/ui": "0.7.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^24.13.3",
|
|
34
34
|
"typescript": "^7.0.2",
|
|
35
|
-
"@moloodev/typescript-config": "0.
|
|
35
|
+
"@moloodev/typescript-config": "0.7.0"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
package/src/api-result.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
const ErrorBodySchema = z.object({
|
|
5
5
|
error: z.string(),
|
|
6
6
|
message: z.string().optional(),
|
|
7
|
-
details: z.array(z.object({ code: z.string().optional(), detail: z.string() })).
|
|
7
|
+
details: z.array(z.object({ code: z.string().optional(), detail: z.string() })).catch([]),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
export function readApiBody(body: string): Record<string, unknown> | undefined {
|
|
@@ -18,6 +18,6 @@ export function apiErrorMessage(status: number, body: string): string {
|
|
|
18
18
|
const result = ErrorBodySchema.safeParse(readApiBody(body)).data;
|
|
19
19
|
const code = result?.error ?? body;
|
|
20
20
|
const message = result?.message ?? publishErrorBody(code).message ?? `Request failed (${status}): ${code}`;
|
|
21
|
-
const details = result?.details
|
|
21
|
+
const details = result?.details.map((finding) => ` ${finding.code ? finding.code + ": " : ""}${finding.detail}`) ?? [];
|
|
22
22
|
return [message, ...details].join("\n");
|
|
23
23
|
}
|
package/src/cli.ts
CHANGED
|
@@ -2,16 +2,17 @@ import { login, logout, whoami } from "./auth";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { runEditorDev } from "./editor-dev";
|
|
4
4
|
import { buildEditorPackage } from "./editor-views";
|
|
5
|
-
import {
|
|
5
|
+
import { isEditorTrack, type EditorTrack } from "@moloodev/platform";
|
|
6
|
+
import { promoteEditorPackage, publishEditorPackage } from "./publish";
|
|
6
7
|
import { printEditorStatus } from "./editor-status";
|
|
7
8
|
import { createEditor, EDITOR_CREATE_USAGE } from "./editor-create";
|
|
8
9
|
import { apiErrorMessage, readApiBody } from "./api-result";
|
|
9
10
|
import { listWorkspaces, useWorkspace, type WorkspaceOptions } from "./workspace";
|
|
10
11
|
|
|
11
12
|
const EDITOR_USAGE =
|
|
12
|
-
`${EDITOR_CREATE_USAGE}\nUsage: moloo editor <dev|build|publish|status> [--dir <path>] [--out-dir <path>] [--view-port <port>] [--workspace <slug>]\nDev view port: 4310 (default)`;
|
|
13
|
+
`${EDITOR_CREATE_USAGE}\nUsage: moloo editor <dev|build|publish|status> [--dir <path>] [--out-dir <path>] [--view-port <port>] [--workspace <slug>]\nUsage: moloo editor publish [--track stable|beta] [--dir <path>] [--workspace <slug>]\nUsage: moloo editor promote beta stable [--dir <path>] [--workspace <slug>]\nDev view port: 4310 (default)`;
|
|
13
14
|
const WORKSPACE_USAGE = "Usage: moloo workspace list | use <slug>";
|
|
14
|
-
const USAGE = `Usage: moloo <command>\n\nCommands:\n login [--device]\n logout\n whoami\n workspace list | use <slug>\n editor create | dev | build | publish | status\n\n${WORKSPACE_USAGE}\n${EDITOR_USAGE}`;
|
|
15
|
+
const USAGE = `Usage: moloo <command>\n\nCommands:\n login [--device]\n logout\n whoami\n workspace list | use <slug>\n editor create | dev | build | publish | promote | status\n\n${WORKSPACE_USAGE}\n${EDITOR_USAGE}`;
|
|
15
16
|
|
|
16
17
|
type Command = (args: readonly string[], options: WorkspaceOptions) => Promise<number>;
|
|
17
18
|
|
|
@@ -52,23 +53,42 @@ const editorCommands: Record<string, Command> = {
|
|
|
52
53
|
return 0;
|
|
53
54
|
},
|
|
54
55
|
async publish(args, options) {
|
|
55
|
-
validateWorkspaceFlags(args);
|
|
56
|
+
validateWorkspaceFlags(args, ["--track"]);
|
|
57
|
+
const track = readTrackFlag(args);
|
|
56
58
|
const result = await publishEditorPackage(packageRootFlag(args), {
|
|
57
|
-
...options, workspace: readFlag(args, "--workspace"),
|
|
59
|
+
...options, workspace: readFlag(args, "--workspace"), track,
|
|
58
60
|
});
|
|
59
61
|
if (result.status === 202) {
|
|
60
62
|
const receipt = readApiBody(result.body)?.data as { id?: string } | undefined;
|
|
61
63
|
if (typeof receipt?.id !== "string") throw new Error("Invalid submission receipt");
|
|
62
|
-
|
|
64
|
+
const next = track === "stable" ? "pending platform review" : "beta after checks pass";
|
|
65
|
+
console.log(`${result.id} submitted — ${next}. Revision: ${receipt.id}`);
|
|
63
66
|
return 0;
|
|
64
67
|
}
|
|
65
68
|
if (result.status !== 200 && result.status !== 201) {
|
|
66
|
-
|
|
67
|
-
return 1;
|
|
69
|
+
throw new Error(apiErrorMessage(result.status, result.body));
|
|
68
70
|
}
|
|
69
71
|
console.log(`${result.id} ${String(result.status)} ${result.body}`);
|
|
70
72
|
return 0;
|
|
71
73
|
},
|
|
74
|
+
async promote(args, options) {
|
|
75
|
+
const [from, to, ...flags] = args;
|
|
76
|
+
if (from !== "beta" || to !== "stable") throw new Error(EDITOR_USAGE);
|
|
77
|
+
validateWorkspaceFlags(flags);
|
|
78
|
+
const result = await promoteEditorPackage(packageRootFlag(flags), {
|
|
79
|
+
...options, workspace: readFlag(flags, "--workspace"),
|
|
80
|
+
});
|
|
81
|
+
if (![200, 201, 202].includes(result.status)) {
|
|
82
|
+
throw new Error(apiErrorMessage(result.status, result.body));
|
|
83
|
+
}
|
|
84
|
+
const data = readApiBody(result.body)?.data as { id?: string; revision?: number } | undefined;
|
|
85
|
+
if (typeof data?.id !== "string" || typeof data.revision !== "number") throw new Error("Invalid promotion receipt");
|
|
86
|
+
const outcome = result.status === 201
|
|
87
|
+
? "promoted to stable"
|
|
88
|
+
: result.status === 200 ? "stable already points at beta" : "submitted for stable — pending platform review";
|
|
89
|
+
console.log(`${result.id} #${String(data.revision)} ${outcome}. Revision: ${data.id}`);
|
|
90
|
+
return 0;
|
|
91
|
+
},
|
|
72
92
|
async status(args, options) {
|
|
73
93
|
validateWorkspaceFlags(args);
|
|
74
94
|
return printEditorStatus(packageRootFlag(args), { ...options, workspace: readFlag(args, "--workspace") });
|
|
@@ -123,8 +143,7 @@ const commands: Record<string, {
|
|
|
123
143
|
? editorCommands[name]
|
|
124
144
|
: undefined;
|
|
125
145
|
if (!command) {
|
|
126
|
-
|
|
127
|
-
return 1;
|
|
146
|
+
throw new Error(EDITOR_USAGE);
|
|
128
147
|
}
|
|
129
148
|
return command(args, options);
|
|
130
149
|
},
|
|
@@ -141,8 +160,7 @@ export async function runCli(argv: readonly string[], options: WorkspaceOptions
|
|
|
141
160
|
return 0;
|
|
142
161
|
}
|
|
143
162
|
if (!command) {
|
|
144
|
-
|
|
145
|
-
return 1;
|
|
163
|
+
throw new Error(USAGE);
|
|
146
164
|
}
|
|
147
165
|
return command.run(args, options);
|
|
148
166
|
}
|
|
@@ -151,9 +169,9 @@ function packageRootFlag(args: readonly string[]): string {
|
|
|
151
169
|
return resolve(readFlag(args, "--dir") ?? process.cwd());
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
function validateWorkspaceFlags(args: readonly string[]): void {
|
|
172
|
+
function validateWorkspaceFlags(args: readonly string[], extra: readonly string[] = []): void {
|
|
155
173
|
for (let index = 0; index < args.length; index += 2) {
|
|
156
|
-
if (!["--dir", "--workspace"].includes(args[index] ?? "") ||
|
|
174
|
+
if (!["--dir", "--workspace", ...extra].includes(args[index] ?? "") ||
|
|
157
175
|
!args[index + 1] || args[index + 1]?.startsWith("--")) throw new Error(EDITOR_USAGE);
|
|
158
176
|
}
|
|
159
177
|
}
|
|
@@ -164,6 +182,12 @@ function readFlag(args: readonly string[], flag: string): string | undefined {
|
|
|
164
182
|
return args[index + 1];
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
function readTrackFlag(args: readonly string[]): EditorTrack {
|
|
186
|
+
const raw = readFlag(args, "--track") ?? "stable";
|
|
187
|
+
if (!isEditorTrack(raw)) throw new Error(`Invalid --track: use stable or beta`);
|
|
188
|
+
return raw;
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
function readPortFlag(args: readonly string[], flag: string): number | undefined {
|
|
168
192
|
const raw = readFlag(args, flag);
|
|
169
193
|
if (raw === undefined) return undefined;
|
package/src/editor-create.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { extract, list } from "tar";
|
|
6
|
-
import { parseQualifiedName } from "@moloodev/platform";
|
|
7
6
|
import { parseEditorId } from "@moloodev/editor-sdk/protocol";
|
|
8
7
|
|
|
9
8
|
export const EDITOR_TEMPLATE_TAG = "v1";
|
|
@@ -18,12 +17,11 @@ export async function createEditor(
|
|
|
18
17
|
fetch?: typeof fetch;
|
|
19
18
|
} = {},
|
|
20
19
|
): Promise<string> {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
throw new Error("Invalid editor id: use publisher.name with a valid publisher handle and lowercase editor DNS label (1–63 characters).
|
|
20
|
+
const editor = parseEditorId(id);
|
|
21
|
+
if (!editor) {
|
|
22
|
+
throw new Error("Invalid editor id: use publisher.name with a valid publisher handle and lowercase editor DNS label (1–63 characters).");
|
|
24
23
|
}
|
|
25
|
-
const
|
|
26
|
-
const destination = here ? resolve(cwd) : resolve(cwd, `${name}-editor`);
|
|
24
|
+
const destination = here ? resolve(cwd) : resolve(cwd, `${editor.editor}-editor`);
|
|
27
25
|
async function checkDestination() {
|
|
28
26
|
if (here) {
|
|
29
27
|
if ((await readdir(destination)).length) throw new Error("--here requires an empty current directory (including hidden files).");
|
|
@@ -61,16 +59,16 @@ export async function createEditor(
|
|
|
61
59
|
const packageFile = join(source, "package.json");
|
|
62
60
|
const pkg = JSON.parse(await readFile(packageFile, "utf8"));
|
|
63
61
|
if (pkg.name !== "@moloodev/example-editor") throw new Error("Invalid editor template package placeholder");
|
|
64
|
-
pkg.name = `@${publisher}/${
|
|
62
|
+
pkg.name = `@${editor.publisher}/${editor.editor}-editor`;
|
|
65
63
|
await writeFile(packageFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
66
64
|
const manifestFile = join(source, "src/manifest.ts");
|
|
67
65
|
let manifest = await readFile(manifestFile, "utf8");
|
|
68
66
|
for (const [token, value] of [
|
|
69
67
|
["example.editor", id],
|
|
70
|
-
["/editor/example", `/editor/${
|
|
71
|
-
[".example.json", `.${
|
|
72
|
-
["https://example.com/schemas/example/v1.json", `https://example.com/schemas/${
|
|
73
|
-
["Example editor", `${
|
|
68
|
+
["/editor/example", `/editor/${editor.editor}`],
|
|
69
|
+
[".example.json", `.${editor.editor}.json`],
|
|
70
|
+
["https://example.com/schemas/example/v1.json", `https://example.com/schemas/${editor.editor}/v1.json`],
|
|
71
|
+
["Example editor", `${editor.editor} editor`],
|
|
74
72
|
] as const) {
|
|
75
73
|
if (!manifest.includes(token)) throw new Error(`Missing editor template placeholder: ${token}`);
|
|
76
74
|
manifest = manifest.replaceAll(token, value);
|
|
@@ -79,22 +77,36 @@ export async function createEditor(
|
|
|
79
77
|
const readmeFile = join(source, "README.md");
|
|
80
78
|
const readme = await readFile(readmeFile, "utf8");
|
|
81
79
|
if (!readme.startsWith("# Example editor\n")) throw new Error("Missing editor template README title");
|
|
82
|
-
await writeFile(readmeFile, readme.replace("# Example editor\n", `# ${
|
|
80
|
+
await writeFile(readmeFile, readme.replace("# Example editor\n", `# ${editor.editor} editor\n`));
|
|
83
81
|
|
|
84
82
|
// Recheck after download; mkdir and exclusive copies also protect against races.
|
|
85
83
|
await checkDestination();
|
|
86
84
|
if (!here) await mkdir(destination);
|
|
85
|
+
const created: string[] = [];
|
|
87
86
|
try {
|
|
88
|
-
for (const entry of await readdir(source)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
88
|
+
const target = join(destination, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
// Claim the directory before copying so partial copies can be removed.
|
|
91
|
+
await mkdir(target);
|
|
92
|
+
created.push(target);
|
|
93
|
+
for (const child of await readdir(join(source, entry.name))) {
|
|
94
|
+
await cp(join(source, entry.name, child), join(target, child), {
|
|
95
|
+
recursive: true, force: false, errorOnExist: true, mode: constants.COPYFILE_EXCL,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
await copyFile(join(source, entry.name), target, constants.COPYFILE_EXCL);
|
|
100
|
+
created.push(target);
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
} catch (error) {
|
|
94
|
-
if (
|
|
104
|
+
if (here) {
|
|
105
|
+
for (const target of created) await rm(target, { recursive: true, force: true });
|
|
106
|
+
} else await rm(destination, { recursive: true, force: true });
|
|
95
107
|
throw error;
|
|
96
108
|
}
|
|
97
|
-
console.log(`Created ${id} in ${destination}\n\nNext steps:\n${here ? "" : ` cd ${
|
|
109
|
+
console.log(`Created ${id} in ${destination}\n\nNext steps:\n${here ? "" : ` cd ${editor.editor}-editor\n`} pnpm install\n pnpm exec moloo login\n pnpm dev\n\nPublish: pnpm publish:editor --workspace <slug>`);
|
|
98
110
|
return destination;
|
|
99
111
|
} finally {
|
|
100
112
|
await rm(staging, { recursive: true, force: true });
|
package/src/editor-status.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import { getApiOrigin } from "@moloodev/platform";
|
|
2
|
-
import { z } from "zod";
|
|
1
|
+
import { editorStatusResponseSchema, getApiOrigin } from "@moloodev/platform";
|
|
3
2
|
import { publishToken } from "./auth";
|
|
4
3
|
import { apiErrorMessage, readApiBody } from "./api-result";
|
|
5
4
|
import { readPackageEditorView } from "./editor-views";
|
|
6
5
|
import { resolveEditorTarget } from "./publish";
|
|
7
6
|
import type { WorkspaceOptions } from "./workspace";
|
|
8
7
|
|
|
9
|
-
const StatusBodySchema = z.object({ data: z.array(z.object({
|
|
10
|
-
id: z.string(), revision: z.number().int(), status: z.string(),
|
|
11
|
-
submittedAt: z.string(), reviewedAt: z.string().nullable(), rejectionReason: z.string().nullable(),
|
|
12
|
-
checkFindings: z.array(z.object({ code: z.string(), detail: z.string() })),
|
|
13
|
-
})) });
|
|
14
|
-
|
|
15
8
|
export async function printEditorStatus(packageRoot: string, options: WorkspaceOptions & { workspace?: string }): Promise<number> {
|
|
16
9
|
const { id } = readPackageEditorView(packageRoot);
|
|
17
10
|
const target = await resolveEditorTarget(id, options);
|
|
@@ -22,16 +15,16 @@ export async function printEditorStatus(packageRoot: string, options: WorkspaceO
|
|
|
22
15
|
});
|
|
23
16
|
const body = await response.text();
|
|
24
17
|
if (response.status !== 200) {
|
|
25
|
-
|
|
26
|
-
return 1;
|
|
18
|
+
throw new Error(apiErrorMessage(response.status, body));
|
|
27
19
|
}
|
|
28
|
-
const parsed =
|
|
20
|
+
const parsed = editorStatusResponseSchema.safeParse(readApiBody(body));
|
|
29
21
|
if (!parsed.success) throw new Error("Invalid editor status response");
|
|
30
22
|
const { data } = parsed.data;
|
|
31
23
|
console.log(`${id} — latest ${data.length} revisions (${target.workspaceSlug})`);
|
|
32
24
|
if (!data.length) console.log("No revisions submitted.");
|
|
33
25
|
for (const row of data) {
|
|
34
|
-
|
|
26
|
+
const tracks = row.tracks.length ? ` [${row.tracks.join(", ")}]` : "";
|
|
27
|
+
console.log(`#${row.revision} ${row.status}${tracks} (${row.id})\n Submitted: ${row.submittedAt}\n Reviewed: ${row.reviewedAt ?? "pending"}`);
|
|
35
28
|
if (row.rejectionReason) console.log(` Rejected: ${row.rejectionReason}`);
|
|
36
29
|
for (const finding of row.checkFindings) console.log(` ${finding.code}: ${finding.detail}`);
|
|
37
30
|
}
|
package/src/editor-views.ts
CHANGED
|
@@ -6,9 +6,8 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import esbuild from "esbuild";
|
|
7
7
|
import { build, type Rollup } from "vite";
|
|
8
8
|
import tailwind from "@tailwindcss/vite";
|
|
9
|
-
import { parseEditorId } from "@moloodev/editor-sdk/protocol";
|
|
10
9
|
import { validateEditorManifest } from "@moloodev/editor-sdk/commands/editor-document";
|
|
11
|
-
import {
|
|
10
|
+
import { parsePublishName, PUBLISH_ERROR_MESSAGES } from "@moloodev/platform";
|
|
12
11
|
import { parseNetwork } from "@moloodev/platform/network";
|
|
13
12
|
import { publishBundlePlugin } from "./publish-bundle";
|
|
14
13
|
import { readEditorManifest } from "./read-editor-manifest";
|
|
@@ -25,19 +24,14 @@ export function readPackageEditorView(packageRoot: string): EditorView & { manif
|
|
|
25
24
|
throw new Error(`Missing editor manifest at ${manifestPath}`);
|
|
26
25
|
}
|
|
27
26
|
const manifest = readEditorManifest(manifestPath);
|
|
28
|
-
if (typeof manifest.id === "string" && manifest.id.includes(".")) {
|
|
29
|
-
const publisher = manifest.id.split(".")[0]!;
|
|
30
|
-
if (!isPublisherHandle(publisher)) throw new Error(PUBLISH_ERROR_MESSAGES.invalid_publisher_handle);
|
|
31
|
-
if (publisher !== "moloo" && isReservedPublisherHandle(publisher)) throw new Error(PUBLISH_ERROR_MESSAGES.publisher_reserved);
|
|
32
|
-
}
|
|
33
|
-
validateEditorManifest(manifest);
|
|
34
27
|
if (typeof manifest.id !== "string") {
|
|
35
28
|
throw new Error(`Invalid editor manifest id in ${manifestPath}`);
|
|
36
29
|
}
|
|
37
|
-
const parsed =
|
|
38
|
-
if (parsed
|
|
39
|
-
throw new Error(
|
|
30
|
+
const parsed = parsePublishName(manifest.id);
|
|
31
|
+
if (!parsed.ok) {
|
|
32
|
+
throw new Error(PUBLISH_ERROR_MESSAGES[parsed.error]);
|
|
40
33
|
}
|
|
34
|
+
validateEditorManifest(manifest);
|
|
41
35
|
const network = parseNetwork("network" in manifest ? manifest.network : undefined);
|
|
42
36
|
return { id: manifest.id, packageRoot, network, manifest };
|
|
43
37
|
}
|
package/src/publish.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { publishToken } from "./auth";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { putScriptUpload } from "@moloodev/editor-store/upload";
|
|
4
|
-
import { getApiOrigin, parseQualifiedName, type PublishTarget } from "@moloodev/platform";
|
|
4
|
+
import { getApiOrigin, parseQualifiedName, type EditorTrack, type PublishTarget } from "@moloodev/platform";
|
|
5
5
|
import { buildEditorPackage, readPackageEditorView } from "./editor-views";
|
|
6
6
|
import { resolveWorkspace, type WorkspaceOptions } from "./workspace";
|
|
7
7
|
|
|
@@ -9,6 +9,10 @@ export function publishEndpoint(): string {
|
|
|
9
9
|
return `${getApiOrigin("production")}/v1/revisions/publish`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function promoteEndpoint(): string {
|
|
13
|
+
return `${getApiOrigin("production")}/v1/revisions/promote`;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export function uploadEndpoint(): string {
|
|
13
17
|
return `${getApiOrigin("production")}/v1/revisions/uploads`;
|
|
14
18
|
}
|
|
@@ -18,6 +22,7 @@ export async function publishEditor(input: {
|
|
|
18
22
|
script: string;
|
|
19
23
|
token: string;
|
|
20
24
|
target: PublishTarget;
|
|
25
|
+
track: EditorTrack;
|
|
21
26
|
}): Promise<{ status: number; body: string }> {
|
|
22
27
|
const uploadUrl = uploadEndpoint();
|
|
23
28
|
const publishUrl = publishEndpoint();
|
|
@@ -48,6 +53,7 @@ export async function publishEditor(input: {
|
|
|
48
53
|
},
|
|
49
54
|
body: JSON.stringify({
|
|
50
55
|
target: input.target,
|
|
56
|
+
track: input.track,
|
|
51
57
|
manifest: input.manifest,
|
|
52
58
|
upload: {
|
|
53
59
|
pathname: upload.pathname,
|
|
@@ -60,7 +66,7 @@ export async function publishEditor(input: {
|
|
|
60
66
|
|
|
61
67
|
export async function publishEditorPackage(
|
|
62
68
|
packageRoot: string,
|
|
63
|
-
options: Omit<WorkspaceOptions, "apiOrigin"> & { workspace?: string } = {},
|
|
69
|
+
options: Omit<WorkspaceOptions, "apiOrigin"> & { workspace?: string; track?: EditorTrack } = {},
|
|
64
70
|
): Promise<{ id: string; status: number; body: string }> {
|
|
65
71
|
const view = readPackageEditorView(packageRoot);
|
|
66
72
|
const target = await resolveEditorTarget(view.id, options);
|
|
@@ -71,10 +77,28 @@ export async function publishEditorPackage(
|
|
|
71
77
|
script: built.script,
|
|
72
78
|
token,
|
|
73
79
|
target,
|
|
80
|
+
track: options.track ?? "stable",
|
|
74
81
|
});
|
|
75
82
|
return { id: built.id, ...result };
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
/** Move the editor's `stable` track to its current `beta` revision. */
|
|
86
|
+
export async function promoteEditorPackage(
|
|
87
|
+
packageRoot: string,
|
|
88
|
+
options: Omit<WorkspaceOptions, "apiOrigin"> & { workspace?: string } = {},
|
|
89
|
+
): Promise<{ id: string; status: number; body: string }> {
|
|
90
|
+
const { id } = readPackageEditorView(packageRoot);
|
|
91
|
+
const target = await resolveEditorTarget(id, options);
|
|
92
|
+
const token = await publishToken(options.store);
|
|
93
|
+
const response = await fetch(promoteEndpoint(), {
|
|
94
|
+
method: "POST",
|
|
95
|
+
redirect: "error",
|
|
96
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ target, editor: id, from: "beta", to: "stable" }),
|
|
98
|
+
});
|
|
99
|
+
return { id, status: response.status, body: await response.text() };
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
export async function resolveEditorTarget(
|
|
79
103
|
id: string,
|
|
80
104
|
options: WorkspaceOptions & { workspace?: string },
|