@punica/editor 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punica/editor",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Punica Editor",
5
5
  "private": false,
6
6
  "type": "module",
package/types/index.d.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  /// <reference path="punica.module.extensions.settings.d.ts" />
26
26
  /// <reference path="punica.module.extensions.i18n.d.ts" />
27
27
  /// <reference path="punica.module.runtime.fs.d.ts" />
28
+ /// <reference path="punica.module.runtime.fileOperations.d.ts" />
28
29
  /// <reference path="punica.module.runtime.search.d.ts" />
29
30
  /// <reference path="punica.module.runtime.vcs.d.ts" />
30
31
  /// <reference path="punica.module.runtime.workspace.d.ts" />
@@ -266,6 +266,48 @@ declare module 'punica' {
266
266
  approvals: ApprovalRecord[];
267
267
  }
268
268
 
269
+ /**
270
+ * A signed approval grant. The gate matches a grant by (kind, id) plus
271
+ * scope and workspace; the token is what lets a later reader prove the
272
+ * grant was really issued and not edited afterwards.
273
+ */
274
+ export interface ApprovalTokenPayload {
275
+ scope: ApprovalScope;
276
+ capabilityId: string;
277
+ expiry: number;
278
+ jti: string;
279
+ issuer: string;
280
+ workspaceId?: string;
281
+ delegatedFrom?: string;
282
+ delegationDepth?: number;
283
+ approval?: PolicyApproval;
284
+ risk?: PolicyRisk;
285
+ }
286
+
287
+ /**
288
+ * Signs approval grants. The substrate default is HMAC-SHA256 over an
289
+ * in-memory ephemeral key, which does not survive a restart; a
290
+ * production host injects its own through
291
+ * `punica.runtime.setApprovalTokenSigner`.
292
+ */
293
+ export interface ApprovalTokenSigner {
294
+ /** Sign `payload`; returns the compact token string. */
295
+ sign(payload: ApprovalTokenPayload): Promise<string>;
296
+ }
297
+
298
+ /** Verifies (and revokes) approval tokens. */
299
+ export interface ApprovalTokenVerifier {
300
+ /**
301
+ * Parse, check the header and signature, check expiry and jti
302
+ * revocation. Returns the payload, or throws with a stable code.
303
+ */
304
+ verify(token: string): Promise<ApprovalTokenPayload>;
305
+ /** Mark a jti revoked; tokens carrying it then fail verification. */
306
+ revoke(jti: string): void;
307
+ /** Is a jti currently revoked? */
308
+ isRevoked(jti: string): boolean;
309
+ }
310
+
269
311
  export namespace Policy {
270
312
  const manager: PolicyApi;
271
313
  }
@@ -70,6 +70,12 @@ declare module 'punica' {
70
70
  lifecyclePhase?: LifecyclePhase;
71
71
  fs: FileSystemProvider;
72
72
  fileOpeners: FileOpenersApi;
73
+ /**
74
+ * Rename/move and delete, announced to the layers that key state by
75
+ * path (content tabs, editors). Prefer this over calling the
76
+ * equivalent `fs` methods directly.
77
+ */
78
+ fileOperations: FileOperationsApi;
73
79
  search: SearchApi;
74
80
  vcs: VcsApi;
75
81
  terminals: TerminalApi;
@@ -181,7 +187,50 @@ declare module 'punica' {
181
187
  setResourcesProvider: (provider: ResourcesApi) => void;
182
188
  setTasksProvider: (provider: TaskRunnerApi) => void;
183
189
  setSecretsProvider: (provider: SecretsApi) => void;
190
+
191
+ /**
192
+ * Approval-token signing. The substrate default mints an in-memory
193
+ * ephemeral HMAC key on the first signature and loses it when the
194
+ * process exits, so a token signed before a restart no longer
195
+ * verifies after one — fine for dev and demo, useless as evidence.
196
+ * A production host installs a stable secret from the OS keychain
197
+ * during bootstrap, before `initialize()`:
198
+ *
199
+ * punica.runtime.installDefaultApprovalTokenPair(secret)
200
+ *
201
+ * The two setters are the same seam for a host that brings its own
202
+ * signer (KMS, HSM); passing `undefined` restores the substrate
203
+ * default. The key belongs to the host and never to an extension —
204
+ * an extension that can reach the signing key can also produce the
205
+ * thing it signs.
206
+ */
207
+ installDefaultApprovalTokenPair: (secret: Uint8Array | string) => void;
208
+ setApprovalTokenSigner: (
209
+ signer: kernel.ApprovalTokenSigner | undefined
210
+ ) => void;
211
+ setApprovalTokenVerifier: (
212
+ verifier: kernel.ApprovalTokenVerifier | undefined
213
+ ) => void;
214
+
215
+ /**
216
+ * Who is at this workstation, so a user-initiated call's audit record
217
+ * names a subject instead of leaving `actor.id` empty. Absent is a
218
+ * normal state — a trial licence has no account — and the substrate
219
+ * invents nothing to fill it. Passing `undefined` clears it.
220
+ */
221
+ setHostIdentity: (identity: HostIdentity | undefined) => void;
222
+ }
223
+
224
+ /**
225
+ * The person a licence belongs to, as the host knows them. `email` is
226
+ * the identifier (a registry guarantees it unique and present);
227
+ * `displayName` is presentation and may be absent.
228
+ */
229
+ export interface HostIdentity {
230
+ email: string;
231
+ displayName?: string | null;
184
232
  }
233
+
185
234
  }
186
235
 
187
236
  export const runtime: runtime.RuntimeApi;
@@ -0,0 +1,141 @@
1
+ declare module 'punica' {
2
+ export namespace runtime {
3
+ /**
4
+ * A rename (or move — the same thing at the filesystem level) that is
5
+ * about to happen, or has just happened. Both paths are
6
+ * workspace-relative, the same shape `FileSystemProvider` takes.
7
+ */
8
+ export interface FileRenameEvent {
9
+ oldPath: string;
10
+ newPath: string;
11
+ isDirectory: boolean;
12
+ }
13
+
14
+ /** A delete that is about to happen, or has just happened. */
15
+ export interface FileDeleteEvent {
16
+ path: string;
17
+ isDirectory: boolean;
18
+ }
19
+
20
+ /**
21
+ * A `will*` hook's refusal. Returning this aborts the operation before
22
+ * the filesystem is touched; `reason` becomes the thrown error's message,
23
+ * so it is read by a human and should say what the user can do about it.
24
+ */
25
+ export interface ParticipantVeto {
26
+ veto: true;
27
+ reason?: string;
28
+ }
29
+
30
+ /**
31
+ * A layer that needs to know about — or object to — file operations.
32
+ *
33
+ * This exists because a path is an identity, not just an argument. The
34
+ * shell keys open content tabs by path, the code editor keys its Monaco
35
+ * instances by path, notebook controllers key their models by path. When
36
+ * a file is renamed underneath them they are all holding a key that no
37
+ * longer names anything, and the failure is silent: a save writes to the
38
+ * old name, or finds no editor and writes nothing at all.
39
+ *
40
+ * So the operation cannot just be `fs.renameFile`. It has to be a
41
+ * transaction the interested layers take part in:
42
+ *
43
+ * - `will*` runs BEFORE the filesystem call and may veto (returning
44
+ * `{ veto: true }` or throwing). Use it for "this must not happen
45
+ * yet" — not for cleanup.
46
+ * - `did*` runs AFTER the filesystem call succeeded, awaited and in
47
+ * registration order. Use it to re-key state. A throwing `did*` hook
48
+ * is logged and skipped; it does not fail the operation, because the
49
+ * file has already moved and reporting failure would be a lie.
50
+ *
51
+ * Participants are host/shell territory. A headless boot registers none,
52
+ * and then an operation is exactly the filesystem call plus its event.
53
+ */
54
+ export interface FileOperationParticipant {
55
+ /** Stable id, used in logs and to make double-registration visible. */
56
+ id: string;
57
+ willRename?(
58
+ event: FileRenameEvent
59
+ ): Promise<void | ParticipantVeto> | void | ParticipantVeto;
60
+ didRename?(event: FileRenameEvent): Promise<void> | void;
61
+ willDelete?(
62
+ event: FileDeleteEvent
63
+ ): Promise<void | ParticipantVeto> | void | ParticipantVeto;
64
+ didDelete?(event: FileDeleteEvent): Promise<void> | void;
65
+ }
66
+
67
+ /** Options for `FileOperationsApi.rename`. */
68
+ export interface FileRenameOptions {
69
+ /**
70
+ * Whether the target is a directory. Callers that already know (the
71
+ * explorer reads it off its tree) should pass it; otherwise it is
72
+ * resolved via `fs.stat` and, on providers without `stat`, assumed to
73
+ * be a file.
74
+ */
75
+ isDirectory?: boolean;
76
+ }
77
+
78
+ /** Options for `FileOperationsApi.delete`. */
79
+ export interface FileDeleteOperationOptions {
80
+ isDirectory?: boolean;
81
+ /** Passed through to `FileSystemProvider.deleteFile` for idempotency. */
82
+ ifExists?: boolean;
83
+ }
84
+
85
+ /**
86
+ * The single seam every file mutation that changes a path should go
87
+ * through: rename/move and delete.
88
+ *
89
+ * `runtime.fs` stays the raw filesystem surface and is still the right
90
+ * call for reads and for content writes, where the path is unchanged and
91
+ * nobody's key breaks. Creating and deleting a path, or moving one, is
92
+ * different: it invalidates state held elsewhere in the app, and the
93
+ * whole point of routing it here is that the invalidation is announced
94
+ * instead of discovered.
95
+ *
96
+ * Two consumers, one code path: the explorer's context menu and an agent
97
+ * invoking the `file-explorer.rename` capability both land here, so tab
98
+ * state stays correct whether a human or a model moved the file.
99
+ *
100
+ * ## Dirty (unsaved) tracking
101
+ *
102
+ * `setDirty` / `isDirty` / `getDirtyPaths` are a working-copy registry.
103
+ * The shell mirrors its content tabs into it, which lets a caller ask
104
+ * "would deleting this lose unsaved work?" without knowing that content
105
+ * tabs exist — the explorer's delete confirmation reads it to say so in
106
+ * the dialog, in one prompt rather than two.
107
+ */
108
+ export interface FileOperationsApi {
109
+ /**
110
+ * Add a participant. Registering the same `id` twice replaces the
111
+ * first. Dispose to remove it — a shell that is torn down must not
112
+ * leave a hook pointing at dead state.
113
+ */
114
+ registerParticipant(participant: FileOperationParticipant): {
115
+ dispose(): void;
116
+ };
117
+
118
+ /** Record (or clear) unsaved-changes state for a path. */
119
+ setDirty(path: string, dirty: boolean): void;
120
+ isDirty(path: string): boolean;
121
+ /**
122
+ * Every path with unsaved changes; with `prefix`, only that path and
123
+ * its descendants (so a folder delete can name what it would lose).
124
+ */
125
+ getDirtyPaths(prefix?: string): string[];
126
+
127
+ /**
128
+ * Move `oldPath` to `newPath`, announcing it. Rejects if a participant
129
+ * vetoes or the filesystem call fails; in both cases nothing has moved.
130
+ */
131
+ rename(
132
+ oldPath: string,
133
+ newPath: string,
134
+ opts?: FileRenameOptions
135
+ ): Promise<void>;
136
+
137
+ /** Delete `path`, announcing it. Same failure contract as `rename`. */
138
+ delete(path: string, opts?: FileDeleteOperationOptions): Promise<void>;
139
+ }
140
+ }
141
+ }
@@ -1,7 +1,24 @@
1
1
  declare module 'punica' {
2
2
  export namespace shell {
3
+ /** One open content tab, as reported by `ContentTabsApi.list`. */
4
+ export interface ContentTabInfo {
5
+ /** Workspace-relative file path, or a scheme URI for command-only views. */
6
+ path: string;
7
+ /** Label shown on the tab (without the unsaved-changes marker). */
8
+ title: string;
9
+ /** Set by whichever extension opened it (e.g. "code", "markdown"). */
10
+ viewType?: string;
11
+ /** Whether the tab holds unsaved edits. */
12
+ dirty: boolean;
13
+ }
14
+
3
15
  /**
4
16
  * Content tab management capability for the main editor area.
17
+ *
18
+ * Tabs are identified by `path` — the same workspace-relative path the
19
+ * filesystem uses — which is what makes `rename` necessary rather than
20
+ * cosmetic: after a file moves, every tab keyed by the old path is
21
+ * holding an identity that no longer exists.
5
22
  */
6
23
  export interface ContentTabsApi {
7
24
  open(options: {
@@ -9,9 +26,18 @@ declare module 'punica' {
9
26
  title?: string;
10
27
  viewType?: string;
11
28
  }): HTMLElement | null;
12
- close(path: string): void;
13
- closeOthers(path: string): void;
14
- closeAll(): void;
29
+ /**
30
+ * Close the tab at `path`, prompting first when it has unsaved changes
31
+ * (Save / Don't Save / Cancel). Resolves when the tab is gone or the
32
+ * user cancelled — check `list()` if you need to know which.
33
+ */
34
+ close(path: string): Promise<void>;
35
+ /** As `close`, addressed by tab id rather than path. */
36
+ closeById(id: string): Promise<void>;
37
+ closeOthers(path: string): Promise<void>;
38
+ /** As `closeOthers`, addressed by tab id rather than path. */
39
+ closeOthersById(id: string): Promise<void>;
40
+ closeAll(): Promise<void>;
15
41
  setDirty(path: string, dirty: boolean): void;
16
42
  /**
17
43
  * Returns the path of the currently active content tab, or null if none.
@@ -23,6 +49,34 @@ declare module 'punica' {
23
49
  * (e.g. "code", "flow-book", "markdown").
24
50
  */
25
51
  getActiveViewType(): string | null;
52
+
53
+ /**
54
+ * Re-key the tab at `oldPath` to `newPath`, keeping its content,
55
+ * dirty marker and selection. When `oldPath` names a directory, every
56
+ * tab beneath it moves too — renaming `src/` has to carry
57
+ * `src/app.ts` with it.
58
+ *
59
+ * This does not touch the filesystem; it is the tab half of a rename
60
+ * that already happened. Callers should not invoke it directly —
61
+ * `runtime.fileOperations.rename` drives it through the shell's
62
+ * participant, so an agent-initiated rename syncs the same way a
63
+ * context-menu one does.
64
+ */
65
+ rename(oldPath: string, newPath: string): void;
66
+
67
+ /**
68
+ * Close the tab at `path` (and, for a directory, everything beneath it)
69
+ * without prompting. For when the file is already gone: there is
70
+ * nothing left to save, so the unsaved-changes dialog would offer a
71
+ * choice that cannot be honored.
72
+ */
73
+ forceClose(path: string): void;
74
+
75
+ /** Whether the tab at `path` holds unsaved edits. False when no such tab. */
76
+ isDirty(path: string): boolean;
77
+
78
+ /** Every open tab, in the order they appear in the tab strip. */
79
+ list(): readonly ContentTabInfo[];
26
80
  }
27
81
 
28
82
  /**