@unbound-app/cli 0.11.0 → 1.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/dist/index.mjs +15124 -3650
- package/dist/oxlint/plugin.d.mts +4240 -0
- package/dist/oxlint/plugin.mjs +154 -0
- package/dist/rollup/index.d.mts +418 -0
- package/dist/rollup/index.mjs +44024 -0
- package/dist/settings-typings-CnJ5F-rP.mjs +2482 -0
- package/package.json +38 -7
- package/templates/plugin/ADDON.md +9 -0
- package/templates/plugin/assets/icon.png +0 -0
- package/templates/plugin/bunfig.toml +3 -0
- package/templates/plugin/gitignore +2 -0
- package/templates/plugin/i18n/en-US.json +5 -0
- package/templates/plugin/media/banner.png +0 -0
- package/templates/plugin/oxlintrc.json +9 -0
- package/templates/plugin/package.json +11 -7
- package/templates/plugin/rollup.config.mjs +2 -42
- package/templates/plugin/settings.ts +9 -0
- package/templates/plugin/src/index.tsx +11 -0
- package/templates/plugin/src/unbound-i18n.d.ts +39 -0
- package/templates/plugin/src/unbound-settings.d.ts +174 -0
- package/templates/plugin/tests/index.test.ts +22 -0
- package/templates/plugin/tsconfig.json +18 -0
- package/templates/plugin/index.tsx +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unbound-app/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The `ubd` command line for the Unbound client: evaluate on-device code and serve the debugger as an MCP.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mario P.",
|
|
@@ -21,20 +21,51 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"module": "./dist/index.mjs",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"import": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./rollup": {
|
|
30
|
+
"types": "./dist/rollup/index.d.mts",
|
|
31
|
+
"import": "./dist/rollup/index.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./oxlint": {
|
|
34
|
+
"types": "./dist/oxlint/plugin.d.mts",
|
|
35
|
+
"import": "./dist/oxlint/plugin.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
24
38
|
"scripts": {
|
|
25
39
|
"build": "tsdown",
|
|
26
40
|
"typecheck": "bun run --filter @unbound-app/web build:types && tsc --noEmit -p ."
|
|
27
41
|
},
|
|
28
42
|
"dependencies": {
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
43
|
+
"@babel/core": "^7.29.7",
|
|
44
|
+
"@babel/plugin-syntax-jsx": "^8.0.1",
|
|
45
|
+
"@babel/plugin-transform-block-scoping": "^8.0.10",
|
|
46
|
+
"@babel/preset-typescript": "^8.0.7",
|
|
47
|
+
"@clack/prompts": "^1.8.1",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
49
|
+
"@rollup/plugin-commonjs": "^29.0.3",
|
|
50
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
51
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
52
|
+
"@unbound-app/debugger-protocol": "0.11.0",
|
|
53
|
+
"@unbound-app/rollup-plugin-hermes": "2.0.0",
|
|
54
|
+
"better-auth": "1.7.16",
|
|
33
55
|
"cleye": "^2.7.0",
|
|
56
|
+
"magic-string": "^1.4.5",
|
|
57
|
+
"marked": "^18.1.3",
|
|
34
58
|
"picocolors": "^1.1.1",
|
|
35
|
-
"
|
|
59
|
+
"rolldown": "^1.2.16",
|
|
60
|
+
"rollup": "^4.67.5",
|
|
61
|
+
"rollup-plugin-iife": "^0.8.0",
|
|
62
|
+
"rollup-plugin-swc3": "^0.12.1",
|
|
63
|
+
"sharp": "^0.35.5",
|
|
64
|
+
"thumbhash": "^0.1.1",
|
|
65
|
+
"typebox": "^1.4.4",
|
|
66
|
+
"typescript-api": "npm:typescript@5.9.3"
|
|
36
67
|
},
|
|
37
68
|
"peerDependencies": {
|
|
38
|
-
"typescript": "^
|
|
69
|
+
"typescript": "^7.0.2"
|
|
39
70
|
}
|
|
40
71
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"jsPlugins": [{ "name": "unbound", "specifier": "@unbound-app/cli/oxlint" }],
|
|
3
|
+
"ignorePatterns": ["dist", "src/unbound-i18n.d.ts", "src/unbound-settings.d.ts"],
|
|
4
|
+
"rules": {
|
|
5
|
+
"unbound/no-window-unbound": "error",
|
|
6
|
+
"unbound/platform-check-only-in-platform-file": "error",
|
|
7
|
+
"unbound/no-module-scope-lookup": ["error", { "lazyByDefault": true }]
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "rollup -c"
|
|
8
|
+
"build": "rollup -c",
|
|
9
|
+
"lint": "oxlint",
|
|
10
|
+
"test": "bun test"
|
|
9
11
|
},
|
|
10
12
|
"devDependencies": {
|
|
11
|
-
"@
|
|
12
|
-
"@rollup/plugin-json": "^6.1.0",
|
|
13
|
-
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
13
|
+
"@types/bun": "latest",
|
|
14
14
|
"@types/react": "^19.2.7",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"@unbound-app/api": "latest",
|
|
16
|
+
"@unbound-app/cli": "latest",
|
|
17
|
+
"oxlint": "^1.91.0",
|
|
18
|
+
"react": "^19.3.1",
|
|
19
|
+
"react-native": "^0.86.0",
|
|
20
|
+
"rollup": "^4.67.5",
|
|
21
|
+
"typescript": "^7.0.2"
|
|
18
22
|
}
|
|
19
23
|
}
|
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { swc } from 'rollup-plugin-swc3';
|
|
3
|
-
import json from '@rollup/plugin-json';
|
|
1
|
+
import { unbound } from '@unbound-app/cli/rollup';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
'@unbound-app/api': 'window.unbound',
|
|
7
|
-
// The bare name reaches the React Unbound passes the bundle, which keeps components' state on hot refresh.
|
|
8
|
-
'react': 'React',
|
|
9
|
-
'react-native': 'window.ReactNative',
|
|
10
|
-
'react-native-reanimated': 'window.unbound.metro.common.Reanimated',
|
|
11
|
-
'@react-native-clipboard/clipboard': 'window.unbound.metro.common.Clipboard',
|
|
12
|
-
'moment': 'window.unbound.metro.common.Moment',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const hot = process.env.UNBOUND_HOT === '1';
|
|
16
|
-
|
|
17
|
-
function keepIifeUnnamedForUnbound(level, log, handler) {
|
|
18
|
-
if (log.code !== 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT') {
|
|
19
|
-
handler(level, log);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** @type {import('rollup').RollupOptions} */
|
|
24
|
-
export default {
|
|
25
|
-
input: 'index.tsx',
|
|
26
|
-
external: Object.keys(globals),
|
|
27
|
-
plugins: [
|
|
28
|
-
nodeResolve(),
|
|
29
|
-
json(),
|
|
30
|
-
swc({
|
|
31
|
-
tsconfig: false,
|
|
32
|
-
jsc: { transform: { react: { refresh: hot, development: hot } } },
|
|
33
|
-
}),
|
|
34
|
-
],
|
|
35
|
-
onLog: keepIifeUnnamedForUnbound,
|
|
36
|
-
output: {
|
|
37
|
-
dir: 'dist',
|
|
38
|
-
format: 'iife',
|
|
39
|
-
compact: true,
|
|
40
|
-
exports: 'named',
|
|
41
|
-
globals,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
3
|
+
export default unbound(import.meta.url);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { settings } from '@unbound-app/settings';
|
|
2
|
+
import { toasts } from '@unbound-app/api';
|
|
3
|
+
|
|
4
|
+
import icon from '../assets/icon.png';
|
|
5
|
+
|
|
6
|
+
/** Greets the user, unless they turned the greeting off. */
|
|
7
|
+
export function start() {
|
|
8
|
+
if (!settings.greetOnStart) return;
|
|
9
|
+
|
|
10
|
+
toasts.success('GREETING_BODY', { name: addon.name }, { icon });
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Generated by ubd from i18n/en-US.json. Do not edit: `ubd build` and `ubd dev` rewrite it.
|
|
2
|
+
|
|
3
|
+
declare module '@unbound-app/i18n' {
|
|
4
|
+
/** Every en-US message, read in the user's locale. */
|
|
5
|
+
export type Messages = {
|
|
6
|
+
/** {name} is running. */
|
|
7
|
+
readonly GREETING_BODY: string;
|
|
8
|
+
/** Greet on start */
|
|
9
|
+
readonly GREET_ON_START: string;
|
|
10
|
+
/** Show a greeting when the plugin starts. */
|
|
11
|
+
readonly GREET_ON_START_SUBTITLE: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The placeholder values each message that has placeholders takes. */
|
|
15
|
+
export type FormatVars = {
|
|
16
|
+
GREETING_BODY: { name: string | number };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Each message in the user's locale, falling back to en-US. */
|
|
20
|
+
export const messages: Messages;
|
|
21
|
+
/** Formats a message that has no placeholders. */
|
|
22
|
+
export function format(key: Exclude<keyof Messages, keyof FormatVars>): string;
|
|
23
|
+
/** Formats a message, filling its placeholders from `vars`. */
|
|
24
|
+
export function format<K extends keyof FormatVars>(key: K, vars: FormatVars[K]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Re-renders the component when the user's locale changes or its translation loads, and returns the locale code.
|
|
27
|
+
* The build adds it to components and hooks that read `messages` or `format`.
|
|
28
|
+
*/
|
|
29
|
+
export function useMessages(): string;
|
|
30
|
+
|
|
31
|
+
/** Every key of the plugin's en-US.json. */
|
|
32
|
+
export type MessageKey = keyof Messages;
|
|
33
|
+
|
|
34
|
+
global {
|
|
35
|
+
/** Gives the plugin's commands, toasts and dialogs its message keys. */
|
|
36
|
+
// oxlint-disable-next-line typescript/consistent-type-definitions -- merging needs an interface.
|
|
37
|
+
interface UnboundMessageKeys extends Record<MessageKey, true> {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Generated by ubd from settings.ts and i18n/en-US.json. Do not edit: `ubd build` and `ubd dev` rewrite it.
|
|
2
|
+
|
|
3
|
+
declare module '@unbound-app/settings/schema' {
|
|
4
|
+
/** A message key of this plugin's i18n/en-US.json. */
|
|
5
|
+
export type MessageKey = import('@unbound-app/i18n').MessageKey;
|
|
6
|
+
type HexColor = import('@unbound-app/settings').HexColor;
|
|
7
|
+
type HttpUrl = import('@unbound-app/settings').HttpUrl;
|
|
8
|
+
type Snowflake = import('@unbound-app/settings').Snowflake;
|
|
9
|
+
type EmojiString = import('@unbound-app/settings').EmojiString;
|
|
10
|
+
type Settings = import('@unbound-app/settings').Settings;
|
|
11
|
+
type ActionName = import('@unbound-app/settings').ActionName;
|
|
12
|
+
type Never<K extends string> = { [P in K]?: never };
|
|
13
|
+
|
|
14
|
+
const stored: unique symbol;
|
|
15
|
+
const item: unique symbol;
|
|
16
|
+
const section: unique symbol;
|
|
17
|
+
const button: unique symbol;
|
|
18
|
+
const notice: unique symbol;
|
|
19
|
+
const definition: unique symbol;
|
|
20
|
+
|
|
21
|
+
/** A stored setting, from one of the `setting` builders. */
|
|
22
|
+
export type Setting = { readonly [stored]: true };
|
|
23
|
+
/** An `array` item, from `setting.string()` or `setting.choice({ options })`; `T` is its value. */
|
|
24
|
+
export type Item<T> = { readonly [item]: T };
|
|
25
|
+
/** A section from `setting.section`. */
|
|
26
|
+
export type Section = { readonly [section]: true };
|
|
27
|
+
/** A button row from `setting.button`; it stores nothing. */
|
|
28
|
+
export type Button = { readonly [button]: true };
|
|
29
|
+
/** A notice row from `setting.notice`; it stores nothing. */
|
|
30
|
+
export type Notice = { readonly [notice]: true };
|
|
31
|
+
/** What `defineSettings` returns: the default export of settings.ts. */
|
|
32
|
+
export type SettingsDefinition = { readonly [definition]: true };
|
|
33
|
+
|
|
34
|
+
/** Shows the row only while it holds: a predicate of every setting, defaults applied, or a boolean setting's key. */
|
|
35
|
+
export type Condition = ((settings: Settings) => boolean) | import('@unbound-app/settings').BooleanSettingKey;
|
|
36
|
+
|
|
37
|
+
/** What every setting, section and button takes. A hidden setting needs no title. */
|
|
38
|
+
export type Common = {
|
|
39
|
+
/** The row's sub-label. */
|
|
40
|
+
subtitle?: MessageKey;
|
|
41
|
+
visibleWhen?: Condition;
|
|
42
|
+
disabledWhen?: Condition;
|
|
43
|
+
/** Changing it shows a hint to restart Discord. */
|
|
44
|
+
requiresRestart?: boolean;
|
|
45
|
+
} & (
|
|
46
|
+
| { /** Stored and typed, but not drawn. */ hidden: true; title?: MessageKey }
|
|
47
|
+
| { hidden?: false; /** The row's label or the section's title. */ title: MessageKey }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
type Range = { min?: number; max?: number };
|
|
51
|
+
type NumberDisplay<Step> =
|
|
52
|
+
| ({ displayAs?: 'input' } & Range & Never<'step' | 'units'>)
|
|
53
|
+
| ({ /** The default when `min` and `max` are set. */ displayAs?: 'slider'; min: number; max: number; step?: number } & Never<'units'>)
|
|
54
|
+
| ({ displayAs: 'stepper' } & Range & Step & Never<'units'>)
|
|
55
|
+
| ({ displayAs: 'duration'; /** The units offered, smallest first. */ units?: ('ms' | 's' | 'm' | 'h' | 'd')[] } & Range & Never<'step'>);
|
|
56
|
+
|
|
57
|
+
type TextOptions = { minLength?: number; maxLength?: number; placeholder?: MessageKey };
|
|
58
|
+
type ColorOptions = { alpha?: boolean; default: HexColor } & (
|
|
59
|
+
| { palette?: false; presets?: HexColor[] }
|
|
60
|
+
| { /** Draws `presets` inline. */ palette: true; presets: [HexColor, ...HexColor[]] }
|
|
61
|
+
);
|
|
62
|
+
type StringDisplay =
|
|
63
|
+
| ({ displayAs?: 'input' | 'multiline'; default: string } & TextOptions & Never<'alpha' | 'presets' | 'palette' | 'maxSize'>)
|
|
64
|
+
| ({ displayAs: 'secret'; default: string; placeholder?: MessageKey } & Never<'minLength' | 'maxLength' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
|
|
65
|
+
| ({ displayAs: 'url'; default: HttpUrl | ''; placeholder?: MessageKey } & Never<'minLength' | 'maxLength' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
|
|
66
|
+
| ({ displayAs: 'color' } & ColorOptions & Never<'minLength' | 'maxLength' | 'placeholder' | 'maxSize'>)
|
|
67
|
+
| ({ displayAs: 'image'; default?: string; /** The largest file, in bytes. */ maxSize?: number } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette'>)
|
|
68
|
+
| ({ displayAs: 'font'; default?: string } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
|
|
69
|
+
| ({ /** `name`, or `name:id` for a custom emoji. */ displayAs: 'emoji'; default?: EmojiString } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
|
|
70
|
+
| ({ displayAs: 'user' | 'channel' | 'guild' | 'role'; default?: Snowflake | '' } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>);
|
|
71
|
+
|
|
72
|
+
/** An option's label: a message key, or a title and a subtitle a radio group shows under it. */
|
|
73
|
+
export type OptionLabel = MessageKey | { title: MessageKey; subtitle?: MessageKey };
|
|
74
|
+
type Options = Record<string, OptionLabel>;
|
|
75
|
+
|
|
76
|
+
/** The builders, one per stored primitive; `displayAs` picks the widget. */
|
|
77
|
+
export const setting: {
|
|
78
|
+
/** A `boolean`: a switch, or a checkbox. */
|
|
79
|
+
boolean(options: Common & { default: boolean; displayAs?: 'switch' | 'checkbox' }): Setting;
|
|
80
|
+
/** A `number`: an input; a slider with `min` and `max`; a stepper, which needs `step`; or a duration in milliseconds. */
|
|
81
|
+
number(options: Common & { default: number } & NumberDisplay<{ step: number }>): Setting;
|
|
82
|
+
/** A whole `number`: an input; a slider with `min` and `max`; a stepper; or a duration in milliseconds. */
|
|
83
|
+
integer(options: Common & { default: number } & NumberDisplay<{ step?: number }>): Setting;
|
|
84
|
+
/** A `string` item of an `array`. */
|
|
85
|
+
string(): Item<string>;
|
|
86
|
+
/** A `string`: an input, a multiline input, a secret, a URL, a color, an image, a font, an emoji or a Discord entity. */
|
|
87
|
+
string(options: Common & StringDisplay): Setting;
|
|
88
|
+
/** A choice item of an `array`. */
|
|
89
|
+
choice<const O extends Options>(options: { options: O }): Item<keyof O & string>;
|
|
90
|
+
/** One of `options`' keys: a radio group (up to 5 options), a picker sheet (more), or a segmented control (up to 4). */
|
|
91
|
+
choice<const O extends Options>(
|
|
92
|
+
options: Common & { options: O; default: NoInfer<keyof O & string>; displayAs?: 'radio' | 'picker' | 'segmented' },
|
|
93
|
+
): Setting;
|
|
94
|
+
/** A list of `of`: strings as an editable list or a Discord entity picker, choices as checkboxes. */
|
|
95
|
+
array<const T>(
|
|
96
|
+
options: Common & { of: Item<T>; default?: NoInfer<T>[]; minItems?: number; maxItems?: number } & (string extends T
|
|
97
|
+
? { displayAs?: 'list' | 'user' | 'channel' | 'guild' | 'role' }
|
|
98
|
+
: { displayAs?: 'checkboxes' }),
|
|
99
|
+
): Setting;
|
|
100
|
+
/** A section of settings, titled by `title`. */
|
|
101
|
+
section(
|
|
102
|
+
options: Common & { displayAs?: 'group' | 'collapsible' },
|
|
103
|
+
properties: Record<string, Setting | Button | Notice>,
|
|
104
|
+
): Section;
|
|
105
|
+
/** A button row that calls `action`, a function src/actions.ts exports, asking `confirm` first when set. */
|
|
106
|
+
button(
|
|
107
|
+
options: Common & { action: ActionName; confirm?: MessageKey; displayAs?: 'button' | 'danger' },
|
|
108
|
+
): Button;
|
|
109
|
+
/** A message row, shown while `visibleWhen` holds; it stores nothing. `tone` is `info` unless set. */
|
|
110
|
+
notice(options: {
|
|
111
|
+
title: MessageKey;
|
|
112
|
+
subtitle?: MessageKey;
|
|
113
|
+
tone?: 'info' | 'warning' | 'danger';
|
|
114
|
+
visibleWhen?: Condition;
|
|
115
|
+
}): Notice;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Declares the plugin's settings; `title` names the section holding the top-level ones. */
|
|
119
|
+
export function defineSettings(
|
|
120
|
+
properties: Record<string, Setting | Section | Button | Notice>,
|
|
121
|
+
options?: { title?: MessageKey },
|
|
122
|
+
): SettingsDefinition;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare module '@unbound-app/settings' {
|
|
126
|
+
/** A `color` setting: `#RRGGBB`, or `#RRGGBBAA` with `alpha`. */
|
|
127
|
+
export type HexColor = import('@unbound-app/api/types').HexColor;
|
|
128
|
+
/** A `url` setting: an http or https URL. */
|
|
129
|
+
export type HttpUrl = import('@unbound-app/api/types').HttpUrl;
|
|
130
|
+
/** A Discord id, as an entity setting holds it. */
|
|
131
|
+
export type Snowflake = import('@unbound-app/api/types').Snowflake;
|
|
132
|
+
/** An `emoji` setting: a Unicode emoji or a custom emoji's name, then `:` and its id when custom. */
|
|
133
|
+
export type EmojiString = import('@unbound-app/api/types').EmojiString;
|
|
134
|
+
|
|
135
|
+
/** The plugin's settings: reads apply defaults, and writes are validated and saved to its Bucket. */
|
|
136
|
+
export type Settings = {
|
|
137
|
+
/**
|
|
138
|
+
* Greet on start
|
|
139
|
+
*
|
|
140
|
+
* Show a greeting when the plugin starts.
|
|
141
|
+
*/
|
|
142
|
+
greetOnStart: boolean;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Each setting and section by its dot-separated key. */
|
|
146
|
+
type SettingValues = {
|
|
147
|
+
/**
|
|
148
|
+
* Greet on start
|
|
149
|
+
*
|
|
150
|
+
* Show a greeting when the plugin starts.
|
|
151
|
+
*/
|
|
152
|
+
"greetOnStart": boolean;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Every setting and section by its dot-separated key; buttons and notices are not settings. */
|
|
156
|
+
export type SettingKey = keyof SettingValues;
|
|
157
|
+
/** The value of the setting or section at `K`. */
|
|
158
|
+
export type SettingsEntry<K extends SettingKey> = SettingValues[K];
|
|
159
|
+
|
|
160
|
+
/** The functions src/actions.ts exports, which a `setting.button` can run. */
|
|
161
|
+
export type ActionName = never;
|
|
162
|
+
|
|
163
|
+
/** The keys of the boolean settings, which `visibleWhen` and `disabledWhen` can name. */
|
|
164
|
+
export type BooleanSettingKey = "greetOnStart";
|
|
165
|
+
|
|
166
|
+
/** The plugin's settings. Assign a property, a section's too, to save it. */
|
|
167
|
+
export const settings: Settings;
|
|
168
|
+
/** Reads a setting or a whole section, and re-renders the component when it changes. */
|
|
169
|
+
export function useSetting<K extends SettingKey>(key: K): SettingsEntry<K>;
|
|
170
|
+
/** Puts every setting back to its default. */
|
|
171
|
+
export function resetSettings(): void;
|
|
172
|
+
/** The settings panel generated from settings.ts; a custom `getSettingsPanel` can render it too. */
|
|
173
|
+
export function SettingsPanel(): import('react').ReactElement;
|
|
174
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { startPlugin, stopPlugin, toasts } from '@unbound-app/api/testing';
|
|
2
|
+
import { settings } from '@unbound-app/settings';
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
describe('start', () => {
|
|
6
|
+
test('greets with the plugin name', async () => {
|
|
7
|
+
await startPlugin();
|
|
8
|
+
|
|
9
|
+
expect(toasts.shown).toMatchObject([
|
|
10
|
+
{ key: 'GREETING_BODY', title: '{{displayName}} is running.' },
|
|
11
|
+
]);
|
|
12
|
+
await stopPlugin();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('stays quiet when greetOnStart is off', async () => {
|
|
16
|
+
settings.greetOnStart = false;
|
|
17
|
+
await startPlugin();
|
|
18
|
+
|
|
19
|
+
expect(toasts.shown).toEqual([]);
|
|
20
|
+
await stopPlugin();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"types": ["bun", "@unbound-app/api/global"],
|
|
13
|
+
"paths": {
|
|
14
|
+
"~/*": ["./src/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "tests", "settings.ts"]
|
|
18
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { metro, patcher } from '@unbound-app/api';
|
|
2
|
-
|
|
3
|
-
const unpatches: (() => void)[] = [];
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
start() {
|
|
7
|
-
const UserStore = metro.findByProps('getCurrentUser', 'getUser');
|
|
8
|
-
|
|
9
|
-
unpatches.push(
|
|
10
|
-
patcher.after(UserStore, 'getCurrentUser', ({ result }) => {
|
|
11
|
-
console.log('[{{displayName}}] getCurrentUser returned', result?.username);
|
|
12
|
-
}),
|
|
13
|
-
);
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
stop() {
|
|
17
|
-
for (const unpatch of unpatches) unpatch();
|
|
18
|
-
|
|
19
|
-
unpatches.length = 0;
|
|
20
|
-
},
|
|
21
|
-
};
|