@deveye/types 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +6 -6
  2. package/src/domain/device.ts +4 -29
  3. package/src/domain/featureRegistry.ts +40 -108
  4. package/src/domain/home.ts +40 -101
  5. package/src/domain/live.ts +36 -87
  6. package/src/domain/metrics.ts +10 -14
  7. package/src/domain/notifications.ts +39 -110
  8. package/src/domain/project.ts +3 -162
  9. package/src/domain/report.ts +54 -95
  10. package/src/domain/secrecy.ts +3 -5
  11. package/src/domain/sharing.ts +33 -70
  12. package/src/domain/syncProtocol.ts +5 -12
  13. package/src/domain/user.ts +8 -14
  14. package/src/domain/workspace.ts +0 -3
  15. package/src/domain/workspaceRole.ts +34 -82
  16. package/src/features/agent.ts +306 -0
  17. package/src/features/live.ts +17 -37
  18. package/src/features/notify.ts +19 -57
  19. package/src/features/registry.ts +7 -44
  20. package/src/features/secrecy.ts +4 -9
  21. package/src/features/sharing.ts +12 -26
  22. package/src/features/user.ts +6 -11
  23. package/src/features/workspace.ts +6 -11
  24. package/src/http/auth.ts +6 -13
  25. package/src/http/device.ts +13 -17
  26. package/src/http/status.ts +6 -10
  27. package/src/index.ts +29 -932
  28. package/src/protocol/agent.ts +40 -70
  29. package/src/protocol/envelope.ts +3 -10
  30. package/src/sdk/client-ambient.d.ts +228 -21
  31. package/src/sdk/client.ts +277 -17
  32. package/src/sdk/devb.ts +120 -0
  33. package/src/sdk/manifest.test.ts +0 -1
  34. package/src/sdk/manifest.ts +94 -25
  35. package/src/sdk/providers.ts +310 -5
  36. package/src/sdk/server.ts +483 -19
  37. package/src/sdk/testing.test.ts +2 -2
  38. package/src/sdk/testing.ts +302 -26
  39. package/src/utils/version.ts +5 -8
  40. package/src/domain/audience.ts +0 -549
  41. package/src/domain/backup.ts +0 -355
  42. package/src/domain/credential.ts +0 -55
  43. package/src/domain/database.ts +0 -467
  44. package/src/domain/deploy.ts +0 -231
  45. package/src/domain/finance.ts +0 -477
  46. package/src/domain/git.ts +0 -419
  47. package/src/domain/mail.ts +0 -394
  48. package/src/domain/note.ts +0 -202
  49. package/src/domain/password.ts +0 -36
  50. package/src/domain/projectBoard.ts +0 -130
  51. package/src/domain/projectChat.ts +0 -46
  52. package/src/domain/projectHistory.ts +0 -82
  53. package/src/domain/projectLink.ts +0 -87
  54. package/src/domain/projectPlan.ts +0 -68
  55. package/src/domain/sentinel.ts +0 -623
  56. package/src/domain/uptime.ts +0 -216
  57. package/src/features/audience.ts +0 -275
  58. package/src/features/backup.ts +0 -230
  59. package/src/features/database.ts +0 -461
  60. package/src/features/deploy.ts +0 -245
  61. package/src/features/device.ts +0 -292
  62. package/src/features/deviceFiles.ts +0 -83
  63. package/src/features/deviceLogs.ts +0 -36
  64. package/src/features/deviceTerminal.ts +0 -57
  65. package/src/features/finance.ts +0 -360
  66. package/src/features/git.ts +0 -368
  67. package/src/features/mail.ts +0 -374
  68. package/src/features/metrics.ts +0 -185
  69. package/src/features/note.ts +0 -189
  70. package/src/features/password.ts +0 -67
  71. package/src/features/project.ts +0 -709
  72. package/src/features/sentinel.ts +0 -233
  73. package/src/features/uptime.ts +0 -186
@@ -39,9 +39,8 @@ import { ProtocolErrorSchema } from './error';
39
39
 
40
40
  /**
41
41
  * Agent <-> Server wire protocol (distinct from the user feature protocol).
42
- *
43
- * The agent authenticates with its device token, then streams metric batches.
44
- * The server acknowledges and may push commands (reserved for later).
42
+ * The agent authenticates with its device token, then streams metric batches,
43
+ * reports and replies; the server acknowledges and pushes orders.
45
44
  */
46
45
 
47
46
  /** Command names the agent may send to the server. */
@@ -301,17 +300,12 @@ export const agentSyncIndexPayloadSchema = z.object({
301
300
  entries: z.array(syncIndexEntrySchema).max(SYNC_INDEX_BATCH_MAX),
302
301
  done: z.boolean(),
303
302
  /**
304
- * L'agent a-t-il RÉELLEMENT parcouru le disque ?
305
- *
306
- * `false` répond à un `sync.scan` en mode `auto` sur un partage que le
307
- * watcher sait intact depuis le dernier scan : aucune entrée n'est envoyée,
308
- * seule `fingerprint` est renseignée, et c'est au serveur de vérifier qu'elle
309
- * correspond à la baseline qu'il détient. Il ne SUPPOSE donc jamais que
310
- * l'appareil est à jour, il le VÉRIFIE — une vérification fausse coûte un
311
- * scan complet, jamais une divergence.
312
- *
313
- * Le défaut `true` est ce qui rend un vieil agent inoffensif : sans le champ,
314
- * le serveur retombe sur le chemin d'aujourd'hui.
303
+ * L'agent a-t-il réellement parcouru le disque ? `false` répond à un
304
+ * `sync.scan` en mode `auto` sur un partage que le watcher sait intact :
305
+ * aucune entrée, seule `fingerprint` est renseignée, et le serveur VÉRIFIE
306
+ * qu'elle correspond à sa baseline (une vérification fausse coûte un scan
307
+ * complet, jamais une divergence). Le défaut `true` rend un vieil agent
308
+ * inoffensif.
315
309
  */
316
310
  scanned: z.boolean().default(true),
317
311
  /** Empreinte de l'index détenu par l'agent, quand il sait la calculer. */
@@ -520,11 +514,8 @@ export const AGENT_ERROR = 'agent.error' as const;
520
514
  export const AGENT_COLLECT = 'agent.collect' as const;
521
515
  /**
522
516
  * Demande un relevé Sentinelle immédiat (persistance + authentification).
523
- *
524
- * Distinct d'`agent.collect` exprès : celui-ci coûte quelques millisecondes et
525
- * peut être déclenché à volonté, tandis qu'un relevé de persistance empreinte
526
- * des centaines de fichiers. Les confondre reviendrait à faire payer ce prix à
527
- * chaque bouton « rafraîchir » de la page Monitoring.
517
+ * Distinct d'`agent.collect` : celui-ci coûte quelques millisecondes, un relevé
518
+ * de persistance empreinte des centaines de fichiers.
528
519
  */
529
520
  export const AGENT_SCAN = 'agent.scan' as const;
530
521
  /** Push the per-device collection config (cadences + capture mode) to the agent. */
@@ -731,11 +722,9 @@ export const agentSyncScanPayloadSchema = z.object({
731
722
  sessionId: syncOpId,
732
723
  shareId: z.number().int().positive(),
733
724
  /**
734
- * Le défaut `full` est ce qui rend un vieux SERVEUR inoffensif : sans le
735
- * champ, un agent récent parcourt le disque comme il l'a toujours fait. Les
736
- * deux sens de la dissymétrie de version dégradent donc vers « scan
737
- * complet », jamais vers « saut » — sauter exige une empreinte que seul un
738
- * agent récent produit et que seul un serveur récent exploite.
725
+ * Le défaut `full` rend un vieux serveur inoffensif : les deux sens de la
726
+ * dissymétrie de version dégradent vers « scan complet », jamais vers
727
+ * « saut ».
739
728
  */
740
729
  mode: syncScanModeSchema.default('full')
741
730
  });
@@ -780,23 +769,20 @@ export const agentSyncApplyChunkPayloadSchema = z.object({
780
769
  mode: z.number().int().min(0).max(0o777).nullable().default(null),
781
770
  /**
782
771
  * Offset de clair à partir duquel CE transfert reprend. Décidé par le
783
- * serveur (à partir du `resumeFrom` annoncé par l'agent) et répété sur
784
- * chaque frame : l'agent tronque son temporaire à cette valeur avant
785
- * d'écrire. C'est ce qui empêche les deux côtés de diverger — l'agent ne
786
- * doit jamais présumer de son propre point de reprise.
772
+ * serveur (d'après le `resumeFrom` de l'agent) et répété sur chaque frame :
773
+ * l'agent tronque son temporaire à cette valeur avant d'écrire, et ne
774
+ * présume jamais de son propre point de reprise.
787
775
  */
788
776
  resumeFrom: z.number().int().nonnegative().default(0)
789
777
  });
790
778
  export type AgentSyncApplyChunkPayload = z.infer<typeof agentSyncApplyChunkPayloadSchema>;
791
779
 
792
780
  /**
793
- * Amorce un download. L'agent répond `sync.opResult` avec `op: 'applyReady'` et
794
- * un `resumeFrom` : le nombre d'octets de clair qu'il détient DÉJÀ dans son
795
- * temporaire pour ce hash exact. Le serveur ne renvoie alors que ce qui manque.
796
- *
797
- * Le temporaire est nommé par hash et non par `opId`, ce qui rend la reprise
798
- * auto-corrective : un fichier modifié entre-temps a un autre hash, donc un
799
- * autre temporaire, donc aucune reprise possible sur des octets périmés.
781
+ * Amorce un download. L'agent répond `sync.opResult` avec `op: 'applyReady'`
782
+ * et un `resumeFrom` : les octets de clair qu'il détient DÉJÀ dans son
783
+ * temporaire pour ce hash exact. Le temporaire est nommé par hash et non par
784
+ * `opId` : un fichier modifié entre-temps a un autre hash, donc aucune reprise
785
+ * sur des octets périmés.
800
786
  */
801
787
  export const AGENT_SYNC_APPLY_START = 'sync.applyStart' as const;
802
788
  export const agentSyncApplyStartPayloadSchema = z.object({
@@ -822,10 +808,9 @@ export const agentSyncApplyDirPayloadSchema = z.object({
822
808
  shareId: z.number().int().positive(),
823
809
  relPath: syncRelPath,
824
810
  /**
825
- * `dir` autorise la CRÉATION du chemin ; `file` interdit de le créer et se
826
- * contente d'ajuster le mode s'il existe. Sans cette distinction, un `chmod`
827
- * sur un fichier momentanément absent ferait naître un DOSSIER à sa place
828
- * que le planner écarterait ensuite pour toujours en « conflit de nature ».
811
+ * `dir` autorise la CRÉATION du chemin ; `file` ne fait qu'ajuster le mode
812
+ * s'il existe. Sans cette distinction, un `chmod` sur un fichier
813
+ * momentanément absent ferait naître un DOSSIER à sa place.
829
814
  */
830
815
  kind: syncEntryKindSchema.default('dir'),
831
816
  mode: z.number().int().min(0).max(0o777).nullable().default(null)
@@ -854,13 +839,9 @@ export type AgentSyncApplyLocalPayload = z.infer<typeof agentSyncApplyLocalPaylo
854
839
 
855
840
  /**
856
841
  * Propage un DÉPLACEMENT : l'agent renomme sur place, sans transfert ni
857
- * corbeille. Le contenu ne bouge pas d'un octet, seul son chemin change — le
858
- * traiter comme « supprime ici, télécharge » coûtait une copie intégrale en
859
- * corbeille, pour une opération qui ne détruit rien.
860
- *
861
- * L'agent vérifie que la source porte bien le contenu attendu avant de bouger.
862
- * En cas d'échec il répond `opResult !ok`, et le serveur retombe sur le chemin
863
- * ordinaire (téléchargement puis suppression), qui reste sûr.
842
+ * corbeille, après avoir vérifié que la source porte le contenu attendu. En
843
+ * cas d'échec il répond `opResult !ok` et le serveur retombe sur le chemin
844
+ * ordinaire (téléchargement puis suppression).
864
845
  */
865
846
  export const AGENT_SYNC_MOVE = 'sync.move' as const;
866
847
  export const agentSyncMovePayloadSchema = z.object({
@@ -900,13 +881,10 @@ export const agentConfigPayloadSchema = z.object({
900
881
  /** How much of the process list to carry on each tick (`off`/`top`/`all`). */
901
882
  processCapture: processCaptureSchema,
902
883
  /**
903
- * Sentinelle est-elle active sur cet appareil ? Éteinte, l'agent ne relève ni
904
- * persistance ni authentification ces deux sondes ne coûtent rien à qui ne
905
- * les demande pas, et une machine qui n'est pas surveillée ne doit pas voir
906
- * ses journaux lus « au cas où ».
907
- *
908
- * Facultatif : un serveur antérieur à Sentinelle n'envoie pas le champ, et
909
- * l'agent se comporte alors comme avant.
884
+ * Sentinelle est-elle active sur cet appareil ? Éteinte, l'agent ne relève
885
+ * ni persistance ni authentification : une machine non surveillée ne doit
886
+ * pas voir ses journaux lus « au cas ». Facultatif : absent, l'agent ne
887
+ * relève rien.
910
888
  */
911
889
  sentinelEnabled: z.boolean().optional(),
912
890
  /** Cadence du manifeste de persistance, en ms. */
@@ -1048,12 +1026,9 @@ export const DEVICE_REPORT_EVENT = 'device.report' as const;
1048
1026
  /** Package-manager inventory, live upgrade progress, and completion (Appareils panel). */
1049
1027
  export const PACKAGE_LIST_EVENT = 'package.list' as const;
1050
1028
  /**
1051
- * Une mise à jour vient d'être **acceptée** pour ce gestionnaire.
1052
- *
1053
- * Émis par le serveur, qui seul sait qu'un verrou vient d'être pris — et avant
1054
- * la première ligne de sortie de l'outil, qui peut se faire attendre. C'est ce
1055
- * qui permet à tous les écrans ouverts, y compris ceux d'autres personnes, de
1056
- * griser le bouton au même instant.
1029
+ * Une mise à jour vient d'être acceptée pour ce gestionnaire. Émis par le
1030
+ * serveur avant la première ligne de sortie de l'outil, pour que tous les
1031
+ * écrans ouverts grisent le bouton au même instant.
1057
1032
  */
1058
1033
  export const PACKAGE_STARTED_EVENT = 'package.started' as const;
1059
1034
  export const PACKAGE_PROGRESS_EVENT = 'package.progress' as const;
@@ -1063,10 +1038,6 @@ export const DEVICE_POWER_EVENT = 'device.powerResult' as const;
1063
1038
  /**
1064
1039
  * Outcome of a persistence/privilege change (autostart, elevate, drop), fanned to
1065
1040
  * subscribers exactly like the power result.
1066
- *
1067
- * Sans lui, la seule trace de l'échec d'une installation de service était une
1068
- * ligne de journal d'audit : l'interface attendait quelques secondes, relisait
1069
- * l'appareil, et n'affichait rien — ni la réussite, ni la raison de l'échec.
1070
1041
  */
1071
1042
  export const DEVICE_SERVICE_EVENT = 'device.serviceResult' as const;
1072
1043
  /** Device log sources inventory + queried log lines, fanned to subscribers. */
@@ -1086,14 +1057,13 @@ export const CLOUD_SYNC_PROGRESS_EVENT = 'cloudSync.progress' as const;
1086
1057
  export const CLOUD_SYNC_STATE_EVENT = 'cloudSync.state' as const;
1087
1058
  export const CLOUD_SYNC_CHUNK_EVENT = 'cloudSync.chunk' as const;
1088
1059
 
1089
- /** Push payloads reuse the agent reply shapes (already carry `deviceId`). */
1060
+ // Push payloads reuse the agent reply shapes (they already carry `deviceId`).
1061
+
1090
1062
  /**
1091
- * L'inventaire des gestionnaires, **enrichi** par le serveur de ce que l'agent
1092
- * ne peut pas savoir : quelles mises à jour tournent déjà.
1093
- *
1094
- * Sans ce champ, un écran ouvert pendant qu'une mise à jour est en cours —
1095
- * après avoir refermé la fenêtre, ou chez quelqu'un d'autre — repartait d'un
1096
- * bouton actif et permettait de relancer la même commande.
1063
+ * L'inventaire des gestionnaires, enrichi par le serveur de ce que l'agent ne
1064
+ * peut pas savoir : quelles mises à jour tournent déjà. Sans ce champ, un
1065
+ * écran ouvert pendant une mise à jour permettrait de relancer la même
1066
+ * commande.
1097
1067
  */
1098
1068
  export const packageListPushSchema = agentPkgListResultPayloadSchema.extend({
1099
1069
  running: z.array(packageManagerIdSchema).default([])
@@ -15,16 +15,9 @@ export const clientMessageSchema = z.object({
15
15
  command: z.string().min(1),
16
16
  /**
17
17
  * Espace de travail visé par la commande. Absent → l'espace personnel de
18
- * l'appelant.
19
- *
20
- * Il vit sur l'enveloppe, et non dans le `payload` de chaque commande, pour
21
- * trois raisons : aucun schéma d'entrée ne porte de `workspaceId`, aucun site
22
- * d'appel client ne le passe à la main (`ws.send` l'estampille), et le
23
- * serveur n'a qu'un seul point de résolution et d'autorisation. Le tenir dans
24
- * un état de session côté serveur serait plus fragile : la socket se
25
- * reconnecte seule (backoff, retour de focus) et une commande émise avant la
26
- * ré-activation viserait le mauvais espace — ici chaque message se décrit
27
- * lui-même.
18
+ * l'appelant. Porté par l'enveloppe et non par chaque `payload` : `ws.send`
19
+ * l'estampille, le serveur n'a qu'un seul point de résolution et
20
+ * d'autorisation, et chaque message se décrit lui-même, reconnexions comprises.
28
21
  */
29
22
  workspaceId: z.number().int().positive().optional(),
30
23
  payload: z.unknown()
@@ -1,32 +1,37 @@
1
1
  /**
2
- * Ambient declaration of `deveye-sdk-client` the runtime barrel the APP
2
+ * Ambient declaration of `deveye-sdk-client`, the runtime barrel the APP
3
3
  * provides when a feature module is compiled in (resolved by alias inside
4
- * DevEye). The module itself is app code and is published nowhere; this file
5
- * is its typed portrait, so a module repo can `tsc --noEmit` standalone.
6
- *
7
- * How it is consumed:
8
- * - a module repo (the template) pulls it into its standalone typecheck via
9
- * one tsconfig `include` entry pointing at this file in node_modules;
10
- * - DevEye itself verifies MECHANICALLY that the real barrel honours this
11
- * declaration (`client/npm run check:sdk`): a drift breaks the app's CI,
12
- * never a third-party build.
13
- *
14
- * Extend it when the app's stable surface grows — same gesture as before,
15
- * one file instead of one per module repo.
4
+ * DevEye). The module itself is app code, published nowhere; this file is its
5
+ * typed portrait, so a module repo can `tsc --noEmit` standalone (one tsconfig
6
+ * `include` entry pointing here). DevEye verifies mechanically that the real
7
+ * barrel honours it (`client/npm run check:sdk`): a drift breaks the app's CI,
8
+ * never a third-party build.
16
9
  */
17
10
  declare module 'deveye-sdk-client' {
18
11
  import type {
19
12
  ButtonHTMLAttributes,
13
+ CSSProperties,
20
14
  ChangeEvent,
21
15
  ComponentType,
22
- CSSProperties,
23
16
  InputHTMLAttributes,
24
17
  ReactNode,
18
+ RefObject,
25
19
  SelectHTMLAttributes
26
20
  } from 'react';
27
21
  import type { z, ZodType } from 'zod';
28
- import type { FeatureAccess, FeatureId, WorkspaceCapability } from '@deveye/types';
29
- import type { FeatureManifest } from '@deveye/types/sdk';
22
+ import type {
23
+ ButtonHTMLAttributes as DialogButtonAttributes,
24
+ PointerEvent as ReactPointerEvent
25
+ } from 'react';
26
+ import type {
27
+ FeatureAccess,
28
+ FeatureId,
29
+ MinimalUser,
30
+ User,
31
+ WorkspaceCapability
32
+ } from '@deveye/types';
33
+ import type { FeatureManifest, ManifestCommand } from '@deveye/types/sdk';
34
+ import type { SdkDeviceSummary } from '@deveye/types/sdk/client';
30
35
 
31
36
  // ── UI kit ─────────────────────────────────────────────────────────────
32
37
  export const Button: ComponentType<
@@ -36,7 +41,7 @@ declare module 'deveye-sdk-client' {
36
41
  }
37
42
  >;
38
43
  export const TextInput: ComponentType<
39
- InputHTMLAttributes<HTMLInputElement> & { enableShowHideButton?: boolean }
44
+ InputHTMLAttributes<HTMLInputElement> & { enableShowHideButton?: boolean; error?: string }
40
45
  >;
41
46
  export const SelectInput: ComponentType<SelectHTMLAttributes<HTMLSelectElement>>;
42
47
  export const Checkbox: ComponentType<{
@@ -45,6 +50,7 @@ declare module 'deveye-sdk-client' {
45
50
  onChange: (checked: boolean) => void;
46
51
  children?: ReactNode;
47
52
  disabled?: boolean;
53
+ className?: string;
48
54
  /** Required when there is no visible label. */
49
55
  'aria-label'?: string;
50
56
  }>;
@@ -78,6 +84,41 @@ declare module 'deveye-sdk-client' {
78
84
  fill?: boolean;
79
85
  holdSecrecy?: boolean;
80
86
  }>;
87
+ /** The "Annuler" button of a Dialog footer: closes through the guarded close. */
88
+ export const DialogCancelButton: ComponentType<
89
+ DialogButtonAttributes<HTMLButtonElement> & {
90
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
91
+ icon?: string;
92
+ }
93
+ >;
94
+ /**
95
+ * The imperative dialog layer over Dialog: mount `<Popup id=...>` once
96
+ * around a form, then drive it with `OpenPopup(id, input)` (resolves with
97
+ * whatever `ClosePopup(id, result)` passes, `null` on dismiss).
98
+ */
99
+ export function Popup<TInput = unknown>(props: {
100
+ children: ReactNode;
101
+ id: string;
102
+ title?: string;
103
+ width?: number;
104
+ headerAction?: ReactNode;
105
+ onInputChange?: ((input: TInput) => void) | null;
106
+ onClosePopup?: ((id: string) => void) | null;
107
+ onSubmit?: () => void;
108
+ autoFocus?: boolean;
109
+ dirty?: boolean;
110
+ onSave?: () => void;
111
+ tall?: boolean;
112
+ holdSecrecy?: boolean;
113
+ }): ReactNode;
114
+ export function OpenPopup<T = object>(id: string, inputData?: unknown): Promise<T | null>;
115
+ export function ClosePopup(id: string, data?: unknown): void;
116
+ /** The app-wide "i" explainer dialog: a heading and a body, one behaviour everywhere. */
117
+ export function openInfo(input: {
118
+ title: string;
119
+ body: ReactNode;
120
+ width?: number;
121
+ }): Promise<unknown>;
81
122
  /** Request the enclosing Dialog's guarded close (the unsaved-changes prompt included). */
82
123
  export function useDialogClose(): () => void;
83
124
  /** Register `fn` as the enclosing Dialog's primary action (Enter triggers it); `null` clears it. */
@@ -112,12 +153,69 @@ declare module 'deveye-sdk-client' {
112
153
  export const FeatureSettingsButton: ComponentType<{
113
154
  scope:
114
155
  | { kind: 'feature'; feature: FeatureId }
115
- | { kind: 'item'; feature: FeatureId; itemId: number; itemLabel: string };
156
+ | {
157
+ kind: 'item';
158
+ feature: FeatureId;
159
+ itemId: number | string;
160
+ itemLabel: string;
161
+ shareable?: boolean;
162
+ };
116
163
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
117
164
  label?: string;
165
+ /** The tab a click opens (one of the manifest's tab ids); the first by default. */
166
+ initialSection?: string;
167
+ /**
168
+ * The shell opened or closed (unmount counts as closed): for the one
169
+ * component that owns the item's presence (`useLiveSegment`) when
170
+ * nothing else announces the item. The shell never declares the level.
171
+ */
172
+ onOpenChange?: (open: boolean) => void;
118
173
  }>;
119
174
  /** The canonical settings row classes (channelRow, field, sectionHint...). */
120
175
  export const settingsStyles: Readonly<Record<string, string>>;
176
+ /** A plain count on a home card: a big number, a noun, a secondary line. */
177
+ export type CountState = { kind: 'loading' } | { kind: 'ready'; count: number };
178
+ export const CountWidget: ComponentType<{
179
+ state: CountState;
180
+ /** Singular noun, pluralized with a trailing "s" unless `plural` says otherwise. */
181
+ noun: string;
182
+ plural?: string;
183
+ /** Secondary line when there is at least one item. */
184
+ hint: string;
185
+ /** Secondary line when the count is zero. */
186
+ empty: string;
187
+ tone?: 'neutral' | 'danger';
188
+ }>;
189
+ /**
190
+ * A workspace count over one of YOUR `.count` commands (or any `...Count`
191
+ * command answering `{ count }`), re-fetched on reconnect and on
192
+ * invalidation of that key. Never gated by the password-encryption unlock:
193
+ * the command must answer while locked.
194
+ */
195
+ export function useWorkspaceCount(
196
+ command: `x-${string}.count` | `${string}.${string}Count`
197
+ ): CountState;
198
+ /**
199
+ * Reorder a list by drag-and-drop, the app's one gesture for it (Pointer
200
+ * Events, a handle per row, an insertion bar the hook positions itself).
201
+ * The list container must be `position: relative`; rows carry
202
+ * `rowSelector`; the handle wires `onGripPointerDown`.
203
+ */
204
+ export function useDragReorder<
205
+ L extends HTMLElement = HTMLElement,
206
+ B extends HTMLElement = HTMLElement
207
+ >(options: {
208
+ ids: (string | number)[];
209
+ rowSelector: string;
210
+ onReorder: (ids: (string | number)[]) => void;
211
+ onDragStateChange?: (dragging: boolean) => void;
212
+ layout?: 'rows' | 'grid';
213
+ }): {
214
+ listRef: RefObject<L | null>;
215
+ barRef: RefObject<B | null>;
216
+ onGripPointerDown: (e: ReactPointerEvent, id: string | number) => void;
217
+ draggingId: string | number | null;
218
+ };
121
219
 
122
220
  // ── Server push events ─────────────────────────────────────────────────
123
221
  /** Typed push subscription: filters `event`, safeParses, drops mismatches. */
@@ -150,6 +248,29 @@ declare module 'deveye-sdk-client' {
150
248
  /** Imperative subscription to one resource key's invalidations. Returns the unsubscribe. */
151
249
  export function onResourceChange(key: ExternalResourceKey, cb: () => void): () => void;
152
250
  export function humanizeError(error: unknown, fallback: string): string;
251
+ /** The error a command rejects with: the server's code (or the socket's), its message, and the validation details when any. */
252
+ export class WsError extends Error {
253
+ constructor(code: string, message: string, details?: unknown);
254
+ readonly code: string;
255
+ readonly details?: unknown;
256
+ }
257
+ /**
258
+ * The typed sender of any list of contracts (`featureApi` is it on a
259
+ * manifest's commands). `commandsApi(agentCommands)` is how a module talks
260
+ * to a device: the native agent transport (`agent.*`: files, terminal,
261
+ * logs, packages, power, live metrics subscription), under the active
262
+ * workspace's `devices` right; deferred answers arrive through
263
+ * `onServerEvent`.
264
+ */
265
+ export function commandsApi<const C extends readonly ManifestCommand[]>(
266
+ commands: C
267
+ ): {
268
+ send<N extends C[number]['command']>(
269
+ name: N,
270
+ input: z.input<Extract<C[number], { command: N }>['input'] & ZodType>,
271
+ opts?: { timeoutMs?: number }
272
+ ): Promise<z.output<Extract<C[number], { command: N }>['output'] & ZodType>>;
273
+ };
153
274
  export function featureApi<const M extends FeatureManifest>(
154
275
  manifest: M
155
276
  ): {
@@ -182,6 +303,14 @@ declare module 'deveye-sdk-client' {
182
303
  export function ensureSecrecyUnlocked(): Promise<void>;
183
304
  /** Runs `run`; on a `locked` error, opens the unlock prompt and retries once. */
184
305
  export function withSecrecy<T>(run: () => Promise<T>): Promise<T>;
306
+ /** Keep the unlocked session alive during a long operation (a mailbox sync the user is watching). */
307
+ export function touchSecrecy(): void;
308
+ /**
309
+ * The rejection of `ensureSecrecyUnlocked` / `withSecrecy` when the user
310
+ * dismisses the unlock prompt: a deliberate cancel, not a failure (close a
311
+ * view that has nothing to show, rather than reporting an error).
312
+ */
313
+ export class UnlockCancelledError extends Error {}
185
314
 
186
315
  // ── Live ───────────────────────────────────────────────────────────────
187
316
  export type LiveSegmentKind = 'view' | 'l1' | 'l2' | 'l3' | 'l4';
@@ -216,13 +345,37 @@ declare module 'deveye-sdk-client' {
216
345
  /** What the folder is for, said by the caller. */
217
346
  description?: string;
218
347
  }>;
219
- /** The workspace's enrolled devices, live (a polled store, not a bare list). */
348
+ /**
349
+ * The workspace's enrolled devices, live: what the Devices module offers
350
+ * the app (`DEVICES_CLIENT_PROVIDER`), refreshed by the `devices` topic.
351
+ * Empty, loaded and without error when that module is not installed.
352
+ */
220
353
  export function useDevices(): {
221
- devices: { id: string; name: string; online: boolean }[];
354
+ devices: readonly SdkDeviceSummary[];
222
355
  loading: boolean;
223
356
  error: string | null;
224
- refresh: () => Promise<void>;
225
357
  };
358
+ /**
359
+ * Acquires a live metrics subscription to a device and returns its release;
360
+ * consumers of one socket are counted so that none unsubscribes another.
361
+ */
362
+ export function acquireMetrics(deviceId: string): () => void;
363
+ /** Device paths as the agent reports them: Windows or POSIX, joined accordingly. */
364
+ export function isWinPath(p: string): boolean;
365
+ export function joinPath(base: string, name: string): string;
366
+ /**
367
+ * The DevEye version this interface was built from: what an agent's
368
+ * reported version is compared against to offer a self-update.
369
+ */
370
+ export const APP_VERSION: string;
371
+ /**
372
+ * A validated GET on one of the app's HTTP routes (the session cookie
373
+ * rides along; an expired access token is renewed and the call replayed
374
+ * once). For the routes that stay HTTP because they serve binaries.
375
+ */
376
+ export function httpGet<T>(path: string, outputSchema?: ZodType<T>): Promise<T>;
377
+ /** Renews the access cookie before a raw `fetch` that bypasses the client (a download). */
378
+ export function ensureFreshAccess(): Promise<void>;
226
379
  export interface LiveOutlineProps {
227
380
  'data-live-peer'?: true;
228
381
  style?: CSSProperties;
@@ -247,6 +400,60 @@ declare module 'deveye-sdk-client' {
247
400
  name: string;
248
401
  } | null;
249
402
  export function useFeatureLifecycle(hooks: { onUnmount?: () => void }): void;
403
+ /** The members of the active workspace, as the session lists them (empty before it answers). */
404
+ export function useWorkspaceMembers(): readonly MinimalUser[];
405
+ /**
406
+ * A member's identity dot: their avatar, or their initial on their account
407
+ * colour (the same one as the live presence). `user` may be undefined: a
408
+ * deleted account must not break a row.
409
+ */
410
+ export const Avatar: ComponentType<{
411
+ user: MinimalUser | undefined;
412
+ size?: number;
413
+ title?: string;
414
+ }>;
415
+ /** The CSS variable of an account colour, the one the live presence paints with. */
416
+ export function userColorVar(color: MinimalUser['color']): string;
417
+ /**
418
+ * Two sticky bands, one under the other: measure the top one and hand the
419
+ * ancestor a `--sticky-head` variable the lower band offsets itself by.
420
+ */
421
+ export interface StickyOffset<T extends HTMLElement> {
422
+ ref: RefObject<T | null>;
423
+ style: CSSProperties;
424
+ }
425
+ export function useStickyOffset<T extends HTMLElement>(): StickyOffset<T>;
426
+ /**
427
+ * The client contract another module offers (`FeatureClient.providers`,
428
+ * keys in `@deveye/types/sdk`): how a module composes another's screens
429
+ * (Projects renders the linked items of Git, Uptime...). `undefined` when
430
+ * that module is not installed: degrade, never assume.
431
+ */
432
+ export function moduleClientProvider<T>(key: string): T | undefined;
433
+ /** The signed-in user, `null` before the session answers. */
434
+ export function useCurrentUser(): User | null;
435
+ /** Image inputs accepted by `fileToSquareDataUrl`. */
436
+ export const ACCEPTED_TYPES: readonly string[];
437
+ export const MAX_INPUT_BYTES: number;
438
+ /** A picked image, resized to a square data URL under `maxLength` characters; throws a readable message. */
439
+ export function fileToSquareDataUrl(
440
+ file: File,
441
+ opts: { size: number; maxLength: number }
442
+ ): Promise<string>;
443
+ /**
444
+ * Open another feature of the active workspace, on one of its items when
445
+ * `itemId` is given (the item's presence segment is its bare id): the
446
+ * host's teleport, the same mechanism as "join someone". The access guard
447
+ * is the host's; a missing item is ignored after a short grace.
448
+ */
449
+ export function openFeature(feature: string, itemId?: number): void;
450
+ /**
451
+ * Ask the feature popup for a wider frame (px) while the calling
452
+ * component is mounted, `null` to ask for nothing. Several requests
453
+ * coexist; the widest wins. For a view that outgrows the default width
454
+ * (a table explorer in expanded mode).
455
+ */
456
+ export function useRequestPopupWidth(px: number | null): void;
250
457
 
251
458
  // Change events re-exported for convenience in handlers.
252
459
  export type InputChange = ChangeEvent<HTMLInputElement>;