@moloodev/cli 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moloodev/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "moloo": "./bin/moloo.mjs"
@@ -22,15 +22,15 @@
22
22
  "tw-animate-css": "^1.4.0",
23
23
  "tsx": "^4.23.13",
24
24
  "vite": "^8.2.2",
25
- "@moloodev/editor-sdk": "0.4.0",
26
- "@moloodev/platform": "0.4.0",
27
- "@moloodev/editor-store": "0.4.0",
28
- "@moloodev/ui": "0.4.0"
25
+ "@moloodev/editor-sdk": "0.5.0",
26
+ "@moloodev/editor-store": "0.5.0",
27
+ "@moloodev/platform": "0.5.0",
28
+ "@moloodev/ui": "0.5.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^24.13.3",
32
32
  "typescript": "^7.0.2",
33
- "@moloodev/typescript-config": "0.4.0"
33
+ "@moloodev/typescript-config": "0.5.0"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
package/src/auth.ts CHANGED
@@ -72,7 +72,7 @@ export async function login(
72
72
 
73
73
  export async function loginToken(store = credentialStore(ISSUER), issuer = ISSUER) {
74
74
  const tokens = await store.read();
75
- if (!tokens) throw new Error("Not logged in. Run moloo login or set MOLOO_SERVICE_TOKEN for CI.");
75
+ if (!tokens) throw new Error("Not logged in. Run moloo login.");
76
76
  if (tokens.expiresAt > Date.now() + 60_000) return tokens.accessToken;
77
77
  const updated = await readTokens(
78
78
  await oauthPost(issuer, "/oauth2/token", {
@@ -100,22 +100,23 @@ export async function logout(store: CredentialStore = credentialStore(ISSUER), i
100
100
  const tokens = await store.read();
101
101
  if (tokens) await revokeTokens(issuer, tokens);
102
102
  await store.delete();
103
- console.log(tokens ? "Logged out." : "Local credentials cleared; no parseable token, server revocation skipped.");
103
+ console.log(tokens ? "Logged out." : "No valid credentials; local entry cleared.");
104
104
  }
105
105
 
106
- export async function publishCredentials(
107
- serviceToken: string | undefined,
106
+ async function authorizePublish(issuer: string, scope: string): Promise<Tokens> {
107
+ if (process.env.CI || !process.stdin.isTTY || !process.stdout.isTTY) {
108
+ throw new Error("Publish scope required. Run moloo editor publish interactively to authorize editor:publish.");
109
+ }
110
+ return authorizeLogin(issuer, scope);
111
+ }
112
+
113
+ export async function publishToken(
108
114
  store = credentialStore(ISSUER),
109
115
  issuer = ISSUER,
110
- authorize = authorizeLogin,
111
- ) {
112
- if (serviceToken !== undefined) {
113
- const token = serviceToken.trim();
114
- if (!token) throw new Error("MOLOO_SERVICE_TOKEN is empty");
115
- return { token, bearer: false };
116
- }
116
+ authorize = authorizePublish,
117
+ ): Promise<string> {
117
118
  const previous = await store.read();
118
- if (!previous) throw new Error("Not logged in. Run moloo login or set MOLOO_SERVICE_TOKEN for CI.");
119
+ if (!previous) throw new Error("Not logged in. Run moloo login.");
119
120
  if (!previous.scopes.includes("editor:publish")) {
120
121
  const scopes = [...new Set([...previous.scopes, "editor:publish"])];
121
122
  const tokens = await authorize(issuer, scopes.join(" "));
@@ -124,9 +125,9 @@ export async function publishCredentials(
124
125
  throw new Error("OAuth authorization did not grant the requested publish scopes.");
125
126
  }
126
127
  await replaceTokens(store, issuer, previous, tokens);
127
- return { token: tokens.accessToken, bearer: true };
128
+ return tokens.accessToken;
128
129
  }
129
- return { token: await loginToken(store, issuer), bearer: true };
130
+ return loginToken(store, issuer);
130
131
  }
131
132
 
132
133
  export async function cliIdentity(
package/src/cli.ts CHANGED
@@ -6,7 +6,7 @@ import { publishEditorPackage } from "./publish";
6
6
  import { listWorkspaces, useWorkspace, type WorkspaceOptions } from "./workspace";
7
7
 
8
8
  const EDITOR_USAGE =
9
- "Usage: moloo editor <dev|build|publish> [--dir <path>] [--out-dir <path>] [--view-port <port>] [--workspace <slug>] [--api-url <https-origin>]\nDev view port: 4310 (default)";
9
+ "Usage: moloo editor <dev|build|publish> [--dir <path>] [--out-dir <path>] [--view-port <port>] [--workspace <slug>]\nDev view port: 4310 (default)";
10
10
  const WORKSPACE_USAGE = "Usage: moloo workspace list | use <slug>";
11
11
  const USAGE = `Usage: moloo <command>\n\nCommands:\n login [--device]\n logout\n whoami\n workspace list | use <slug>\n editor dev | build | publish\n\n${WORKSPACE_USAGE}\n${EDITOR_USAGE}`;
12
12
 
@@ -40,10 +40,21 @@ const editorCommands: Record<string, Command> = {
40
40
  );
41
41
  return 0;
42
42
  },
43
- async publish(args) {
43
+ async publish(args, options) {
44
+ for (let index = 0; index < args.length; index += 2) {
45
+ if (!["--dir", "--workspace"].includes(args[index] ?? "") ||
46
+ !args[index + 1] || args[index + 1]?.startsWith("--")) {
47
+ throw new Error(EDITOR_USAGE);
48
+ }
49
+ }
44
50
  const result = await publishEditorPackage(packageRootFlag(args), {
45
- apiUrl: readFlag(args, "--api-url"),
51
+ ...options, workspace: readFlag(args, "--workspace"),
46
52
  });
53
+ if (result.status === 202) {
54
+ const receipt = JSON.parse(result.body) as { data: { id: string } };
55
+ console.log(`${result.id} 제출됨·심사 필요 ${receipt.data.id}`);
56
+ return 0;
57
+ }
47
58
  console.log(`${result.id} ${String(result.status)} ${result.body}`);
48
59
  return result.status === 200 || result.status === 201 ? 0 : 1;
49
60
  },
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ export {
16
16
  publishEditor,
17
17
  publishEditorPackage,
18
18
  publishEndpoint,
19
- reservedEditorId,
20
19
  uploadEndpoint,
21
20
  } from "./publish";
22
21
  export { readEditorManifest } from "./read-editor-manifest";
package/src/publish.ts CHANGED
@@ -1,34 +1,31 @@
1
- import { publishCredentials } from "./auth";
1
+ import { publishToken } from "./auth";
2
2
  import { createHash } from "node:crypto";
3
3
  import { putScriptUpload } from "@moloodev/editor-store/upload";
4
- import { getApiOrigin } from "@moloodev/platform";
5
- import { builtinEditors } from "@moloodev/platform/builtin-editors";
6
- import { buildEditorPackage } from "./editor-views";
4
+ import { getApiOrigin, parseQualifiedName, type PublishTarget } from "@moloodev/platform";
5
+ import { buildEditorPackage, readPackageEditorView } from "./editor-views";
6
+ import { resolveWorkspace, type WorkspaceOptions } from "./workspace";
7
7
 
8
- const DEFAULT_SERVICE = "editor-origin-build";
9
-
10
- export function publishEndpoint(apiUrl?: string): string {
11
- return `${apiBase(apiUrl)}/v1/revisions/publish`;
8
+ export function publishEndpoint(): string {
9
+ return `${getApiOrigin("production")}/v1/revisions/publish`;
12
10
  }
13
11
 
14
- export function uploadEndpoint(apiUrl?: string): string {
15
- return `${apiBase(apiUrl)}/v1/revisions/uploads`;
12
+ export function uploadEndpoint(): string {
13
+ return `${getApiOrigin("production")}/v1/revisions/uploads`;
16
14
  }
17
15
 
18
16
  export async function publishEditor(input: {
19
17
  manifest: Record<string, unknown>;
20
18
  script: string;
21
19
  token: string;
22
- apiUrl?: string;
23
- service?: string;
24
- bearer?: boolean;
20
+ target: PublishTarget;
25
21
  }): Promise<{ status: number; body: string }> {
26
- const uploadUrl = uploadEndpoint(input.apiUrl);
27
- const publishUrl = publishEndpoint(input.apiUrl);
28
- const service = input.service ?? DEFAULT_SERVICE;
22
+ const uploadUrl = uploadEndpoint();
23
+ const publishUrl = publishEndpoint();
29
24
  const issued = await fetch(uploadUrl, {
30
25
  method: "POST",
31
- headers: input.bearer ? { authorization: `Bearer ${input.token}` } : serviceHeaders(input.token, service),
26
+ redirect: "error",
27
+ headers: { authorization: `Bearer ${input.token}`, "content-type": "application/json" },
28
+ body: JSON.stringify({ target: input.target, manifestId: input.manifest.id }),
32
29
  });
33
30
  const issuedBody = await issued.text();
34
31
  if (issued.status !== 201) return { status: issued.status, body: issuedBody };
@@ -44,13 +41,14 @@ export async function publishEditor(input: {
44
41
 
45
42
  const response = await fetch(publishUrl, {
46
43
  method: "POST",
44
+ redirect: "error",
47
45
  headers: {
48
- ...(input.bearer ? { authorization: `Bearer ${input.token}` } : serviceHeaders(input.token, service)),
46
+ authorization: `Bearer ${input.token}`,
49
47
  "content-type": "application/json",
50
48
  },
51
49
  body: JSON.stringify({
50
+ target: input.target,
52
51
  manifest: input.manifest,
53
- editorId: reservedEditorId(input.manifest),
54
52
  upload: {
55
53
  pathname: upload.pathname,
56
54
  sha256: createHash("sha256").update(bytes).digest("hex"),
@@ -62,27 +60,31 @@ export async function publishEditor(input: {
62
60
 
63
61
  export async function publishEditorPackage(
64
62
  packageRoot: string,
65
- options: { token?: string; apiUrl?: string; service?: string } = {},
63
+ options: Omit<WorkspaceOptions, "apiOrigin"> & { workspace?: string } = {},
66
64
  ): Promise<{ id: string; status: number; body: string }> {
67
- const credentials = await publishCredentials(options.token ?? process.env.MOLOO_SERVICE_TOKEN);
65
+ const view = readPackageEditorView(packageRoot);
66
+ const name = parseQualifiedName(view.id);
67
+ if (!name) throw new Error("Invalid editor manifest id");
68
+ let target: PublishTarget;
69
+ if (name.publisher === "moloo") {
70
+ if (options.workspace !== undefined) throw new Error("Built-in editors do not accept --workspace.");
71
+ target = { kind: "builtin" };
72
+ } else {
73
+ target = { kind: "workspace", workspaceSlug: await resolveWorkspace({
74
+ flag: options.workspace, store: options.store, configPath: options.configPath,
75
+ }) };
76
+ }
77
+ const token = await publishToken(options.store);
68
78
  const built = await buildEditorPackage(packageRoot);
69
79
  const result = await publishEditor({
70
80
  manifest: built.manifest,
71
81
  script: built.script,
72
- ...credentials,
73
- apiUrl: options.apiUrl,
74
- service: options.service,
82
+ token,
83
+ target,
75
84
  });
76
85
  return { id: built.id, ...result };
77
86
  }
78
87
 
79
- function serviceHeaders(token: string, service: string): Record<string, string> {
80
- return {
81
- "x-moloo-service-token": token,
82
- "x-moloo-service": service,
83
- };
84
- }
85
-
86
88
  function readUpload(text: string): { pathname: string; token: string } | undefined {
87
89
  let value: unknown;
88
90
  try {
@@ -99,27 +101,6 @@ function readUpload(text: string): { pathname: string; token: string } | undefin
99
101
  return { pathname, token };
100
102
  }
101
103
 
102
- function apiBase(apiUrl: string | undefined): string {
103
- const raw = apiUrl?.trim() ?? "";
104
- const base = raw.length === 0 ? getApiOrigin("production") : raw;
105
- let url: URL;
106
- try {
107
- url = new URL(base);
108
- } catch {
109
- throw new Error("publish API origin is invalid");
110
- }
111
- if (url.protocol !== "https:") throw new Error("publish API origin must be https");
112
- return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
113
- }
114
-
115
- export function reservedEditorId(manifest: Record<string, unknown>): string {
116
- const id = manifest.id;
117
- if (typeof id !== "string") throw new Error("builtin manifest id is missing");
118
- const row = builtinEditors.find((item) => `${item.publisherHandle}.${item.slug}` === id);
119
- if (row === undefined) throw new Error(`builtin editor ${id} is missing`);
120
- return row.id;
121
- }
122
-
123
104
  function isRecord(value: unknown): value is Record<string, unknown> {
124
105
  return typeof value === "object" && value !== null && !Array.isArray(value);
125
106
  }