@swipt/api 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/accessibility.d.ts +83 -0
- package/accessibility.mjs +8 -0
- package/cli.mjs +441 -0
- package/device.d.ts +49 -0
- package/device.mjs +17 -0
- package/file-system.d.ts +30 -0
- package/file-system.mjs +2 -0
- package/image.d.ts +114 -0
- package/image.mjs +5 -0
- package/index.d.ts +23 -0
- package/index.mjs +1 -0
- package/input.d.ts +24 -0
- package/input.mjs +15 -0
- package/package.json +87 -4
- package/repl.d.ts +12 -0
- package/screen.d.ts +63 -0
- package/screen.mjs +11 -0
- package/swiplet.d.ts +106 -0
- package/swiplet.mjs +8 -0
- 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.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CallOptions, Point, Rect } from "./index";
|
|
2
|
+
import type { TapOptions } from "./input";
|
|
3
|
+
|
|
4
|
+
export type AccessibilityTrait = "button" | "link" | "image" | "playsSound" | "keyboardKey" | "staticText" | "summaryElement"
|
|
5
|
+
| "updatesFrequently" | "searchField" | "startsMediaSession" | "adjustable" | "allowsDirectInteraction"
|
|
6
|
+
| "causesPageTurn" | "tabBar" | "header" | "webContent" | "textEntry";
|
|
7
|
+
|
|
8
|
+
export type AccessibilityAction = "press" | "increment" | "decrement" | "escape" | "menu"
|
|
9
|
+
| "scrollUp" | "scrollDown" | "scrollLeft" | "scrollRight" | "scrollToVisible";
|
|
10
|
+
|
|
11
|
+
export interface AccessibilityElement {
|
|
12
|
+
readonly role: string;
|
|
13
|
+
readonly class?: string;
|
|
14
|
+
readonly traits: readonly AccessibilityTrait[];
|
|
15
|
+
readonly label?: string;
|
|
16
|
+
readonly value?: string;
|
|
17
|
+
readonly identifier?: string;
|
|
18
|
+
readonly placeholder?: string;
|
|
19
|
+
readonly hint?: string;
|
|
20
|
+
readonly url?: string;
|
|
21
|
+
readonly frame?: Rect;
|
|
22
|
+
readonly point?: Point;
|
|
23
|
+
readonly partial?: true;
|
|
24
|
+
readonly offScreen?: true;
|
|
25
|
+
readonly inert?: true;
|
|
26
|
+
readonly remote?: true;
|
|
27
|
+
readonly disabled?: true;
|
|
28
|
+
readonly focused?: true;
|
|
29
|
+
readonly selected?: true;
|
|
30
|
+
readonly expanded?: boolean;
|
|
31
|
+
readonly secure?: true;
|
|
32
|
+
readonly selection?: [location: number, length: number];
|
|
33
|
+
readonly actions?: readonly string[];
|
|
34
|
+
readonly children: readonly AccessibilityElement[];
|
|
35
|
+
readonly parent?: AccessibilityElement;
|
|
36
|
+
find(selector: AccessibilitySelector): AccessibilityElement | null;
|
|
37
|
+
findAll(selector: AccessibilitySelector): AccessibilityElement[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AccessibilitySnapshot {
|
|
41
|
+
readonly app: string;
|
|
42
|
+
readonly time: number;
|
|
43
|
+
readonly root: AccessibilityElement;
|
|
44
|
+
readonly system?: AccessibilityElement;
|
|
45
|
+
find(selector: AccessibilitySelector): AccessibilityElement | null;
|
|
46
|
+
findAll(selector: AccessibilitySelector): AccessibilityElement[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TextField = "role" | "class" | "label" | "value" | "identifier" | "placeholder" | "hint" | "url" | "app";
|
|
50
|
+
type Flag = "disabled" | "focused" | "selected" | "expanded" | "secure" | "partial" | "offScreen" | "inert" | "remote";
|
|
51
|
+
|
|
52
|
+
export type AccessibilityQuery = { traits?: AccessibilityTrait[]; within?: AccessibilityQuery; parent?: AccessibilityQuery; has?: AccessibilityQuery; not?: AccessibilityQuery; index?: number }
|
|
53
|
+
& { [field in TextField]?: string | RegExp }
|
|
54
|
+
& { [field in Flag]?: boolean };
|
|
55
|
+
|
|
56
|
+
export type AccessibilitySelector =
|
|
57
|
+
| ({ traits?: AccessibilityTrait[]; within?: AccessibilitySelector | AccessibilityElement; parent?: AccessibilitySelector | AccessibilityElement; has?: AccessibilitySelector | AccessibilityElement; not?: AccessibilitySelector | AccessibilityElement; index?: number }
|
|
58
|
+
& { [field in TextField]?: string | RegExp }
|
|
59
|
+
& { [field in Flag]?: boolean })
|
|
60
|
+
| ((element: AccessibilityElement) => boolean);
|
|
61
|
+
|
|
62
|
+
export interface SnapshotOptions extends CallOptions {
|
|
63
|
+
readonly scope?: "screen" | "frontApp";
|
|
64
|
+
readonly full?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface LocatorOptions extends CallOptions {
|
|
68
|
+
/** 10000 when omitted; Infinity waits without a limit. */
|
|
69
|
+
readonly timeoutMs?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AccessibilityLocator {
|
|
73
|
+
tap(options?: LocatorOptions & TapOptions): Promise<void>;
|
|
74
|
+
/** Taps each element as it comes to match, once while it stays matched, and resolves when the app in front at the call leaves the front, at once if the query names only other apps. */
|
|
75
|
+
tapEach(options?: TapOptions): Promise<void>;
|
|
76
|
+
setText(text: string, options?: LocatorOptions): Promise<void>;
|
|
77
|
+
perform(action: AccessibilityAction | (string & {}), options?: LocatorOptions): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare function locator(query: AccessibilityQuery | readonly AccessibilityQuery[]): AccessibilityLocator;
|
|
81
|
+
export declare function snapshot(options?: SnapshotOptions): Promise<AccessibilitySnapshot>;
|
|
82
|
+
export declare function find(selector: AccessibilitySelector, options?: CallOptions): Promise<AccessibilityElement | null>;
|
|
83
|
+
export declare function tap(element: AccessibilityElement, options?: TapOptions): Promise<void>;
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
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 path from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import vm from "node:vm";
|
|
8
|
+
|
|
9
|
+
import { build } from "esbuild";
|
|
10
|
+
|
|
11
|
+
const { version, peerDependencies } = JSON.parse(fs.readFileSync(new URL("package.json", import.meta.url), "utf8"));
|
|
12
|
+
|
|
13
|
+
const appModules = [
|
|
14
|
+
"react",
|
|
15
|
+
"react/jsx-runtime",
|
|
16
|
+
"react-native",
|
|
17
|
+
"expo",
|
|
18
|
+
"expo-localization",
|
|
19
|
+
"@expo/ui",
|
|
20
|
+
"@expo/ui/*",
|
|
21
|
+
"@swipt/api",
|
|
22
|
+
"@swipt/api/swiplet",
|
|
23
|
+
"@swipt/api/file-system",
|
|
24
|
+
];
|
|
25
|
+
const extensionModules = [
|
|
26
|
+
"@swipt/api",
|
|
27
|
+
"@swipt/api/swiplet",
|
|
28
|
+
"@swipt/api/file-system",
|
|
29
|
+
"@swipt/api/accessibility",
|
|
30
|
+
"@swipt/api/input",
|
|
31
|
+
"@swipt/api/device",
|
|
32
|
+
"@swipt/api/screen",
|
|
33
|
+
"@swipt/api/image",
|
|
34
|
+
"expo-localization",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const idPattern = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
38
|
+
|
|
39
|
+
const storedSuffixes = [".webp", ".jxl", ".avif", ".heic", ".heif", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mov", ".m4a", ".mp3", ".aac", ".zip", ".gz", ".br", ".zst", ".xz"].join(":");
|
|
40
|
+
|
|
41
|
+
function loadProject(root) {
|
|
42
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
43
|
+
const config = manifest.swiplet ?? {};
|
|
44
|
+
if (typeof config.id !== "string" || !idPattern.test(config.id)) {
|
|
45
|
+
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');
|
|
46
|
+
}
|
|
47
|
+
if (typeof config.name !== "string" || config.name === "") {
|
|
48
|
+
throw new Error('"swiplet.name" in package.json must be a non-empty string');
|
|
49
|
+
}
|
|
50
|
+
const localizedNames = config.localizedNames ?? {};
|
|
51
|
+
if (localizedNames === null || typeof localizedNames !== "object" || !Object.values(localizedNames).every(name => typeof name === "string")) {
|
|
52
|
+
throw new Error('"swiplet.localizedNames" in package.json must map languages to names');
|
|
53
|
+
}
|
|
54
|
+
for (const field of ["description", "updateURL"]) {
|
|
55
|
+
if (config[field] !== undefined && typeof config[field] !== "string") {
|
|
56
|
+
throw new Error(`"swiplet.${field}" in package.json must be a string`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (config.versionCode !== undefined && (!Number.isInteger(config.versionCode) || config.versionCode < 1)) {
|
|
60
|
+
throw new Error('"swiplet.versionCode" in package.json must be a positive integer');
|
|
61
|
+
}
|
|
62
|
+
const entry = name => (config[name] === undefined ? undefined : path.resolve(root, config[name]));
|
|
63
|
+
return { root, manifest, config, localizedNames, app: entry("app"), extension: entry("extension") };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const typeImport = extension => ({
|
|
67
|
+
name: "swiplet-type-import",
|
|
68
|
+
setup(build) {
|
|
69
|
+
build.onLoad({ filter: /.*/ }, args => {
|
|
70
|
+
if (args.path !== extension) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return { errors: [{ text: `the interface imports ${path.basename(extension)} at run time; import its type with \`import type\`` }] };
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
async function bundle(project) {
|
|
79
|
+
const out = { app: "", extension: "", extensionMap: "" };
|
|
80
|
+
if (project.extension) {
|
|
81
|
+
const result = await build({
|
|
82
|
+
entryPoints: [project.extension],
|
|
83
|
+
outfile: path.join(project.root, "dist", project.config.id, "src", "extension.js"),
|
|
84
|
+
bundle: true,
|
|
85
|
+
format: "cjs",
|
|
86
|
+
platform: "neutral",
|
|
87
|
+
mainFields: ["module", "main"],
|
|
88
|
+
target: "es2022",
|
|
89
|
+
external: extensionModules,
|
|
90
|
+
sourcemap: "external",
|
|
91
|
+
sourcesContent: false,
|
|
92
|
+
write: false,
|
|
93
|
+
logLevel: "silent",
|
|
94
|
+
});
|
|
95
|
+
out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
|
|
96
|
+
out.extensionMap = result.outputFiles.find(file => file.path.endsWith(".map")).text;
|
|
97
|
+
}
|
|
98
|
+
if (project.app) {
|
|
99
|
+
const result = await build({
|
|
100
|
+
entryPoints: [project.app],
|
|
101
|
+
bundle: true,
|
|
102
|
+
format: "cjs",
|
|
103
|
+
platform: "neutral",
|
|
104
|
+
mainFields: ["react-native", "browser", "module", "main"],
|
|
105
|
+
conditions: ["react-native"],
|
|
106
|
+
resolveExtensions: [".ios.tsx", ".ios.ts", ".native.tsx", ".native.ts", ".tsx", ".ts", ".ios.jsx", ".ios.js", ".native.js", ".jsx", ".js", ".json"],
|
|
107
|
+
jsx: "automatic",
|
|
108
|
+
target: "es2020",
|
|
109
|
+
define: { "process.env.NODE_ENV": '"production"', __DEV__: "false" },
|
|
110
|
+
external: appModules,
|
|
111
|
+
write: false,
|
|
112
|
+
logLevel: "silent",
|
|
113
|
+
plugins: project.extension ? [typeImport(project.extension)] : [],
|
|
114
|
+
});
|
|
115
|
+
out.app = result.outputFiles[0].text;
|
|
116
|
+
}
|
|
117
|
+
if (out.app === "" && out.extension === "") {
|
|
118
|
+
throw new Error('package.json names neither "swiplet.app" nor "swiplet.extension"');
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const definitionKey = Symbol("definition");
|
|
124
|
+
|
|
125
|
+
function describeExtension(source) {
|
|
126
|
+
if (source === "") {
|
|
127
|
+
return { service: false, actions: [] };
|
|
128
|
+
}
|
|
129
|
+
const loading = name => {
|
|
130
|
+
const fail = () => {
|
|
131
|
+
throw new Error(`${name} is used while extension.js loads; use it inside run or an action`);
|
|
132
|
+
};
|
|
133
|
+
const get = (_, key) => (key === "__esModule" ? true : typeof key === "symbol" || key === "then" ? undefined : fail());
|
|
134
|
+
return new Proxy(fail, { get, apply: fail, construct: fail });
|
|
135
|
+
};
|
|
136
|
+
const modules = {
|
|
137
|
+
"@swipt/api": {},
|
|
138
|
+
"@swipt/api/swiplet": {
|
|
139
|
+
defineExtension: definition => ({ [definitionKey]: definition }),
|
|
140
|
+
defineAction: definition => definition,
|
|
141
|
+
actions: loading("actions"),
|
|
142
|
+
useService: loading("useService"),
|
|
143
|
+
},
|
|
144
|
+
"@swipt/api/file-system": {
|
|
145
|
+
get dataDirectory() {
|
|
146
|
+
throw new Error("dataDirectory is used while extension.js loads; use it inside run or an action");
|
|
147
|
+
},
|
|
148
|
+
get assets() {
|
|
149
|
+
throw new Error("assets is used while extension.js loads; use it inside run or an action");
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
"@swipt/api/accessibility": loading("accessibility"),
|
|
153
|
+
"@swipt/api/input": loading("input"),
|
|
154
|
+
"@swipt/api/device": loading("device"),
|
|
155
|
+
"@swipt/api/screen": loading("screen"),
|
|
156
|
+
"@swipt/api/image": loading("image"),
|
|
157
|
+
"expo-localization": { getLocales: () => [{ languageTag: "en", languageCode: "en" }], getCalendars: () => [] },
|
|
158
|
+
};
|
|
159
|
+
const ignored = () => 0;
|
|
160
|
+
const context = vm.createContext({
|
|
161
|
+
console, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, structuredClone, queueMicrotask, crypto,
|
|
162
|
+
AbortController, AbortSignal, Event, EventTarget, CustomEvent, DOMException, Headers, Request, Response, Blob,
|
|
163
|
+
fetch: loading("fetch"),
|
|
164
|
+
setTimeout: ignored, setInterval: ignored, clearTimeout: ignored, clearInterval: ignored,
|
|
165
|
+
navigator: { userAgent: "Swipt", language: "en", languages: ["en"] },
|
|
166
|
+
});
|
|
167
|
+
const factory = vm.runInContext(`(function (require, module, exports) {${source}\n})`, context, { filename: "extension.js" });
|
|
168
|
+
const module = { exports: {} };
|
|
169
|
+
factory(
|
|
170
|
+
specifier => {
|
|
171
|
+
if (!Object.hasOwn(modules, specifier)) {
|
|
172
|
+
throw new Error(`Swipt doesn't provide "${specifier}" to extensions; bundle it into extension.js`);
|
|
173
|
+
}
|
|
174
|
+
return modules[specifier];
|
|
175
|
+
},
|
|
176
|
+
module,
|
|
177
|
+
module.exports,
|
|
178
|
+
);
|
|
179
|
+
const definition = (module.exports.default ?? module.exports)?.[definitionKey];
|
|
180
|
+
if (definition === undefined) {
|
|
181
|
+
throw new Error("the extension's default export is not the result of defineExtension");
|
|
182
|
+
}
|
|
183
|
+
const schema = (name, value, side) => {
|
|
184
|
+
if (value === undefined) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const converter = value?.["~standard"]?.jsonSchema;
|
|
188
|
+
if (typeof converter?.[side] !== "function") {
|
|
189
|
+
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`);
|
|
190
|
+
}
|
|
191
|
+
return converter[side]({ target: "draft-2020-12" });
|
|
192
|
+
};
|
|
193
|
+
const actions = Object.entries(definition.actions ?? {}).map(([name, action]) => {
|
|
194
|
+
if (typeof action?.title !== "string" || action.title === "") {
|
|
195
|
+
throw new Error(`the action ${JSON.stringify(name)} needs a title`);
|
|
196
|
+
}
|
|
197
|
+
const expose = action.expose ?? [];
|
|
198
|
+
if (!Array.isArray(expose) || !expose.every(caller => caller === "shortcuts")) {
|
|
199
|
+
throw new Error(`the action ${JSON.stringify(name)} can only expose itself to "shortcuts"`);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
title: action.title,
|
|
204
|
+
...(action.description ? { description: action.description } : {}),
|
|
205
|
+
...(expose.length > 0 ? { expose } : {}),
|
|
206
|
+
...(action.input ? { input: schema(name, action.input, "input") } : {}),
|
|
207
|
+
...(action.output ? { output: schema(name, action.output, "output") } : {}),
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return { service: typeof definition.run === "function", actions };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function swipletManifest(project, output) {
|
|
214
|
+
const { service, actions } = describeExtension(output.extension);
|
|
215
|
+
const { config, localizedNames } = project;
|
|
216
|
+
return {
|
|
217
|
+
id: config.id,
|
|
218
|
+
name: config.name,
|
|
219
|
+
...(Object.keys(localizedNames).length > 0 ? { localizedNames } : {}),
|
|
220
|
+
...(config.description ? { description: config.description } : {}),
|
|
221
|
+
...(project.manifest.version ? { version: project.manifest.version } : {}),
|
|
222
|
+
...(config.versionCode ? { versionCode: config.versionCode } : {}),
|
|
223
|
+
...(config.icon ? { icon: config.icon } : {}),
|
|
224
|
+
...(config.updateURL ? { updateURL: config.updateURL } : {}),
|
|
225
|
+
...(output.app ? { interface: true } : {}),
|
|
226
|
+
...(service ? { service } : {}),
|
|
227
|
+
...(actions.length > 0 ? { actions } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sourceFiles = output => ({
|
|
232
|
+
...(output.app ? { "app.js": output.app } : {}),
|
|
233
|
+
...(output.extension ? { "extension.js": output.extension } : {}),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
async function buildCommand(root) {
|
|
237
|
+
const project = loadProject(root);
|
|
238
|
+
const output = await bundle(project);
|
|
239
|
+
const manifest = swipletManifest(project, output);
|
|
240
|
+
const dist = path.join(root, "dist");
|
|
241
|
+
const folder = path.join(dist, manifest.id);
|
|
242
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
243
|
+
fs.mkdirSync(path.join(folder, "src"), { recursive: true });
|
|
244
|
+
fs.writeFileSync(path.join(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
245
|
+
for (const [file, contents] of Object.entries(sourceFiles(output))) {
|
|
246
|
+
fs.writeFileSync(path.join(folder, "src", file), contents);
|
|
247
|
+
}
|
|
248
|
+
if (output.extensionMap) {
|
|
249
|
+
fs.writeFileSync(path.join(folder, "src", "extension.js.map"), output.extensionMap);
|
|
250
|
+
}
|
|
251
|
+
if (fs.existsSync(path.join(root, "icon.png"))) {
|
|
252
|
+
fs.copyFileSync(path.join(root, "icon.png"), path.join(folder, "icon.png"));
|
|
253
|
+
}
|
|
254
|
+
if (fs.existsSync(path.join(root, "assets"))) {
|
|
255
|
+
fs.cpSync(path.join(root, "assets"), path.join(folder, "assets"), { recursive: true, filter: source => !path.basename(source).startsWith(".") });
|
|
256
|
+
}
|
|
257
|
+
const archive = path.join(dist, `${manifest.id}.swiplet`);
|
|
258
|
+
fs.rmSync(archive, { force: true });
|
|
259
|
+
fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
|
|
260
|
+
execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, manifest.id, "-x", "*.map"], { cwd: dist });
|
|
261
|
+
console.log(path.relative(process.cwd(), archive));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const manifestEntry = /^(?:[^/]+\/)?manifest\.json$/;
|
|
265
|
+
|
|
266
|
+
function sourceCommand(files, base, output) {
|
|
267
|
+
if (!base) {
|
|
268
|
+
throw new UsageError("source needs --url, the address the swiplets are published under");
|
|
269
|
+
}
|
|
270
|
+
let prefix;
|
|
271
|
+
try {
|
|
272
|
+
prefix = new URL(base.endsWith("/") ? base : `${base}/`);
|
|
273
|
+
} catch {
|
|
274
|
+
throw new UsageError(`--url ${JSON.stringify(base)} is not an absolute URL`);
|
|
275
|
+
}
|
|
276
|
+
const swiplets = new Map();
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
const entry = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }).split("\n").filter(name => manifestEntry.test(name)).sort((a, b) => a.length - b.length)[0];
|
|
279
|
+
if (entry === undefined) {
|
|
280
|
+
throw new Error(`${file} has no manifest.json`);
|
|
281
|
+
}
|
|
282
|
+
const manifest = JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
|
|
283
|
+
if (typeof manifest.id !== "string" || !idPattern.test(manifest.id)) {
|
|
284
|
+
throw new Error(`${file} has no valid swiplet id in its manifest`);
|
|
285
|
+
}
|
|
286
|
+
if (swiplets.has(manifest.id)) {
|
|
287
|
+
throw new Error(`${manifest.id} is both ${swiplets.get(manifest.id).file} and ${file}`);
|
|
288
|
+
}
|
|
289
|
+
const data = fs.readFileSync(file);
|
|
290
|
+
swiplets.set(manifest.id, {
|
|
291
|
+
file,
|
|
292
|
+
entry: { ...manifest, url: new URL(path.basename(file), prefix).href, size: data.length, sha256: createHash("sha256").update(data).digest("hex") },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const listed = `${JSON.stringify({ swiplets: [...swiplets.keys()].sort().map(id => swiplets.get(id).entry) }, null, 2)}\n`;
|
|
296
|
+
if (output) {
|
|
297
|
+
fs.writeFileSync(output, listed);
|
|
298
|
+
} else {
|
|
299
|
+
process.stdout.write(listed);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const idOf = name => name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "").slice(0, 128).replace(/[._-]+$/, "") || "swiplet";
|
|
304
|
+
|
|
305
|
+
const templates = {
|
|
306
|
+
"package.json": name =>
|
|
307
|
+
`${JSON.stringify(
|
|
308
|
+
{
|
|
309
|
+
name: idOf(name),
|
|
310
|
+
version: "0.1.0",
|
|
311
|
+
private: true,
|
|
312
|
+
swiplet: {
|
|
313
|
+
id: idOf(name),
|
|
314
|
+
name,
|
|
315
|
+
versionCode: 1,
|
|
316
|
+
icon: { color: "blue", symbol: "sparkles" },
|
|
317
|
+
app: "./src/App.tsx",
|
|
318
|
+
extension: "./src/extension.ts",
|
|
319
|
+
},
|
|
320
|
+
scripts: { build: "swiplet build", check: "tsc" },
|
|
321
|
+
dependencies: { zod: "^4.6.7" },
|
|
322
|
+
devDependencies: {
|
|
323
|
+
"@swipt/api": version,
|
|
324
|
+
typescript: "^7.0.2",
|
|
325
|
+
...Object.fromEntries(["@expo/ui", "@types/react", "react"].map(name => [name, peerDependencies[name]])),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
null,
|
|
329
|
+
2,
|
|
330
|
+
)}\n`,
|
|
331
|
+
"tsconfig.json": () =>
|
|
332
|
+
`${JSON.stringify(
|
|
333
|
+
{
|
|
334
|
+
compilerOptions: {
|
|
335
|
+
target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", lib: ["ES2023", "DOM"],
|
|
336
|
+
strict: true, noEmit: true, skipLibCheck: true,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
null,
|
|
340
|
+
2,
|
|
341
|
+
)}\n`,
|
|
342
|
+
".gitignore": () => "node_modules/\ndist/\n",
|
|
343
|
+
"src/extension.ts": () => `import * as device from "@swipt/api/device";
|
|
344
|
+
import { defineAction, defineExtension } from "@swipt/api/swiplet";
|
|
345
|
+
|
|
346
|
+
export default defineExtension({
|
|
347
|
+
actions: {
|
|
348
|
+
frontApp: defineAction({
|
|
349
|
+
title: "Find the App in Front",
|
|
350
|
+
run: (_, { signal }) => device.foregroundApplication.get({ signal }),
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
`,
|
|
355
|
+
"src/App.tsx": () => `import { Button, Form, Host, Section, Text } from "@expo/ui/swift-ui";
|
|
356
|
+
import { useState } from "react";
|
|
357
|
+
import { actions } from "@swipt/api/swiplet";
|
|
358
|
+
|
|
359
|
+
import type extension from "./extension";
|
|
360
|
+
|
|
361
|
+
const { frontApp } = actions<typeof extension>();
|
|
362
|
+
|
|
363
|
+
export default function App() {
|
|
364
|
+
const [app, setApp] = useState("");
|
|
365
|
+
return (
|
|
366
|
+
<Host style={{ flex: 1 }}>
|
|
367
|
+
<Form>
|
|
368
|
+
<Section>
|
|
369
|
+
<Button label="Find the App in Front" onPress={async () => setApp(await frontApp())} />
|
|
370
|
+
{app === "" ? null : <Text>{app}</Text>}
|
|
371
|
+
</Section>
|
|
372
|
+
</Form>
|
|
373
|
+
</Host>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
`,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const quote = value => (/^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`);
|
|
380
|
+
|
|
381
|
+
function createCommand(directory) {
|
|
382
|
+
const root = path.resolve(directory);
|
|
383
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length > 0) {
|
|
384
|
+
throw new Error(`${directory} is not empty`);
|
|
385
|
+
}
|
|
386
|
+
for (const [file, template] of Object.entries(templates)) {
|
|
387
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
388
|
+
fs.writeFileSync(path.join(root, file), template(path.basename(root)));
|
|
389
|
+
}
|
|
390
|
+
const manager = process.env.npm_config_user_agent?.split("/")[0] ?? "npm";
|
|
391
|
+
console.log(`cd ${quote(directory)} && ${manager} install && ${manager} run build`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const usage = `usage: swiplet create <directory>
|
|
395
|
+
swiplet build
|
|
396
|
+
swiplet source --url <address> [--output <file>] <swiplet>...
|
|
397
|
+
|
|
398
|
+
create make a swiplet project in a new or empty directory
|
|
399
|
+
build bundle the project in this directory into dist/<swiplet.id>.swiplet
|
|
400
|
+
source list built swiplets in a source that Swipt installs and updates from
|
|
401
|
+
|
|
402
|
+
Type-check a project with its "check" script.`;
|
|
403
|
+
|
|
404
|
+
const commands = { create: 1, build: 0, source: -1 };
|
|
405
|
+
|
|
406
|
+
class UsageError extends Error {}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
let parsed;
|
|
410
|
+
try {
|
|
411
|
+
parsed = parseArgs({ allowPositionals: true, options: { help: { type: "boolean", short: "h" }, url: { type: "string" }, output: { type: "string" } } });
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throw new UsageError(error.message);
|
|
414
|
+
}
|
|
415
|
+
const [command, ...rest] = parsed.positionals;
|
|
416
|
+
if (parsed.values.help) {
|
|
417
|
+
console.log(usage);
|
|
418
|
+
} else if (command === undefined) {
|
|
419
|
+
throw new UsageError("a command is missing");
|
|
420
|
+
} else if (!Object.hasOwn(commands, command)) {
|
|
421
|
+
throw new UsageError(`unknown command ${JSON.stringify(command)}`);
|
|
422
|
+
} else if (commands[command] < 0 ? rest.length === 0 : rest.length !== commands[command]) {
|
|
423
|
+
throw new UsageError(
|
|
424
|
+
commands[command] === 0 ? `${command} takes no arguments` : commands[command] < 0 ? `${command} takes one or more swiplets` : `${command} takes one directory`,
|
|
425
|
+
);
|
|
426
|
+
} else if (command === "create") {
|
|
427
|
+
createCommand(rest[0]);
|
|
428
|
+
} else if (command === "source") {
|
|
429
|
+
sourceCommand(rest, parsed.values.url, parsed.values.output);
|
|
430
|
+
} else {
|
|
431
|
+
await buildCommand(process.cwd());
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof UsageError) {
|
|
435
|
+
console.error(`error: ${error.message}\n\n${usage}`);
|
|
436
|
+
process.exitCode = 2;
|
|
437
|
+
} else {
|
|
438
|
+
console.error(error.message);
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
}
|
|
441
|
+
}
|
package/device.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CallOptions, ObservableState } from "./index";
|
|
2
|
+
import type { FileHandle } from "./file-system";
|
|
3
|
+
|
|
4
|
+
export type DisplayState = "off" | "dimmed" | "inactiveOn" | "activeOn";
|
|
5
|
+
|
|
6
|
+
export interface DeviceInfo {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly model: string;
|
|
9
|
+
readonly os: string;
|
|
10
|
+
readonly build: string;
|
|
11
|
+
readonly language: string;
|
|
12
|
+
readonly screen: {
|
|
13
|
+
readonly width: number;
|
|
14
|
+
readonly height: number;
|
|
15
|
+
readonly scale: number;
|
|
16
|
+
readonly orientation: "portrait" | "portraitUpsideDown" | "landscape";
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface InstalledApplication {
|
|
21
|
+
readonly bundleId: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly version?: string;
|
|
24
|
+
readonly build?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ApplicationStateEvent {
|
|
28
|
+
readonly type: "launch" | "foreground" | "background";
|
|
29
|
+
readonly bundleId: string;
|
|
30
|
+
readonly pid: number;
|
|
31
|
+
readonly time: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare function info(options?: CallOptions): Promise<DeviceInfo>;
|
|
35
|
+
export declare const foregroundApplication: ObservableState<string>;
|
|
36
|
+
export declare const locked: ObservableState<boolean>;
|
|
37
|
+
export declare const displayState: ObservableState<DisplayState>;
|
|
38
|
+
export declare function unlock(options?: CallOptions): Promise<void>;
|
|
39
|
+
|
|
40
|
+
export declare function applicationEvents(options?: CallOptions): AsyncIterable<ApplicationStateEvent>;
|
|
41
|
+
export declare function installedApplications(options?: CallOptions): Promise<InstalledApplication[]>;
|
|
42
|
+
export declare function applicationIcon(bundleId: string, options?: CallOptions): Promise<FileHandle>;
|
|
43
|
+
export declare function launchApplication(bundleId: string, options?: CallOptions & { url?: string; restart?: boolean }): Promise<void>;
|
|
44
|
+
|
|
45
|
+
export declare function pasteboard(options?: CallOptions): Promise<string | null>;
|
|
46
|
+
export declare function setPasteboard(text: string | null, options?: CallOptions): Promise<void>;
|
|
47
|
+
|
|
48
|
+
export declare function simulateLocation(latitude: number, longitude: number, options?: CallOptions): Promise<void>;
|
|
49
|
+
export declare function clearSimulatedLocation(options?: CallOptions): Promise<void>;
|
package/device.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const outside = () => {
|
|
2
|
+
throw new Error("@swipt/api/device runs inside Swipt; build the swiplet with `swiplet build`");
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const info = outside;
|
|
6
|
+
export const foregroundApplication = undefined;
|
|
7
|
+
export const locked = undefined;
|
|
8
|
+
export const displayState = undefined;
|
|
9
|
+
export const unlock = outside;
|
|
10
|
+
export const applicationEvents = outside;
|
|
11
|
+
export const installedApplications = outside;
|
|
12
|
+
export const applicationIcon = outside;
|
|
13
|
+
export const launchApplication = outside;
|
|
14
|
+
export const pasteboard = outside;
|
|
15
|
+
export const setPasteboard = outside;
|
|
16
|
+
export const simulateLocation = outside;
|
|
17
|
+
export const clearSimulatedLocation = outside;
|
package/file-system.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const dataDirectory: DirectoryHandle;
|
|
2
|
+
export declare const assets: DirectoryHandle;
|
|
3
|
+
|
|
4
|
+
export interface FileStat {
|
|
5
|
+
readonly size: number;
|
|
6
|
+
readonly modified: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DirectoryHandle {
|
|
10
|
+
readonly uri: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
file(path: string): FileHandle;
|
|
13
|
+
directory(path: string): DirectoryHandle;
|
|
14
|
+
exists(): Promise<boolean>;
|
|
15
|
+
create(): Promise<void>;
|
|
16
|
+
list(): Promise<(FileHandle | DirectoryHandle)[]>;
|
|
17
|
+
delete(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FileHandle {
|
|
21
|
+
readonly uri: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
stat(): Promise<FileStat | null>;
|
|
24
|
+
text(): Promise<string>;
|
|
25
|
+
bytes(): Promise<Uint8Array>;
|
|
26
|
+
write(data: string | Uint8Array | Blob): Promise<void>;
|
|
27
|
+
copy(to: FileHandle): Promise<void>;
|
|
28
|
+
move(to: FileHandle): Promise<void>;
|
|
29
|
+
delete(): Promise<void>;
|
|
30
|
+
}
|
package/file-system.mjs
ADDED
package/image.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { CallOptions, Point, Quad, Rect } from "./index";
|
|
2
|
+
import type { FileHandle } from "./file-system";
|
|
3
|
+
|
|
4
|
+
export type Color = `#${string}` | readonly [red: number, green: number, blue: number];
|
|
5
|
+
export type ImageFormat = "png" | "jpeg" | "heic";
|
|
6
|
+
|
|
7
|
+
export interface TemplateMatch {
|
|
8
|
+
readonly frame: Rect;
|
|
9
|
+
readonly center: Point;
|
|
10
|
+
readonly score: number;
|
|
11
|
+
readonly scale: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TemplateMatchOptions extends CallOptions {
|
|
15
|
+
readonly threshold?: number;
|
|
16
|
+
readonly region?: Rect;
|
|
17
|
+
readonly scales?: readonly number[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ColorCheck {
|
|
21
|
+
readonly point: Point;
|
|
22
|
+
readonly color: Color;
|
|
23
|
+
readonly tolerance?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ConnectedComponent {
|
|
27
|
+
readonly frame: Rect;
|
|
28
|
+
readonly pixels: number;
|
|
29
|
+
readonly centroid: Point;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ImageStats {
|
|
33
|
+
readonly mean: readonly number[];
|
|
34
|
+
readonly min: readonly number[];
|
|
35
|
+
readonly max: readonly number[];
|
|
36
|
+
readonly deviation: readonly number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TextRecognitionOptions extends CallOptions {
|
|
40
|
+
readonly region?: Rect;
|
|
41
|
+
readonly languages?: readonly string[];
|
|
42
|
+
readonly detectLanguage?: boolean;
|
|
43
|
+
readonly level?: "fast" | "accurate";
|
|
44
|
+
readonly words?: readonly string[];
|
|
45
|
+
readonly correction?: boolean;
|
|
46
|
+
readonly minimumHeight?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RecognizedTextBox {
|
|
50
|
+
readonly quad: Quad;
|
|
51
|
+
readonly frame: Rect;
|
|
52
|
+
readonly center: Point;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RecognizedTextCandidate {
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly confidence: number;
|
|
58
|
+
boundingBox(start: number, end: number): RecognizedTextBox | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RecognizedTextLine extends RecognizedTextCandidate, RecognizedTextBox {
|
|
62
|
+
readonly candidates: readonly RecognizedTextCandidate[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ImageDifference {
|
|
66
|
+
readonly ratio: number;
|
|
67
|
+
readonly frames: readonly Rect[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DetectedBarcode {
|
|
71
|
+
readonly payload: string | null;
|
|
72
|
+
readonly symbology: string;
|
|
73
|
+
readonly quad: Quad;
|
|
74
|
+
readonly frame: Rect;
|
|
75
|
+
readonly center: Point;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RasterImage {
|
|
79
|
+
readonly width: number;
|
|
80
|
+
readonly height: number;
|
|
81
|
+
readonly scale: number;
|
|
82
|
+
readonly frame: Rect;
|
|
83
|
+
readonly time: number | null;
|
|
84
|
+
|
|
85
|
+
crop(rect: Rect): RasterImage;
|
|
86
|
+
resize(scale: number): RasterImage;
|
|
87
|
+
resize(size: { readonly width?: number; readonly height?: number }): RasterImage;
|
|
88
|
+
rotate(degrees: number): RasterImage;
|
|
89
|
+
grayscale(): RasterImage;
|
|
90
|
+
threshold(level?: number | "otsu"): RasterImage;
|
|
91
|
+
mask(color: Color, options?: { readonly tolerance?: number }): RasterImage;
|
|
92
|
+
mask(range: readonly [low: Color, high: Color]): RasterImage;
|
|
93
|
+
invert(): RasterImage;
|
|
94
|
+
dilate(radius?: number): RasterImage;
|
|
95
|
+
erode(radius?: number): RasterImage;
|
|
96
|
+
|
|
97
|
+
encode(format?: ImageFormat, options?: CallOptions & { readonly quality?: number }): Promise<Uint8Array>;
|
|
98
|
+
save(file: FileHandle | string, options?: CallOptions & { readonly format?: ImageFormat; readonly quality?: number }): Promise<FileHandle>;
|
|
99
|
+
pixel(point: Point, options?: CallOptions): Promise<[red: number, green: number, blue: number, alpha: number]>;
|
|
100
|
+
stats(options?: CallOptions & { readonly region?: Rect }): Promise<ImageStats>;
|
|
101
|
+
release(): void;
|
|
102
|
+
|
|
103
|
+
find(template: RasterImage, options?: TemplateMatchOptions): Promise<TemplateMatch | null>;
|
|
104
|
+
findAll(template: RasterImage, options?: TemplateMatchOptions & { readonly limit?: number; readonly overlap?: number }): Promise<TemplateMatch[]>;
|
|
105
|
+
matchColors(checks: readonly ColorCheck[], options?: CallOptions): Promise<boolean[]>;
|
|
106
|
+
components(options?: CallOptions & { readonly minimumPixels?: number }): Promise<ConnectedComponent[]>;
|
|
107
|
+
recognizeText(options?: TextRecognitionOptions): Promise<RecognizedTextLine[]>;
|
|
108
|
+
diff(other: RasterImage, options?: CallOptions & { readonly threshold?: number }): Promise<ImageDifference>;
|
|
109
|
+
distance(other: RasterImage, options?: CallOptions): Promise<number>;
|
|
110
|
+
nearest(candidates: readonly RasterImage[], options?: CallOptions): Promise<{ readonly index: number; readonly distance: number }>;
|
|
111
|
+
detectBarcodes(options?: CallOptions): Promise<DetectedBarcode[]>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export declare function decode(data: Uint8Array | FileHandle | string, options?: CallOptions & { readonly scale?: number }): Promise<RasterImage>;
|
package/image.mjs
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CallOptions {
|
|
2
|
+
readonly signal?: AbortSignal;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ObservableState<T> {
|
|
6
|
+
get(options?: CallOptions): Promise<T>;
|
|
7
|
+
values(options?: CallOptions): AsyncIterable<T>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Point = [x: number, y: number];
|
|
11
|
+
export type Rect = [x: number, y: number, width: number, height: number];
|
|
12
|
+
export type Quad = [topLeft: Point, topRight: Point, bottomRight: Point, bottomLeft: Point];
|
|
13
|
+
|
|
14
|
+
export type ErrorName = "AbortError" | "TimeoutError" | "NotFoundError" | "UnavailableError";
|
|
15
|
+
|
|
16
|
+
export type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
|
|
17
|
+
|
|
18
|
+
export type Jsonify<T> = T extends string | number | boolean | null ? T
|
|
19
|
+
: T extends undefined | symbol | ((...args: any[]) => unknown) ? never
|
|
20
|
+
: T extends { toJSON(): infer J } ? Jsonify<J>
|
|
21
|
+
: T extends readonly (infer U)[] ? Jsonify<U>[]
|
|
22
|
+
: T extends object ? { [K in keyof T]: Jsonify<Exclude<T[K], undefined>> }
|
|
23
|
+
: never;
|
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/input.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CallOptions, Point } from "./index";
|
|
2
|
+
|
|
3
|
+
export interface TapOptions extends CallOptions {
|
|
4
|
+
readonly count?: number;
|
|
5
|
+
readonly holdMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type KeyboardKey = "return" | "backspace" | "forwardDelete" | "tab" | "escape" | "space" | "left" | "right" | "up" | "down"
|
|
9
|
+
| "home" | "end" | "pageUp" | "pageDown" | `f${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`;
|
|
10
|
+
export type KeyboardModifier = "cmd" | "shift" | "ctrl" | "alt";
|
|
11
|
+
export type KeyCombination = KeyboardKey | `${KeyboardModifier}+${KeyboardKey}` | (string & {});
|
|
12
|
+
export type TouchSample = [x: number, y: number, ms: number];
|
|
13
|
+
|
|
14
|
+
export declare function tap(point: Point, options?: TapOptions): Promise<void>;
|
|
15
|
+
export declare function swipe(from: Point, to: Point, options?: CallOptions & { durationMs?: number }): Promise<void>;
|
|
16
|
+
export declare function drag(from: Point, to: Point, options?: CallOptions & { durationMs?: number; pressMs?: number }): Promise<void>;
|
|
17
|
+
export declare function pinch(center: Point, scale: number, options?: CallOptions & { durationMs?: number }): Promise<void>;
|
|
18
|
+
export declare function touch(fingers: readonly (readonly TouchSample[])[], options?: CallOptions): Promise<void>;
|
|
19
|
+
export declare function pressKey(combination: KeyCombination, options?: CallOptions): Promise<void>;
|
|
20
|
+
export declare function pressHome(options?: CallOptions): Promise<void>;
|
|
21
|
+
export declare function openAppSwitcher(options?: CallOptions): Promise<void>;
|
|
22
|
+
export declare function pressPower(options?: CallOptions): Promise<void>;
|
|
23
|
+
export declare function pressVolumeUp(options?: CallOptions): Promise<void>;
|
|
24
|
+
export declare function pressVolumeDown(options?: CallOptions): Promise<void>;
|
package/input.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const outside = () => {
|
|
2
|
+
throw new Error("@swipt/api/input runs inside Swipt; build the swiplet with `swiplet build`");
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const tap = outside;
|
|
6
|
+
export const swipe = outside;
|
|
7
|
+
export const drag = outside;
|
|
8
|
+
export const pinch = outside;
|
|
9
|
+
export const touch = outside;
|
|
10
|
+
export const pressKey = outside;
|
|
11
|
+
export const pressHome = outside;
|
|
12
|
+
export const openAppSwitcher = outside;
|
|
13
|
+
export const pressPower = outside;
|
|
14
|
+
export const pressVolumeUp = outside;
|
|
15
|
+
export const pressVolumeDown = outside;
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swipt/api",
|
|
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"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"swiplet": "cli.mjs"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"default": "./index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./swiplet": {
|
|
20
|
+
"types": "./swiplet.d.ts",
|
|
21
|
+
"default": "./swiplet.mjs"
|
|
22
|
+
},
|
|
23
|
+
"./file-system": {
|
|
24
|
+
"types": "./file-system.d.ts",
|
|
25
|
+
"default": "./file-system.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./accessibility": {
|
|
28
|
+
"types": "./accessibility.d.ts",
|
|
29
|
+
"default": "./accessibility.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./input": {
|
|
32
|
+
"types": "./input.d.ts",
|
|
33
|
+
"default": "./input.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./device": {
|
|
36
|
+
"types": "./device.d.ts",
|
|
37
|
+
"default": "./device.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./screen": {
|
|
40
|
+
"types": "./screen.d.ts",
|
|
41
|
+
"default": "./screen.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./image": {
|
|
44
|
+
"types": "./image.d.ts",
|
|
45
|
+
"default": "./image.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"cli.mjs",
|
|
51
|
+
"*.d.ts",
|
|
52
|
+
"*.mjs"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@standard-schema/spec": "^1.1.0",
|
|
59
|
+
"esbuild": "^0.28.2"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@expo/ui": "~58.0.25",
|
|
63
|
+
"@types/react": "~19.3.0",
|
|
64
|
+
"expo": "~58.0.21",
|
|
65
|
+
"expo-localization": "~58.0.5",
|
|
66
|
+
"react": "19.3.0",
|
|
67
|
+
"react-native": "0.88.1"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@expo/ui": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@types/react": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"expo": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"expo-localization": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"react-native": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/repl.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
const accessibility: typeof import("@swipt/api/accessibility");
|
|
3
|
+
const input: typeof import("@swipt/api/input");
|
|
4
|
+
const device: typeof import("@swipt/api/device");
|
|
5
|
+
const screen: typeof import("@swipt/api/screen");
|
|
6
|
+
const image: typeof import("@swipt/api/image");
|
|
7
|
+
const dialog: import("@swipt/api/swiplet").IntentDialog;
|
|
8
|
+
|
|
9
|
+
function show(image: import("@swipt/api/image").RasterImage): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {};
|
package/screen.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CallOptions, ObservableState, Rect } from "./index";
|
|
2
|
+
import type { FileHandle } from "./file-system";
|
|
3
|
+
import type { ImageDifference, RasterImage } from "./image";
|
|
4
|
+
|
|
5
|
+
export interface WaitOptions extends CallOptions {
|
|
6
|
+
readonly region?: Rect;
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
readonly threshold?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ScreenChange {
|
|
12
|
+
readonly time: number;
|
|
13
|
+
readonly end: number;
|
|
14
|
+
readonly frames: readonly Rect[];
|
|
15
|
+
readonly app: string | null;
|
|
16
|
+
readonly before: RasterImage;
|
|
17
|
+
readonly after: RasterImage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TimelineEvent =
|
|
21
|
+
| { readonly time: number; readonly type: "operation"; readonly name: string; readonly arguments?: readonly unknown[]; readonly durationMs?: number; readonly error?: string }
|
|
22
|
+
| { readonly time: number; readonly type: "action"; readonly swiplet: string; readonly action: string; readonly caller: "app" | "shortcuts" | "api" }
|
|
23
|
+
| { readonly time: number; readonly type: "foreground"; readonly app: string }
|
|
24
|
+
| { readonly time: number; readonly type: "log"; readonly swiplet: string; readonly level: "error" | "warn" | "info" | "debug"; readonly message: string };
|
|
25
|
+
|
|
26
|
+
export interface ScreenRecording {
|
|
27
|
+
readonly file: FileHandle;
|
|
28
|
+
readonly start: number | null;
|
|
29
|
+
readonly duration: number;
|
|
30
|
+
readonly width: number;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
readonly scale: number;
|
|
33
|
+
readonly changes: readonly ScreenChange[];
|
|
34
|
+
readonly events: readonly TimelineEvent[];
|
|
35
|
+
|
|
36
|
+
frame(time: number): RasterImage;
|
|
37
|
+
frames(times: readonly number[] | { readonly everyMs: number; readonly from?: number; readonly to?: number }): RasterImage[];
|
|
38
|
+
grid(options?: CallOptions & { readonly times?: readonly number[]; readonly columns?: number; readonly width?: number }): Promise<RasterImage>;
|
|
39
|
+
diff(from: number, to: number, options?: CallOptions & { readonly threshold?: number }): Promise<ImageDifference>;
|
|
40
|
+
export(options?: CallOptions & {
|
|
41
|
+
readonly file?: FileHandle | string;
|
|
42
|
+
readonly from?: number;
|
|
43
|
+
readonly to?: number;
|
|
44
|
+
readonly region?: Rect;
|
|
45
|
+
readonly scale?: number;
|
|
46
|
+
readonly format?: "mov" | "mp4" | "gif";
|
|
47
|
+
readonly frameRate?: number;
|
|
48
|
+
}): Promise<FileHandle>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RecordingSession {
|
|
52
|
+
readonly file: FileHandle;
|
|
53
|
+
readonly start: number;
|
|
54
|
+
stop(options?: CallOptions): Promise<ScreenRecording>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export declare function screenshot(options?: CallOptions): Promise<RasterImage>;
|
|
58
|
+
export declare const frames: ObservableState<RasterImage>;
|
|
59
|
+
export declare function waitFor(predicate: (frame: RasterImage) => boolean | Promise<boolean>, options?: CallOptions & { readonly timeoutMs?: number }): Promise<RasterImage>;
|
|
60
|
+
export declare function waitForChange(options?: WaitOptions & { readonly from?: RasterImage }): Promise<RasterImage>;
|
|
61
|
+
export declare function waitForStable(options?: WaitOptions & { readonly durationMs?: number }): Promise<RasterImage>;
|
|
62
|
+
export declare function record(options?: CallOptions & { readonly file?: FileHandle | string }): Promise<RecordingSession>;
|
|
63
|
+
export declare function openRecording(file: FileHandle | string, options?: CallOptions): Promise<ScreenRecording>;
|
package/screen.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const outside = () => {
|
|
2
|
+
throw new Error("@swipt/api/screen runs inside Swipt; build the swiplet with `swiplet build`");
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const screenshot = outside;
|
|
6
|
+
export const frames = undefined;
|
|
7
|
+
export const waitFor = outside;
|
|
8
|
+
export const waitForChange = outside;
|
|
9
|
+
export const waitForStable = outside;
|
|
10
|
+
export const record = outside;
|
|
11
|
+
export const openRecording = outside;
|
package/swiplet.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
import type { CallOptions, Jsonify } from "./index";
|
|
4
|
+
|
|
5
|
+
export type IntentChoiceStyle = "default" | "destructive" | "cancel";
|
|
6
|
+
|
|
7
|
+
export type ConfirmationActionName = "add" | "addData" | "book" | "buy" | "call" | "checkIn" | "continue" | "create" | "do" | "download"
|
|
8
|
+
| "filter" | "find" | "get" | "go" | "log" | "open" | "order" | "pay" | "play" | "playSound" | "post" | "request" | "run"
|
|
9
|
+
| "search" | "send" | "set" | "share" | "start" | "startNavigation" | "toggle" | "turnOff" | "turnOn" | "view";
|
|
10
|
+
|
|
11
|
+
export interface Person {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly handle?: { type: "phoneNumber" | "emailAddress"; value: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Placemark {
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
readonly coordinate?: [latitude: number, longitude: number];
|
|
19
|
+
readonly address?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IntentDialog {
|
|
23
|
+
requestChoice<T extends string | { title: string; style?: IntentChoiceStyle }>(choices: T[], prompt: string, options?: CallOptions): Promise<T>;
|
|
24
|
+
requestConfirmation(prompt: string, options?: CallOptions & { actionName?: ConfirmationActionName | { accept: string; deny: string; destructive?: boolean } }): Promise<void>;
|
|
25
|
+
requestValue(type: "text", prompt?: string, options?: CallOptions & { multiline?: boolean; literal?: boolean }): Promise<string>;
|
|
26
|
+
requestValue(type: "integer", prompt?: string, options?: CallOptions & { stepper?: boolean; min?: number; max?: number }): Promise<number>;
|
|
27
|
+
requestValue(type: "number", prompt?: string, options?: CallOptions & { min?: number; max?: number }): Promise<number>;
|
|
28
|
+
requestValue(type: "date" | "time" | "dateTime", prompt?: string, options?: CallOptions): Promise<Date>;
|
|
29
|
+
requestValue(type: "file", prompt?: string, options?: CallOptions): Promise<{ name: string; type: string; bytes: Uint8Array }>;
|
|
30
|
+
requestValue(type: "person", prompt?: string, options?: CallOptions): Promise<Person>;
|
|
31
|
+
requestValue(type: "placemark", prompt?: string, options?: CallOptions): Promise<Placemark>;
|
|
32
|
+
requestDisambiguation(values: string[], prompt: string, options?: CallOptions): Promise<string>;
|
|
33
|
+
requestValueConfirmation(value: string, prompt: string, options?: CallOptions): Promise<boolean>;
|
|
34
|
+
continueInForeground(prompt?: string, options?: CallOptions & { alwaysConfirm?: boolean }): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ActionCaller = "app" | "shortcuts" | "api";
|
|
38
|
+
|
|
39
|
+
export type Schema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & StandardJSONSchemaV1<Input, Output>;
|
|
40
|
+
|
|
41
|
+
declare const types: unique symbol;
|
|
42
|
+
|
|
43
|
+
export interface Action<Input = void, Output = void> {
|
|
44
|
+
readonly [types]: { input: Input; output: Output };
|
|
45
|
+
readonly title: string;
|
|
46
|
+
readonly description?: string;
|
|
47
|
+
readonly expose: readonly "shortcuts"[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Extension<Actions extends Record<string, Action<any, any>> = {}, HasRun extends boolean = boolean> {
|
|
51
|
+
readonly [types]: { hasRun: HasRun };
|
|
52
|
+
readonly actions: Actions;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RunContext {
|
|
56
|
+
readonly signal: AbortSignal;
|
|
57
|
+
readonly dialog: IntentDialog;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ActionContext extends RunContext {
|
|
61
|
+
readonly caller: ActionCaller;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type Received<S> = S extends Schema ? StandardSchemaV1.InferOutput<S> : void;
|
|
65
|
+
type Sent<S> = S extends Schema ? StandardSchemaV1.InferInput<S> : void;
|
|
66
|
+
type Returned<S, R> = S extends Schema ? StandardSchemaV1.InferInput<S> : R;
|
|
67
|
+
type Delivered<S, R> = S extends Schema ? StandardSchemaV1.InferOutput<S> : R;
|
|
68
|
+
|
|
69
|
+
export declare function defineAction<I extends Schema | undefined = undefined, O extends Schema | undefined = undefined, R = void>(definition: {
|
|
70
|
+
readonly title: string;
|
|
71
|
+
readonly description?: string;
|
|
72
|
+
readonly input?: I;
|
|
73
|
+
readonly output?: O;
|
|
74
|
+
readonly expose?: readonly "shortcuts"[];
|
|
75
|
+
run(input: Received<I>, context: ActionContext): Returned<O, R> | Promise<Returned<O, R>>;
|
|
76
|
+
}): Action<Sent<I>, Jsonify<Awaited<Delivered<O, R>>>>;
|
|
77
|
+
|
|
78
|
+
export declare function defineExtension<Actions extends Record<string, Action<any, any>> = {}>(definition: {
|
|
79
|
+
run(context: RunContext): Promise<void>;
|
|
80
|
+
readonly actions?: Actions;
|
|
81
|
+
}): Extension<Actions, true>;
|
|
82
|
+
export declare function defineExtension<Actions extends Record<string, Action<any, any>> = {}>(definition: {
|
|
83
|
+
readonly actions?: Actions;
|
|
84
|
+
}): Extension<Actions, false>;
|
|
85
|
+
|
|
86
|
+
export type ServiceStatus = "starting" | "running" | "stopped" | "failed";
|
|
87
|
+
|
|
88
|
+
type ActionCall<A> = A extends Action<infer Input, infer Output>
|
|
89
|
+
? [Input] extends [void]
|
|
90
|
+
? (input?: undefined, options?: { signal?: AbortSignal }) => Promise<Output>
|
|
91
|
+
: (input: Input, options?: { signal?: AbortSignal }) => Promise<Output>
|
|
92
|
+
: never;
|
|
93
|
+
|
|
94
|
+
export type ActionCalls<E extends Extension<any, any>> = { readonly [K in keyof E["actions"]]: ActionCall<E["actions"][K]> };
|
|
95
|
+
|
|
96
|
+
export interface ServiceHandle {
|
|
97
|
+
readonly enabled: boolean;
|
|
98
|
+
readonly status: ServiceStatus;
|
|
99
|
+
readonly error: string | null;
|
|
100
|
+
readonly since: Date | null;
|
|
101
|
+
setEnabled(enabled: boolean): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export declare function actions<E extends Extension<any, any>>(): ActionCalls<E>;
|
|
105
|
+
|
|
106
|
+
export declare function useService<E extends Extension<any, true>>(): ServiceHandle;
|
package/swiplet.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const outside = () => {
|
|
2
|
+
throw new Error("@swipt/api/swiplet runs inside Swipt; build the swiplet with `swiplet build`");
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const defineExtension = outside;
|
|
6
|
+
export const defineAction = outside;
|
|
7
|
+
export const actions = outside;
|
|
8
|
+
export const useService = outside;
|
package/README.md
DELETED