@moloodev/cli 0.3.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/src/publish.ts CHANGED
@@ -1,32 +1,31 @@
1
+ import { publishToken } from "./auth";
1
2
  import { createHash } from "node:crypto";
2
3
  import { putScriptUpload } from "@moloodev/editor-store/upload";
3
- import { getApiOrigin } from "@moloodev/platform";
4
- import { builtinEditors } from "@moloodev/platform/builtin-editors";
5
- 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";
6
7
 
7
- const DEFAULT_SERVICE = "editor-origin-build";
8
-
9
- export function publishEndpoint(apiUrl?: string): string {
10
- return `${apiBase(apiUrl)}/v1/revisions/publish`;
8
+ export function publishEndpoint(): string {
9
+ return `${getApiOrigin("production")}/v1/revisions/publish`;
11
10
  }
12
11
 
13
- export function uploadEndpoint(apiUrl?: string): string {
14
- return `${apiBase(apiUrl)}/v1/revisions/uploads`;
12
+ export function uploadEndpoint(): string {
13
+ return `${getApiOrigin("production")}/v1/revisions/uploads`;
15
14
  }
16
15
 
17
16
  export async function publishEditor(input: {
18
17
  manifest: Record<string, unknown>;
19
18
  script: string;
20
19
  token: string;
21
- apiUrl?: string;
22
- service?: string;
20
+ target: PublishTarget;
23
21
  }): Promise<{ status: number; body: string }> {
24
- const uploadUrl = uploadEndpoint(input.apiUrl);
25
- const publishUrl = publishEndpoint(input.apiUrl);
26
- const service = input.service ?? DEFAULT_SERVICE;
22
+ const uploadUrl = uploadEndpoint();
23
+ const publishUrl = publishEndpoint();
27
24
  const issued = await fetch(uploadUrl, {
28
25
  method: "POST",
29
- headers: 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 }),
30
29
  });
31
30
  const issuedBody = await issued.text();
32
31
  if (issued.status !== 201) return { status: issued.status, body: issuedBody };
@@ -42,13 +41,14 @@ export async function publishEditor(input: {
42
41
 
43
42
  const response = await fetch(publishUrl, {
44
43
  method: "POST",
44
+ redirect: "error",
45
45
  headers: {
46
- ...serviceHeaders(input.token, service),
46
+ authorization: `Bearer ${input.token}`,
47
47
  "content-type": "application/json",
48
48
  },
49
49
  body: JSON.stringify({
50
+ target: input.target,
50
51
  manifest: input.manifest,
51
- editorId: reservedEditorId(input.manifest),
52
52
  upload: {
53
53
  pathname: upload.pathname,
54
54
  sha256: createHash("sha256").update(bytes).digest("hex"),
@@ -60,28 +60,31 @@ export async function publishEditor(input: {
60
60
 
61
61
  export async function publishEditorPackage(
62
62
  packageRoot: string,
63
- options: { token?: string; apiUrl?: string; service?: string } = {},
63
+ options: Omit<WorkspaceOptions, "apiOrigin"> & { workspace?: string } = {},
64
64
  ): Promise<{ id: string; status: number; body: string }> {
65
- const token = (options.token ?? process.env.MOLOO_SERVICE_TOKEN ?? "").trim();
66
- if (token.length === 0) throw new Error("MOLOO_SERVICE_TOKEN is empty");
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);
67
78
  const built = await buildEditorPackage(packageRoot);
68
79
  const result = await publishEditor({
69
80
  manifest: built.manifest,
70
81
  script: built.script,
71
82
  token,
72
- apiUrl: options.apiUrl,
73
- service: options.service,
83
+ target,
74
84
  });
75
85
  return { id: built.id, ...result };
76
86
  }
77
87
 
78
- function serviceHeaders(token: string, service: string): Record<string, string> {
79
- return {
80
- "x-moloo-service-token": token,
81
- "x-moloo-service": service,
82
- };
83
- }
84
-
85
88
  function readUpload(text: string): { pathname: string; token: string } | undefined {
86
89
  let value: unknown;
87
90
  try {
@@ -98,27 +101,6 @@ function readUpload(text: string): { pathname: string; token: string } | undefin
98
101
  return { pathname, token };
99
102
  }
100
103
 
101
- function apiBase(apiUrl: string | undefined): string {
102
- const raw = apiUrl?.trim() ?? "";
103
- const base = raw.length === 0 ? getApiOrigin("production") : raw;
104
- let url: URL;
105
- try {
106
- url = new URL(base);
107
- } catch {
108
- throw new Error("publish API origin is invalid");
109
- }
110
- if (url.protocol !== "https:") throw new Error("publish API origin must be https");
111
- return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
112
- }
113
-
114
- export function reservedEditorId(manifest: Record<string, unknown>): string {
115
- const id = manifest.id;
116
- if (typeof id !== "string") throw new Error("builtin manifest id is missing");
117
- const row = builtinEditors.find((item) => `${item.publisherHandle}.${item.slug}` === id);
118
- if (row === undefined) throw new Error(`builtin editor ${id} is missing`);
119
- return row.id;
120
- }
121
-
122
104
  function isRecord(value: unknown): value is Record<string, unknown> {
123
105
  return typeof value === "object" && value !== null && !Array.isArray(value);
124
106
  }
@@ -0,0 +1,68 @@
1
+ import { cliIdentity } from "./auth";
2
+ import { readConfig, writeConfig } from "./config";
3
+ import type { CredentialStore } from "./credentials";
4
+
5
+ export type WorkspaceOptions = {
6
+ store?: CredentialStore;
7
+ apiOrigin?: string;
8
+ configPath?: string;
9
+ };
10
+
11
+ export async function listWorkspaces(options: WorkspaceOptions = {}): Promise<void> {
12
+ const { workspaces: available } = await cliIdentity(options.store, options.apiOrigin);
13
+ const config = await readConfig(options.configPath);
14
+ for (const workspace of available) {
15
+ console.log(`${workspace.slug}\t${workspace.name}${workspace.slug === config.workspace ? " (default)" : ""}`);
16
+ }
17
+ if (!available.length) console.log("No workspaces.");
18
+ }
19
+
20
+ export async function useWorkspace(slug: string, options: WorkspaceOptions = {}): Promise<void> {
21
+ const { workspaces: available } = await cliIdentity(options.store, options.apiOrigin);
22
+ const workspace = available.find((item) => item.slug === slug);
23
+ if (!workspace) {
24
+ throw new Error(`Workspace "${slug}" is not in your memberships. Run moloo workspace use <slug>. Available: ${available.map((item) => item.slug).join(", ") || "none"}.`);
25
+ }
26
+ await writeConfig({ workspace: workspace.slug }, options.configPath);
27
+ console.log(`Default workspace: ${workspace.slug} (${workspace.name})`);
28
+ }
29
+
30
+ /** Resolve against current memberships even when the slug came from saved config. */
31
+ export async function resolveWorkspace({
32
+ flag,
33
+ interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
34
+ choose = chooseWorkspace,
35
+ ...options
36
+ }: WorkspaceOptions & {
37
+ flag?: string;
38
+ interactive?: boolean;
39
+ choose?: (workspaces: { slug: string; name: string }[]) => Promise<string>;
40
+ } = {}): Promise<string> {
41
+ const { workspaces } = await cliIdentity(options.store, options.apiOrigin);
42
+ const selected = flag ?? (await readConfig(options.configPath)).workspace;
43
+ const available = workspaces.map((workspace) => workspace.slug).join(", ") || "none";
44
+ const guidance = `Run moloo workspace use <slug>. Available: ${available}.`;
45
+ if (selected !== undefined) {
46
+ if (workspaces.some((workspace) => workspace.slug === selected)) return selected;
47
+ throw new Error(`Workspace "${selected}" is not in your memberships. ${guidance}`);
48
+ }
49
+ if (workspaces.length === 1 && workspaces[0]) return workspaces[0].slug;
50
+ if (!workspaces.length || !interactive) throw new Error(`Select a workspace. ${guidance}`);
51
+ const slug = await choose(workspaces);
52
+ if (!workspaces.some((workspace) => workspace.slug === slug)) {
53
+ throw new Error(`Invalid workspace selection. ${guidance}`);
54
+ }
55
+ return slug;
56
+ }
57
+
58
+ async function chooseWorkspace(workspaces: { slug: string; name: string }[]): Promise<string> {
59
+ const { createInterface } = await import("node:readline/promises");
60
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
61
+ try {
62
+ const choices = workspaces.map((workspace, index) => `${index + 1}. ${workspace.slug} (${workspace.name})`).join("\n");
63
+ const answer = (await prompt.question(`${choices}\nWorkspace number: `)).trim();
64
+ return /^[1-9][0-9]*$/.test(answer) ? workspaces[Number(answer) - 1]?.slug ?? "" : "";
65
+ } finally {
66
+ prompt.close();
67
+ }
68
+ }