dsh-reference-anything 0.2.0 → 0.2.2
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 +44 -30
- package/lib/client.js +445 -129
- package/lib/contract.d.ts +11 -0
- package/lib/contract.js +10 -0
- package/lib/host.d.ts +3 -0
- package/lib/host.js +3 -0
- package/lib/opencli.d.ts +1 -1
- package/lib/opencli.js +40 -14
- package/lib/sources/web-chat/index.d.ts +5 -0
- package/lib/sources/web-chat/index.js +16 -0
- package/lib/typert.js +3 -0
- package/lib/update.d.ts +43 -0
- package/lib/update.js +206 -0
- package/opencli-plugin/common.js +31 -43
- package/opencli-plugin/opencli-plugin.json +1 -1
- package/opencli-plugin/package.json +1 -1
- package/package.json +1 -1
package/lib/contract.d.ts
CHANGED
|
@@ -99,6 +99,17 @@ export declare const openCliDiscoverySchema: z.ZodReadonly<z.ZodObject<{
|
|
|
99
99
|
version: z.ZodString;
|
|
100
100
|
error: z.ZodOptional<z.ZodString>;
|
|
101
101
|
}, z.core.$strip>>;
|
|
102
|
+
export declare const packageUpdateStatusSchema: z.ZodReadonly<z.ZodObject<{
|
|
103
|
+
currentVersion: z.ZodString;
|
|
104
|
+
latestVersion: z.ZodString;
|
|
105
|
+
updateAvailable: z.ZodBoolean;
|
|
106
|
+
checkedAt: z.ZodNumber;
|
|
107
|
+
error: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$strip>>;
|
|
109
|
+
export declare const packageUpdateResultSchema: z.ZodReadonly<z.ZodObject<{
|
|
110
|
+
version: z.ZodString;
|
|
111
|
+
restartRequired: z.ZodBoolean;
|
|
112
|
+
}, z.core.$strip>>;
|
|
102
113
|
export declare const providerStatsSchema: z.ZodReadonly<z.ZodObject<{
|
|
103
114
|
provider: z.ZodEnum<{
|
|
104
115
|
chatgpt: "chatgpt";
|
package/lib/contract.js
CHANGED
|
@@ -26,6 +26,13 @@ export const browserProfileSchema = z.object({ id: z.string(), alias: z.string()
|
|
|
26
26
|
export const openCliDiscoverySchema = z.object({
|
|
27
27
|
found: z.boolean(), executable: z.string(), version: z.string(), error: z.string().optional(),
|
|
28
28
|
}).readonly();
|
|
29
|
+
export const packageUpdateStatusSchema = z.object({
|
|
30
|
+
currentVersion: z.string(), latestVersion: z.string(), updateAvailable: z.boolean(),
|
|
31
|
+
checkedAt: z.number().int().nonnegative(), error: z.string().optional(),
|
|
32
|
+
}).readonly();
|
|
33
|
+
export const packageUpdateResultSchema = z.object({
|
|
34
|
+
version: z.string(), restartRequired: z.boolean(),
|
|
35
|
+
}).readonly();
|
|
29
36
|
export const providerStatsSchema = z.object({
|
|
30
37
|
provider: providerSchema, conversations: z.number().int().nonnegative(), lastSyncedAt: z.string(),
|
|
31
38
|
status: z.enum(['ready', 'syncing', 'error', 'empty']), error: z.string().optional(),
|
|
@@ -74,6 +81,9 @@ export const REFERENCE_ANYTHING_INVOCATIONS = [
|
|
|
74
81
|
descriptor('installOpenCli', [], strict('OpenCliDiscovery', openCliDiscoverySchema), true),
|
|
75
82
|
descriptor('installAdapter', [], strict('Boolean', z.boolean()), true),
|
|
76
83
|
descriptor('restartDaemon', [], strict('Boolean', z.boolean()), true),
|
|
84
|
+
descriptor('updateStatus', [], strict('PackageUpdateStatus', packageUpdateStatusSchema), true),
|
|
85
|
+
descriptor('checkUpdate', [], strict('PackageUpdateStatus', packageUpdateStatusSchema), true),
|
|
86
|
+
descriptor('installUpdate', [], strict('PackageUpdateResult', packageUpdateResultSchema), true),
|
|
77
87
|
descriptor('stats', [], strict('ProviderStats[]', z.array(providerStatsSchema))),
|
|
78
88
|
descriptor('syncStart', [{ name: 'input', wire: 'input', source: 'json', codec: strict('SyncStart', syncStartSchema) }], strict('JobId', z.string())),
|
|
79
89
|
descriptor('syncStatus', [{ name: 'input', wire: 'input', source: 'json', codec: strict('JobInput', jobInputSchema) }], strict('SyncStatus?', syncStatusSchema.optional())),
|
package/lib/host.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export declare class ReferenceAnythingRemote extends TypertRemoteService {
|
|
|
26
26
|
installOpenCli(signal: AbortSignal): Promise<import("./opencli.ts").OpenCliDiscovery>;
|
|
27
27
|
installAdapter(signal: AbortSignal): Promise<boolean>;
|
|
28
28
|
restartDaemon(signal: AbortSignal): Promise<boolean>;
|
|
29
|
+
updateStatus(signal: AbortSignal): Promise<import("./update.ts").PackageUpdateStatus>;
|
|
30
|
+
checkUpdate(signal: AbortSignal): Promise<import("./update.ts").PackageUpdateStatus>;
|
|
31
|
+
installUpdate(signal: AbortSignal): Promise<import("./update.ts").PackageUpdateResult>;
|
|
29
32
|
stats(): import("./store/store.ts").ProviderStats[];
|
|
30
33
|
syncStart(input: {
|
|
31
34
|
providers: ChatProvider[];
|
package/lib/host.js
CHANGED
|
@@ -18,6 +18,9 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
|
|
|
18
18
|
installOpenCli(signal) { return this.ctx.referenceChatHistory.installOpenCli(signal); }
|
|
19
19
|
installAdapter(signal) { return this.ctx.referenceChatHistory.installAdapter(signal); }
|
|
20
20
|
restartDaemon(signal) { return this.ctx.referenceChatHistory.restartDaemon(signal); }
|
|
21
|
+
updateStatus(signal) { return this.ctx.referenceChatHistory.updateStatus(signal); }
|
|
22
|
+
checkUpdate(signal) { return this.ctx.referenceChatHistory.checkUpdate(signal); }
|
|
23
|
+
installUpdate(signal) { return this.ctx.referenceChatHistory.installUpdate(signal); }
|
|
21
24
|
stats() { return this.ctx.referenceChatHistory.stats(); }
|
|
22
25
|
syncStart(input) {
|
|
23
26
|
return this.ctx.referenceChatHistory.sync.start(input.providers, input.mode);
|
package/lib/opencli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatProvider } from './store/spec.ts';
|
|
2
2
|
import type { ProviderConversationRow, ProviderTurnRow } from './store/store.ts';
|
|
3
3
|
export declare const MIN_OPENCLI_VERSION = "1.8.6";
|
|
4
|
-
export declare const MIN_ADAPTER_VERSION = "0.2.
|
|
4
|
+
export declare const MIN_ADAPTER_VERSION = "0.2.1";
|
|
5
5
|
export declare const OPENCLI_NPM_PACKAGE = "@jackwener/opencli";
|
|
6
6
|
export type OpenCliErrorCode = 'EXTENSION_NOT_CONNECTED' | 'PROVIDER_TIMEOUT' | 'PROVIDER_NOT_LOGGED_IN' | 'PROVIDER_ACCOUNT_MISMATCH' | 'OPENCLI_CONFIGURATION' | 'OPENCLI_OUTPUT_TOO_LARGE' | 'OPENCLI_FAILED' | 'OPENCLI_INSTALL_FAILED';
|
|
7
7
|
export declare class OpenCliError extends Error {
|
package/lib/opencli.js
CHANGED
|
@@ -5,13 +5,20 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
const execFile = promisify(nodeExecFile);
|
|
7
7
|
export const MIN_OPENCLI_VERSION = '1.8.6';
|
|
8
|
-
export const MIN_ADAPTER_VERSION = '0.2.
|
|
8
|
+
export const MIN_ADAPTER_VERSION = '0.2.1';
|
|
9
9
|
export const OPENCLI_NPM_PACKAGE = '@jackwener/opencli';
|
|
10
10
|
const SITE = {
|
|
11
11
|
chatgpt: 'dsh-chatgpt', claude: 'dsh-claude', gemini: 'dsh-gemini',
|
|
12
12
|
deepseek: 'dsh-deepseek', grok: 'dsh-grok', kimi: 'dsh-kimi',
|
|
13
13
|
};
|
|
14
14
|
const REQUIRED_ADAPTER_COMMANDS = ['chatgpt', 'claude', 'gemini', 'deepseek', 'grok', 'kimi'];
|
|
15
|
+
/**
|
|
16
|
+
* A persistent OpenCLI site session owns one mutable browser tab. Serialize
|
|
17
|
+
* commands that target the same executable/profile/site across runner
|
|
18
|
+
* instances so concurrent detail workers cannot navigate that tab over one
|
|
19
|
+
* another. Different provider sites still run in parallel.
|
|
20
|
+
*/
|
|
21
|
+
const SITE_COMMAND_TAILS = new Map();
|
|
15
22
|
export class OpenCliError extends Error {
|
|
16
23
|
code;
|
|
17
24
|
constructor(message, code, options) {
|
|
@@ -185,19 +192,25 @@ export class OpenCliRunner {
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
async json(site, operation, args, signal) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
const queueKey = [this.executable, ...this.prefixArgs, this.profile, site].join('\0');
|
|
196
|
+
return serializeSiteCommand(queueKey, async () => {
|
|
197
|
+
// Pin adapter traffic to one persistent background session. OpenCLI
|
|
198
|
+
// otherwise lets caller flags override the adapter lifecycle metadata,
|
|
199
|
+
// which could release its tab or make an unattended sync steal focus.
|
|
200
|
+
const stdout = await this.raw([
|
|
201
|
+
site, operation, ...args,
|
|
202
|
+
'--site-session', 'persistent', '--window', 'background', '-f', 'json',
|
|
203
|
+
], signal);
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(stdout);
|
|
206
|
+
if (!Array.isArray(parsed) || parsed.some(row => !row || typeof row !== 'object' || Array.isArray(row)))
|
|
207
|
+
throw new Error('expected object array');
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw new OpenCliError('OpenCLI returned malformed JSON', 'OPENCLI_CONFIGURATION', { cause: error });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
201
214
|
}
|
|
202
215
|
async raw(args, signal) {
|
|
203
216
|
const invocation = resolveInvocation(this.executable);
|
|
@@ -224,6 +237,19 @@ export class OpenCliRunner {
|
|
|
224
237
|
}
|
|
225
238
|
}
|
|
226
239
|
}
|
|
240
|
+
async function serializeSiteCommand(key, task) {
|
|
241
|
+
const previous = SITE_COMMAND_TAILS.get(key) ?? Promise.resolve();
|
|
242
|
+
const current = previous.then(task, task);
|
|
243
|
+
const settled = current.then(() => undefined, () => undefined);
|
|
244
|
+
SITE_COMMAND_TAILS.set(key, settled);
|
|
245
|
+
try {
|
|
246
|
+
return await current;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (SITE_COMMAND_TAILS.get(key) === settled)
|
|
250
|
+
SITE_COMMAND_TAILS.delete(key);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
227
253
|
/** Locate a working OpenCLI without asking a command-line newcomer for PATH details. */
|
|
228
254
|
export async function discoverOpenCli(configured = 'opencli', signal) {
|
|
229
255
|
const candidates = openCliCandidates(configured);
|
|
@@ -5,6 +5,7 @@ import type { ChatProvider, SettingsRecord } from '../../store/spec.ts';
|
|
|
5
5
|
import { ConversationStore, type MatchedVia } from '../../store/store.ts';
|
|
6
6
|
import { ConversationSyncManager } from '../../sync/index.ts';
|
|
7
7
|
import { type OpenCliDiscovery } from '../../opencli.ts';
|
|
8
|
+
import { type PackageUpdateResult, type PackageUpdateStatus } from '../../update.ts';
|
|
8
9
|
import { parseProviderQuery } from '../../search.ts';
|
|
9
10
|
export { parseProviderQuery };
|
|
10
11
|
export declare const name = "reference-web-chat";
|
|
@@ -51,6 +52,7 @@ export default class WebChatHistoryService extends Service implements ReferenceS
|
|
|
51
52
|
private syncValue?;
|
|
52
53
|
private autoSyncInterval?;
|
|
53
54
|
private autoSyncCatchUp?;
|
|
55
|
+
private readonly packageUpdates;
|
|
54
56
|
private readonly liveAttachments;
|
|
55
57
|
constructor(ctx: Context, config?: Config);
|
|
56
58
|
protected [Service.init](): Promise<void>;
|
|
@@ -84,6 +86,9 @@ export default class WebChatHistoryService extends Service implements ReferenceS
|
|
|
84
86
|
/** Durable per-provider sync status, independent of any single in-flight job. */
|
|
85
87
|
syncStates(): import("../../store/store.ts").ProviderSyncState[];
|
|
86
88
|
getSettings(): SettingsRecord;
|
|
89
|
+
updateStatus(signal?: AbortSignal): Promise<PackageUpdateStatus>;
|
|
90
|
+
checkUpdate(signal?: AbortSignal): Promise<PackageUpdateStatus>;
|
|
91
|
+
installUpdate(signal?: AbortSignal): Promise<PackageUpdateResult>;
|
|
87
92
|
restartDaemon(signal?: AbortSignal): Promise<boolean>;
|
|
88
93
|
discoverOpenCli(signal?: AbortSignal): Promise<OpenCliDiscovery>;
|
|
89
94
|
installOpenCli(signal?: AbortSignal): Promise<OpenCliDiscovery>;
|
|
@@ -4,6 +4,7 @@ import { providerSchema, referenceAnythingDomainSpec, settingsRecordSchema } fro
|
|
|
4
4
|
import { ConversationStore } from "../../store/store.js";
|
|
5
5
|
import { ConversationSyncManager } from "../../sync/index.js";
|
|
6
6
|
import { OpenCliError, OpenCliRunner, discoverOpenCli, installOpenCli as installOpenCliPackage } from "../../opencli.js";
|
|
7
|
+
import { PackageUpdateManager } from "../../update.js";
|
|
7
8
|
import { ReferenceAnythingError } from "../../errors.js";
|
|
8
9
|
import { parseProviderQuery } from "../../search.js";
|
|
9
10
|
export { parseProviderQuery };
|
|
@@ -26,6 +27,7 @@ export default class WebChatHistoryService extends Service {
|
|
|
26
27
|
syncValue;
|
|
27
28
|
autoSyncInterval;
|
|
28
29
|
autoSyncCatchUp;
|
|
30
|
+
packageUpdates = new PackageUpdateManager();
|
|
29
31
|
liveAttachments = new Map();
|
|
30
32
|
constructor(ctx, config = {}) {
|
|
31
33
|
super(ctx, 'referenceChatHistory');
|
|
@@ -47,6 +49,13 @@ export default class WebChatHistoryService extends Service {
|
|
|
47
49
|
timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes,
|
|
48
50
|
}), this.ctx.logger);
|
|
49
51
|
await this.store.collectExpired();
|
|
52
|
+
const updateAbort = new AbortController();
|
|
53
|
+
this.ctx.effect(() => () => { updateAbort.abort(); }, 'reference-web-chat.updateCheckCleanup');
|
|
54
|
+
void this.packageUpdates.check(updateAbort.signal).then(status => {
|
|
55
|
+
if (status.updateAvailable) {
|
|
56
|
+
this.ctx.logger.info(`reference anything update available: ${status.currentVersion} -> ${status.latestVersion}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
50
59
|
this.ctx.references.registerSource(this);
|
|
51
60
|
this.ctx.effect(() => () => { this.clearAutoSync(); }, 'reference-web-chat.autoSyncCleanup');
|
|
52
61
|
this.armAutoSync();
|
|
@@ -182,6 +191,13 @@ export default class WebChatHistoryService extends Service {
|
|
|
182
191
|
/** Durable per-provider sync status, independent of any single in-flight job. */
|
|
183
192
|
syncStates() { return this.store.syncStateSummary(); }
|
|
184
193
|
getSettings() { return this.store.settings; }
|
|
194
|
+
updateStatus(signal) { return this.packageUpdates.status(signal); }
|
|
195
|
+
checkUpdate(signal) { return this.packageUpdates.check(signal); }
|
|
196
|
+
async installUpdate(signal) {
|
|
197
|
+
if (this.sync.isRunning())
|
|
198
|
+
throw new ReferenceAnythingError('wait for the current conversation sync to finish before updating', 'REFERENCE_SYNC_IN_PROGRESS');
|
|
199
|
+
return this.packageUpdates.update(signal);
|
|
200
|
+
}
|
|
185
201
|
async restartDaemon(signal) { await this.runner().restartDaemon(signal); return true; }
|
|
186
202
|
async discoverOpenCli(signal) {
|
|
187
203
|
return discoverOpenCli(this.store.settings.opencliPath, signal);
|
package/lib/typert.js
CHANGED
|
@@ -14,6 +14,9 @@ export const TYPERT_MANIFEST = {
|
|
|
14
14
|
{ kind: 'method', name: 'installOpenCli', signature: 'installOpenCli(signal: AbortSignal): Promise<OpenCliDiscovery>' },
|
|
15
15
|
{ kind: 'method', name: 'installAdapter', signature: 'installAdapter(signal: AbortSignal): Promise<boolean>' },
|
|
16
16
|
{ kind: 'method', name: 'restartDaemon', signature: 'restartDaemon(signal: AbortSignal): Promise<boolean>' },
|
|
17
|
+
{ kind: 'method', name: 'updateStatus', signature: 'updateStatus(signal: AbortSignal): Promise<PackageUpdateStatus>' },
|
|
18
|
+
{ kind: 'method', name: 'checkUpdate', signature: 'checkUpdate(signal: AbortSignal): Promise<PackageUpdateStatus>' },
|
|
19
|
+
{ kind: 'method', name: 'installUpdate', signature: 'installUpdate(signal: AbortSignal): Promise<PackageUpdateResult>' },
|
|
17
20
|
{ kind: 'method', name: 'stats', signature: 'stats(): ProviderStats[]' },
|
|
18
21
|
{ kind: 'method', name: 'syncStart', signature: 'syncStart(input: SyncStart): string' },
|
|
19
22
|
{ kind: 'method', name: 'syncStatus', signature: 'syncStatus(input: JobInput): SyncStatus | undefined' },
|
package/lib/update.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "dsh-reference-anything";
|
|
2
|
+
export declare const NPM_LATEST_URL = "https://registry.npmjs.org/dsh-reference-anything/latest";
|
|
3
|
+
export interface PackageUpdateStatus {
|
|
4
|
+
currentVersion: string;
|
|
5
|
+
latestVersion: string;
|
|
6
|
+
updateAvailable: boolean;
|
|
7
|
+
checkedAt: number;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PackageUpdateResult {
|
|
11
|
+
version: string;
|
|
12
|
+
restartRequired: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface UpdateManagerOptions {
|
|
15
|
+
packageRoot?: string;
|
|
16
|
+
dshHome?: string;
|
|
17
|
+
fetchLatest?: (signal?: AbortSignal) => Promise<string>;
|
|
18
|
+
install?: (profileDir: string, version: string, signal?: AbortSignal) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** One startup/manual-check cache and the confirmed profile-local updater. */
|
|
21
|
+
export declare class PackageUpdateManager {
|
|
22
|
+
private readonly packageRoot;
|
|
23
|
+
private readonly dshHome?;
|
|
24
|
+
private readonly fetchLatest;
|
|
25
|
+
private readonly install;
|
|
26
|
+
private cached?;
|
|
27
|
+
private checking?;
|
|
28
|
+
constructor(options?: UpdateManagerOptions);
|
|
29
|
+
/** Return the startup result, waiting for it when it is still in flight. */
|
|
30
|
+
status(signal?: AbortSignal): Promise<PackageUpdateStatus>;
|
|
31
|
+
/** Force a registry read, while collapsing concurrent startup/manual clicks. */
|
|
32
|
+
check(signal?: AbortSignal): Promise<PackageUpdateStatus>;
|
|
33
|
+
/** Install only the exact version returned by a fresh registry check. */
|
|
34
|
+
update(signal?: AbortSignal): Promise<PackageUpdateResult>;
|
|
35
|
+
private runCheck;
|
|
36
|
+
}
|
|
37
|
+
export declare function fetchLatestVersion(signal?: AbortSignal): Promise<string>;
|
|
38
|
+
export declare function readPackageVersion(packageRoot?: string): Promise<string>;
|
|
39
|
+
/** Find the profile that owns this exact installed package, including pnpm links. */
|
|
40
|
+
export declare function findOwningProfileDir(packageRoot?: string, configuredDshHome?: string): Promise<string>;
|
|
41
|
+
export declare function installPackageVersion(profileDir: string, version: string, signal?: AbortSignal): Promise<void>;
|
|
42
|
+
export declare function compareVersions(left: string, right: string): number;
|
|
43
|
+
export {};
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { access, readFile, readdir, realpath } from 'node:fs/promises';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execFile = promisify(nodeExecFile);
|
|
8
|
+
export const PACKAGE_NAME = 'dsh-reference-anything';
|
|
9
|
+
export const NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
10
|
+
const PACKAGE_ROOT = resolve(fileURLToPath(new URL('../', import.meta.url)));
|
|
11
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
12
|
+
const CHECK_TIMEOUT_MS = 10_000;
|
|
13
|
+
/** One startup/manual-check cache and the confirmed profile-local updater. */
|
|
14
|
+
export class PackageUpdateManager {
|
|
15
|
+
packageRoot;
|
|
16
|
+
dshHome;
|
|
17
|
+
fetchLatest;
|
|
18
|
+
install;
|
|
19
|
+
cached;
|
|
20
|
+
checking;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.packageRoot = resolve(options.packageRoot ?? PACKAGE_ROOT);
|
|
23
|
+
this.dshHome = options.dshHome;
|
|
24
|
+
this.fetchLatest = options.fetchLatest ?? fetchLatestVersion;
|
|
25
|
+
this.install = options.install ?? installPackageVersion;
|
|
26
|
+
}
|
|
27
|
+
/** Return the startup result, waiting for it when it is still in flight. */
|
|
28
|
+
status(signal) {
|
|
29
|
+
if (this.checking)
|
|
30
|
+
return this.checking;
|
|
31
|
+
if (this.cached)
|
|
32
|
+
return Promise.resolve(this.cached);
|
|
33
|
+
return this.check(signal);
|
|
34
|
+
}
|
|
35
|
+
/** Force a registry read, while collapsing concurrent startup/manual clicks. */
|
|
36
|
+
check(signal) {
|
|
37
|
+
if (this.checking)
|
|
38
|
+
return this.checking;
|
|
39
|
+
const request = this.runCheck(signal).finally(() => {
|
|
40
|
+
if (this.checking === request)
|
|
41
|
+
this.checking = undefined;
|
|
42
|
+
});
|
|
43
|
+
this.checking = request;
|
|
44
|
+
return request;
|
|
45
|
+
}
|
|
46
|
+
/** Install only the exact version returned by a fresh registry check. */
|
|
47
|
+
async update(signal) {
|
|
48
|
+
const status = await this.check(signal);
|
|
49
|
+
if (status.error)
|
|
50
|
+
throw new Error(status.error);
|
|
51
|
+
if (!status.updateAvailable)
|
|
52
|
+
return { version: status.currentVersion, restartRequired: false };
|
|
53
|
+
const profileDir = await findOwningProfileDir(this.packageRoot, this.dshHome);
|
|
54
|
+
await this.install(profileDir, status.latestVersion, signal);
|
|
55
|
+
this.cached = {
|
|
56
|
+
currentVersion: status.latestVersion,
|
|
57
|
+
latestVersion: status.latestVersion,
|
|
58
|
+
updateAvailable: false,
|
|
59
|
+
checkedAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
return { version: status.latestVersion, restartRequired: true };
|
|
62
|
+
}
|
|
63
|
+
async runCheck(signal) {
|
|
64
|
+
const checkedAt = Date.now();
|
|
65
|
+
let currentVersion = '0.0.0';
|
|
66
|
+
try {
|
|
67
|
+
currentVersion = await readPackageVersion(this.packageRoot);
|
|
68
|
+
const latestVersion = await this.fetchLatest(signal);
|
|
69
|
+
const status = {
|
|
70
|
+
currentVersion,
|
|
71
|
+
latestVersion,
|
|
72
|
+
updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
|
|
73
|
+
checkedAt,
|
|
74
|
+
};
|
|
75
|
+
this.cached = status;
|
|
76
|
+
return status;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const status = {
|
|
80
|
+
currentVersion,
|
|
81
|
+
latestVersion: '',
|
|
82
|
+
updateAvailable: false,
|
|
83
|
+
checkedAt,
|
|
84
|
+
error: `Unable to check npm for updates: ${error instanceof Error ? error.message : String(error)}`,
|
|
85
|
+
};
|
|
86
|
+
this.cached = status;
|
|
87
|
+
return status;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function fetchLatestVersion(signal) {
|
|
92
|
+
const timeout = AbortSignal.timeout(CHECK_TIMEOUT_MS);
|
|
93
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
94
|
+
// The registry's package metadata endpoint accepts the install-v1 media
|
|
95
|
+
// type, but its `/latest` dist-tag endpoint responds 406 to that same
|
|
96
|
+
// header. Request ordinary JSON because this endpoint returns one manifest.
|
|
97
|
+
headers: { accept: 'application/json' },
|
|
98
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok)
|
|
101
|
+
throw new Error(`npm registry returned HTTP ${String(response.status)}`);
|
|
102
|
+
const body = await response.json();
|
|
103
|
+
if (typeof body.version !== 'string' || !isVersion(body.version))
|
|
104
|
+
throw new Error('npm registry returned an invalid version');
|
|
105
|
+
return body.version;
|
|
106
|
+
}
|
|
107
|
+
export async function readPackageVersion(packageRoot = PACKAGE_ROOT) {
|
|
108
|
+
const manifest = await readJson(join(packageRoot, 'package.json'));
|
|
109
|
+
if (typeof manifest.version !== 'string' || !isVersion(manifest.version))
|
|
110
|
+
throw new Error('installed package has an invalid version');
|
|
111
|
+
return manifest.version;
|
|
112
|
+
}
|
|
113
|
+
/** Find the profile that owns this exact installed package, including pnpm links. */
|
|
114
|
+
export async function findOwningProfileDir(packageRoot = PACKAGE_ROOT, configuredDshHome) {
|
|
115
|
+
const resolvedRoot = resolve(packageRoot);
|
|
116
|
+
for (let cursor = resolvedRoot; dirname(cursor) !== cursor; cursor = dirname(cursor)) {
|
|
117
|
+
if (await isOwningProfile(cursor, resolvedRoot))
|
|
118
|
+
return cursor;
|
|
119
|
+
}
|
|
120
|
+
const dshHome = resolve(configuredDshHome || process.env.DSH_HOME || join(homedir(), '.dsh'));
|
|
121
|
+
const profilesRoot = join(dshHome, 'profiles');
|
|
122
|
+
let entries = [];
|
|
123
|
+
try {
|
|
124
|
+
entries = await readdir(profilesRoot, { withFileTypes: true });
|
|
125
|
+
}
|
|
126
|
+
catch { /* handled below */ }
|
|
127
|
+
const matches = [];
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isDirectory())
|
|
130
|
+
continue;
|
|
131
|
+
const profileDir = join(profilesRoot, entry.name);
|
|
132
|
+
if (await isOwningProfile(profileDir, resolvedRoot))
|
|
133
|
+
matches.push(profileDir);
|
|
134
|
+
}
|
|
135
|
+
if (matches.length === 1)
|
|
136
|
+
return matches[0];
|
|
137
|
+
if (matches.length > 1)
|
|
138
|
+
throw new Error('more than one DSH profile points to this installation; update it from the command line');
|
|
139
|
+
throw new Error('the owning DSH profile could not be identified; update it with dsh plugin --profile <name> add dsh-reference-anything@latest');
|
|
140
|
+
}
|
|
141
|
+
export async function installPackageVersion(profileDir, version, signal) {
|
|
142
|
+
if (!isVersion(version))
|
|
143
|
+
throw new Error('refusing to install an invalid package version');
|
|
144
|
+
try {
|
|
145
|
+
await execFile('pnpm', ['add', `${PACKAGE_NAME}@${version}`], {
|
|
146
|
+
cwd: profileDir,
|
|
147
|
+
encoding: 'utf8',
|
|
148
|
+
timeout: UPDATE_TIMEOUT_MS,
|
|
149
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
shell: process.platform === 'win32',
|
|
152
|
+
signal,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const detail = error;
|
|
157
|
+
const output = String(detail.stderr || detail.stdout || '').trim().slice(0, 2_000);
|
|
158
|
+
throw new Error(output || 'pnpm could not update the DSH profile package', { cause: error });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function compareVersions(left, right) {
|
|
162
|
+
if (!isVersion(left) || !isVersion(right))
|
|
163
|
+
throw new Error('cannot compare an invalid package version');
|
|
164
|
+
const [leftCore, leftPre] = left.split('-', 2);
|
|
165
|
+
const [rightCore, rightPre] = right.split('-', 2);
|
|
166
|
+
const a = leftCore.split('.').map(Number);
|
|
167
|
+
const b = rightCore.split('.').map(Number);
|
|
168
|
+
for (let index = 0; index < 3; index++) {
|
|
169
|
+
if (a[index] !== b[index])
|
|
170
|
+
return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
171
|
+
}
|
|
172
|
+
if (leftPre === rightPre)
|
|
173
|
+
return 0;
|
|
174
|
+
if (leftPre === undefined)
|
|
175
|
+
return 1;
|
|
176
|
+
if (rightPre === undefined)
|
|
177
|
+
return -1;
|
|
178
|
+
return leftPre.localeCompare(rightPre, 'en', { numeric: true });
|
|
179
|
+
}
|
|
180
|
+
function isVersion(value) {
|
|
181
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(value);
|
|
182
|
+
}
|
|
183
|
+
async function isOwningProfile(profileDir, packageRoot) {
|
|
184
|
+
let manifest;
|
|
185
|
+
try {
|
|
186
|
+
manifest = await readJson(join(profileDir, 'package.json'));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const dsh = manifest.dsh;
|
|
192
|
+
const dependencies = manifest.dependencies;
|
|
193
|
+
if (!dsh?.profile || typeof dependencies?.[PACKAGE_NAME] !== 'string')
|
|
194
|
+
return false;
|
|
195
|
+
const installedRoot = join(profileDir, 'node_modules', PACKAGE_NAME);
|
|
196
|
+
try {
|
|
197
|
+
await access(join(installedRoot, 'package.json'));
|
|
198
|
+
return await realpath(installedRoot) === await realpath(packageRoot);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function readJson(path) {
|
|
205
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
206
|
+
}
|
package/opencli-plugin/common.js
CHANGED
|
@@ -49,23 +49,6 @@ function assertResult(result, domain, operation) {
|
|
|
49
49
|
return result.rows
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Release only the one-shot task tab. Browser Bridge deliberately keeps its
|
|
54
|
-
* empty adapter container alive so the next sync can reuse it; if the user
|
|
55
|
-
* closes that container, Browser Bridge recreates it on demand.
|
|
56
|
-
*/
|
|
57
|
-
async function inTemporaryTab(page, task) {
|
|
58
|
-
try {
|
|
59
|
-
return await task()
|
|
60
|
-
} finally {
|
|
61
|
-
try {
|
|
62
|
-
await page.closeTab?.()
|
|
63
|
-
} catch {
|
|
64
|
-
// Preserve the provider result/error if the browser closed first.
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
52
|
export function registerProvider(config) {
|
|
70
53
|
const browserSession = SYNC_BROWSER_SESSION
|
|
71
54
|
|
|
@@ -81,15 +64,15 @@ export function registerProvider(config) {
|
|
|
81
64
|
navigateBefore: false,
|
|
82
65
|
args: [],
|
|
83
66
|
columns: ['identity'],
|
|
84
|
-
func: async (page) =>
|
|
85
|
-
await page.goto(config.home, { settleMs: 500 })
|
|
86
|
-
const result = await evaluate(page, config.whoamiScript, {}, `${config.site} whoami`)
|
|
87
|
-
if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
67
|
+
func: async (page) => {
|
|
68
|
+
await page.goto(config.home, { settleMs: 500 })
|
|
69
|
+
const result = await evaluate(page, config.whoamiScript, {}, `${config.site} whoami`)
|
|
70
|
+
if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
88
71
|
if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
|
|
89
72
|
throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
|
|
90
|
-
}
|
|
91
|
-
return [{ identity: result.identity }]
|
|
92
|
-
}
|
|
73
|
+
}
|
|
74
|
+
return [{ identity: result.identity }]
|
|
75
|
+
},
|
|
93
76
|
})
|
|
94
77
|
|
|
95
78
|
cli({
|
|
@@ -107,7 +90,7 @@ export function registerProvider(config) {
|
|
|
107
90
|
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
108
91
|
}, { name: 'accountScope', help: 'Previously observed hashed account scope; mismatch forces a full listing' }],
|
|
109
92
|
columns: SYNC_INDEX_COLUMNS,
|
|
110
|
-
func: async (page, kwargs) =>
|
|
93
|
+
func: async (page, kwargs) => {
|
|
111
94
|
const since = String(kwargs.since || '').trim()
|
|
112
95
|
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
113
96
|
await page.goto(config.home, { settleMs: 1200 })
|
|
@@ -127,7 +110,7 @@ export function registerProvider(config) {
|
|
|
127
110
|
{ kind: 'identity', identity: identity.identity, sinceApplied: effectiveSince },
|
|
128
111
|
...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
|
|
129
112
|
]
|
|
130
|
-
}
|
|
113
|
+
},
|
|
131
114
|
})
|
|
132
115
|
|
|
133
116
|
cli({
|
|
@@ -145,17 +128,17 @@ export function registerProvider(config) {
|
|
|
145
128
|
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
146
129
|
}],
|
|
147
130
|
columns: HISTORY_COLUMNS,
|
|
148
|
-
func: async (page, kwargs) =>
|
|
149
|
-
const since = String(kwargs.since || '').trim()
|
|
131
|
+
func: async (page, kwargs) => {
|
|
132
|
+
const since = String(kwargs.since || '').trim()
|
|
150
133
|
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
151
134
|
await page.goto(config.home, { settleMs: 1200 })
|
|
152
135
|
const rows = assertResult(
|
|
153
136
|
await evaluate(page, config.historyScript, { since }, `${config.site} history-all`),
|
|
154
137
|
config.domain,
|
|
155
138
|
`${config.site} history-all`,
|
|
156
|
-
)
|
|
139
|
+
)
|
|
157
140
|
return rows
|
|
158
|
-
}
|
|
141
|
+
},
|
|
159
142
|
})
|
|
160
143
|
|
|
161
144
|
cli({
|
|
@@ -173,7 +156,7 @@ export function registerProvider(config) {
|
|
|
173
156
|
{ name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
|
|
174
157
|
],
|
|
175
158
|
columns: DETAIL_COLUMNS,
|
|
176
|
-
func: async (page, kwargs) =>
|
|
159
|
+
func: async (page, kwargs) => {
|
|
177
160
|
const id = String(kwargs.id || '').trim()
|
|
178
161
|
if (!id) throw new ArgumentError('id must be a non-empty conversation id')
|
|
179
162
|
await page.goto(config.home, { settleMs: 600 })
|
|
@@ -194,10 +177,10 @@ export function registerProvider(config) {
|
|
|
194
177
|
await page.goto(config.conversationUrl(id), { settleMs: 1800 })
|
|
195
178
|
result = await evaluate(page, config.fallbackScript, { id }, `${config.site} detail fallback`)
|
|
196
179
|
}
|
|
197
|
-
const rows = assertResult(result, config.domain, `${config.site} detail`)
|
|
198
|
-
if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
|
|
199
|
-
return rows
|
|
200
|
-
}
|
|
180
|
+
const rows = assertResult(result, config.domain, `${config.site} detail`)
|
|
181
|
+
if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
|
|
182
|
+
return rows
|
|
183
|
+
},
|
|
201
184
|
})
|
|
202
185
|
|
|
203
186
|
cli({
|
|
@@ -216,8 +199,8 @@ export function registerProvider(config) {
|
|
|
216
199
|
{ name: 'maxBytes', type: 'int', default: 26214400, help: 'Maximum decoded bytes' },
|
|
217
200
|
],
|
|
218
201
|
columns: ['attachmentId', 'name', 'mimeType', 'size', 'status', 'localPath'],
|
|
219
|
-
func: async (page, kwargs) =>
|
|
220
|
-
const locator = String(kwargs.locator || '').trim()
|
|
202
|
+
func: async (page, kwargs) => {
|
|
203
|
+
const locator = String(kwargs.locator || '').trim()
|
|
221
204
|
const output = resolve(String(kwargs.output || ''))
|
|
222
205
|
const maxBytes = Number(kwargs.maxBytes ?? 26_214_400)
|
|
223
206
|
if (!locator) throw new ArgumentError('locator must not be empty')
|
|
@@ -243,16 +226,21 @@ export function registerProvider(config) {
|
|
|
243
226
|
name: result.name || output.split(/[\\/]/).pop() || 'attachment',
|
|
244
227
|
mimeType: result.mimeType || 'application/octet-stream',
|
|
245
228
|
size: bytes.byteLength,
|
|
246
|
-
status: 'available',
|
|
247
|
-
localPath: output,
|
|
248
|
-
}]
|
|
249
|
-
}
|
|
229
|
+
status: 'available',
|
|
230
|
+
localPath: output,
|
|
231
|
+
}]
|
|
232
|
+
},
|
|
250
233
|
})
|
|
251
234
|
}
|
|
252
235
|
|
|
253
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Keep one stable, background Browser Bridge session per provider site.
|
|
238
|
+
* OpenCLI maps persistent sessions to `site:<config.site>` and keeps their tab
|
|
239
|
+
* lease after both successful and failed commands, so later sync work reuses
|
|
240
|
+
* the same tab. If the user closes it manually, OpenCLI recreates it on demand.
|
|
241
|
+
*/
|
|
254
242
|
export const SYNC_BROWSER_SESSION = Object.freeze({
|
|
255
|
-
siteSession: '
|
|
243
|
+
siteSession: 'persistent',
|
|
256
244
|
defaultWindowMode: 'background',
|
|
257
245
|
})
|
|
258
246
|
|