@moloodev/cli 0.7.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  `moloo editor dev` does not support the local app host because the parent app origin is fixed to production (`https://app.moloo.dev`).
2
2
 
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.
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. The first time, click **로컬 뷰 연결** in the app and allow Chrome local network access; once granted, the app connects automatically. 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
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
6
 
@@ -9,3 +9,7 @@ Editors have two release tracks, `stable` (the default) and `beta`. Each track p
9
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`.
10
10
 
11
11
  The `moloo` bin delegates to a project's installed `@moloodev/cli`, searching from the current directory upward. This keeps global invocations on the project's CLI version.
12
+
13
+ `moloo --version` (or `-v`) prints the version of the CLI that runs the command. Inside a project with its own `@moloodev/cli`, commands are delegated to that CLI, so this shows the project's version.
14
+
15
+ `moloo upgrade` upgrades the globally installed CLI to the npm `latest` version and is never delegated to a project's CLI. It detects npm, pnpm, yarn or bun from the install path and runs that manager's global install; if the version is already latest it only says so. When the running CLI is a project dependency, it prints how to upgrade it with the project's package manager and changes nothing.
package/bin/moloo.mjs CHANGED
@@ -3,7 +3,8 @@ import { spawnSync } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { findLocalCli, LOCAL_CLI_ENV } from "./local-cli.mjs";
5
5
 
6
- const local = findLocalCli(process.cwd(), fileURLToPath(import.meta.url));
6
+ // `upgrade` targets the globally installed CLI itself, so it never hands off.
7
+ const local = process.argv[2] === "upgrade" ? null : findLocalCli(process.cwd(), fileURLToPath(import.meta.url));
7
8
  if (local) {
8
9
  const child = spawnSync(process.execPath, [local, ...process.argv.slice(2)], {
9
10
  stdio: "inherit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moloodev/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0",
28
- "@moloodev/platform": "0.7.0",
29
- "@moloodev/editor-store": "0.7.0",
30
- "@moloodev/ui": "0.7.0"
27
+ "@moloodev/editor-sdk": "0.9.0",
28
+ "@moloodev/editor-store": "0.9.0",
29
+ "@moloodev/platform": "0.9.0",
30
+ "@moloodev/ui": "0.9.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.13.3",
34
34
  "typescript": "^7.0.2",
35
- "@moloodev/typescript-config": "0.7.0"
35
+ "@moloodev/typescript-config": "0.9.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
package/src/cli.ts CHANGED
@@ -7,12 +7,13 @@ import { promoteEditorPackage, publishEditorPackage } from "./publish";
7
7
  import { printEditorStatus } from "./editor-status";
8
8
  import { createEditor, EDITOR_CREATE_USAGE } from "./editor-create";
9
9
  import { apiErrorMessage, readApiBody } from "./api-result";
10
+ import { cliVersion, upgradeCli, UPGRADE_USAGE } from "./upgrade";
10
11
  import { listWorkspaces, useWorkspace, type WorkspaceOptions } from "./workspace";
11
12
 
12
13
  const EDITOR_USAGE =
13
14
  `${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)`;
14
15
  const WORKSPACE_USAGE = "Usage: moloo workspace list | use <slug>";
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}`;
16
+ const USAGE = `Usage: moloo <command>\n moloo --version | -v\n\nCommands:\n login [--device]\n logout\n whoami\n upgrade\n workspace list | use <slug>\n editor create | dev | build | publish | promote | status\n\n${WORKSPACE_USAGE}\n${EDITOR_USAGE}`;
16
17
 
17
18
  type Command = (args: readonly string[], options: WorkspaceOptions) => Promise<number>;
18
19
 
@@ -123,6 +124,13 @@ const commands: Record<string, {
123
124
  return 0;
124
125
  },
125
126
  },
127
+ upgrade: {
128
+ usage: UPGRADE_USAGE,
129
+ async run(args) {
130
+ if (args.length) throw new Error(UPGRADE_USAGE);
131
+ return upgradeCli();
132
+ },
133
+ },
126
134
  workspace: {
127
135
  usage: WORKSPACE_USAGE,
128
136
  async run([name, ...args], options) {
@@ -154,6 +162,10 @@ export async function runCli(argv: readonly string[], options: WorkspaceOptions
154
162
  const [name, ...args] = argv;
155
163
  const command =
156
164
  name === undefined ? undefined : Object.hasOwn(commands, name) ? commands[name] : undefined;
165
+ if (argv.length === 1 && (name === "--version" || name === "-v")) {
166
+ console.log(cliVersion());
167
+ return 0;
168
+ }
157
169
  const wantsHelp = argv.includes("--help") || argv.includes("-h");
158
170
  if (wantsHelp) {
159
171
  console.log(command?.usage ?? USAGE);
package/src/editor-dev.ts CHANGED
@@ -28,6 +28,7 @@ export async function runEditorDev(
28
28
  try {
29
29
  const destination = new URL(`/w/${encodeURIComponent(slug)}/dev-editor`, getWorkspaceHostUrl("production"));
30
30
  destination.searchParams.set("view", server.viewOrigin);
31
+ console.log(`${server.editorId} browser -> ${destination.href}`);
31
32
  console.log(`${server.editorId} view origin -> ${server.viewOrigin}`);
32
33
  await deps.open(destination.href);
33
34
  await deps.stopped();
package/src/upgrade.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { dirname, join, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const UPGRADE_USAGE = "Usage: moloo upgrade";
7
+
8
+ const PACKAGE = "@moloodev/cli";
9
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE.replace("/", "%2F")}/latest`;
10
+
11
+ export type GlobalManager = "npm" | "pnpm" | "yarn" | "bun";
12
+ export type Install = { kind: "global"; manager: GlobalManager } | { kind: "local" };
13
+
14
+ const INSTALL_COMMANDS: Record<GlobalManager, readonly [string, ...string[]]> = {
15
+ npm: ["npm", "install", "--global", `${PACKAGE}@latest`],
16
+ pnpm: ["pnpm", "add", "--global", `${PACKAGE}@latest`],
17
+ yarn: ["yarn", "global", "add", `${PACKAGE}@latest`],
18
+ bun: ["bun", "add", "--global", `${PACKAGE}@latest`],
19
+ };
20
+
21
+ /** Classify the installed package directory (realpath) by each manager's global store layout. */
22
+ export function detectInstall(packageRoot: string): Install {
23
+ const path = packageRoot.split(sep).join("/");
24
+ if (/\/pnpm\/global\/|\/\.pnpm-global\//.test(path)) return { kind: "global", manager: "pnpm" };
25
+ if (/\/\.bun\/install\/global\//.test(path)) return { kind: "global", manager: "bun" };
26
+ if (/\/yarn\/global\/|\/\.yarn-global\//.test(path)) return { kind: "global", manager: "yarn" };
27
+ if (/\/lib\/node_modules\/@moloodev\/cli$|\/npm\/node_modules\/@moloodev\/cli$/.test(path)) {
28
+ return { kind: "global", manager: "npm" };
29
+ }
30
+ return { kind: "local" };
31
+ }
32
+
33
+ export type Runner = (command: string, args: readonly string[]) => number;
34
+
35
+ export interface UpgradeDeps {
36
+ packageRoot?: string;
37
+ currentVersion?: string;
38
+ fetchLatest?: () => Promise<string>;
39
+ run?: Runner;
40
+ log?: (line: string) => void;
41
+ }
42
+
43
+ export async function upgradeCli(deps: UpgradeDeps = {}): Promise<number> {
44
+ const packageRoot = deps.packageRoot ?? cliPackageRoot();
45
+ const current = deps.currentVersion ?? readVersion(packageRoot);
46
+ const log = deps.log ?? console.log;
47
+ const install = detectInstall(packageRoot);
48
+ if (install.kind === "local") {
49
+ log(`${PACKAGE} ${current} is a project dependency (${packageRoot}).`);
50
+ log(`Upgrade it with the project's package manager, e.g. pnpm add -D ${PACKAGE}@latest`);
51
+ return 1;
52
+ }
53
+ const latest = await (deps.fetchLatest ?? fetchLatestVersion)();
54
+ if (latest === current) {
55
+ log(`${PACKAGE} ${current} is already the latest.`);
56
+ return 0;
57
+ }
58
+ if (compareVersions(current, latest) > 0) {
59
+ log(`${PACKAGE} ${current} is newer than latest ${latest}; not downgrading.`);
60
+ return 0;
61
+ }
62
+ const [command, ...args] = INSTALL_COMMANDS[install.manager];
63
+ log(`Upgrading ${PACKAGE} ${current} -> ${latest}: ${[command, ...args].join(" ")}`);
64
+ const status = (deps.run ?? spawnRunner)(command, args);
65
+ if (status !== 0) throw new Error(`${command} exited with ${String(status)}`);
66
+ log(`${PACKAGE} upgraded to ${latest}.`);
67
+ return 0;
68
+ }
69
+
70
+ /** Minimal semver order: numeric core, then a release outranks its prereleases. */
71
+ export function compareVersions(a: string, b: string): number {
72
+ const [coreA = "", preA] = a.split("-", 2);
73
+ const [coreB = "", preB] = b.split("-", 2);
74
+ const numsA = coreA.split(".").map(Number);
75
+ const numsB = coreB.split(".").map(Number);
76
+ for (let index = 0; index < 3; index += 1) {
77
+ const diff = (numsA[index] ?? 0) - (numsB[index] ?? 0);
78
+ if (diff) return Math.sign(diff);
79
+ }
80
+ if (preA === preB) return 0;
81
+ if (preA === undefined) return 1;
82
+ if (preB === undefined) return -1;
83
+ return preA.localeCompare(preB, "en", { numeric: true });
84
+ }
85
+
86
+ function cliPackageRoot(): string {
87
+ return realpathSync(join(dirname(fileURLToPath(import.meta.url)), ".."));
88
+ }
89
+
90
+ /** Version of the CLI package that is running this code. */
91
+ export function cliVersion(): string {
92
+ return readVersion(cliPackageRoot());
93
+ }
94
+
95
+ function readVersion(packageRoot: string): string {
96
+ const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as { version?: unknown };
97
+ if (typeof pkg.version !== "string") throw new Error(`No version in ${packageRoot}/package.json`);
98
+ return pkg.version;
99
+ }
100
+
101
+ async function fetchLatestVersion(): Promise<string> {
102
+ const response = await fetch(REGISTRY_URL);
103
+ if (!response.ok) throw new Error(`npm registry returned ${String(response.status)} for ${PACKAGE}`);
104
+ const body = (await response.json()) as { version?: unknown };
105
+ if (typeof body.version !== "string") throw new Error(`npm registry has no latest version for ${PACKAGE}`);
106
+ return body.version;
107
+ }
108
+
109
+ const spawnRunner: Runner = (command, args) => {
110
+ const child = spawnSync(command, args, { stdio: "inherit", shell: process.platform === "win32" });
111
+ if (child.error) throw child.error;
112
+ return child.status ?? 1;
113
+ };