@xpufx/paseo-plugin-updates 0.1.0 → 0.1.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 +9 -11
- package/client/updates.tsx +9 -152
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +11 -4
- package/index.client.tsx +10 -0
- package/index.server.ts +8 -0
- package/package.json +5 -3
- package/server/updates.ts +1 -325
- package/shared/native-lifecycle.ts +30 -0
- package/shared/updates.ts +1 -33
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# plugin-updates
|
|
2
2
|
|
|
3
|
-
Git-source update monitor
|
|
3
|
+
Read-only Git-source update monitor for [Paseo](https://github.com/getpaseo/paseo) plugins (v0.8+).
|
|
4
4
|
|
|
5
5
|
<table align="center">
|
|
6
6
|
<tr>
|
|
@@ -9,13 +9,13 @@ Git-source update monitor and updater for [Paseo](https://github.com/getpaseo/pa
|
|
|
9
9
|
</tr>
|
|
10
10
|
</table>
|
|
11
11
|
|
|
12
|
-
Adds a header button to every workspace that opens a popover listing the installed plugins. Each row reports whether the plugin's installed git source is current, behind, pinned, missing, or has no upstream
|
|
12
|
+
Adds a header button to every workspace that opens a popover listing the installed plugins. Each row reports whether the plugin's installed git source is current, behind, pinned, missing, or has no upstream.
|
|
13
13
|
|
|
14
14
|
Built on [paseo-plugin-helper](https://github.com/xpufx/paseo/tree/main/packages/paseo-plugin-helper), the shared Paseo plugin runtime.
|
|
15
15
|
|
|
16
16
|
> [!NOTE]
|
|
17
17
|
> **Prerequisites & Platform Support**:
|
|
18
|
-
> - The system `git` CLI must be available, because update checks
|
|
18
|
+
> - The system `git` CLI must be available, because update checks shell out to `git`.
|
|
19
19
|
> - Developed and tested primarily on **Linux**.
|
|
20
20
|
|
|
21
21
|
## Core Capabilities
|
|
@@ -45,12 +45,11 @@ Each row resolves the plugin to its git repository root, ref, and subdirectory,
|
|
|
45
45
|
- GitHub, GitLab, and unknown forges get an appropriate glyph, and links deep-link into the subdirectory for GitHub (`/tree/<ref>/<subdir>`) and Forgejo/Gitea-style (`/src/branch/<ref>/<subdir>`) hosts.
|
|
46
46
|
- A `dirty` badge appears on a directory install when the plugin scope has uncommitted changes.
|
|
47
47
|
|
|
48
|
-
### 5.
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- When more than one plugin has an update, an **Update all** button appears. If any failure requires an override, **Force update all** is offered instead.
|
|
48
|
+
### 5. Native lifecycle blocker
|
|
49
|
+
|
|
50
|
+
The 0.9.0-beta.2 daemon client has native plugin preview/proposal/apply/reload methods, but they are not exposed through the plugin-facing `PaseoApi` returned by `getPaseoClient(serverId)`. The server lifecycle API is limited to agent/workspace events. Consequently this plugin deliberately has no update action for managed Git or npm sources, local directories, or self-update: it never spawns `paseo plugin update`, pulls, or reloads another plugin.
|
|
51
|
+
|
|
52
|
+
`useHosts()` is not an update lifecycle API. Choosing a configured host could operate on a different daemon than the installation being displayed, so it is never used as a targeting substitute. Use the host's own plugin management UI/CLI to review and apply updates. The Git diagnostics remain available because native source metadata does not expose their per-subdirectory tree/ref comparison.
|
|
54
53
|
|
|
55
54
|
### 6. Orphaned Directory Flagging
|
|
56
55
|
- Directories left behind in Paseo's managed plugin location by an uninstalled or failed install are listed in a separate **Orphaned directories** section with a name and path.
|
|
@@ -81,9 +80,8 @@ paseo plugin add ./plugins/plugin-updates
|
|
|
81
80
|
## Usage & Configuration
|
|
82
81
|
|
|
83
82
|
- No settings are required. The header button registers automatically in each workspace that has an agent session.
|
|
84
|
-
- Open the button to see installed plugins, their status, and
|
|
83
|
+
- Open the button to see installed plugins, their status, and detected remote changes. Review and apply updates in the owning Paseo host.
|
|
85
84
|
- Checks run against the plugin's install remote only, so a fork or mirror installed as the source is what gets compared.
|
|
86
|
-
- Pulls run with `--ff-only`. A dirty working tree or a diverged branch must be reconciled manually before an update can apply.
|
|
87
85
|
|
|
88
86
|
## Limitations
|
|
89
87
|
|
package/client/updates.tsx
CHANGED
|
@@ -13,19 +13,17 @@ import {
|
|
|
13
13
|
KeyValue,
|
|
14
14
|
KeyValueGroup,
|
|
15
15
|
PluginThemeProvider,
|
|
16
|
-
ProgressBar,
|
|
17
16
|
Row,
|
|
18
17
|
SectionHeader,
|
|
19
18
|
StatusDot,
|
|
20
19
|
usePluginTheme,
|
|
21
|
-
} from "paseo-plugin-helper/
|
|
22
|
-
import { useQuery
|
|
23
|
-
import { useEffect
|
|
20
|
+
} from "./vendor/paseo-plugin-helper/index";
|
|
21
|
+
import { useQuery } from "@tanstack/react-query";
|
|
22
|
+
import { useEffect } from "react";
|
|
24
23
|
import { ActivityIndicator, Linking, Text, View } from "react-native";
|
|
24
|
+
import { NATIVE_PLUGIN_UPDATE_BLOCKER } from "../shared/native-lifecycle";
|
|
25
25
|
import {
|
|
26
26
|
pluginUpdatesCheckRpc,
|
|
27
|
-
pluginUpdatesUpdateAllRpc,
|
|
28
|
-
pluginUpdatesUpdateRpc,
|
|
29
27
|
resolveSourceRef,
|
|
30
28
|
shortHash,
|
|
31
29
|
type PluginUpdate,
|
|
@@ -102,14 +100,8 @@ function hashValue(value: string | null): string | null {
|
|
|
102
100
|
|
|
103
101
|
function PluginRow({
|
|
104
102
|
plugin,
|
|
105
|
-
updating,
|
|
106
|
-
forceNeeded,
|
|
107
|
-
onUpdate,
|
|
108
103
|
}: {
|
|
109
104
|
plugin: PluginUpdate;
|
|
110
|
-
updating: boolean;
|
|
111
|
-
forceNeeded: boolean;
|
|
112
|
-
onUpdate: (pluginId: string, force: boolean) => void;
|
|
113
105
|
}) {
|
|
114
106
|
const { colors, typography, padding } = usePluginTheme();
|
|
115
107
|
const detail = fallbackDetail(plugin);
|
|
@@ -138,16 +130,6 @@ function PluginRow({
|
|
|
138
130
|
/>
|
|
139
131
|
) : null}
|
|
140
132
|
{plugin.dirty === true && !isOrphan ? <Badge label="dirty" variant="warning" dot /> : null}
|
|
141
|
-
{plugin.updateAvailable && !isOrphan ? (
|
|
142
|
-
<Button
|
|
143
|
-
label={forceNeeded ? "Force update" : "Update"}
|
|
144
|
-
variant={forceNeeded ? "primary" : "secondary"}
|
|
145
|
-
size="sm"
|
|
146
|
-
loading={updating}
|
|
147
|
-
disabled={updating}
|
|
148
|
-
onPress={() => onUpdate(plugin.id, forceNeeded)}
|
|
149
|
-
/>
|
|
150
|
-
) : null}
|
|
151
133
|
</Row>
|
|
152
134
|
<Text selectable numberOfLines={detailLines} style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
153
135
|
{detail}
|
|
@@ -252,103 +234,23 @@ export function PluginUpdatesIcon(props: PluginButtonIconProps) {
|
|
|
252
234
|
);
|
|
253
235
|
}
|
|
254
236
|
|
|
255
|
-
interface Failure {
|
|
256
|
-
pluginId: string;
|
|
257
|
-
error: string;
|
|
258
|
-
requiresForce: boolean;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
237
|
function PluginUpdatesPopoverInner(props: PluginButtonContentProps) {
|
|
262
238
|
const { colors, typography, padding } = usePluginTheme();
|
|
263
239
|
const toast = useToast();
|
|
264
|
-
const queryClient = useQueryClient();
|
|
265
240
|
const query = usePluginUpdates(props.workspaceId);
|
|
266
|
-
const update = useRpc(pluginUpdatesUpdateRpc);
|
|
267
|
-
const updateAll = useRpc(pluginUpdatesUpdateAllRpc);
|
|
268
|
-
const [activeUpdate, setActiveUpdate] = useState<string | "all" | null>(null);
|
|
269
|
-
const [progress, setProgress] = useState(0);
|
|
270
|
-
const [failures, setFailures] = useState<Failure[]>([]);
|
|
271
241
|
|
|
272
242
|
const plugins = query.data?.plugins ?? [];
|
|
273
243
|
const rows = partitionedRows(plugins);
|
|
274
244
|
const orphanSection = buildOrphanSection(plugins);
|
|
275
245
|
const staleCount = plugins.filter((plugin) => plugin.updateAvailable).length;
|
|
276
|
-
const forceCount = failures.filter((failure) => failure.requiresForce).length;
|
|
277
|
-
|
|
278
246
|
const refresh = async () => {
|
|
279
247
|
try {
|
|
280
|
-
setFailures([]);
|
|
281
248
|
await query.refetch();
|
|
282
249
|
} catch (error) {
|
|
283
250
|
toast.show(error instanceof Error ? error.message : String(error), { variant: "error" });
|
|
284
251
|
}
|
|
285
252
|
};
|
|
286
253
|
|
|
287
|
-
const recordFailure = (failure: Failure) => {
|
|
288
|
-
setFailures((current) => [
|
|
289
|
-
...current.filter((item) => item.pluginId !== failure.pluginId),
|
|
290
|
-
failure,
|
|
291
|
-
]);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const clearFailure = (pluginId: string) => {
|
|
295
|
-
setFailures((current) => current.filter((failure) => failure.pluginId !== pluginId));
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const runUpdate = async (pluginId: string, force = false) => {
|
|
299
|
-
setActiveUpdate(pluginId);
|
|
300
|
-
setProgress(0.25);
|
|
301
|
-
try {
|
|
302
|
-
const result = await update({ workspaceId: props.workspaceId, pluginId, force });
|
|
303
|
-
if (result.status === "error") {
|
|
304
|
-
recordFailure({
|
|
305
|
-
pluginId,
|
|
306
|
-
error: result.error || "Update failed",
|
|
307
|
-
requiresForce: result.requiresForce === true,
|
|
308
|
-
});
|
|
309
|
-
toast.show(result.error || `Failed to update ${pluginId}`, { variant: "error" });
|
|
310
|
-
} else {
|
|
311
|
-
clearFailure(pluginId);
|
|
312
|
-
toast.show(`${pluginId} updated`, { variant: "success" });
|
|
313
|
-
}
|
|
314
|
-
setProgress(1);
|
|
315
|
-
await queryClient.invalidateQueries({ queryKey: [...QUERY_KEY, props.workspaceId] });
|
|
316
|
-
} catch (error) {
|
|
317
|
-
toast.show(error instanceof Error ? error.message : String(error), { variant: "error" });
|
|
318
|
-
} finally {
|
|
319
|
-
setActiveUpdate(null);
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const runUpdateAll = async (force = false) => {
|
|
324
|
-
setActiveUpdate("all");
|
|
325
|
-
setProgress(0.1);
|
|
326
|
-
try {
|
|
327
|
-
const result = await updateAll({ workspaceId: props.workspaceId, force });
|
|
328
|
-
const failed = result.results.filter((item) => item.status === "error");
|
|
329
|
-
setFailures(
|
|
330
|
-
failed.map((item) => ({
|
|
331
|
-
pluginId: item.pluginId,
|
|
332
|
-
error: item.error || "Update failed",
|
|
333
|
-
requiresForce: item.requiresForce === true,
|
|
334
|
-
})),
|
|
335
|
-
);
|
|
336
|
-
setProgress(1);
|
|
337
|
-
if (failed.length > 0) {
|
|
338
|
-
toast.show(`${failed.length} plugin update${failed.length === 1 ? "" : "s"} failed`, {
|
|
339
|
-
variant: "error",
|
|
340
|
-
});
|
|
341
|
-
} else {
|
|
342
|
-
toast.show("All plugins updated", { variant: "success" });
|
|
343
|
-
}
|
|
344
|
-
await queryClient.invalidateQueries({ queryKey: [...QUERY_KEY, props.workspaceId] });
|
|
345
|
-
} catch (error) {
|
|
346
|
-
toast.show(error instanceof Error ? error.message : String(error), { variant: "error" });
|
|
347
|
-
} finally {
|
|
348
|
-
setActiveUpdate(null);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
254
|
useEffect(() => {
|
|
353
255
|
if (query.isError) {
|
|
354
256
|
toast.show("Unable to check plugin updates", { variant: "error" });
|
|
@@ -368,7 +270,10 @@ function PluginUpdatesPopoverInner(props: PluginButtonContentProps) {
|
|
|
368
270
|
<View style={{ flex: 1 }}>
|
|
369
271
|
<Text style={{ color: colors.foreground, ...typography.heading }}>Plugin updates</Text>
|
|
370
272
|
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
371
|
-
{query.isFetching ? "Checking installed plugins…" : `${staleCount}
|
|
273
|
+
{query.isFetching ? "Checking installed plugins…" : `${staleCount} remote change${staleCount === 1 ? "" : "s"} detected`}
|
|
274
|
+
</Text>
|
|
275
|
+
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
276
|
+
Diagnostics only — {NATIVE_PLUGIN_UPDATE_BLOCKER}
|
|
372
277
|
</Text>
|
|
373
278
|
</View>
|
|
374
279
|
<Button
|
|
@@ -376,12 +281,11 @@ function PluginUpdatesPopoverInner(props: PluginButtonContentProps) {
|
|
|
376
281
|
variant="ghost"
|
|
377
282
|
size="sm"
|
|
378
283
|
loading={query.isFetching}
|
|
379
|
-
disabled={query.isFetching
|
|
284
|
+
disabled={query.isFetching}
|
|
380
285
|
accessibilityLabel="Refresh plugin update check"
|
|
381
286
|
onPress={refresh}
|
|
382
287
|
/>
|
|
383
288
|
</Row>
|
|
384
|
-
{activeUpdate ? <ProgressBar value={progress} autoStatusColor label="Updating plugins" showValueText /> : null}
|
|
385
289
|
{query.isLoading && !query.data ? (
|
|
386
290
|
<View style={{ alignItems: "center", paddingVertical: padding.vertical * 2 }}>
|
|
387
291
|
<ActivityIndicator color={colors.foregroundMuted} />
|
|
@@ -394,58 +298,11 @@ function PluginUpdatesPopoverInner(props: PluginButtonContentProps) {
|
|
|
394
298
|
<PluginRow
|
|
395
299
|
key={plugin.id}
|
|
396
300
|
plugin={plugin}
|
|
397
|
-
updating={activeUpdate === plugin.id}
|
|
398
|
-
forceNeeded={failures.some((failure) => failure.pluginId === plugin.id && failure.requiresForce)}
|
|
399
|
-
onUpdate={runUpdate}
|
|
400
301
|
/>
|
|
401
302
|
))}
|
|
402
303
|
{orphanSection ? <OrphanSection section={orphanSection} /> : null}
|
|
403
304
|
</>
|
|
404
305
|
)}
|
|
405
|
-
{failures.map((failure) => (
|
|
406
|
-
<Card key={`failure-${failure.pluginId}`} variant="elevated">
|
|
407
|
-
<View style={{ gap: padding.gap }}>
|
|
408
|
-
<Row align="center">
|
|
409
|
-
<StatusDot variant="danger" />
|
|
410
|
-
<Text style={{ color: colors.statusDanger, ...typography.bodyStrong, flex: 1 }}>
|
|
411
|
-
{failure.pluginId} update failed
|
|
412
|
-
</Text>
|
|
413
|
-
{failure.requiresForce ? (
|
|
414
|
-
<Button
|
|
415
|
-
label="Force"
|
|
416
|
-
variant="secondary"
|
|
417
|
-
size="sm"
|
|
418
|
-
loading={activeUpdate === failure.pluginId}
|
|
419
|
-
disabled={activeUpdate !== null}
|
|
420
|
-
onPress={() => runUpdate(failure.pluginId, true)}
|
|
421
|
-
/>
|
|
422
|
-
) : null}
|
|
423
|
-
</Row>
|
|
424
|
-
<Text selectable style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
425
|
-
{failure.error}
|
|
426
|
-
</Text>
|
|
427
|
-
</View>
|
|
428
|
-
</Card>
|
|
429
|
-
))}
|
|
430
|
-
{forceCount > 0 ? (
|
|
431
|
-
<Button
|
|
432
|
-
label="Force update all"
|
|
433
|
-
variant="secondary"
|
|
434
|
-
icon="AlertTriangle"
|
|
435
|
-
loading={activeUpdate === "all"}
|
|
436
|
-
disabled={activeUpdate !== null}
|
|
437
|
-
onPress={() => runUpdateAll(true)}
|
|
438
|
-
/>
|
|
439
|
-
) : staleCount > 1 ? (
|
|
440
|
-
<Button
|
|
441
|
-
label={`Update all (${staleCount})`}
|
|
442
|
-
variant="primary"
|
|
443
|
-
icon="DownloadCloud"
|
|
444
|
-
loading={activeUpdate === "all"}
|
|
445
|
-
disabled={activeUpdate !== null}
|
|
446
|
-
onPress={() => runUpdateAll(false)}
|
|
447
|
-
/>
|
|
448
|
-
) : null}
|
|
449
306
|
</ScrollView>
|
|
450
307
|
</PluginThemeProvider>
|
|
451
308
|
);
|
|
@@ -24,16 +24,23 @@ export interface ClipboardEnvironment {
|
|
|
24
24
|
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
25
|
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
26
|
* item in place while the UI claims success (xpufx-org/paseo#278); it is only
|
|
27
|
-
* usable off-DOM (native), where it is the real platform clipboard.
|
|
28
|
-
*
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. The same
|
|
28
|
+
* applies to any `setStringAsync` whose DOM fallback is that unverified
|
|
29
|
+
* `execCommand`. In a DOM the checked `execCommand` fallback is preferred to
|
|
30
|
+
* those unverifiable paths.
|
|
29
31
|
*/
|
|
30
32
|
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
33
|
const tiers: ClipboardTier[] = [];
|
|
32
34
|
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
35
|
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
-
|
|
36
|
+
// On a DOM both RN-web paths are unverifiable: `setString` reports success
|
|
37
|
+
// even when its `execCommand` no-ops, and a `setStringAsync` that falls back
|
|
38
|
+
// to it does the same. Off-DOM they are the real native clipboard, so they
|
|
39
|
+
// lead there; on a DOM only the checked `execCommand` fallback is honest.
|
|
40
|
+
const rnDomOk = !env.isDom;
|
|
41
|
+
if (env.hasRnSetStringAsync && rnDomOk) {
|
|
35
42
|
tiers.push("rnAsync");
|
|
36
|
-
} else if (env.hasRnClipboard &&
|
|
43
|
+
} else if (env.hasRnClipboard && rnDomOk) {
|
|
37
44
|
tiers.push("rnSync");
|
|
38
45
|
}
|
|
39
46
|
tiers.push("execCommand");
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import { Icon, Modal, ScrollView, useToast } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import { initClientHelpers } from "./client/vendor/paseo-plugin-helper/index";
|
|
5
|
+
import { registerPluginUpdateHeaders } from "./client/updates";
|
|
6
|
+
|
|
7
|
+
export default function contribute(client: PluginClientContext) {
|
|
8
|
+
initClientHelpers({ Icon, Modal, useRpc, ScrollView, useToast });
|
|
9
|
+
return registerPluginUpdateHeaders(client);
|
|
10
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { pluginUpdatesCheckRpc } from "./shared/updates";
|
|
3
|
+
import { checkInstalledPlugins } from "./server/updates";
|
|
4
|
+
|
|
5
|
+
export default function contribute(server: PluginServerContext) {
|
|
6
|
+
server.handle(pluginUpdatesCheckRpc, ({ workspaceId }) => checkInstalledPlugins(workspaceId));
|
|
7
|
+
return () => {};
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-plugin-updates",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.1.
|
|
6
|
-
"description": "
|
|
5
|
+
"version": "0.1.2",
|
|
6
|
+
"description": "Read-only monitor for installed Paseo plugin source status, links, and per-subdirectory Git diagnostics.",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"test": "npm run typecheck && npx tsx --test shared/updates.test.ts server/updates.test.ts client/orphans.test.ts",
|
|
9
|
+
"test": "npm run typecheck && npx tsx --test shared/updates.test.ts shared/native-lifecycle.test.ts server/updates.test.ts client/orphans.test.ts",
|
|
10
10
|
"test:vitest": "npm run typecheck && vitest run --config vitest.config.ts"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"paseo-plugin.json",
|
|
23
23
|
"README.md",
|
|
24
24
|
"LICENSE",
|
|
25
|
+
"index.client.tsx",
|
|
26
|
+
"index.server.ts",
|
|
25
27
|
"client",
|
|
26
28
|
"server",
|
|
27
29
|
"shared",
|
package/server/updates.ts
CHANGED
|
@@ -2,31 +2,16 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { listPlugins, safeSpawn, type PaseoPluginInfo, type SafeSpawnResult } from "paseo-plugin-helper/
|
|
5
|
+
import { listPlugins, safeSpawn, type PaseoPluginInfo, type SafeSpawnResult } from "./vendor/paseo-plugin-helper/index";
|
|
6
6
|
import { deriveSourceUrl, shortHash } from "../shared/updates";
|
|
7
7
|
import type {
|
|
8
8
|
PluginUpdate,
|
|
9
|
-
PluginUpdateActionResult,
|
|
10
9
|
PluginUpdateChange,
|
|
11
10
|
RefKind,
|
|
12
11
|
} from "../shared/updates";
|
|
13
12
|
|
|
14
13
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
15
14
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
16
|
-
const UPDATE_TIMEOUT_MS = 120_000;
|
|
17
|
-
const RELOAD_TIMEOUT_MS = 60_000;
|
|
18
|
-
|
|
19
|
-
// This plugin's own install id. Paseo runs plugin backend code in the plugin's
|
|
20
|
-
// subprocess, so `paseo plugin reload plugin-updates` terminates the very
|
|
21
|
-
// process executing the update loop (#210). We therefore never reload ourselves
|
|
22
|
-
// from inside ourselves.
|
|
23
|
-
const SELF_PLUGIN_ID = "plugin-updates";
|
|
24
|
-
|
|
25
|
-
// Shown when the updater updated itself: the pull succeeded, but applying it
|
|
26
|
-
// requires a reload that only the host can safely perform.
|
|
27
|
-
const SELF_UPDATE_DETAIL =
|
|
28
|
-
"Updated in place. Reload plugin-updates from Settings to apply (a plugin cannot safely reload itself).";
|
|
29
|
-
|
|
30
15
|
// Report-only verdict for an install whose tracked ref or plugin subdirectory is gone.
|
|
31
16
|
const MISSING_SOURCE_DETAIL = "Plugin does not exist at the source it was installed from.";
|
|
32
17
|
|
|
@@ -1008,325 +993,16 @@ async function scanOrphanedDirs(
|
|
|
1008
993
|
return rows;
|
|
1009
994
|
}
|
|
1010
995
|
|
|
1011
|
-
// ---------------------------------------------------------------------------
|
|
1012
|
-
// Actions
|
|
1013
|
-
// ---------------------------------------------------------------------------
|
|
1014
|
-
|
|
1015
|
-
function actionResult(
|
|
1016
|
-
pluginId: string,
|
|
1017
|
-
status: "updated" | "error",
|
|
1018
|
-
error: string | null = null,
|
|
1019
|
-
output: string | null = null,
|
|
1020
|
-
requiresForce = false,
|
|
1021
|
-
): PluginUpdateActionResult {
|
|
1022
|
-
return {
|
|
1023
|
-
pluginId,
|
|
1024
|
-
status,
|
|
1025
|
-
output: output ?? null,
|
|
1026
|
-
error: error ?? null,
|
|
1027
|
-
...(requiresForce ? { requiresForce: true } : {}),
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function invokePaseoUpdate(
|
|
1032
|
-
args: string[],
|
|
1033
|
-
runner: CommandRunner,
|
|
1034
|
-
): Promise<{ status: "updated" | "error"; output: string | null; error: string | null }> {
|
|
1035
|
-
try {
|
|
1036
|
-
const result = await runner("paseo", ["plugin", "update", ...args], {
|
|
1037
|
-
timeoutMs: UPDATE_TIMEOUT_MS,
|
|
1038
|
-
});
|
|
1039
|
-
const output = outputOf(result) || null;
|
|
1040
|
-
if (result.code === 0) return { status: "updated", output, error: null };
|
|
1041
|
-
return { status: "error", output, error: output || `paseo plugin update exited with code ${result.code}` };
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
return { status: "error", output: null, error: errorOf(error) };
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async function reloadPlugin(
|
|
1048
|
-
pluginId: string,
|
|
1049
|
-
runner: CommandRunner,
|
|
1050
|
-
): Promise<{ ok: boolean; output: string | null; error: string | null }> {
|
|
1051
|
-
try {
|
|
1052
|
-
const result = await runner("paseo", ["plugin", "reload", pluginId], {
|
|
1053
|
-
timeoutMs: RELOAD_TIMEOUT_MS,
|
|
1054
|
-
});
|
|
1055
|
-
const output = outputOf(result) || null;
|
|
1056
|
-
if (result.code === 0) return { ok: true, output, error: null };
|
|
1057
|
-
return { ok: false, output, error: output || `paseo plugin reload exited with code ${result.code}` };
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
return { ok: false, output: null, error: errorOf(error) };
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Splits the plugins affected by a pull into those we may reload and the ones
|
|
1065
|
-
* we must not.
|
|
1066
|
-
*
|
|
1067
|
-
* `plugin-updates` MUST never reload itself: its handlers run inside the plugin
|
|
1068
|
-
* subprocess, so `paseo plugin reload plugin-updates` kills the process that is
|
|
1069
|
-
* mid-update — the crash/loop in #210. Callers report the self entry as updated
|
|
1070
|
-
* with a "reload from Settings" note instead.
|
|
1071
|
-
*/
|
|
1072
|
-
export function planReloads(
|
|
1073
|
-
ids: string[],
|
|
1074
|
-
selfId: string = SELF_PLUGIN_ID,
|
|
1075
|
-
): { reload: string[]; skippedSelf: string[] } {
|
|
1076
|
-
const reload: string[] = [];
|
|
1077
|
-
const skippedSelf: string[] = [];
|
|
1078
|
-
const seen = new Set<string>();
|
|
1079
|
-
for (const id of ids) {
|
|
1080
|
-
if (seen.has(id)) continue;
|
|
1081
|
-
seen.add(id);
|
|
1082
|
-
if (id === selfId) skippedSelf.push(id);
|
|
1083
|
-
else reload.push(id);
|
|
1084
|
-
}
|
|
1085
|
-
return { reload, skippedSelf };
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Reloads plugins strictly one at a time.
|
|
1090
|
-
*
|
|
1091
|
-
* Reloads are serialized because each `paseo plugin reload` restarts a plugin
|
|
1092
|
-
* subprocess; overlapping them races the daemon's plugin registry (#210).
|
|
1093
|
-
*/
|
|
1094
|
-
async function reloadSerially(
|
|
1095
|
-
ids: string[],
|
|
1096
|
-
runner: CommandRunner,
|
|
1097
|
-
): Promise<Array<{ id: string; ok: boolean; output: string | null; error: string | null }>> {
|
|
1098
|
-
const outcomes: Array<{ id: string; ok: boolean; output: string | null; error: string | null }> = [];
|
|
1099
|
-
for (const id of ids) {
|
|
1100
|
-
const reload = await reloadPlugin(id, runner);
|
|
1101
|
-
outcomes.push({ id, ...reload });
|
|
1102
|
-
}
|
|
1103
|
-
return outcomes;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
interface PullOutcome {
|
|
1107
|
-
ok: boolean;
|
|
1108
|
-
requiresForce: boolean;
|
|
1109
|
-
output: string | null;
|
|
1110
|
-
error: string | null;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
async function checkDirty(root: string, runner: CommandRunner): Promise<boolean | null> {
|
|
1114
|
-
const statusResult = await git(runner, ["status", "--porcelain"], root);
|
|
1115
|
-
if (statusResult.code !== 0) return null;
|
|
1116
|
-
return statusResult.stdout.trim().length > 0;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
async function pullRoot(root: string, force: boolean, runner: CommandRunner): Promise<PullOutcome> {
|
|
1120
|
-
const dirty = await checkDirty(root, runner);
|
|
1121
|
-
if (dirty === null) {
|
|
1122
|
-
return { ok: false, requiresForce: false, output: null, error: "git status failed for the plugin repository" };
|
|
1123
|
-
}
|
|
1124
|
-
if (dirty && !force) {
|
|
1125
|
-
return {
|
|
1126
|
-
ok: false,
|
|
1127
|
-
requiresForce: true,
|
|
1128
|
-
output: null,
|
|
1129
|
-
error: "Working tree is dirty — refusing to pull. Re-run with force to override.",
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const head = parseCommit((await git(runner, ["rev-parse", "HEAD"], root)).stdout);
|
|
1134
|
-
const upstream = await git(runner, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root);
|
|
1135
|
-
const upstreamName = upstream.code === 0 ? upstream.stdout.trim() : "";
|
|
1136
|
-
if (head && upstreamName && upstreamName !== "HEAD") {
|
|
1137
|
-
const upstreamCommit = parseCommit((await git(runner, ["rev-parse", upstreamName], root)).stdout);
|
|
1138
|
-
if (upstreamCommit) {
|
|
1139
|
-
const remoteBehind = await isAncestor(runner, root, upstreamCommit, head);
|
|
1140
|
-
const localBehind = await isAncestor(runner, root, head, upstreamCommit);
|
|
1141
|
-
if (remoteBehind === false && localBehind === false) {
|
|
1142
|
-
return {
|
|
1143
|
-
ok: false,
|
|
1144
|
-
requiresForce: false,
|
|
1145
|
-
output: null,
|
|
1146
|
-
error: `Branch has diverged from ${upstreamName} — refusing to pull. Reconcile manually.`,
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const pull = await git(runner, ["pull", "--ff-only"], root, UPDATE_TIMEOUT_MS);
|
|
1153
|
-
const output = outputOf(pull) || null;
|
|
1154
|
-
if (pull.code !== 0) {
|
|
1155
|
-
return { ok: false, requiresForce: false, output, error: output || `git pull --ff-only exited with code ${pull.code}` };
|
|
1156
|
-
}
|
|
1157
|
-
return { ok: true, requiresForce: false, output, error: null };
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
async function idsSharingRoot(
|
|
1161
|
-
installed: PaseoPluginInfo[],
|
|
1162
|
-
records: Map<string, ManagedRecord>,
|
|
1163
|
-
root: string,
|
|
1164
|
-
runner: CommandRunner,
|
|
1165
|
-
): Promise<string[]> {
|
|
1166
|
-
const resolved = await Promise.all(
|
|
1167
|
-
installed.map(async (plugin) => ({
|
|
1168
|
-
id: plugin.id,
|
|
1169
|
-
root: plugin.path ? (await resolveIdentity(plugin, records.get(plugin.id) ?? null, runner)).repoRoot : null,
|
|
1170
|
-
})),
|
|
1171
|
-
);
|
|
1172
|
-
return resolved.filter((entry) => entry.root === root).map((entry) => entry.id);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
export interface UpdateOptions {
|
|
1176
|
-
force?: boolean;
|
|
1177
|
-
runner?: CommandRunner;
|
|
1178
|
-
installedOverride?: PaseoPluginInfo[];
|
|
1179
|
-
deps?: ProbeDeps;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
export async function updatePlugin(
|
|
1183
|
-
pluginId: string,
|
|
1184
|
-
_workspaceId?: string,
|
|
1185
|
-
options: UpdateOptions = {},
|
|
1186
|
-
): Promise<PluginUpdateActionResult> {
|
|
1187
|
-
const runner = options.runner ?? runCommand;
|
|
1188
|
-
const deps = options.deps ?? {};
|
|
1189
|
-
let installed: PaseoPluginInfo[];
|
|
1190
|
-
try {
|
|
1191
|
-
installed = options.installedOverride ?? (await listPlugins({ forceRefresh: true }));
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
return actionResult(pluginId, "error", `Unable to list installed plugins: ${errorOf(error)}`);
|
|
1194
|
-
}
|
|
1195
|
-
const info = installed.find((plugin) => plugin.id === pluginId);
|
|
1196
|
-
if (!info) return actionResult(pluginId, "error", `Plugin '${pluginId}' is not installed`);
|
|
1197
|
-
if (!info.path) return actionResult(pluginId, "error", `Plugin '${pluginId}' has no directory path`);
|
|
1198
|
-
|
|
1199
|
-
if (info.source === "git") {
|
|
1200
|
-
const result = await invokePaseoUpdate([pluginId], runner);
|
|
1201
|
-
return actionResult(pluginId, result.status, result.error, result.output);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
const records = await loadManagedRecords(deps);
|
|
1205
|
-
const identity = await resolveIdentity(info, records.get(pluginId) ?? null, runner);
|
|
1206
|
-
if (!identity.repoRoot) {
|
|
1207
|
-
return actionResult(pluginId, "error", `Plugin '${pluginId}' is not a git repository — no git update possible`);
|
|
1208
|
-
}
|
|
1209
|
-
const pull = await pullRoot(identity.repoRoot, options.force ?? false, runner);
|
|
1210
|
-
if (!pull.ok) {
|
|
1211
|
-
return actionResult(pluginId, "error", pull.error, pull.output, pull.requiresForce);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
const ids = await idsSharingRoot(installed, records, identity.repoRoot, runner);
|
|
1215
|
-
// Never reload ourselves: this handler runs inside the plugin subprocess, so
|
|
1216
|
-
// reloading plugin-updates would kill the process mid-update (#210).
|
|
1217
|
-
const { reload, skippedSelf } = planReloads(ids);
|
|
1218
|
-
const reloadFailures: string[] = [];
|
|
1219
|
-
for (const outcome of await reloadSerially(reload, runner)) {
|
|
1220
|
-
if (!outcome.ok) reloadFailures.push(`${outcome.id}: ${outcome.error}`);
|
|
1221
|
-
}
|
|
1222
|
-
const output = pull.output ?? `Pulled ${identity.repoRoot}`;
|
|
1223
|
-
if (reloadFailures.length > 0) {
|
|
1224
|
-
return actionResult(pluginId, "error", `Pulled but failed to reload: ${reloadFailures.join("; ")}`, output);
|
|
1225
|
-
}
|
|
1226
|
-
// Updating ourselves: the pull succeeded, but only the host can apply it.
|
|
1227
|
-
if (skippedSelf.length > 0) {
|
|
1228
|
-
return actionResult(pluginId, "updated", null, `${output}\n${SELF_UPDATE_DETAIL}`);
|
|
1229
|
-
}
|
|
1230
|
-
return actionResult(pluginId, "updated", null, output);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
export async function updateAllPlugins(
|
|
1234
|
-
_workspaceId?: string,
|
|
1235
|
-
options: UpdateOptions = {},
|
|
1236
|
-
): Promise<{ results: PluginUpdateActionResult[] }> {
|
|
1237
|
-
const runner = options.runner ?? runCommand;
|
|
1238
|
-
const deps = options.deps ?? {};
|
|
1239
|
-
let installed: PaseoPluginInfo[];
|
|
1240
|
-
try {
|
|
1241
|
-
installed = options.installedOverride ?? (await listPlugins({ forceRefresh: true }));
|
|
1242
|
-
} catch (error) {
|
|
1243
|
-
return {
|
|
1244
|
-
results: [actionResult("all", "error", `Unable to list installed plugins: ${errorOf(error)}`)],
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const rows = await probeInstalled(installed, runner, { ...deps, scanOrphans: false });
|
|
1249
|
-
const directoryGroups = new Map<string, PluginUpdate[]>();
|
|
1250
|
-
const gitRows: PluginUpdate[] = [];
|
|
1251
|
-
for (const row of rows) {
|
|
1252
|
-
if (!row.updateAvailable) continue;
|
|
1253
|
-
if (row.source === "git") {
|
|
1254
|
-
gitRows.push(row);
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
if (!row.repoRoot) continue;
|
|
1258
|
-
const group = directoryGroups.get(row.repoRoot) ?? [];
|
|
1259
|
-
group.push(row);
|
|
1260
|
-
directoryGroups.set(row.repoRoot, group);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const results: PluginUpdateActionResult[] = [];
|
|
1264
|
-
for (const row of gitRows) {
|
|
1265
|
-
const result = await invokePaseoUpdate([row.id], runner);
|
|
1266
|
-
results.push(actionResult(row.id, result.status, result.error, result.output));
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// Phase 1: pull every affected directory root, collecting the outcome. Each
|
|
1270
|
-
// root is pulled exactly once (`directoryGroups` is keyed by root); reloads
|
|
1271
|
-
// are deliberately NOT interleaved here — a reload restarts a plugin
|
|
1272
|
-
// subprocess, and doing that mid-batch is what let a self/shared-root update
|
|
1273
|
-
// tear down the running updater and loop (#210).
|
|
1274
|
-
const pulled = new Map<string, { ids: string[]; pull: PullOutcome }>();
|
|
1275
|
-
for (const [root, group] of directoryGroups) {
|
|
1276
|
-
const ids = group.map((row) => row.id);
|
|
1277
|
-
const pull = await pullRoot(root, options.force ?? false, runner);
|
|
1278
|
-
pulled.set(root, { ids, pull });
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// Phase 2: reload once, after every pull has completed, strictly serially.
|
|
1282
|
-
for (const [root, { ids, pull }] of pulled) {
|
|
1283
|
-
if (!pull.ok) {
|
|
1284
|
-
for (const id of ids) {
|
|
1285
|
-
results.push(actionResult(id, "error", pull.error, pull.output, pull.requiresForce));
|
|
1286
|
-
}
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
const { reload, skippedSelf } = planReloads(ids);
|
|
1290
|
-
const outcomes = await reloadSerially(reload, runner);
|
|
1291
|
-
const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
|
|
1292
|
-
for (const id of ids) {
|
|
1293
|
-
if (skippedSelf.includes(id)) {
|
|
1294
|
-
results.push(
|
|
1295
|
-
actionResult(id, "updated", null, `${pull.output ?? `Pulled ${root}`}\n${SELF_UPDATE_DETAIL}`),
|
|
1296
|
-
);
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const reload = byId.get(id);
|
|
1300
|
-
results.push(
|
|
1301
|
-
reload?.ok
|
|
1302
|
-
? actionResult(id, "updated", null, pull.output ?? `Pulled ${root}`)
|
|
1303
|
-
: actionResult(id, "error", reload?.error ?? "reload did not run", pull.output),
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (results.length === 0) {
|
|
1309
|
-
results.push(actionResult("all", "updated", null, "No plugins with remote updates"));
|
|
1310
|
-
}
|
|
1311
|
-
return { results };
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
996
|
export const testing = {
|
|
1315
997
|
probePlugin,
|
|
1316
998
|
probeInstalled,
|
|
1317
|
-
pullRoot,
|
|
1318
999
|
scanOrphanedDirs,
|
|
1319
1000
|
checkInstalledPlugins,
|
|
1320
|
-
updatePlugin,
|
|
1321
|
-
updateAllPlugins,
|
|
1322
|
-
planReloads,
|
|
1323
1001
|
loadManagedRecords,
|
|
1324
1002
|
resolveIdentity,
|
|
1325
1003
|
resolveRef,
|
|
1326
1004
|
classifyBranch,
|
|
1327
1005
|
classifyTag,
|
|
1328
|
-
SELF_PLUGIN_ID,
|
|
1329
1006
|
PROBE_TIMEOUT_MS,
|
|
1330
1007
|
FETCH_TIMEOUT_MS,
|
|
1331
|
-
UPDATE_TIMEOUT_MS,
|
|
1332
1008
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK 0.9.0-beta.2 exposes plugin update methods on DaemonClient, but plugins
|
|
3
|
+
* receive only PaseoApi through getPaseoClient(serverId). PaseoApi deliberately
|
|
4
|
+
* omits those methods, so a plugin cannot safely choose a host or mutate an
|
|
5
|
+
* installation on the daemon's behalf.
|
|
6
|
+
*/
|
|
7
|
+
export const NATIVE_PLUGIN_UPDATE_BLOCKER =
|
|
8
|
+
"Paseo SDK 0.9.0-beta.2 does not expose plugin update preview/proposal/apply/reload on plugin-facing PaseoApi.";
|
|
9
|
+
|
|
10
|
+
export type ManagedPluginSource = "git" | "npm" | "directory" | "unknown";
|
|
11
|
+
|
|
12
|
+
export interface NativePluginUpdateCapability {
|
|
13
|
+
supported: false;
|
|
14
|
+
source: ManagedPluginSource;
|
|
15
|
+
/** No serverId is accepted: selecting another configured host would be unsafe. */
|
|
16
|
+
hostTarget: null;
|
|
17
|
+
blocker: typeof NATIVE_PLUGIN_UPDATE_BLOCKER;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Formal, source-independent fallback for every installation kind. Do not turn
|
|
22
|
+
* useHosts() or getPaseoClient(serverId) into a lifecycle substitute: neither
|
|
23
|
+
* grants the absent plugin-management API, and a selected server could be the
|
|
24
|
+
* wrong daemon for this plugin installation.
|
|
25
|
+
*/
|
|
26
|
+
export function nativePluginUpdateCapability(source: string | null | undefined): NativePluginUpdateCapability {
|
|
27
|
+
const normalized: ManagedPluginSource =
|
|
28
|
+
source === "git" || source === "npm" || source === "directory" ? source : "unknown";
|
|
29
|
+
return { supported: false, source: normalized, hostTarget: null, blocker: NATIVE_PLUGIN_UPDATE_BLOCKER };
|
|
30
|
+
}
|
package/shared/updates.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { defineContract } from "paseo-plugin-helper/
|
|
2
|
+
import { defineContract } from "./vendor/paseo-plugin-helper/index";
|
|
3
3
|
|
|
4
4
|
export const PluginUpdateStatusSchema = z.enum([
|
|
5
5
|
"checking",
|
|
@@ -205,35 +205,3 @@ export const pluginUpdatesCheckRpc = defineContract({
|
|
|
205
205
|
plugins: z.array(PluginUpdateSchema),
|
|
206
206
|
}),
|
|
207
207
|
});
|
|
208
|
-
|
|
209
|
-
export const PluginUpdateActionSchema = z.object({
|
|
210
|
-
pluginId: z.string(),
|
|
211
|
-
status: z.enum(["updated", "error"]),
|
|
212
|
-
output: z.string().nullable(),
|
|
213
|
-
error: z.string().nullable(),
|
|
214
|
-
requiresForce: z.boolean().optional(),
|
|
215
|
-
});
|
|
216
|
-
export type PluginUpdateActionResult = z.infer<typeof PluginUpdateActionSchema>;
|
|
217
|
-
|
|
218
|
-
export const pluginUpdatesUpdateRpc = defineContract({
|
|
219
|
-
name: "plugin-updates.update",
|
|
220
|
-
description: "Pulls and reloads one installed plugin in place",
|
|
221
|
-
input: z.object({
|
|
222
|
-
workspaceId: z.string().optional(),
|
|
223
|
-
pluginId: z.string(),
|
|
224
|
-
force: z.boolean().optional(),
|
|
225
|
-
}),
|
|
226
|
-
output: PluginUpdateActionSchema,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
export const pluginUpdatesUpdateAllRpc = defineContract({
|
|
230
|
-
name: "plugin-updates.update-all",
|
|
231
|
-
description: "Pulls and reloads every installed plugin with a remote update",
|
|
232
|
-
input: z.object({
|
|
233
|
-
workspaceId: z.string().optional(),
|
|
234
|
-
force: z.boolean().optional(),
|
|
235
|
-
}),
|
|
236
|
-
output: z.object({
|
|
237
|
-
results: z.array(PluginUpdateActionSchema),
|
|
238
|
-
}),
|
|
239
|
-
});
|