stikfix 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/dist/host/src/bootstrap/register.js +116 -17
- package/dist/host/src/config.js +23 -7
- package/dist/host/src/doctor.js +459 -0
- package/dist/host/src/exe-main.js +254 -0
- package/dist/host/src/index.js +156 -118
- package/dist/host/src/native-host.js +140 -41
- package/dist/host/stikfix-init.cjs +61 -28
- package/dist/host/stikfix-native.cjs +81 -29
- package/dist/lib/pin-position.js +69 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
[](https://www.npmjs.com/package/stikfix)
|
|
11
|
+
[](https://github.com/omernesh/stikfix/releases/latest)
|
|
11
12
|
[](#load-the-extension)
|
|
12
13
|
|
|
13
14
|
</div>
|
|
@@ -33,7 +34,26 @@ No cloud. No accounts. No sign-up. Everything stays on `127.0.0.1` — **your co
|
|
|
33
34
|
|
|
34
35
|
## Quick start
|
|
35
36
|
|
|
36
|
-
###
|
|
37
|
+
### Windows: one-click installer (recommended)
|
|
38
|
+
|
|
39
|
+
1. Download **`stikfix-setup-1.6.0.exe`** from the [latest release](https://github.com/omernesh/stikfix/releases/latest).
|
|
40
|
+
2. Run it. It needs **administrator rights** — it writes a browser policy so the extension can install itself.
|
|
41
|
+
3. Pick a setup type:
|
|
42
|
+
- **Complete** — installs the host, force-installs the extension into every Chromium browser it finds (Chrome/Edge/Brave), sets the host to run on Windows login, and adds a desktop shortcut. Fully automatic — no terminal.
|
|
43
|
+
- **Custom** — pick components (the host; which browsers get the extension) and tasks (run-on-startup, desktop shortcut) individually, and choose your notes folder.
|
|
44
|
+
4. The wizard finishes with a **post-install health check** that verifies the host binds, native messaging is registered, the browser force-install policy is set, and the notes folder is writable.
|
|
45
|
+
5. Open Chrome/Edge/Brave — the extension is already there, next time the browser starts. You'll see **"Installed by your organization"** next to it; that's expected and correct for a self-hosted extension. It's force-installed via enterprise policy (`ExtensionInstallForcelist`) pointed at a self-hosted update manifest, not a Chrome Web Store listing.
|
|
46
|
+
6. Open your app, click **Enter Review Mode**, and start dropping notes.
|
|
47
|
+
|
|
48
|
+
The installer ships **`stikfix-host.exe`**, a self-contained binary — no Node.js needs to be installed on the machine. Re-run the health check anytime with `stikfix-host.exe doctor`, or the **"Stikfix Health Check"** shortcut it adds to the Start menu. To remove everything — host, browser policy entry, startup entry, native-messaging registration — use Windows **Add or remove programs**.
|
|
49
|
+
|
|
50
|
+
For advanced use or troubleshooting, the same binary takes subcommands directly: `stikfix-host.exe serve --root <dir>`, `stikfix-host.exe doctor` (prints the health checklist), `stikfix-host.exe register` / `stikfix-host.exe uninstall`.
|
|
51
|
+
|
|
52
|
+
### Developer path (macOS, Linux, or from source on Windows)
|
|
53
|
+
|
|
54
|
+
Everything the Windows installer automates above, you can also do by hand — this is the path for macOS/Linux, or for building and loading the extension from source.
|
|
55
|
+
|
|
56
|
+
#### 1. Install the host (one command)
|
|
37
57
|
|
|
38
58
|
```bash
|
|
39
59
|
npx stikfix init --root /path/to/your/project
|
|
@@ -48,9 +68,9 @@ This turnkey installer:
|
|
|
48
68
|
|
|
49
69
|
> **Keep it current:** re-run `npx --yes stikfix@latest init --root /path/to/your/project` anytime to update the host.
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
#### 2. Load the extension
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
On the developer path the extension is loaded **unpacked** (a Chrome Web Store listing is coming next — the Windows installer above already force-installs it automatically, so unpacked loading isn't needed there):
|
|
54
74
|
|
|
55
75
|
1. Open `chrome://extensions` (or `edge://extensions`).
|
|
56
76
|
2. Turn on **Developer mode**.
|
|
@@ -58,15 +78,15 @@ For v1.0 the extension is loaded **unpacked** (a Chrome Web Store listing is com
|
|
|
58
78
|
|
|
59
79
|
The loaded ID matches what the installer printed — nothing to type. (Building `.output/chrome-mv3/` from source is covered under [Development](#development).)
|
|
60
80
|
|
|
61
|
-
|
|
81
|
+
#### 3. Start the backend
|
|
62
82
|
|
|
63
83
|
Double-click the **Stikfix Host** launcher on your Desktop. That's it — no commands, and double-clicking again is safe (it won't launch a second copy). The host listens on a port in the `39240–39260` range; the extension discovers it automatically.
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
#### 4. Pair in one click
|
|
66
86
|
|
|
67
87
|
Open the stikfix popup in your toolbar and hit **Pair with host**. The token is handed over automatically through the OS native-messaging channel — you never see it, never paste it. Pair once and it stays paired, even after restarts.
|
|
68
88
|
|
|
69
|
-
|
|
89
|
+
#### 5. Drop a note
|
|
70
90
|
|
|
71
91
|
Open your app, click **Enter Review Mode**, and start dropping notes. The first note on a new site opens an **OS folder picker** so you can choose where that origin's notes live; it's remembered after that. When you're ready, tell your AI agent **"read my notes."**
|
|
72
92
|
|
|
@@ -80,7 +100,7 @@ Open your app, click **Enter Review Mode**, and start dropping notes. The first
|
|
|
80
100
|
- **Notes panel** — a chip-toggled list of every note: counts by status, filter chips, text search, and click-to-jump that scrolls right to the pin. Flip **All pages** to browse every note across the project, not just the current page.
|
|
81
101
|
- **Live updates** — while Review Mode is on, pins and the panel refresh on their own (~4s, only when the tab is visible) as the agent writes replies and resolves notes. No manual reload.
|
|
82
102
|
- **Per-origin project routing** — the first note on a new site opens an OS folder picker; after that, every tab routes to the right project's `notes/` folder automatically. No per-note picking.
|
|
83
|
-
- **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass.
|
|
103
|
+
- **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass. On Windows, the one-click installer does this (and the browser-side install) for you.
|
|
84
104
|
- **The `review-notes` AI skill** — the portable agent half of the loop (see below).
|
|
85
105
|
|
|
86
106
|
## How notes reach your AI
|
|
@@ -268,15 +268,22 @@ export function createLauncherFiles(opts) {
|
|
|
268
268
|
const hostEntry = opts.hostEntryPath;
|
|
269
269
|
const rootArg = opts.root;
|
|
270
270
|
const portArg = opts.port !== undefined ? ` --port ${opts.port}` : '';
|
|
271
|
+
// Standalone-exe launcher: run `"<exe>" serve --root ...` instead of
|
|
272
|
+
// `"node" "<hostEntry>" --root ...`. Absent → exact prior node behavior.
|
|
273
|
+
const useExe = typeof opts.hostExe === 'string' && opts.hostExe.length > 0;
|
|
274
|
+
const hostExe = opts.hostExe ?? '';
|
|
271
275
|
if (plat === 'win32') {
|
|
272
276
|
// Write batch file — plain text, no exec required
|
|
277
|
+
const batchLaunch = useExe
|
|
278
|
+
? `"${hostExe}" serve --root "${rootArg}"${portArg}`
|
|
279
|
+
: `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`;
|
|
273
280
|
const batchContent = [
|
|
274
281
|
'@echo off',
|
|
275
282
|
`rem Stikfix HTTP host launcher — double-click to start the backend`,
|
|
276
283
|
`rem Generated by: npx stikfix init`,
|
|
277
284
|
`rem Root: ${rootArg}`,
|
|
278
285
|
``,
|
|
279
|
-
|
|
286
|
+
batchLaunch,
|
|
280
287
|
`if %ERRORLEVEL% NEQ 0 pause`,
|
|
281
288
|
].join('\r\n');
|
|
282
289
|
const batchPath = paths.launcher;
|
|
@@ -287,17 +294,33 @@ export function createLauncherFiles(opts) {
|
|
|
287
294
|
// VBS string-literal escaping: a literal " is doubled ("").
|
|
288
295
|
const vq = (s) => s.replace(/"/g, '""');
|
|
289
296
|
const vbsHostEntry = vq(hostEntry);
|
|
297
|
+
const vbsHostExe = vq(hostExe);
|
|
290
298
|
const vbsRoot = vq(rootArg);
|
|
291
299
|
const vbsPortArg = opts.port !== undefined ? ` --port ${opts.port}` : '';
|
|
300
|
+
// The hidden-launch line (window style 0).
|
|
301
|
+
//
|
|
302
|
+
// exe mode runs the ABSOLUTE exe path DIRECTLY via sh.Run — NOT through
|
|
303
|
+
// `cmd /c`. Reason: `cmd /c "<quoted exe>" ... "<quoted root>"` starts and
|
|
304
|
+
// ends with a quote, so cmd.exe strips the outer pair (documented in
|
|
305
|
+
// `cmd /?`) and mangles the spaced "C:\Program Files\..." path → the host
|
|
306
|
+
// never starts. WScript.Shell.Run parses the quoted exe path correctly, so
|
|
307
|
+
// no cmd wrapper (and no PATH lookup) is needed for an absolute exe.
|
|
308
|
+
//
|
|
309
|
+
// node mode keeps `cmd /c` so a relative `node` on PATH resolves; that line
|
|
310
|
+
// begins with `node` (not a quote), so cmd's outer-quote strip never fires.
|
|
311
|
+
const vbsRunLine = useExe
|
|
312
|
+
? `sh.Run """" & hostExe & """ serve --root """ & root & """${vbsPortArg}", 0, False`
|
|
313
|
+
: `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`;
|
|
292
314
|
const vbsContent = [
|
|
293
315
|
'Option Explicit',
|
|
294
|
-
'Dim sh, fso, hostEntry, root, portFile, msg, port, f',
|
|
316
|
+
'Dim sh, fso, hostEntry, hostExe, root, portFile, msg, port, f',
|
|
295
317
|
'Set sh = CreateObject("WScript.Shell")',
|
|
296
318
|
'Set fso = CreateObject("Scripting.FileSystemObject")',
|
|
297
319
|
`hostEntry = "${vbsHostEntry}"`,
|
|
320
|
+
`hostExe = "${vbsHostExe}"`,
|
|
298
321
|
`root = "${vbsRoot}"`,
|
|
299
322
|
"' Launch the host hidden (window style 0), do not wait",
|
|
300
|
-
|
|
323
|
+
vbsRunLine,
|
|
301
324
|
"' Give it a moment to bind and write the port file",
|
|
302
325
|
'WScript.Sleep 1800',
|
|
303
326
|
'msg = "Stikfix host is running." & vbCrLf & vbCrLf & "You can start dropping notes."',
|
|
@@ -319,34 +342,56 @@ export function createLauncherFiles(opts) {
|
|
|
319
342
|
// execFile (not exec) — no shell spawned, no injection vector.
|
|
320
343
|
// If this fails for any reason, fall through to the warning (batch file is the fallback).
|
|
321
344
|
if (paths.shortcut) {
|
|
322
|
-
|
|
345
|
+
// paths.shortcut (from getLauncherPaths) is a HARDCODED GUESS —
|
|
346
|
+
// join(home, 'Desktop', ...) — which is wrong when Windows OneDrive
|
|
347
|
+
// "Known Folder Move" (or a custom Shell-Folder registry override)
|
|
348
|
+
// redirects the Desktop elsewhere. Resolve the REAL current Desktop
|
|
349
|
+
// folder at shortcut-creation time via PowerShell's
|
|
350
|
+
// [Environment]::GetFolderPath('Desktop'), which honors that
|
|
351
|
+
// redirection, and build the .lnk path from that instead. The guess is
|
|
352
|
+
// kept only as the non-fatal fallback message below.
|
|
353
|
+
const guessedLnkPath = paths.shortcut;
|
|
323
354
|
const iconPath = opts.iconPath ?? '';
|
|
324
355
|
// Build PowerShell script as a single string — paths passed as PS variables
|
|
325
356
|
// set from the -Command string. No shell metacharacter interpolation from
|
|
326
357
|
// external data; all values are constants determined at registration time.
|
|
327
358
|
// PowerShell single-quote escaping: replace ' with '' in path strings.
|
|
328
|
-
const
|
|
359
|
+
const safeLnkFileName = LAUNCHER_LNK_FILENAME.replace(/'/g, "''");
|
|
329
360
|
const safeIconPath = iconPath.replace(/'/g, "''");
|
|
330
361
|
const wscriptPath = join(process.env['SystemRoot'] ?? 'C:\\Windows', 'System32', 'wscript.exe');
|
|
331
362
|
const safeWscript = wscriptPath.replace(/'/g, "''");
|
|
332
363
|
const safeVbsPath = vbsPath.replace(/'/g, "''");
|
|
333
|
-
|
|
334
|
-
|
|
364
|
+
// $desktop resolves the REAL Desktop (OneDrive KFM / Shell-Folder aware);
|
|
365
|
+
// the script prints the final .lnk path on stdout so Node can capture the
|
|
366
|
+
// ACTUAL path created instead of trusting the hardcoded guess.
|
|
367
|
+
const psScript = `$desktop = [Environment]::GetFolderPath('Desktop');` +
|
|
368
|
+
`$lnkPath = Join-Path $desktop '${safeLnkFileName}';` +
|
|
369
|
+
`$ws = New-Object -ComObject WScript.Shell;` +
|
|
370
|
+
`$s = $ws.CreateShortcut($lnkPath);` +
|
|
335
371
|
`$s.TargetPath = '${safeWscript}';` +
|
|
336
372
|
`$s.Arguments = '"${safeVbsPath}"';` +
|
|
337
373
|
`$s.Description = 'Start the Stikfix HTTP backend host';` +
|
|
338
374
|
(safeIconPath ? `$s.IconLocation = '${safeIconPath},0';` : '') +
|
|
339
|
-
`$s.Save()
|
|
375
|
+
`$s.Save();` +
|
|
376
|
+
`Write-Output $lnkPath`;
|
|
340
377
|
// execFileSync — NEVER exec; arg array is static with respect to user input.
|
|
341
378
|
// Synchronous so init reports the shortcut accurately (the async variant
|
|
342
379
|
// raced existsSync in the CLI and printed a false "skipped" message).
|
|
343
380
|
try {
|
|
344
|
-
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psScript], { timeout: 15_000,
|
|
381
|
+
const stdout = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psScript], { timeout: 15_000, encoding: 'utf8' });
|
|
382
|
+
// The resolved .lnk path is the last non-empty stdout line. Fall back to
|
|
383
|
+
// the hardcoded guess only if PowerShell somehow printed nothing.
|
|
384
|
+
const resolvedLnkPath = stdout
|
|
385
|
+
.split(/\r?\n/)
|
|
386
|
+
.map((line) => line.trim())
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.pop() ?? guessedLnkPath;
|
|
345
389
|
// Only record the shortcut once it actually exists on disk.
|
|
346
|
-
written.push(
|
|
390
|
+
written.push(resolvedLnkPath);
|
|
347
391
|
}
|
|
348
392
|
catch (err) {
|
|
349
|
-
// Non-fatal: batch file is the acceptable fallback
|
|
393
|
+
// Non-fatal: batch file is the acceptable fallback. The shortcut was not
|
|
394
|
+
// created at guessedLnkPath (or anywhere else), matching prior behavior.
|
|
350
395
|
warnings.push(`Desktop shortcut creation failed (non-fatal): ${String(err.message)}. ` +
|
|
351
396
|
`The batch file at ${batchPath} can be used directly.`);
|
|
352
397
|
}
|
|
@@ -359,7 +404,9 @@ export function createLauncherFiles(opts) {
|
|
|
359
404
|
`# Stikfix HTTP host launcher — double-click in Finder or drag to Dock`,
|
|
360
405
|
`# Generated by: npx stikfix init`,
|
|
361
406
|
``,
|
|
362
|
-
|
|
407
|
+
useExe
|
|
408
|
+
? `exec "${hostExe}" serve --root "${rootArg}"${portArg}`
|
|
409
|
+
: `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
|
|
363
410
|
].join('\n');
|
|
364
411
|
const commandPath = paths.launcher;
|
|
365
412
|
writeFileSync(commandPath, commandContent, { encoding: 'utf8', mode: 0o755 });
|
|
@@ -372,7 +419,9 @@ export function createLauncherFiles(opts) {
|
|
|
372
419
|
`# Stikfix HTTP host launcher`,
|
|
373
420
|
`# Generated by: npx stikfix init`,
|
|
374
421
|
``,
|
|
375
|
-
|
|
422
|
+
useExe
|
|
423
|
+
? `exec "${hostExe}" serve --root "${rootArg}"${portArg}`
|
|
424
|
+
: `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
|
|
376
425
|
].join('\n');
|
|
377
426
|
const shPath = paths.launcher;
|
|
378
427
|
writeFileSync(shPath, shContent, { encoding: 'utf8' });
|
|
@@ -479,10 +528,14 @@ export function registerNativeHost(opts) {
|
|
|
479
528
|
const home = opts.home ?? homedir();
|
|
480
529
|
const browser = opts.browser ?? 'chrome';
|
|
481
530
|
const manifestPath = nativeManifestPath(plat, home, browser);
|
|
482
|
-
// Chrome's CreateProcess (and Firefox on Windows)
|
|
483
|
-
// point the manifest at a per-OS wrapper that
|
|
484
|
-
|
|
485
|
-
|
|
531
|
+
// Default (npx/node install): Chrome's CreateProcess (and Firefox on Windows)
|
|
532
|
+
// cannot run a .cjs directly — point the manifest at a per-OS wrapper that
|
|
533
|
+
// runs `node <abs cjs>`. Standalone-exe install (directPath): the exe self-
|
|
534
|
+
// detects native mode, so point the manifest straight at it — no wrapper.
|
|
535
|
+
const manifestTarget = opts.directPath
|
|
536
|
+
? resolve(opts.hostBinPath)
|
|
537
|
+
: writeNativeWrapper(opts.hostBinPath, plat, home, browser);
|
|
538
|
+
const manifest = buildManifest(opts.extensionId, manifestTarget, browser);
|
|
486
539
|
writeManifest(manifest, manifestPath);
|
|
487
540
|
if (plat === 'win32') {
|
|
488
541
|
const execReg = opts.execReg ?? ((args) => {
|
|
@@ -613,5 +666,51 @@ export function enumerateArtifacts(opts = {}) {
|
|
|
613
666
|
: [];
|
|
614
667
|
return { paths, registryKeys };
|
|
615
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Shared uninstall teardown used by BOTH `bin/stikfix.ts uninstall` and the
|
|
671
|
+
* standalone exe's `stikfix-host uninstall` subcommand, so the two entry
|
|
672
|
+
* points stay behaviorally equivalent and neither duplicates the removal
|
|
673
|
+
* logic inline:
|
|
674
|
+
* - remove the native-messaging manifest + HKCU registry keys, launcher
|
|
675
|
+
* files, and Desktop shortcut (via unregisterNativeHost)
|
|
676
|
+
* - remove the Windows startup (HKCU Run) autoload entry (via unregisterStartup)
|
|
677
|
+
* - remove the stikfix config.json
|
|
678
|
+
*
|
|
679
|
+
* Idempotent: every step tolerates an already-absent file/key — matches the
|
|
680
|
+
* existing idempotent contracts of unregisterNativeHost/unregisterStartup.
|
|
681
|
+
* No console output here — callers own their own user-facing messages/format
|
|
682
|
+
* (bin/stikfix.ts prints plain text lines; the exe prints JSON).
|
|
683
|
+
*/
|
|
684
|
+
export function teardownHost(opts = {}) {
|
|
685
|
+
const plat = opts.plat ?? process.platform;
|
|
686
|
+
const home = opts.home ?? homedir();
|
|
687
|
+
const browser = opts.browser ?? 'chrome';
|
|
688
|
+
const result = {
|
|
689
|
+
manifestRemoved: true,
|
|
690
|
+
configPath: CONFIG_PATH(home),
|
|
691
|
+
configRemoved: false,
|
|
692
|
+
};
|
|
693
|
+
try {
|
|
694
|
+
unregisterNativeHost({ plat, home, browser });
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
result.manifestRemoved = false;
|
|
698
|
+
result.manifestError = err instanceof Error ? err.message : String(err);
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
unregisterStartup({ plat, home });
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
result.startupError = err instanceof Error ? err.message : String(err);
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
rmSync(result.configPath, { force: true });
|
|
708
|
+
result.configRemoved = true;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
result.configRemoved = false;
|
|
712
|
+
}
|
|
713
|
+
return result;
|
|
714
|
+
}
|
|
616
715
|
// Re-export isInsideDir for consumers that need path validation
|
|
617
716
|
export { isInsideDir };
|
package/dist/host/src/config.js
CHANGED
|
@@ -13,13 +13,29 @@ import { resolve, join, basename, dirname } from 'node:path';
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { isInsideDir } from './security.js';
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
function resolveVersion() {
|
|
17
|
+
// 1. SEA exe: the bundle has no package.json at a stable relative path, so the
|
|
18
|
+
// version is inlined at build time. `typeof` guards against ReferenceError
|
|
19
|
+
// in builds where the constant was never defined.
|
|
20
|
+
try {
|
|
21
|
+
if (typeof __STIKFIX_VERSION__ === 'string' && __STIKFIX_VERSION__.length > 0) {
|
|
22
|
+
return __STIKFIX_VERSION__;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Not defined in this build — fall through to the on-disk read.
|
|
27
|
+
}
|
|
28
|
+
// 2. Normal tsc/node build: dist/host/src/config.js → ../../../package.json.
|
|
29
|
+
try {
|
|
30
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const pkg = JSON.parse(readFileSync(join(dir, '../../../package.json'), 'utf8'));
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '0.0.0';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export const VERSION = resolveVersion();
|
|
23
39
|
// ---------------------------------------------------------------------------
|
|
24
40
|
// resolveConfigValues — three-tier env resolution (exported for testing)
|
|
25
41
|
// ---------------------------------------------------------------------------
|