@swipt/cli 0.0.0-stage → 0.1.0-nightly.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 +17 -0
- package/cli.mjs +444 -0
- package/package.json +24 -4
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Copyright (C) 2026 by nekohasekai <contact-sagernet@sekai.icu>
|
|
2
|
+
|
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
|
4
|
+
it under the terms of the GNU General Public License as published by
|
|
5
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
(at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful,
|
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
GNU General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU General Public License
|
|
14
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
In addition, no derivative work may use the name or imply association
|
|
17
|
+
with this application without prior consent.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import vm from "node:vm";
|
|
9
|
+
|
|
10
|
+
import { build } from "esbuild";
|
|
11
|
+
|
|
12
|
+
const { version } = JSON.parse(fs.readFileSync(new URL("package.json", import.meta.url), "utf8"));
|
|
13
|
+
const { peerDependencies } = createRequire(import.meta.url)("@swipt/api/package.json");
|
|
14
|
+
|
|
15
|
+
const appModules = [
|
|
16
|
+
"react",
|
|
17
|
+
"react/jsx-runtime",
|
|
18
|
+
"react-native",
|
|
19
|
+
"expo",
|
|
20
|
+
"expo-localization",
|
|
21
|
+
"@expo/ui",
|
|
22
|
+
"@expo/ui/*",
|
|
23
|
+
"@swipt/api",
|
|
24
|
+
"@swipt/api/swiplet",
|
|
25
|
+
"@swipt/api/file-system",
|
|
26
|
+
];
|
|
27
|
+
const extensionModules = [
|
|
28
|
+
"@swipt/api",
|
|
29
|
+
"@swipt/api/swiplet",
|
|
30
|
+
"@swipt/api/file-system",
|
|
31
|
+
"@swipt/api/accessibility",
|
|
32
|
+
"@swipt/api/input",
|
|
33
|
+
"@swipt/api/device",
|
|
34
|
+
"@swipt/api/screen",
|
|
35
|
+
"@swipt/api/image",
|
|
36
|
+
"expo-localization",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const idPattern = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
40
|
+
|
|
41
|
+
const storedSuffixes = [".webp", ".jxl", ".avif", ".heic", ".heif", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mov", ".m4a", ".mp3", ".aac", ".zip", ".gz", ".br", ".zst", ".xz"].join(":");
|
|
42
|
+
|
|
43
|
+
function loadProject(root) {
|
|
44
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
45
|
+
const config = manifest.swiplet ?? {};
|
|
46
|
+
if (typeof config.id !== "string" || !idPattern.test(config.id)) {
|
|
47
|
+
throw new Error('"swiplet.id" in package.json must be 1 to 128 lowercase letters, digits, dots, underscores and hyphens starting with a letter or a digit');
|
|
48
|
+
}
|
|
49
|
+
if (typeof config.name !== "string" || config.name === "") {
|
|
50
|
+
throw new Error('"swiplet.name" in package.json must be a non-empty string');
|
|
51
|
+
}
|
|
52
|
+
const localizedNames = config.localizedNames ?? {};
|
|
53
|
+
if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
|
|
54
|
+
throw new Error('"swiplet.localizedNames" in package.json must map languages to names');
|
|
55
|
+
}
|
|
56
|
+
for (const field of ["description", "updateURL"]) {
|
|
57
|
+
if (config[field] !== undefined && typeof config[field] !== "string") {
|
|
58
|
+
throw new Error(`"swiplet.${field}" in package.json must be a string`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (config.versionCode !== undefined && (!Number.isInteger(config.versionCode) || config.versionCode < 1)) {
|
|
62
|
+
throw new Error('"swiplet.versionCode" in package.json must be a positive integer');
|
|
63
|
+
}
|
|
64
|
+
const entry = name => (config[name] === undefined ? undefined : path.resolve(root, config[name]));
|
|
65
|
+
return { root, manifest, config, localizedNames, app: entry("app"), extension: entry("extension") };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const typeImport = extension => ({
|
|
69
|
+
name: "swiplet-type-import",
|
|
70
|
+
setup(build) {
|
|
71
|
+
build.onLoad({ filter: /.*/ }, args => {
|
|
72
|
+
if (args.path !== extension) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return { errors: [{ text: `the interface imports ${path.basename(extension)} at run time; import its type with \`import type\`` }] };
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function bundle(project) {
|
|
81
|
+
const out = { app: "", extension: "", extensionMap: "" };
|
|
82
|
+
if (project.extension) {
|
|
83
|
+
const result = await build({
|
|
84
|
+
entryPoints: [project.extension],
|
|
85
|
+
outfile: path.join(project.root, "dist", project.config.id, "src", "extension.js"),
|
|
86
|
+
bundle: true,
|
|
87
|
+
format: "cjs",
|
|
88
|
+
platform: "neutral",
|
|
89
|
+
mainFields: ["module", "main"],
|
|
90
|
+
target: "es2022",
|
|
91
|
+
external: extensionModules,
|
|
92
|
+
sourcemap: "external",
|
|
93
|
+
sourcesContent: false,
|
|
94
|
+
write: false,
|
|
95
|
+
logLevel: "silent",
|
|
96
|
+
});
|
|
97
|
+
out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
|
|
98
|
+
out.extensionMap = result.outputFiles.find(file => file.path.endsWith(".map")).text;
|
|
99
|
+
}
|
|
100
|
+
if (project.app) {
|
|
101
|
+
const result = await build({
|
|
102
|
+
entryPoints: [project.app],
|
|
103
|
+
bundle: true,
|
|
104
|
+
format: "cjs",
|
|
105
|
+
platform: "neutral",
|
|
106
|
+
mainFields: ["react-native", "browser", "module", "main"],
|
|
107
|
+
conditions: ["react-native"],
|
|
108
|
+
resolveExtensions: [".ios.tsx", ".ios.ts", ".native.tsx", ".native.ts", ".tsx", ".ts", ".ios.jsx", ".ios.js", ".native.js", ".jsx", ".js", ".json"],
|
|
109
|
+
jsx: "automatic",
|
|
110
|
+
target: "es2020",
|
|
111
|
+
define: { "process.env.NODE_ENV": '"production"', __DEV__: "false" },
|
|
112
|
+
external: appModules,
|
|
113
|
+
write: false,
|
|
114
|
+
logLevel: "silent",
|
|
115
|
+
plugins: project.extension ? [typeImport(project.extension)] : [],
|
|
116
|
+
});
|
|
117
|
+
out.app = result.outputFiles[0].text;
|
|
118
|
+
}
|
|
119
|
+
if (out.app === "" && out.extension === "") {
|
|
120
|
+
throw new Error('package.json names neither "swiplet.app" nor "swiplet.extension"');
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const definitionKey = Symbol("definition");
|
|
126
|
+
|
|
127
|
+
function describeExtension(source) {
|
|
128
|
+
if (source === "") {
|
|
129
|
+
return { service: false, actions: [] };
|
|
130
|
+
}
|
|
131
|
+
const loading = name => {
|
|
132
|
+
const fail = () => {
|
|
133
|
+
throw new Error(`${name} is used while extension.js loads; use it inside run or an action`);
|
|
134
|
+
};
|
|
135
|
+
const get = (_, key) => (key === "__esModule" ? true : typeof key === "symbol" || key === "then" ? undefined : fail());
|
|
136
|
+
return new Proxy(fail, { get, apply: fail, construct: fail });
|
|
137
|
+
};
|
|
138
|
+
const modules = {
|
|
139
|
+
"@swipt/api": {},
|
|
140
|
+
"@swipt/api/swiplet": {
|
|
141
|
+
defineExtension: definition => ({ [definitionKey]: definition }),
|
|
142
|
+
defineAction: definition => definition,
|
|
143
|
+
actions: loading("actions"),
|
|
144
|
+
useService: loading("useService"),
|
|
145
|
+
},
|
|
146
|
+
"@swipt/api/file-system": {
|
|
147
|
+
get dataDirectory() {
|
|
148
|
+
throw new Error("dataDirectory is used while extension.js loads; use it inside run or an action");
|
|
149
|
+
},
|
|
150
|
+
get assets() {
|
|
151
|
+
throw new Error("assets is used while extension.js loads; use it inside run or an action");
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"@swipt/api/accessibility": loading("accessibility"),
|
|
155
|
+
"@swipt/api/input": loading("input"),
|
|
156
|
+
"@swipt/api/device": loading("device"),
|
|
157
|
+
"@swipt/api/screen": loading("screen"),
|
|
158
|
+
"@swipt/api/image": loading("image"),
|
|
159
|
+
"expo-localization": { getLocales: () => [{ languageTag: "en", languageCode: "en" }], getCalendars: () => [] },
|
|
160
|
+
};
|
|
161
|
+
const ignored = () => 0;
|
|
162
|
+
const context = vm.createContext({
|
|
163
|
+
console, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, structuredClone, queueMicrotask, crypto,
|
|
164
|
+
AbortController, AbortSignal, Event, EventTarget, CustomEvent, DOMException, Headers, Request, Response, Blob,
|
|
165
|
+
fetch: loading("fetch"),
|
|
166
|
+
setTimeout: ignored, setInterval: ignored, clearTimeout: ignored, clearInterval: ignored,
|
|
167
|
+
navigator: { userAgent: "Swipt", language: "en", languages: ["en"] },
|
|
168
|
+
});
|
|
169
|
+
const factory = vm.runInContext(`(function (require, module, exports) {${source}\n})`, context, { filename: "extension.js" });
|
|
170
|
+
const module = { exports: {} };
|
|
171
|
+
factory(
|
|
172
|
+
specifier => {
|
|
173
|
+
if (!Object.hasOwn(modules, specifier)) {
|
|
174
|
+
throw new Error(`Swipt doesn't provide "${specifier}" to extensions; bundle it into extension.js`);
|
|
175
|
+
}
|
|
176
|
+
return modules[specifier];
|
|
177
|
+
},
|
|
178
|
+
module,
|
|
179
|
+
module.exports,
|
|
180
|
+
);
|
|
181
|
+
const definition = (module.exports.default ?? module.exports)?.[definitionKey];
|
|
182
|
+
if (definition === undefined) {
|
|
183
|
+
throw new Error("the extension's default export is not the result of defineExtension");
|
|
184
|
+
}
|
|
185
|
+
const schema = (name, value, side) => {
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const converter = value?.["~standard"]?.jsonSchema;
|
|
190
|
+
if (typeof converter?.[side] !== "function") {
|
|
191
|
+
throw new Error(`the ${side} schema of the action ${JSON.stringify(name)} doesn't implement Standard JSON Schema; use a library that does, such as Zod 4`);
|
|
192
|
+
}
|
|
193
|
+
return converter[side]({ target: "draft-2020-12" });
|
|
194
|
+
};
|
|
195
|
+
const actions = Object.entries(definition.actions ?? {}).map(([name, action]) => {
|
|
196
|
+
if (typeof action?.title !== "string" || action.title === "") {
|
|
197
|
+
throw new Error(`the action ${JSON.stringify(name)} needs a title`);
|
|
198
|
+
}
|
|
199
|
+
const expose = action.expose ?? [];
|
|
200
|
+
if (!Array.isArray(expose) || !expose.every(caller => caller === "shortcuts")) {
|
|
201
|
+
throw new Error(`the action ${JSON.stringify(name)} can only expose itself to "shortcuts"`);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
name,
|
|
205
|
+
title: action.title,
|
|
206
|
+
...(action.description ? { description: action.description } : {}),
|
|
207
|
+
...(expose.length > 0 ? { expose } : {}),
|
|
208
|
+
...(action.input ? { input: schema(name, action.input, "input") } : {}),
|
|
209
|
+
...(action.output ? { output: schema(name, action.output, "output") } : {}),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
return { service: typeof definition.run === "function", actions };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function swipletManifest(project, output) {
|
|
216
|
+
const { service, actions } = describeExtension(output.extension);
|
|
217
|
+
const { config, localizedNames } = project;
|
|
218
|
+
return {
|
|
219
|
+
id: config.id,
|
|
220
|
+
name: config.name,
|
|
221
|
+
...(Object.keys(localizedNames).length > 0 ? { localizedNames } : {}),
|
|
222
|
+
...(config.description ? { description: config.description } : {}),
|
|
223
|
+
...(project.manifest.version ? { version: project.manifest.version } : {}),
|
|
224
|
+
...(config.versionCode ? { versionCode: config.versionCode } : {}),
|
|
225
|
+
...(config.icon ? { icon: config.icon } : {}),
|
|
226
|
+
...(config.updateURL ? { updateURL: config.updateURL } : {}),
|
|
227
|
+
...(output.app ? { interface: true } : {}),
|
|
228
|
+
...(service ? { service } : {}),
|
|
229
|
+
...(actions.length > 0 ? { actions } : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sourceFiles = output => ({
|
|
234
|
+
...(output.app ? { "app.js": output.app } : {}),
|
|
235
|
+
...(output.extension ? { "extension.js": output.extension } : {}),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
async function buildCommand(root) {
|
|
239
|
+
const project = loadProject(root);
|
|
240
|
+
const output = await bundle(project);
|
|
241
|
+
const manifest = swipletManifest(project, output);
|
|
242
|
+
const dist = path.join(root, "dist");
|
|
243
|
+
const folder = path.join(dist, manifest.id);
|
|
244
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
245
|
+
fs.mkdirSync(path.join(folder, "src"), { recursive: true });
|
|
246
|
+
fs.writeFileSync(path.join(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
247
|
+
for (const [file, contents] of Object.entries(sourceFiles(output))) {
|
|
248
|
+
fs.writeFileSync(path.join(folder, "src", file), contents);
|
|
249
|
+
}
|
|
250
|
+
if (output.extensionMap) {
|
|
251
|
+
fs.writeFileSync(path.join(folder, "src", "extension.js.map"), output.extensionMap);
|
|
252
|
+
}
|
|
253
|
+
if (fs.existsSync(path.join(root, "icon.png"))) {
|
|
254
|
+
fs.copyFileSync(path.join(root, "icon.png"), path.join(folder, "icon.png"));
|
|
255
|
+
}
|
|
256
|
+
if (fs.existsSync(path.join(root, "assets"))) {
|
|
257
|
+
fs.cpSync(path.join(root, "assets"), path.join(folder, "assets"), { recursive: true, filter: source => !path.basename(source).startsWith(".") });
|
|
258
|
+
}
|
|
259
|
+
const archive = path.join(dist, `${manifest.id}.swiplet`);
|
|
260
|
+
fs.rmSync(archive, { force: true });
|
|
261
|
+
fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
|
|
262
|
+
execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, manifest.id, "-x", "*.map"], { cwd: dist });
|
|
263
|
+
console.log(path.relative(process.cwd(), archive));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const manifestEntry = /^(?:[^/]+\/)?manifest\.json$/;
|
|
267
|
+
|
|
268
|
+
function sourceCommand(files, base, output) {
|
|
269
|
+
if (!base) {
|
|
270
|
+
throw new UsageError("source needs --url, the address the swiplets are published under");
|
|
271
|
+
}
|
|
272
|
+
let prefix;
|
|
273
|
+
try {
|
|
274
|
+
prefix = new URL(base.endsWith("/") ? base : `${base}/`);
|
|
275
|
+
} catch {
|
|
276
|
+
throw new UsageError(`--url ${JSON.stringify(base)} is not an absolute URL`);
|
|
277
|
+
}
|
|
278
|
+
const swiplets = new Map();
|
|
279
|
+
for (const file of files) {
|
|
280
|
+
const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }).split("\n").filter(name => manifestEntry.test(name)).sort((a, b) => a.length - b.length)[0];
|
|
281
|
+
if (entry === undefined) {
|
|
282
|
+
throw new Error(`${file} has no manifest.json`);
|
|
283
|
+
}
|
|
284
|
+
const manifest = JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
|
|
285
|
+
if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
|
|
286
|
+
throw new Error(`${file} has no valid swiplet id in its manifest`);
|
|
287
|
+
}
|
|
288
|
+
if (swiplets.has(manifest.id)) {
|
|
289
|
+
throw new Error(`${manifest.id} is both ${swiplets.get(manifest.id).file} and ${file}`);
|
|
290
|
+
}
|
|
291
|
+
const data = fs.readFileSync(file);
|
|
292
|
+
swiplets.set(manifest.id, {
|
|
293
|
+
file,
|
|
294
|
+
entry: { ...manifest, url: new URL(path.basename(file), prefix).href, size: data.length, sha256: createHash("sha256").update(data).digest("hex") },
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const listed = `${JSON.stringify({ swiplets: [...swiplets.keys()].sort().map(id => swiplets.get(id).entry) }, null, 2)}\n`;
|
|
298
|
+
if (output) {
|
|
299
|
+
fs.writeFileSync(output, listed);
|
|
300
|
+
} else {
|
|
301
|
+
process.stdout.write(listed);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const idOf = name => name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "").slice(0, 128).replace(/[._-]+$/, "") || "swiplet";
|
|
306
|
+
|
|
307
|
+
const templates = {
|
|
308
|
+
"package.json": name =>
|
|
309
|
+
`${JSON.stringify(
|
|
310
|
+
{
|
|
311
|
+
name: idOf(name),
|
|
312
|
+
version: "0.1.0",
|
|
313
|
+
private: true,
|
|
314
|
+
swiplet: {
|
|
315
|
+
id: idOf(name),
|
|
316
|
+
name,
|
|
317
|
+
versionCode: 1,
|
|
318
|
+
icon: { color: "blue", symbol: "sparkles" },
|
|
319
|
+
app: "./src/App.tsx",
|
|
320
|
+
extension: "./src/extension.ts",
|
|
321
|
+
},
|
|
322
|
+
scripts: { build: "swiplet build", check: "tsc" },
|
|
323
|
+
dependencies: { zod: "^4.6.7" },
|
|
324
|
+
devDependencies: {
|
|
325
|
+
"@swipt/api": version,
|
|
326
|
+
"@swipt/cli": version,
|
|
327
|
+
typescript: "^7.0.2",
|
|
328
|
+
...Object.fromEntries(["@expo/ui", "@types/react", "react"].map(name => [name, peerDependencies[name]])),
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
null,
|
|
332
|
+
2,
|
|
333
|
+
)}\n`,
|
|
334
|
+
"tsconfig.json": () =>
|
|
335
|
+
`${JSON.stringify(
|
|
336
|
+
{
|
|
337
|
+
compilerOptions: {
|
|
338
|
+
target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", lib: ["ES2023", "DOM"],
|
|
339
|
+
strict: true, noEmit: true, skipLibCheck: true,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
null,
|
|
343
|
+
2,
|
|
344
|
+
)}\n`,
|
|
345
|
+
".gitignore": () => "node_modules/\ndist/\n",
|
|
346
|
+
"src/extension.ts": () => `import * as device from "@swipt/api/device";
|
|
347
|
+
import { defineAction, defineExtension } from "@swipt/api/swiplet";
|
|
348
|
+
|
|
349
|
+
export default defineExtension({
|
|
350
|
+
actions: {
|
|
351
|
+
frontApp: defineAction({
|
|
352
|
+
title: "Find the App in Front",
|
|
353
|
+
run: (_, { signal }) => device.foregroundApplication.get({ signal }),
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
`,
|
|
358
|
+
"src/App.tsx": () => `import { Button, Form, Host, Section, Text } from "@expo/ui/swift-ui";
|
|
359
|
+
import { useState } from "react";
|
|
360
|
+
import { actions } from "@swipt/api/swiplet";
|
|
361
|
+
|
|
362
|
+
import type extension from "./extension";
|
|
363
|
+
|
|
364
|
+
const { frontApp } = actions<typeof extension>();
|
|
365
|
+
|
|
366
|
+
export default function App() {
|
|
367
|
+
const [app, setApp] = useState("");
|
|
368
|
+
return (
|
|
369
|
+
<Host style={{ flex: 1 }}>
|
|
370
|
+
<Form>
|
|
371
|
+
<Section>
|
|
372
|
+
<Button label="Find the App in Front" onPress={async () => setApp(await frontApp())} />
|
|
373
|
+
{app === "" ? null : <Text>{app}</Text>}
|
|
374
|
+
</Section>
|
|
375
|
+
</Form>
|
|
376
|
+
</Host>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
`,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const quote = value => (/^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`);
|
|
383
|
+
|
|
384
|
+
function createCommand(directory) {
|
|
385
|
+
const root = path.resolve(directory);
|
|
386
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length > 0) {
|
|
387
|
+
throw new Error(`${directory} is not empty`);
|
|
388
|
+
}
|
|
389
|
+
for (const [file, template] of Object.entries(templates)) {
|
|
390
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
391
|
+
fs.writeFileSync(path.join(root, file), template(path.basename(root)));
|
|
392
|
+
}
|
|
393
|
+
const manager = process.env.npm_config_user_agent?.split("/")[0] ?? "npm";
|
|
394
|
+
console.log(`cd ${quote(directory)} && ${manager} install && ${manager} run build`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const usage = `usage: swiplet create <directory>
|
|
398
|
+
swiplet build
|
|
399
|
+
swiplet source --url <address> [--output <file>] <swiplet>...
|
|
400
|
+
|
|
401
|
+
create make a swiplet project in a new or empty directory
|
|
402
|
+
build bundle the project in this directory into dist/<swiplet.id>.swiplet
|
|
403
|
+
source list built swiplets in a source that Swipt installs and updates from
|
|
404
|
+
|
|
405
|
+
Type-check a project with its "check" script.`;
|
|
406
|
+
|
|
407
|
+
const commands = { create: 1, build: 0, source: -1 };
|
|
408
|
+
|
|
409
|
+
class UsageError extends Error {}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = parseArgs({ allowPositionals: true, options: { help: { type: "boolean", short: "h" }, url: { type: "string" }, output: { type: "string" } } });
|
|
415
|
+
} catch (error) {
|
|
416
|
+
throw new UsageError(error.message);
|
|
417
|
+
}
|
|
418
|
+
const [command, ...rest] = parsed.positionals;
|
|
419
|
+
if (parsed.values.help) {
|
|
420
|
+
console.log(usage);
|
|
421
|
+
} else if (command === undefined) {
|
|
422
|
+
throw new UsageError("a command is missing");
|
|
423
|
+
} else if (!Object.hasOwn(commands, command)) {
|
|
424
|
+
throw new UsageError(`unknown command ${JSON.stringify(command)}`);
|
|
425
|
+
} else if (commands[command] < 0 ? rest.length === 0 : rest.length !== commands[command]) {
|
|
426
|
+
throw new UsageError(
|
|
427
|
+
commands[command] === 0 ? `${command} takes no arguments` : commands[command] < 0 ? `${command} takes one or more swiplets` : `${command} takes one directory`,
|
|
428
|
+
);
|
|
429
|
+
} else if (command === "create") {
|
|
430
|
+
createCommand(rest[0]);
|
|
431
|
+
} else if (command === "source") {
|
|
432
|
+
sourceCommand(rest, parsed.values.url, parsed.values.output);
|
|
433
|
+
} else {
|
|
434
|
+
await buildCommand(process.cwd());
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error instanceof UsageError) {
|
|
438
|
+
console.error(`error: ${error.message}\n\n${usage}`);
|
|
439
|
+
process.exitCode = 2;
|
|
440
|
+
} else {
|
|
441
|
+
console.error(error.message);
|
|
442
|
+
process.exitCode = 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swipt/cli",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-nightly.0",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/swipt-automation/swipt.git",
|
|
8
|
+
"directory": "api/cli"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"swiplet": "cli.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"cli.mjs"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"esbuild": "^0.28.2"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@swipt/api": "0.1.0-nightly.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/README.md
DELETED