@memoket-ai/cli 2.0.1

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.
@@ -0,0 +1,227 @@
1
+ // publish.js — pick a build target (dev|test|prod), bake it, and publish to
2
+ // npm with the chosen env baked into the tarball. Skips `prepack` so the
3
+ // interactive choice isn't clobbered back to prod.
4
+ //
5
+ // node scripts/publish.js [target] [flags] # target optional, interactive if missing
6
+ // node scripts/publish.js --dry-run # verify tarball + auth, no upload
7
+ //
8
+ // Flags:
9
+ // --dry-run validate tarball + auth without uploading
10
+ // --yes skip the pre-publish confirmation prompt
11
+ // --tag=<name> npm dist-tag (default: latest). Non-latest triggers
12
+ // an automatic prerelease version bump.
13
+ // e.g. --tag=dev on 2.0.0 → version 2.0.1-dev.0
14
+ //
15
+ // LOCAL-PUBLISH GUARD: this script is intentionally blocked from doing a real
16
+ // `npm publish` outside of CI. The IP-restricted NPM_TOKEN provides the same
17
+ // guarantee at the network layer; this guard is a fast, local fail-safe so a
18
+ // `node scripts/publish.js prod` on a developer machine errors immediately
19
+ // rather than reaching npm and being rejected there. To run a real publish
20
+ // locally, set `CI=true` (e.g. `CI=true node scripts/publish.js prod`).
21
+ //
22
+ // Auth (one of):
23
+ // - GH Actions secrets.NPM_TOKEN (production publish path)
24
+ // - `CI=true` env + npm login (local dev — only effective for dry-run; live
25
+ // publish from a developer machine is still rejected by the IP-allowlist
26
+ // on the production token)
27
+ //
28
+ // Re-publish rules (enforced by npm):
29
+ // - the same version cannot be re-published; bump version in package.json first
30
+
31
+ import { spawn, spawnSync } from "node:child_process";
32
+ import { createInterface } from "node:readline";
33
+ import { readFileSync, writeFileSync } from "node:fs";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+
37
+ const __dirname = dirname(fileURLToPath(import.meta.url));
38
+ const root = join(__dirname, "..");
39
+
40
+ const TARGETS = ["dev", "test", "prod"];
41
+ const argv = process.argv.slice(2);
42
+ const flagValue = (name) => {
43
+ const pref = `--${name}=`;
44
+ const hit = argv.find((a) => a.startsWith(pref));
45
+ return hit ? hit.slice(pref.length) : null;
46
+ };
47
+ const dryRun = argv.includes("--dry-run");
48
+ const inCI = process.env.CI === "true";
49
+ const explicitTag = flagValue("tag");
50
+ const targetArg = argv.find((a) => !a.startsWith("--") && !a.includes("="));
51
+
52
+ // Local-publish guard: block real `npm publish` from a developer machine.
53
+ // Dry-run is always allowed (useful for "what would I publish" sanity checks).
54
+ if (!inCI && !dryRun) {
55
+ console.error("✗ local publish blocked: this script only publishes from CI.");
56
+ console.error(" Set CI=true to override (e.g. `CI=true node scripts/publish.js prod`)");
57
+ console.error(" The IP-restricted NPM_TOKEN will still reject the actual upload outside GH Actions.");
58
+ console.error(" For a real publish, use the GitHub Actions dispatch workflow or push a release tag.");
59
+ process.exit(1);
60
+ }
61
+
62
+ async function promptTarget() {
63
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
64
+ return new Promise((resolve) => {
65
+ console.log("Select environment to publish:");
66
+ for (const [i, t] of TARGETS.entries()) console.log(` ${i + 1}) ${t}`);
67
+ rl.question("> ", (answer) => {
68
+ rl.close();
69
+ const idx = parseInt(answer, 10) - 1;
70
+ const t = TARGETS[idx];
71
+ if (!t) {
72
+ console.error(`✗ invalid choice: ${answer}`);
73
+ process.exit(1);
74
+ }
75
+ resolve(t);
76
+ });
77
+ });
78
+ }
79
+
80
+ const target = (targetArg ? targetArg.toLowerCase() : await promptTarget());
81
+
82
+ if (!TARGETS.includes(target)) {
83
+ console.error(`✗ unknown publish target: ${target}`);
84
+ console.error(` expected one of: ${TARGETS.join(", ")}`);
85
+ process.exit(1);
86
+ }
87
+
88
+ const pkgPath = join(root, "package.json");
89
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
90
+ const registry = (pkg.publishConfig && pkg.publishConfig.registry) || "https://registry.npmjs.org";
91
+ const access = (pkg.publishConfig && pkg.publishConfig.access) || "public";
92
+
93
+ // resolve dist-tag: explicit --tag wins; otherwise prompt
94
+ let tag = explicitTag;
95
+ if (!tag) {
96
+ tag = await promptTag();
97
+ }
98
+ if (tag !== "latest" && !/^[a-z0-9][a-z0-9._-]{0,213}$/i.test(tag)) {
99
+ console.error(`✗ invalid dist-tag: ${tag}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ async function promptTag() {
104
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
105
+ return new Promise((resolve) => {
106
+ console.log("Select npm dist-tag:");
107
+ console.log(" 1) latest (production)");
108
+ console.log(" 2) dev (prerelease, auto-bumps version)");
109
+ console.log(" 3) rc (prerelease, auto-bumps version)");
110
+ rl.question("> ", (answer) => {
111
+ rl.close();
112
+ const map = { 1: "latest", 2: "dev", 3: "rc" };
113
+ const t = map[parseInt(answer, 10)];
114
+ if (!t) {
115
+ console.error(`✗ invalid choice: ${answer}`);
116
+ process.exit(1);
117
+ }
118
+ resolve(t);
119
+ });
120
+ });
121
+ }
122
+
123
+ function bumpVersion(preid) {
124
+ // `npm version prerelease --preid=dev` mutates package.json in place
125
+ // 2.0.0 → 2.0.1-dev.0, then 2.0.1-dev.0 → 2.0.1-dev.1, etc.
126
+ const r = spawnSync("npm", ["version", "prerelease", `--preid=${preid}`, "--no-git-tag-version"], {
127
+ cwd: root,
128
+ stdio: "inherit",
129
+ });
130
+ if (r.status !== 0) {
131
+ console.error(`✗ npm version bump failed (exit ${r.status})`);
132
+ process.exit(r.status ?? 1);
133
+ }
134
+ }
135
+
136
+ function rollbackVersion() {
137
+ // restore package.json from git (the bump above is the only uncommitted change)
138
+ spawnSync("git", ["checkout", "--", "package.json"], { cwd: root, stdio: "ignore" });
139
+ }
140
+
141
+ function checkNotPublished(version) {
142
+ const r = spawnSync("npm", ["view", `${pkg.name}@${version}`, "version"], {
143
+ cwd: root,
144
+ encoding: "utf8",
145
+ });
146
+ if (r.status === 0) {
147
+ console.error(`✗ ${pkg.name}@${version} is already published on npm`);
148
+ console.error(` bump the version or pick a different dist-tag.`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ const skipConfirm = argv.includes("--yes") || dryRun;
154
+
155
+ async function confirmPublish(version) {
156
+ if (skipConfirm) return;
157
+ console.log("");
158
+ console.log("About to publish:");
159
+ console.log(` package: ${pkg.name}@${version}`);
160
+ console.log(` target: ${target}`);
161
+ console.log(` tag: ${tag}`);
162
+ console.log(` registry: ${registry}`);
163
+ console.log(` access: ${access}`);
164
+ if (dryRun) console.log(" mode: --dry-run (no upload)");
165
+ console.log("");
166
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
167
+ const answer = await new Promise((resolve) => {
168
+ rl.question("Continue? [y/N] > ", resolve);
169
+ });
170
+ rl.close();
171
+ if (!/^y(es)?$/i.test(answer.trim())) {
172
+ if (tag !== "latest") rollbackVersion();
173
+ console.error("✗ aborted by user");
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ // 1) bump version if non-latest (dry-run will roll back after)
179
+ if (tag !== "latest") {
180
+ bumpVersion(tag);
181
+ }
182
+ // re-read package.json after possible bump
183
+ const pkgAfter = JSON.parse(readFileSync(pkgPath, "utf8"));
184
+ const version = pkgAfter.version;
185
+
186
+ checkNotPublished(version);
187
+
188
+ // 2) bake the chosen env
189
+ const build = spawn(process.execPath, [join(root, "scripts", "build.js"), target], {
190
+ stdio: "inherit",
191
+ });
192
+ build.on("exit", async (code) => {
193
+ if (code !== 0) {
194
+ console.error(`✗ build failed (exit ${code})`);
195
+ process.exit(code ?? 1);
196
+ }
197
+ await confirmPublish(version);
198
+ publish();
199
+ });
200
+
201
+ function publish() {
202
+ // 3) npm publish with --ignore-scripts (don't let prepack clobber our bake)
203
+ // and --access public (matches package.json publishConfig).
204
+ // --provenance is intentionally NOT passed: the source repo is private,
205
+ // and npm refuses provenance bundles from private repos (would leak
206
+ // build metadata into a public Sigstore transparency log). The package
207
+ // itself is published public; only the source stays private.
208
+ const args = ["publish", "--ignore-scripts", `--access=${access}`];
209
+ if (tag !== "latest") args.push(`--tag=${tag}`);
210
+ if (dryRun) args.push("--dry-run");
211
+
212
+ console.log(`→ npm ${args.join(" ")}`);
213
+
214
+ const pub = spawn("npm", args, { stdio: "inherit", cwd: root });
215
+ pub.on("exit", (code) => {
216
+ if (code !== 0) {
217
+ console.error(`✗ npm publish failed (exit ${code})`);
218
+ if (dryRun && tag !== "latest") rollbackVersion();
219
+ process.exit(code ?? 1);
220
+ }
221
+ console.log(`✓ published ${pkgAfter.name}@${version} (tag=${tag}, target=${target})${dryRun ? " (dry-run)" : ""}`);
222
+ if (dryRun && tag !== "latest") {
223
+ rollbackVersion();
224
+ console.log(`→ rolled back package.json (dry-run side-effect)`);
225
+ }
226
+ });
227
+ }
@@ -0,0 +1,65 @@
1
+ // Non-browser login: email + password → gateway POST /auth/v1/login → save userToken to auth.json.
2
+ // Runs alongside the existing browser OAuth login; userToken is used as a Bearer for
3
+ // `token issue` / `/v1/api-tokens`.
4
+
5
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { apiBaseFromMCPURL } from "./http-integration.js";
8
+
9
+ // authPath 是 userToken 的持久化位置(长期持有;与浏览器 OAuth 的 credentials.json 分开)。
10
+ export async function authPath() {
11
+ const home = process.env.HOME || process.env.USERPROFILE || "";
12
+ if (!home) throw new Error("cannot determine home directory");
13
+ const sep = process.platform === "win32" ? "\\" : "/";
14
+ return `${home}${sep}.memoket${sep}auth.json`;
15
+ }
16
+
17
+ async function prompt(question) {
18
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
19
+ try {
20
+ return (await rl.question(question)).trim();
21
+ } finally {
22
+ rl.close();
23
+ }
24
+ }
25
+
26
+ // loginBasic 用邮箱密码换 userToken 并落库。email/password 可来自参数、环境变量或交互输入。
27
+ export async function loginBasic(mcpURL, { email, password } = {}) {
28
+ email = email || process.env.MEMOKET_EMAIL || (await prompt("Email: "));
29
+ password = password || process.env.MEMOKET_PASSWORD || (await prompt("Password: "));
30
+ if (!email || !password) throw new Error("email and password are required");
31
+
32
+ const apiBase = apiBaseFromMCPURL(mcpURL);
33
+ const res = await fetch(`${apiBase}/auth/v1/login`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ email, password }),
37
+ signal: AbortSignal.timeout(30_000),
38
+ });
39
+ const text = await res.text();
40
+ let data = {};
41
+ try {
42
+ data = JSON.parse(text);
43
+ } catch {
44
+ /* non-json */
45
+ }
46
+ if (res.status < 200 || res.status >= 300 || !data.userToken) {
47
+ const detail = data?.error?.message || text || res.statusText;
48
+ throw new Error(`login failed (${res.status}): ${detail}`);
49
+ }
50
+
51
+ const p = await authPath();
52
+ await mkdir(p.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
53
+ await writeFile(p, JSON.stringify({ userToken: data.userToken }, null, 2), { mode: 0o600 });
54
+ return p;
55
+ }
56
+
57
+ // loadUserToken 读回 userToken(无则 null)。
58
+ export async function loadUserToken() {
59
+ try {
60
+ const p = await authPath();
61
+ return JSON.parse(await readFile(p, "utf8")).userToken || null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }