@tiagojct/subsub 0.5.0 → 0.6.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/README.md +16 -2
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/dist/cli.js +12 -0
- package/dist/init.js +2 -2
- package/dist/shortcut.js +165 -0
- package/dist/subsub.js +40 -0
- package/dist/web.js +702 -0
- package/docs/Subsub.md +11 -0
- package/package.json +5 -3
- package/src/cli.ts +12 -0
- package/src/init.ts +2 -2
- package/src/shortcut.ts +184 -0
- package/src/subsub.ts +38 -0
- package/src/web.ts +710 -0
- package/web/app.css +203 -0
- package/web/app.js +859 -0
- package/web/fonts/IBMPlexMono.woff2 +0 -0
- package/web/fonts/IBMPlexSans.woff2 +0 -0
- package/web/fonts/IBMPlexSerif.woff2 +0 -0
- package/web/glauca.css +82 -0
- package/web/i18n.js +263 -0
- package/web/icon.svg +1 -0
- package/web/index.html +112 -0
- package/web/vendor/marked.esm.js +78 -0
package/docs/Subsub.md
CHANGED
|
@@ -52,6 +52,17 @@ Templates (type the name, then the argument):
|
|
|
52
52
|
- Librarian: `/tag-batch [size]`, `/clean-tags`, `/import-queue [note path]`
|
|
53
53
|
- Researcher: `/lit-note <citekey>`, `/synthesis <topic>`, `/gaps <topic>`, `/manuscript <path>`, `/alert`
|
|
54
54
|
|
|
55
|
+
## Web view
|
|
56
|
+
|
|
57
|
+
1. Type `subsub web`. Sub-Sub opens in the browser, with the same modes, profiles, tools and approvals. The approval dialog shows the server preview; the change runs only after Yes.
|
|
58
|
+
2. To add a shortcut (Applications on macOS), type `subsub shortcut`. To remove it, type `subsub shortcut --remove`.
|
|
59
|
+
3. The server listens only on 127.0.0.1 and needs the random key in the address that `subsub web` opens. A second `subsub web` opens the running one.
|
|
60
|
+
4. The server stops 10 minutes after the last page closes. `subsub web --stay` keeps it running.
|
|
61
|
+
5. The labels are in European Portuguese when the language setting is Portuguese.
|
|
62
|
+
6. The model button lists the models and saves an API key for a provider (the same auth.json as `/login`).
|
|
63
|
+
|
|
64
|
+
The log is in ~/.subsub/web.log.
|
|
65
|
+
|
|
55
66
|
## Profiles
|
|
56
67
|
|
|
57
68
|
The profile sets how much Sub-Sub does. To see the profile, type `/profile`. To change it, type `/profile reader` (or scholar, author, editor). The change is saved.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiagojct/subsub",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sub-Sub: a Zotero librarian and research assistant in the terminal, built on pi",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Sub-Sub: a Zotero librarian and research assistant in the browser or the terminal, built on pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Tiago Jacinto",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"README.md",
|
|
47
|
+
"assets",
|
|
47
48
|
"bin",
|
|
48
49
|
"dist",
|
|
49
50
|
"docs",
|
|
@@ -53,7 +54,8 @@
|
|
|
53
54
|
"scripts",
|
|
54
55
|
"src",
|
|
55
56
|
"templates",
|
|
56
|
-
"themes"
|
|
57
|
+
"themes",
|
|
58
|
+
"web"
|
|
57
59
|
],
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@types/node": "^24.19.0",
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* - `subsub review preview|apply NOTE...` runs zotero-review (tag review notes,
|
|
13
13
|
* several at a time) with Sub-Sub's server folder and settings.
|
|
14
14
|
* - `subsub init` sets Sub-Sub up; `subsub doctor` checks the setup.
|
|
15
|
+
* - `subsub web` (or `subsub serve`) opens Sub-Sub in the browser;
|
|
16
|
+
* `subsub shortcut` adds a Sub-Sub shortcut that does the same.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { spawnSync } from "node:child_process";
|
|
@@ -148,6 +150,16 @@ export async function run(argv: string[] = process.argv.slice(2)): Promise<void>
|
|
|
148
150
|
process.exitCode = await initMain(args.slice(1), env);
|
|
149
151
|
return;
|
|
150
152
|
}
|
|
153
|
+
if (args[0] === "web" || args[0] === "serve") {
|
|
154
|
+
const { webMain } = await import("./web.ts");
|
|
155
|
+
process.exitCode = await webMain([...args.slice(1), ...(here ? ["--here"] : [])], env);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (args[0] === "shortcut") {
|
|
159
|
+
const { shortcutMain } = await import("./shortcut.ts");
|
|
160
|
+
process.exitCode = shortcutMain(args.slice(1), env);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
151
163
|
if (args[0] === "doctor") {
|
|
152
164
|
const { doctorMain } = await import("./doctor.ts");
|
|
153
165
|
process.exitCode = await doctorMain(env);
|
package/src/init.ts
CHANGED
|
@@ -184,8 +184,8 @@ export async function runInit(
|
|
|
184
184
|
? "Zotero: reachable."
|
|
185
185
|
: "Zotero: not reachable. Start Zotero 10, then in Zotero open Settings > Advanced and turn on \"Allow other applications on this computer to communicate with Zotero\".",
|
|
186
186
|
);
|
|
187
|
-
if (fmup) io.say("FMUP / U.Porto: the FMUP model service will come in a later version. Until then,
|
|
188
|
-
io.say("Next: type subsub
|
|
187
|
+
if (fmup) io.say("FMUP / U.Porto: the FMUP model service will come in a later version. Until then, connect a model provider yourself.");
|
|
188
|
+
io.say("Next: type subsub web to open Sub-Sub in your browser (or subsub for the terminal). Connect a model provider with the model button (or /login in the terminal). To check the setup, type subsub doctor.");
|
|
189
189
|
return { configFile, envFile, notes, created, zotero };
|
|
190
190
|
}
|
|
191
191
|
|
package/src/shortcut.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `subsub shortcut`: a Sub-Sub shortcut that opens the web view, so students
|
|
3
|
+
* can start Sub-Sub without a terminal.
|
|
4
|
+
*
|
|
5
|
+
* - macOS: ~/Applications/Sub-Sub.app (a small app bundle that runs `subsub web`;
|
|
6
|
+
* no Dock icon, it stops by itself when the page has been closed for a while).
|
|
7
|
+
* - Windows: Sub-Sub in the Start menu and on the desktop.
|
|
8
|
+
* - Linux: ~/.local/share/applications/sub-sub.desktop.
|
|
9
|
+
*
|
|
10
|
+
* The shortcut runs this copy of Sub-Sub with this Node.js (absolute paths), so
|
|
11
|
+
* it works without PATH. Run `subsub shortcut` again after moving either.
|
|
12
|
+
* `subsub shortcut --remove` removes what this command created.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
20
|
+
|
|
21
|
+
export interface ShortcutPlan {
|
|
22
|
+
platform: NodeJS.Platform;
|
|
23
|
+
node: string;
|
|
24
|
+
bin: string;
|
|
25
|
+
home: string;
|
|
26
|
+
packageDir: string;
|
|
27
|
+
desktop: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function planFor(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform, desktop = true): ShortcutPlan {
|
|
31
|
+
const home = platform === "win32" ? (env.USERPROFILE ?? homedir()) : (env.HOME ?? homedir());
|
|
32
|
+
return { platform, node: process.execPath, bin: join(PACKAGE_DIR, "bin", "subsub.js"), home, packageDir: PACKAGE_DIR, desktop };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Single-quote a string for sh. */
|
|
36
|
+
export function shQuote(s: string): string {
|
|
37
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Quote an argument for the Exec line of a .desktop file. */
|
|
41
|
+
export function desktopQuote(s: string): string {
|
|
42
|
+
return `"${s.replace(/(["`$\\])/g, "\\$1")}"`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Single-quote a string for PowerShell. */
|
|
46
|
+
export function psQuote(s: string): string {
|
|
47
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function macScript(p: ShortcutPlan): string {
|
|
51
|
+
return [
|
|
52
|
+
"#!/bin/sh",
|
|
53
|
+
"# Opens Sub-Sub in the browser. Written by: subsub shortcut",
|
|
54
|
+
'cd "$HOME" || exit 1',
|
|
55
|
+
`exec ${shQuote(p.node)} ${shQuote(p.bin)} web >/dev/null 2>&1`,
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function macPlist(version: string): string {
|
|
61
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
62
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
63
|
+
<plist version="1.0">
|
|
64
|
+
<dict>
|
|
65
|
+
<key>CFBundleName</key><string>Sub-Sub</string>
|
|
66
|
+
<key>CFBundleDisplayName</key><string>Sub-Sub</string>
|
|
67
|
+
<key>CFBundleIdentifier</key><string>eu.tiagojacinto.subsub</string>
|
|
68
|
+
<key>CFBundleExecutable</key><string>Sub-Sub</string>
|
|
69
|
+
<key>CFBundleIconFile</key><string>icon</string>
|
|
70
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
71
|
+
<key>CFBundleShortVersionString</key><string>${version}</string>
|
|
72
|
+
<key>CFBundleVersion</key><string>${version}</string>
|
|
73
|
+
<key>LSUIElement</key><true/>
|
|
74
|
+
</dict>
|
|
75
|
+
</plist>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function desktopEntry(p: ShortcutPlan): string {
|
|
80
|
+
return [
|
|
81
|
+
"[Desktop Entry]",
|
|
82
|
+
"Type=Application",
|
|
83
|
+
"Name=Sub-Sub",
|
|
84
|
+
"Comment=Zotero librarian and research assistant",
|
|
85
|
+
`Exec=${desktopQuote(p.node)} ${desktopQuote(p.bin)} web`,
|
|
86
|
+
`Path=${p.home}`,
|
|
87
|
+
`Icon=${join(p.packageDir, "assets", "icon.png")}`,
|
|
88
|
+
"Terminal=false",
|
|
89
|
+
"Categories=Education;Office;",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function windowsScript(p: ShortcutPlan): string {
|
|
95
|
+
const lines = [
|
|
96
|
+
"$ErrorActionPreference = 'Stop'",
|
|
97
|
+
"$shell = New-Object -ComObject WScript.Shell",
|
|
98
|
+
"$places = @([Environment]::GetFolderPath('Programs'))",
|
|
99
|
+
...(p.desktop ? ["$places += [Environment]::GetFolderPath('Desktop')"] : []),
|
|
100
|
+
"foreach ($dir in $places) {",
|
|
101
|
+
" $s = $shell.CreateShortcut((Join-Path $dir 'Sub-Sub.lnk'))",
|
|
102
|
+
` $s.TargetPath = ${psQuote(p.node)}`,
|
|
103
|
+
` $s.Arguments = ${psQuote(`"${p.bin}" web`)}`,
|
|
104
|
+
` $s.WorkingDirectory = ${psQuote(p.home)}`,
|
|
105
|
+
` $s.IconLocation = ${psQuote(join(p.packageDir, "assets", "icon.ico"))}`,
|
|
106
|
+
" $s.Description = 'Sub-Sub: Zotero librarian and research assistant'",
|
|
107
|
+
" $s.WindowStyle = 7",
|
|
108
|
+
" $s.Save()",
|
|
109
|
+
" Write-Output (Join-Path $dir 'Sub-Sub.lnk')",
|
|
110
|
+
"}",
|
|
111
|
+
];
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function runPowerShell(script: string): { ok: boolean; out: string } {
|
|
116
|
+
const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { encoding: "utf8", windowsHide: true });
|
|
117
|
+
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? r.error.message : ""}`.trim() };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createShortcuts(p: ShortcutPlan): string[] {
|
|
121
|
+
if (p.platform === "darwin") {
|
|
122
|
+
const app = join(p.home, "Applications", "Sub-Sub.app");
|
|
123
|
+
mkdirSync(join(app, "Contents", "MacOS"), { recursive: true });
|
|
124
|
+
mkdirSync(join(app, "Contents", "Resources"), { recursive: true });
|
|
125
|
+
writeFileSync(join(app, "Contents", "Info.plist"), macPlist(packageVersion(p.packageDir)));
|
|
126
|
+
const exe = join(app, "Contents", "MacOS", "Sub-Sub");
|
|
127
|
+
writeFileSync(exe, macScript(p));
|
|
128
|
+
chmodSync(exe, 0o755);
|
|
129
|
+
const icns = join(p.packageDir, "assets", "icon.icns");
|
|
130
|
+
if (existsSync(icns)) copyFileSync(icns, join(app, "Contents", "Resources", "icon.icns"));
|
|
131
|
+
return [app];
|
|
132
|
+
}
|
|
133
|
+
if (p.platform === "win32") {
|
|
134
|
+
const r = runPowerShell(windowsScript(p));
|
|
135
|
+
if (!r.ok) throw new Error(`could not create the shortcut: ${r.out}`);
|
|
136
|
+
return r.out.split(/\r?\n/).filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
const dir = join(p.home, ".local", "share", "applications");
|
|
139
|
+
mkdirSync(dir, { recursive: true });
|
|
140
|
+
const file = join(dir, "sub-sub.desktop");
|
|
141
|
+
writeFileSync(file, desktopEntry(p));
|
|
142
|
+
chmodSync(file, 0o755);
|
|
143
|
+
return [file];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function removeShortcuts(p: ShortcutPlan): string[] {
|
|
147
|
+
if (p.platform === "win32") {
|
|
148
|
+
const script = [
|
|
149
|
+
"foreach ($dir in @([Environment]::GetFolderPath('Programs'), [Environment]::GetFolderPath('Desktop'))) {",
|
|
150
|
+
" $f = Join-Path $dir 'Sub-Sub.lnk'",
|
|
151
|
+
" if (Test-Path $f) { Remove-Item $f; Write-Output $f }",
|
|
152
|
+
"}",
|
|
153
|
+
].join("\n");
|
|
154
|
+
const r = runPowerShell(script);
|
|
155
|
+
return r.out.split(/\r?\n/).filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
const target = p.platform === "darwin" ? join(p.home, "Applications", "Sub-Sub.app") : join(p.home, ".local", "share", "applications", "sub-sub.desktop");
|
|
158
|
+
if (!existsSync(target)) return [];
|
|
159
|
+
rmSync(target, { recursive: true, force: true });
|
|
160
|
+
return [target];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function shortcutMain(args: string[], env: NodeJS.ProcessEnv = process.env): number {
|
|
164
|
+
const plan = planFor(env, process.platform, !args.includes("--no-desktop"));
|
|
165
|
+
try {
|
|
166
|
+
if (args.includes("--remove")) {
|
|
167
|
+
const gone = removeShortcuts(plan);
|
|
168
|
+
console.log(gone.length ? `Removed:\n${gone.join("\n")}` : "No Sub-Sub shortcut found.");
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
const made = createShortcuts(plan);
|
|
172
|
+
const where =
|
|
173
|
+
plan.platform === "darwin"
|
|
174
|
+
? "Open Sub-Sub from Applications, Launchpad or Spotlight."
|
|
175
|
+
: plan.platform === "win32"
|
|
176
|
+
? "Open Sub-Sub from the Start menu or the desktop."
|
|
177
|
+
: "Open Sub-Sub from your applications menu.";
|
|
178
|
+
console.log(`Shortcut:\n${made.join("\n")}\n${where} It opens Sub-Sub in your browser.`);
|
|
179
|
+
return 0;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(`Sub-Sub: ${(err as Error).message}`);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/subsub.ts
CHANGED
|
@@ -108,6 +108,30 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
108
108
|
const library: LibraryState = { zotero: "checking" };
|
|
109
109
|
const quote = cfg.quotes === false ? undefined : QUOTES[Math.floor(Math.random() * QUOTES.length)];
|
|
110
110
|
const lookOn = (ctx: ExtensionContext) => standalone && cfg.look !== false && ctx.mode === "tui";
|
|
111
|
+
/** True under `subsub web`: pi runs in RPC mode and the web view shows the state that the terminal header shows. */
|
|
112
|
+
const webView = process.env.SUBSUB_WEB === "1";
|
|
113
|
+
let webCtx: ExtensionContext | undefined;
|
|
114
|
+
function emitWeb(): void {
|
|
115
|
+
const ctx = webCtx;
|
|
116
|
+
if (!webView || !ctx || ctx.mode !== "rpc") return;
|
|
117
|
+
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
118
|
+
const state = {
|
|
119
|
+
mode,
|
|
120
|
+
profile,
|
|
121
|
+
profileLabel: profileSpec(profile).label,
|
|
122
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : null,
|
|
123
|
+
down,
|
|
124
|
+
library,
|
|
125
|
+
language: cfg.language,
|
|
126
|
+
userName: cfg.userName ?? null,
|
|
127
|
+
vault: cfg.vault ?? null,
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
ctx.ui.setStatus("subsub.web", JSON.stringify(state));
|
|
131
|
+
} catch {
|
|
132
|
+
/* the context is gone after shutdown */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
111
135
|
|
|
112
136
|
function applyTheme(ctx: ExtensionContext): void {
|
|
113
137
|
if (!lookOn(ctx) || cfg.themes === false) return;
|
|
@@ -145,6 +169,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
145
169
|
library.zotero = "down";
|
|
146
170
|
}
|
|
147
171
|
headerTui?.requestRender();
|
|
172
|
+
emitWeb();
|
|
148
173
|
}
|
|
149
174
|
|
|
150
175
|
function setHeader(ctx: ExtensionContext): void {
|
|
@@ -183,6 +208,8 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
183
208
|
ctx.ui.setStatus("subsub", `Sub-Sub: ${mode} | ${profileSpec(profile).label} | ${modelId}${down.length ? ` | not running: ${down.join(", ")}` : ""}`);
|
|
184
209
|
if (standalone) ctx.ui.setTitle(`Sub-Sub: ${mode}`);
|
|
185
210
|
headerTui?.requestRender();
|
|
211
|
+
webCtx = ctx;
|
|
212
|
+
emitWeb();
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
async function applyMode(ctx: ExtensionContext, next: Mode, remember: boolean): Promise<void> {
|
|
@@ -218,6 +245,8 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
218
245
|
if (lookOn(ctx)) {
|
|
219
246
|
setHeader(ctx);
|
|
220
247
|
void refreshLibrary();
|
|
248
|
+
} else if (webView) {
|
|
249
|
+
void refreshLibrary();
|
|
221
250
|
}
|
|
222
251
|
for (const [name, err] of Object.entries(bridge.errors)) {
|
|
223
252
|
ctx.ui.notify(`Sub-Sub: the ${name} server did not start: ${err.split("\n")[0]}`, "error");
|
|
@@ -382,10 +411,19 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
382
411
|
}
|
|
383
412
|
}
|
|
384
413
|
headerTui?.requestRender();
|
|
414
|
+
emitWeb();
|
|
385
415
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
386
416
|
},
|
|
387
417
|
});
|
|
388
418
|
|
|
419
|
+
pi.registerCommand("subsub-refresh", {
|
|
420
|
+
description: "Sub-Sub: refresh the library counts shown in the web view",
|
|
421
|
+
handler: async (_args, ctx) => {
|
|
422
|
+
webCtx = ctx;
|
|
423
|
+
await refreshLibrary();
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
389
427
|
pi.registerCommand("history", {
|
|
390
428
|
description: "Sub-Sub: recent library changes",
|
|
391
429
|
handler: async (_args, ctx) => {
|