@mterminal/mtx 0.1.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/LICENSE +21 -0
- package/bin/mtx.mjs +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +508 -0
- package/package.json +40 -0
- package/src/commands/init.ts +94 -0
- package/src/commands/keygen.ts +29 -0
- package/src/commands/login.ts +40 -0
- package/src/commands/pack.ts +53 -0
- package/src/commands/publish.ts +49 -0
- package/src/commands/whoami.ts +14 -0
- package/src/commands/yank.ts +13 -0
- package/src/index.ts +53 -0
- package/src/lib/api.test.ts +138 -0
- package/src/lib/api.ts +118 -0
- package/src/lib/config.test.ts +41 -0
- package/src/lib/config.ts +54 -0
- package/src/lib/keystore.ts +46 -0
- package/src/lib/pack.test.ts +104 -0
- package/src/lib/pack.ts +93 -0
- package/src/lib/sign.test.ts +32 -0
- package/src/lib/sign.ts +33 -0
- package/templates/minimal/README.md +17 -0
- package/templates/minimal/package.json +29 -0
- package/templates/minimal/src/main.ts +2 -0
- package/templates/minimal/src/renderer.tsx +2 -0
- package/templates/minimal/tsup.config.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mTerminal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/mtx.mjs
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { cac } from "cac";
|
|
3
|
+
|
|
4
|
+
// src/commands/init.ts
|
|
5
|
+
import fs2 from "fs/promises";
|
|
6
|
+
import path2 from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import prompts from "prompts";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
|
|
11
|
+
// src/lib/config.ts
|
|
12
|
+
import fs from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import os from "os";
|
|
15
|
+
function configDir() {
|
|
16
|
+
return process.env.MTX_HOME ?? path.join(os.homedir(), ".mtx");
|
|
17
|
+
}
|
|
18
|
+
function configPath() {
|
|
19
|
+
return path.join(configDir(), "config.json");
|
|
20
|
+
}
|
|
21
|
+
function defaultEndpoint() {
|
|
22
|
+
return process.env.MTX_ENDPOINT ?? "https://marketplace.mterminal.dev";
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig() {
|
|
25
|
+
const file = configPath();
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(file, "utf8");
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
return {
|
|
30
|
+
endpoint: parsed.endpoint ?? defaultEndpoint(),
|
|
31
|
+
authorId: parsed.authorId,
|
|
32
|
+
apiKey: parsed.apiKey,
|
|
33
|
+
githubLogin: parsed.githubLogin,
|
|
34
|
+
activeKeyId: parsed.activeKeyId
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return { endpoint: defaultEndpoint() };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function saveConfig(cfg) {
|
|
41
|
+
const dir = configDir();
|
|
42
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
43
|
+
const file = configPath();
|
|
44
|
+
await fs.writeFile(file, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
45
|
+
}
|
|
46
|
+
async function updateConfig(patch) {
|
|
47
|
+
const cfg = await loadConfig();
|
|
48
|
+
const next = { ...cfg, ...patch };
|
|
49
|
+
await saveConfig(next);
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/commands/init.ts
|
|
54
|
+
var CATEGORIES = ["productivity", "language", "theme", "remote", "ai", "git", "other"];
|
|
55
|
+
function templatesDir() {
|
|
56
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
return path2.resolve(here, "..", "templates", "minimal");
|
|
58
|
+
}
|
|
59
|
+
async function copyDir(src, dest) {
|
|
60
|
+
await fs2.mkdir(dest, { recursive: true });
|
|
61
|
+
const items = await fs2.readdir(src, { withFileTypes: true });
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const s = path2.join(src, item.name);
|
|
64
|
+
const d = path2.join(dest, item.name);
|
|
65
|
+
if (item.isDirectory()) await copyDir(s, d);
|
|
66
|
+
else await fs2.copyFile(s, d);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function initCommand(suggestedName) {
|
|
70
|
+
const cfg = await loadConfig();
|
|
71
|
+
const answers = await prompts([
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
name: "id",
|
|
75
|
+
message: "extension id (kebab-case)",
|
|
76
|
+
initial: suggestedName ?? "",
|
|
77
|
+
validate: (v) => /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(v) ? true : "must be kebab-case"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
name: "displayName",
|
|
82
|
+
message: "display name",
|
|
83
|
+
initial: (prev) => prev
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "select",
|
|
87
|
+
name: "category",
|
|
88
|
+
message: "category",
|
|
89
|
+
choices: CATEGORIES.map((c) => ({ title: c, value: c })),
|
|
90
|
+
initial: 6
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
name: "dir",
|
|
95
|
+
message: "directory",
|
|
96
|
+
initial: (_p, all) => `./${all.id}`
|
|
97
|
+
}
|
|
98
|
+
]);
|
|
99
|
+
if (!answers.id) {
|
|
100
|
+
console.log(pc.yellow("aborted"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const target = path2.resolve(answers.dir);
|
|
104
|
+
await copyDir(templatesDir(), target);
|
|
105
|
+
const pkgPath = path2.join(target, "package.json");
|
|
106
|
+
const raw = await fs2.readFile(pkgPath, "utf8");
|
|
107
|
+
const pkg = JSON.parse(raw);
|
|
108
|
+
pkg.name = `mterminal-plugin-${answers.id}`;
|
|
109
|
+
const mt = pkg.mterminal ?? {};
|
|
110
|
+
mt.id = answers.id;
|
|
111
|
+
mt.displayName = answers.displayName || answers.id;
|
|
112
|
+
mt.category = answers.category;
|
|
113
|
+
if (cfg.authorId && cfg.activeKeyId) {
|
|
114
|
+
mt.publisher = { authorId: cfg.authorId, keyId: cfg.activeKeyId };
|
|
115
|
+
}
|
|
116
|
+
pkg.mterminal = mt;
|
|
117
|
+
await fs2.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
118
|
+
console.log(pc.green(`created ${path2.relative(process.cwd(), target)}`));
|
|
119
|
+
console.log("next:");
|
|
120
|
+
console.log(" cd", path2.relative(process.cwd(), target));
|
|
121
|
+
console.log(" npm i");
|
|
122
|
+
console.log(" npm run build");
|
|
123
|
+
console.log(" mtx pack");
|
|
124
|
+
console.log(" mtx publish");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/commands/login.ts
|
|
128
|
+
import pc2 from "picocolors";
|
|
129
|
+
|
|
130
|
+
// src/lib/api.ts
|
|
131
|
+
async function expectJson(res) {
|
|
132
|
+
const text = await res.text();
|
|
133
|
+
let json;
|
|
134
|
+
try {
|
|
135
|
+
json = JSON.parse(text);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`expected json response, got: ${text.slice(0, 200)}`);
|
|
138
|
+
}
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = json;
|
|
141
|
+
throw new Error(`HTTP ${res.status}: ${body.error ?? JSON.stringify(json)}`);
|
|
142
|
+
}
|
|
143
|
+
return json;
|
|
144
|
+
}
|
|
145
|
+
async function deviceStart(opts) {
|
|
146
|
+
const r = await fetch(`${opts.endpoint}/v1/auth/device/start`, { method: "POST" });
|
|
147
|
+
return expectJson(r);
|
|
148
|
+
}
|
|
149
|
+
async function devicePoll(opts, deviceCode) {
|
|
150
|
+
const r = await fetch(`${opts.endpoint}/v1/auth/device/poll`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "content-type": "application/json" },
|
|
153
|
+
body: JSON.stringify({ deviceCode })
|
|
154
|
+
});
|
|
155
|
+
return expectJson(r);
|
|
156
|
+
}
|
|
157
|
+
async function registerKey(opts, body) {
|
|
158
|
+
if (!opts.apiKey) throw new Error("apiKey required");
|
|
159
|
+
const r = await fetch(`${opts.endpoint}/v1/keys`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"content-type": "application/json",
|
|
163
|
+
authorization: `Bearer ${opts.apiKey}`
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(body)
|
|
166
|
+
});
|
|
167
|
+
return expectJson(r);
|
|
168
|
+
}
|
|
169
|
+
async function publishPackage(opts, data) {
|
|
170
|
+
if (!opts.apiKey) throw new Error("apiKey required");
|
|
171
|
+
const fd = new FormData();
|
|
172
|
+
fd.append("package", new Blob([data], { type: "application/zip" }), "package.mtx");
|
|
173
|
+
const r = await fetch(`${opts.endpoint}/v1/publish`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { authorization: `Bearer ${opts.apiKey}` },
|
|
176
|
+
body: fd
|
|
177
|
+
});
|
|
178
|
+
const text = await r.text();
|
|
179
|
+
let json;
|
|
180
|
+
try {
|
|
181
|
+
json = JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
throw new Error(`unexpected response: ${text.slice(0, 200)}`);
|
|
184
|
+
}
|
|
185
|
+
return json;
|
|
186
|
+
}
|
|
187
|
+
async function yankVersion(opts, id, version) {
|
|
188
|
+
if (!opts.apiKey) throw new Error("apiKey required");
|
|
189
|
+
const r = await fetch(`${opts.endpoint}/v1/extensions/${id}/yank/${version}`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { authorization: `Bearer ${opts.apiKey}` }
|
|
192
|
+
});
|
|
193
|
+
await expectJson(r);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/commands/login.ts
|
|
197
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
198
|
+
async function loginCommand() {
|
|
199
|
+
const cfg = await loadConfig();
|
|
200
|
+
console.log(pc2.cyan(`endpoint: ${cfg.endpoint}`));
|
|
201
|
+
const start = await deviceStart({ endpoint: cfg.endpoint });
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(`open ${pc2.bold(start.verificationUri)} and enter code:`);
|
|
204
|
+
console.log(pc2.bold(pc2.yellow(start.userCode)));
|
|
205
|
+
console.log();
|
|
206
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
207
|
+
let interval = Math.max(start.interval, 2);
|
|
208
|
+
while (Date.now() < deadline) {
|
|
209
|
+
await sleep(interval * 1e3);
|
|
210
|
+
const poll = await devicePoll({ endpoint: cfg.endpoint }, start.deviceCode);
|
|
211
|
+
if (poll.status === "authorized" && poll.apiKey) {
|
|
212
|
+
await updateConfig({
|
|
213
|
+
apiKey: poll.apiKey,
|
|
214
|
+
authorId: poll.authorId,
|
|
215
|
+
githubLogin: poll.githubLogin
|
|
216
|
+
});
|
|
217
|
+
console.log(pc2.green(`logged in as ${poll.githubLogin} (${poll.authorId})`));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (poll.status === "denied") {
|
|
221
|
+
console.log(pc2.red("access denied"));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (poll.status === "expired") {
|
|
225
|
+
console.log(pc2.red("device flow expired, try again"));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
console.log(pc2.red("timed out waiting for authorization"));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/commands/keygen.ts
|
|
233
|
+
import pc3 from "picocolors";
|
|
234
|
+
|
|
235
|
+
// src/lib/sign.ts
|
|
236
|
+
import * as ed from "@noble/ed25519";
|
|
237
|
+
import { sha256, sha512 } from "@noble/hashes/sha2";
|
|
238
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
239
|
+
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
240
|
+
function deterministicHashHex(entries) {
|
|
241
|
+
const sorted = entries.filter((e) => e.path !== "signature.sig").map((e) => ({ path: e.path, hash: bytesToHex(sha256(e.content)) })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
242
|
+
const lines = sorted.map((e) => `${e.path} ${e.hash}
|
|
243
|
+
`).join("");
|
|
244
|
+
return bytesToHex(sha256(new TextEncoder().encode(lines)));
|
|
245
|
+
}
|
|
246
|
+
async function signEntries(entries, privKey) {
|
|
247
|
+
const hashHex = deterministicHashHex(entries);
|
|
248
|
+
const message = new TextEncoder().encode(hashHex);
|
|
249
|
+
const sig = await ed.signAsync(message, privKey);
|
|
250
|
+
return Buffer.from(sig).toString("base64");
|
|
251
|
+
}
|
|
252
|
+
async function generateKeyPair() {
|
|
253
|
+
const priv = ed.utils.randomPrivateKey();
|
|
254
|
+
const pub = await ed.getPublicKeyAsync(priv);
|
|
255
|
+
return { priv, pubB64: Buffer.from(pub).toString("base64") };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/lib/keystore.ts
|
|
259
|
+
import fs3 from "fs/promises";
|
|
260
|
+
import path3 from "path";
|
|
261
|
+
function keysDir() {
|
|
262
|
+
return path3.join(configDir(), "keys");
|
|
263
|
+
}
|
|
264
|
+
function keyPaths(keyId) {
|
|
265
|
+
const safe = keyId.replace(/[^a-zA-Z0-9_:.-]/g, "_");
|
|
266
|
+
return {
|
|
267
|
+
privPath: path3.join(keysDir(), `${safe}.priv`),
|
|
268
|
+
pubPath: path3.join(keysDir(), `${safe}.pub`)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function writeKey(keyId, priv, pubB64) {
|
|
272
|
+
const dir = keysDir();
|
|
273
|
+
await fs3.mkdir(dir, { recursive: true, mode: 448 });
|
|
274
|
+
const paths = keyPaths(keyId);
|
|
275
|
+
await fs3.writeFile(paths.privPath, Buffer.from(priv).toString("base64"), {
|
|
276
|
+
mode: 384
|
|
277
|
+
});
|
|
278
|
+
await fs3.writeFile(paths.pubPath, pubB64, { mode: 384 });
|
|
279
|
+
return paths;
|
|
280
|
+
}
|
|
281
|
+
async function readPrivKey(keyId) {
|
|
282
|
+
const { privPath } = keyPaths(keyId);
|
|
283
|
+
const raw = await fs3.readFile(privPath, "utf8");
|
|
284
|
+
return new Uint8Array(Buffer.from(raw.trim(), "base64"));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/commands/keygen.ts
|
|
288
|
+
async function keygenCommand(opts = {}) {
|
|
289
|
+
const cfg = await loadConfig();
|
|
290
|
+
if (!cfg.apiKey) {
|
|
291
|
+
console.log(pc3.red("not logged in. run `mtx login` first."));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const { priv, pubB64 } = await generateKeyPair();
|
|
295
|
+
const res = await registerKey(
|
|
296
|
+
{ endpoint: cfg.endpoint, apiKey: cfg.apiKey },
|
|
297
|
+
{ pubkeyB64: pubB64, name: opts.name }
|
|
298
|
+
);
|
|
299
|
+
await writeKey(res.keyId, priv, pubB64);
|
|
300
|
+
if (opts.setActive !== false) {
|
|
301
|
+
await updateConfig({ activeKeyId: res.keyId });
|
|
302
|
+
}
|
|
303
|
+
console.log(pc3.green(`registered key ${res.keyId}`));
|
|
304
|
+
console.log(`active key: ${res.keyId}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/commands/pack.ts
|
|
308
|
+
import fs5 from "fs/promises";
|
|
309
|
+
import path5 from "path";
|
|
310
|
+
import pc4 from "picocolors";
|
|
311
|
+
|
|
312
|
+
// src/lib/pack.ts
|
|
313
|
+
import fs4 from "fs/promises";
|
|
314
|
+
import path4 from "path";
|
|
315
|
+
import { zipSync, strToU8 } from "fflate";
|
|
316
|
+
import { validateManifest } from "@mterminal/manifest-validator";
|
|
317
|
+
var INCLUDED_DIRS = ["dist", "themes"];
|
|
318
|
+
var INCLUDED_FILES = ["package.json", "README.md", "readme.md", "icon.png"];
|
|
319
|
+
async function walk(root, prefix = "") {
|
|
320
|
+
const out = [];
|
|
321
|
+
let stat;
|
|
322
|
+
try {
|
|
323
|
+
stat = await fs4.stat(root);
|
|
324
|
+
} catch {
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
if (!stat.isDirectory()) return out;
|
|
328
|
+
const entries = await fs4.readdir(root, { withFileTypes: true });
|
|
329
|
+
for (const e of entries) {
|
|
330
|
+
const abs = path4.join(root, e.name);
|
|
331
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
332
|
+
if (e.isDirectory()) {
|
|
333
|
+
out.push(...await walk(abs, rel));
|
|
334
|
+
} else if (e.isFile()) {
|
|
335
|
+
const content = await fs4.readFile(abs);
|
|
336
|
+
out.push({ path: rel, content: new Uint8Array(content) });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
async function pack(opts) {
|
|
342
|
+
const pkgPath = path4.join(opts.cwd, "package.json");
|
|
343
|
+
const raw = await fs4.readFile(pkgPath, "utf8");
|
|
344
|
+
const pkg = JSON.parse(raw);
|
|
345
|
+
const mt = pkg.mterminal ?? {};
|
|
346
|
+
const publisher = mt.publisher ?? {};
|
|
347
|
+
if (publisher.authorId !== opts.authorId || publisher.keyId !== opts.keyId) {
|
|
348
|
+
mt.publisher = { authorId: opts.authorId, keyId: opts.keyId };
|
|
349
|
+
pkg.mterminal = mt;
|
|
350
|
+
await fs4.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
351
|
+
}
|
|
352
|
+
const validation = validateManifest(pkg);
|
|
353
|
+
if (!validation.ok) {
|
|
354
|
+
throw new Error(`manifest invalid:
|
|
355
|
+
- ${validation.errors.join("\n - ")}`);
|
|
356
|
+
}
|
|
357
|
+
const entries = [];
|
|
358
|
+
entries.push({
|
|
359
|
+
path: "package.json",
|
|
360
|
+
content: new Uint8Array(await fs4.readFile(pkgPath))
|
|
361
|
+
});
|
|
362
|
+
for (const dir of INCLUDED_DIRS) {
|
|
363
|
+
entries.push(...await walk(path4.join(opts.cwd, dir), dir));
|
|
364
|
+
}
|
|
365
|
+
for (const file of INCLUDED_FILES) {
|
|
366
|
+
if (file === "package.json") continue;
|
|
367
|
+
const abs = path4.join(opts.cwd, file);
|
|
368
|
+
try {
|
|
369
|
+
const buf2 = await fs4.readFile(abs);
|
|
370
|
+
entries.push({ path: file, content: new Uint8Array(buf2) });
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const sigB64 = await signEntries(entries, opts.privKey);
|
|
375
|
+
entries.push({ path: "signature.sig", content: strToU8(sigB64) });
|
|
376
|
+
const zipMap = {};
|
|
377
|
+
for (const e of entries) zipMap[e.path] = e.content;
|
|
378
|
+
const buf = zipSync(zipMap);
|
|
379
|
+
return {
|
|
380
|
+
buf,
|
|
381
|
+
manifestId: validation.manifest.id,
|
|
382
|
+
version: validation.manifest.version
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/commands/pack.ts
|
|
387
|
+
import { spawn } from "child_process";
|
|
388
|
+
function runNpmBuild(cwd) {
|
|
389
|
+
return new Promise((resolve, reject) => {
|
|
390
|
+
const child = spawn("npm", ["run", "build"], { cwd, stdio: "inherit" });
|
|
391
|
+
child.on("exit", (code) => {
|
|
392
|
+
if (code === 0) resolve();
|
|
393
|
+
else reject(new Error(`npm run build exited with ${code}`));
|
|
394
|
+
});
|
|
395
|
+
child.on("error", reject);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async function packCommand(opts = {}) {
|
|
399
|
+
const cwd = path5.resolve(opts.cwd ?? process.cwd());
|
|
400
|
+
const cfg = await loadConfig();
|
|
401
|
+
if (!cfg.authorId || !cfg.activeKeyId) {
|
|
402
|
+
console.log(pc4.red("not configured. run `mtx login` and `mtx keygen` first."));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
if (opts.build !== false) {
|
|
406
|
+
const pkgPath = path5.join(cwd, "package.json");
|
|
407
|
+
const pkg = JSON.parse(await fs5.readFile(pkgPath, "utf8"));
|
|
408
|
+
if (pkg.scripts?.build) await runNpmBuild(cwd);
|
|
409
|
+
}
|
|
410
|
+
const priv = await readPrivKey(cfg.activeKeyId);
|
|
411
|
+
const result = await pack({
|
|
412
|
+
cwd,
|
|
413
|
+
authorId: cfg.authorId,
|
|
414
|
+
keyId: cfg.activeKeyId,
|
|
415
|
+
privKey: priv
|
|
416
|
+
});
|
|
417
|
+
const out = path5.resolve(opts.out ?? `${result.manifestId}-${result.version}.mtx`);
|
|
418
|
+
await fs5.writeFile(out, result.buf);
|
|
419
|
+
console.log(pc4.green(`packed ${result.manifestId}@${result.version} \u2192 ${out} (${result.buf.length} bytes)`));
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/commands/publish.ts
|
|
424
|
+
import fs6 from "fs/promises";
|
|
425
|
+
import path6 from "path";
|
|
426
|
+
import pc5 from "picocolors";
|
|
427
|
+
async function publishCommand(opts = {}) {
|
|
428
|
+
const cfg = await loadConfig();
|
|
429
|
+
if (!cfg.apiKey) {
|
|
430
|
+
console.log(pc5.red("not logged in. run `mtx login` first."));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
let file;
|
|
434
|
+
if (opts.file) {
|
|
435
|
+
file = path6.resolve(opts.file);
|
|
436
|
+
} else {
|
|
437
|
+
file = await packCommand({ cwd: opts.cwd, build: opts.build });
|
|
438
|
+
}
|
|
439
|
+
const buf = new Uint8Array(await fs6.readFile(file));
|
|
440
|
+
console.log(pc5.cyan(`uploading ${path6.basename(file)} (${buf.length} bytes)...`));
|
|
441
|
+
const result = await publishPackage(
|
|
442
|
+
{ endpoint: cfg.endpoint, apiKey: cfg.apiKey },
|
|
443
|
+
buf
|
|
444
|
+
);
|
|
445
|
+
if (result.ok) {
|
|
446
|
+
console.log(pc5.green(`published ${result.id}@${result.version}`));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log(pc5.red("publish failed:"));
|
|
450
|
+
for (const e of result.errors ?? []) {
|
|
451
|
+
console.log(pc5.red(` [${e.code}]`));
|
|
452
|
+
for (const issue of e.issues) {
|
|
453
|
+
const loc = issue.path ? ` (${issue.path}${issue.line ? `:${issue.line}` : ""}${issue.col ? `:${issue.col}` : ""})` : "";
|
|
454
|
+
console.log(` - ${issue.message}${loc}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/commands/yank.ts
|
|
461
|
+
import pc6 from "picocolors";
|
|
462
|
+
async function yankCommand(id, version) {
|
|
463
|
+
const cfg = await loadConfig();
|
|
464
|
+
if (!cfg.apiKey) {
|
|
465
|
+
console.log(pc6.red("not logged in"));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
await yankVersion({ endpoint: cfg.endpoint, apiKey: cfg.apiKey }, id, version);
|
|
469
|
+
console.log(pc6.green(`yanked ${id}@${version}`));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/commands/whoami.ts
|
|
473
|
+
import pc7 from "picocolors";
|
|
474
|
+
async function whoamiCommand() {
|
|
475
|
+
const cfg = await loadConfig();
|
|
476
|
+
if (!cfg.apiKey) {
|
|
477
|
+
console.log(pc7.yellow("not logged in"));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.log(`author : ${cfg.authorId ?? "(unknown)"}`);
|
|
481
|
+
console.log(`login : ${cfg.githubLogin ?? "(unknown)"}`);
|
|
482
|
+
console.log(`key : ${cfg.activeKeyId ?? "(none)"}`);
|
|
483
|
+
console.log(`server : ${cfg.endpoint}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/index.ts
|
|
487
|
+
var VERSION = "0.1.0";
|
|
488
|
+
async function run(argv) {
|
|
489
|
+
const cli = cac("mtx");
|
|
490
|
+
cli.command("init [name]", "scaffold a new extension").action(async (name) => initCommand(name));
|
|
491
|
+
cli.command("login", "authenticate via github device flow").action(async () => loginCommand());
|
|
492
|
+
cli.command("keygen", "generate and register a new ed25519 keypair").option("--name <name>", "human-readable name for the key").action(async (opts) => keygenCommand({ name: opts.name }));
|
|
493
|
+
cli.command("pack", "build and bundle the extension into a .mtx file").option("--out <file>", "output file path").option("--no-build", "skip npm run build").action(
|
|
494
|
+
async (opts) => packCommand({ out: opts.out, build: opts.build })
|
|
495
|
+
);
|
|
496
|
+
cli.command("publish", "pack and upload to the marketplace").option("--file <file>", "use an existing .mtx file instead of packing").option("--no-build", "skip npm run build when packing").action(
|
|
497
|
+
async (opts) => publishCommand({ file: opts.file, build: opts.build })
|
|
498
|
+
);
|
|
499
|
+
cli.command("yank <id> <version>", "mark a published version as yanked").action(async (id, version) => yankCommand(id, version));
|
|
500
|
+
cli.command("whoami", "show current login info").action(async () => whoamiCommand());
|
|
501
|
+
cli.help();
|
|
502
|
+
cli.version(VERSION);
|
|
503
|
+
cli.parse(["node", "mtx", ...argv], { run: false });
|
|
504
|
+
await cli.runMatchedCommand();
|
|
505
|
+
}
|
|
506
|
+
export {
|
|
507
|
+
run
|
|
508
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mterminal/mtx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "command-line interface for publishing mterminal extensions to the marketplace",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mtx": "./bin/mtx.mjs"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"dist",
|
|
15
|
+
"templates",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@noble/ed25519": "^2.1.0",
|
|
20
|
+
"@noble/hashes": "^1.5.0",
|
|
21
|
+
"cac": "^6.7.14",
|
|
22
|
+
"fflate": "^0.8.2",
|
|
23
|
+
"picocolors": "^1.1.1",
|
|
24
|
+
"prompts": "^2.4.2",
|
|
25
|
+
"@mterminal/marketplace-types": "0.1.0",
|
|
26
|
+
"@mterminal/manifest-validator": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.9.0",
|
|
30
|
+
"@types/prompts": "^2.4.9",
|
|
31
|
+
"tsup": "^8.3.5",
|
|
32
|
+
"typescript": "^5.6.3",
|
|
33
|
+
"vitest": "^2.1.5"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import prompts from 'prompts'
|
|
5
|
+
import pc from 'picocolors'
|
|
6
|
+
import { loadConfig } from '../lib/config'
|
|
7
|
+
|
|
8
|
+
interface InitAnswers {
|
|
9
|
+
id: string
|
|
10
|
+
displayName: string
|
|
11
|
+
category: string
|
|
12
|
+
dir: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CATEGORIES = ['productivity', 'language', 'theme', 'remote', 'ai', 'git', 'other']
|
|
16
|
+
|
|
17
|
+
function templatesDir(): string {
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
return path.resolve(here, '..', 'templates', 'minimal')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function copyDir(src: string, dest: string): Promise<void> {
|
|
23
|
+
await fs.mkdir(dest, { recursive: true })
|
|
24
|
+
const items = await fs.readdir(src, { withFileTypes: true })
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
const s = path.join(src, item.name)
|
|
27
|
+
const d = path.join(dest, item.name)
|
|
28
|
+
if (item.isDirectory()) await copyDir(s, d)
|
|
29
|
+
else await fs.copyFile(s, d)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function initCommand(suggestedName?: string): Promise<void> {
|
|
34
|
+
const cfg = await loadConfig()
|
|
35
|
+
const answers = (await prompts([
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
name: 'id',
|
|
39
|
+
message: 'extension id (kebab-case)',
|
|
40
|
+
initial: suggestedName ?? '',
|
|
41
|
+
validate: (v: string) =>
|
|
42
|
+
/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(v) ? true : 'must be kebab-case',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
name: 'displayName',
|
|
47
|
+
message: 'display name',
|
|
48
|
+
initial: (prev: string) => prev,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'select',
|
|
52
|
+
name: 'category',
|
|
53
|
+
message: 'category',
|
|
54
|
+
choices: CATEGORIES.map((c) => ({ title: c, value: c })),
|
|
55
|
+
initial: 6,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
name: 'dir',
|
|
60
|
+
message: 'directory',
|
|
61
|
+
initial: (_p: unknown, all: { id: string }) => `./${all.id}`,
|
|
62
|
+
},
|
|
63
|
+
])) as InitAnswers
|
|
64
|
+
|
|
65
|
+
if (!answers.id) {
|
|
66
|
+
console.log(pc.yellow('aborted'))
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const target = path.resolve(answers.dir)
|
|
71
|
+
await copyDir(templatesDir(), target)
|
|
72
|
+
|
|
73
|
+
const pkgPath = path.join(target, 'package.json')
|
|
74
|
+
const raw = await fs.readFile(pkgPath, 'utf8')
|
|
75
|
+
const pkg = JSON.parse(raw) as Record<string, unknown>
|
|
76
|
+
pkg.name = `mterminal-plugin-${answers.id}`
|
|
77
|
+
const mt = (pkg.mterminal ?? {}) as Record<string, unknown>
|
|
78
|
+
mt.id = answers.id
|
|
79
|
+
mt.displayName = answers.displayName || answers.id
|
|
80
|
+
mt.category = answers.category
|
|
81
|
+
if (cfg.authorId && cfg.activeKeyId) {
|
|
82
|
+
mt.publisher = { authorId: cfg.authorId, keyId: cfg.activeKeyId }
|
|
83
|
+
}
|
|
84
|
+
pkg.mterminal = mt
|
|
85
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
86
|
+
|
|
87
|
+
console.log(pc.green(`created ${path.relative(process.cwd(), target)}`))
|
|
88
|
+
console.log('next:')
|
|
89
|
+
console.log(' cd', path.relative(process.cwd(), target))
|
|
90
|
+
console.log(' npm i')
|
|
91
|
+
console.log(' npm run build')
|
|
92
|
+
console.log(' mtx pack')
|
|
93
|
+
console.log(' mtx publish')
|
|
94
|
+
}
|