@ericsanchezok/synergy-util 3.0.24 → 3.1.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/LICENSE.codex +201 -0
- package/NOTICE +9 -0
- package/dist/activity.d.ts +9 -4
- package/dist/activity.js +20 -19
- package/dist/asset-reference.d.ts +15 -0
- package/dist/asset-reference.js +98 -0
- package/dist/atomic-file.d.ts +9 -0
- package/dist/atomic-file.js +86 -0
- package/dist/attachment-presentation.d.ts +26 -0
- package/dist/attachment-presentation.js +11 -0
- package/dist/capability.d.ts +1 -1
- package/dist/capability.js +5 -19
- package/dist/cli-command.d.ts +12 -0
- package/dist/cli-command.js +3 -0
- package/dist/encode.d.ts +3 -0
- package/dist/encode.js +7 -0
- package/dist/error.d.ts +36 -56
- package/dist/feature-config.d.ts +4 -0
- package/dist/feature-config.js +23 -0
- package/dist/features.d.ts +134 -0
- package/dist/features.js +195 -0
- package/dist/file-patch.d.ts +31 -0
- package/dist/file-patch.js +218 -0
- package/dist/filesystem-identity.d.ts +28 -0
- package/dist/filesystem-identity.js +71 -0
- package/dist/fn.d.ts +1 -1
- package/dist/fs-lock.d.ts +5 -1
- package/dist/fs-lock.js +14 -6
- package/dist/input-assistance.d.ts +51 -0
- package/dist/input-assistance.js +61 -0
- package/dist/installed-launcher.d.ts +18 -0
- package/dist/installed-launcher.js +39 -0
- package/dist/io-retry.d.ts +11 -0
- package/dist/io-retry.js +36 -0
- package/dist/json-value.d.ts +4 -0
- package/dist/json-value.js +12 -0
- package/dist/markdown-assets.d.ts +6 -0
- package/dist/markdown-assets.js +29 -0
- package/dist/markdown-link.d.ts +2 -0
- package/dist/markdown-link.js +8 -0
- package/dist/native-assets.d.ts +7 -0
- package/dist/native-assets.js +28 -0
- package/dist/network-error.d.ts +7 -0
- package/dist/network-error.js +111 -0
- package/dist/plugin-policy.d.ts +1 -1
- package/dist/process-group.d.ts +45 -0
- package/dist/process-group.js +156 -0
- package/dist/process-identity.js +5 -2
- package/dist/projection-patch.d.ts +9 -0
- package/dist/projection-patch.js +74 -0
- package/dist/reasoning-item.d.ts +1 -0
- package/dist/reasoning-item.js +16 -0
- package/dist/render-artifact.d.ts +352 -0
- package/dist/render-artifact.js +115 -0
- package/dist/render-ui.d.ts +202 -0
- package/dist/render-ui.js +208 -0
- package/dist/resource-reference.d.ts +73 -0
- package/dist/resource-reference.js +227 -0
- package/dist/retry.d.ts +4 -0
- package/dist/retry.js +56 -23
- package/dist/runtime-startup.d.ts +240 -2
- package/dist/runtime-startup.js +108 -9
- package/dist/terminal.d.ts +217 -0
- package/dist/terminal.js +111 -0
- package/dist/tool-display.d.ts +10 -0
- package/dist/tool-display.js +22 -1
- package/dist/tool-timeout.d.ts +21 -0
- package/dist/tool-timeout.js +1 -0
- package/dist/tool.d.ts +6 -7
- package/dist/workbench-view.d.ts +692 -0
- package/dist/workbench-view.js +90 -0
- package/dist/worker-plan.d.ts +39 -0
- package/dist/worker-plan.js +33 -0
- package/package.json +28 -4
package/dist/capability.js
CHANGED
|
@@ -27,12 +27,6 @@ export const SYNERGY_CAPABILITY_DETAILS = {
|
|
|
27
27
|
description: "Can operate observed native application windows using background input in Full Access mode.",
|
|
28
28
|
nonBypassable: true,
|
|
29
29
|
},
|
|
30
|
-
shell_read: {
|
|
31
|
-
category: "runtime",
|
|
32
|
-
severity: "low",
|
|
33
|
-
title: "Run read-only shell commands",
|
|
34
|
-
description: "Can run shell commands classified as read-only.",
|
|
35
|
-
},
|
|
36
30
|
shell: {
|
|
37
31
|
category: "runtime",
|
|
38
32
|
severity: "medium",
|
|
@@ -51,13 +45,6 @@ export const SYNERGY_CAPABILITY_DETAILS = {
|
|
|
51
45
|
title: "Run remote-write shell commands",
|
|
52
46
|
description: "Can modify existing remote state outside ordinary branch/PR publication workflows.",
|
|
53
47
|
},
|
|
54
|
-
shell_remote_execute: {
|
|
55
|
-
category: "runtime",
|
|
56
|
-
severity: "high",
|
|
57
|
-
title: "Execute commands on remote host",
|
|
58
|
-
description: "Can run bash/process commands on a remote Synergy Link host.",
|
|
59
|
-
nonBypassable: true,
|
|
60
|
-
},
|
|
61
48
|
shell_remote_publish: {
|
|
62
49
|
category: "runtime",
|
|
63
50
|
severity: "medium",
|
|
@@ -68,14 +55,14 @@ export const SYNERGY_CAPABILITY_DETAILS = {
|
|
|
68
55
|
category: "runtime",
|
|
69
56
|
severity: "high",
|
|
70
57
|
title: "Run destructive shell commands",
|
|
71
|
-
description: "Can run commands that
|
|
58
|
+
description: "Can run commands that the OS sandbox cannot express: host-level or system-location destruction, irreversible local history loss, and privilege escalation.",
|
|
72
59
|
nonBypassable: true,
|
|
73
60
|
},
|
|
74
61
|
shell_hardline: {
|
|
75
62
|
category: "runtime",
|
|
76
63
|
severity: "high",
|
|
77
64
|
title: "Run forbidden shell commands",
|
|
78
|
-
description: "Matches
|
|
65
|
+
description: "Matches the machine-wide commands Synergy refuses under every profile: filesystem-root and home destruction, filesystem-format and power tools, fork bombs, and raw-device writes.",
|
|
79
66
|
nonBypassable: true,
|
|
80
67
|
},
|
|
81
68
|
file_write: {
|
|
@@ -315,10 +302,8 @@ export const SYNERGY_CAPABILITY_DETAILS = {
|
|
|
315
302
|
export const SYNERGY_PROFILE_CAPABILITIES = [
|
|
316
303
|
"file_read",
|
|
317
304
|
"file_write",
|
|
318
|
-
"shell_read",
|
|
319
305
|
"shell",
|
|
320
306
|
"shell_branch_mutation",
|
|
321
|
-
"shell_remote_execute",
|
|
322
307
|
"shell_remote_publish",
|
|
323
308
|
"shell_remote_write",
|
|
324
309
|
"shell_destructive",
|
|
@@ -371,6 +356,7 @@ export const SYNERGY_PERMISSION_CAPABILITY = {
|
|
|
371
356
|
edit: "file_write",
|
|
372
357
|
write: "file_write",
|
|
373
358
|
revise_file: "file_write",
|
|
359
|
+
apply_patch: "file_write",
|
|
374
360
|
resolve_conflicts: "file_write",
|
|
375
361
|
save_file: "file_write",
|
|
376
362
|
bash: "shell",
|
|
@@ -381,7 +367,6 @@ export const SYNERGY_PERMISSION_CAPABILITY = {
|
|
|
381
367
|
scan_document: "file_read",
|
|
382
368
|
look_at: "file_read",
|
|
383
369
|
view_image: "file_read",
|
|
384
|
-
attach: "file_read",
|
|
385
370
|
ast_grep: "file_read",
|
|
386
371
|
lsp: "file_read",
|
|
387
372
|
dagread: "file_read",
|
|
@@ -390,7 +375,7 @@ export const SYNERGY_PERMISSION_CAPABILITY = {
|
|
|
390
375
|
task_output: "file_read",
|
|
391
376
|
question: "file_read",
|
|
392
377
|
skill: "file_read",
|
|
393
|
-
render: "
|
|
378
|
+
render: "session_state",
|
|
394
379
|
agenda_list: "file_read",
|
|
395
380
|
agenda_logs: "file_read",
|
|
396
381
|
session_list: "file_read",
|
|
@@ -409,6 +394,7 @@ export const SYNERGY_PERMISSION_CAPABILITY = {
|
|
|
409
394
|
doom_loop: "session_state",
|
|
410
395
|
worktree_enter: "file_write",
|
|
411
396
|
worktree_leave: "file_write",
|
|
397
|
+
worktree_archive: "file_write",
|
|
412
398
|
agent_config: "config:write",
|
|
413
399
|
session_data: "session_data",
|
|
414
400
|
workspace_data: "workspace_data",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
export type { CommandModule } from "yargs";
|
|
3
|
+
export interface CommandEntry {
|
|
4
|
+
command: string | string[];
|
|
5
|
+
describe: string;
|
|
6
|
+
storage?: "maintenance" | ((positionals: readonly (string | number)[]) => "maintenance" | undefined);
|
|
7
|
+
load(): Promise<CommandModule>;
|
|
8
|
+
}
|
|
9
|
+
type WithDoubleDash<T> = T & {
|
|
10
|
+
"--"?: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>): CommandModule<T, WithDoubleDash<U>>;
|
package/dist/encode.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export declare function base64Encode(value: string): string;
|
|
2
|
+
/** RFC 4648 standard base64 (with padding) for opaque labels like data: URLs;
|
|
3
|
+
* unlike base64Encode this output is safe for strict atob decoders. */
|
|
4
|
+
export declare function base64EncodeStandard(value: string): string;
|
|
2
5
|
export declare function base64Decode(value: string): string;
|
|
3
6
|
export declare function hash(content: string, algorithm?: string): Promise<string>;
|
|
4
7
|
export declare function checksum(content: string): string | undefined;
|
package/dist/encode.js
CHANGED
|
@@ -3,6 +3,13 @@ export function base64Encode(value) {
|
|
|
3
3
|
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
4
4
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
5
5
|
}
|
|
6
|
+
/** RFC 4648 standard base64 (with padding) for opaque labels like data: URLs;
|
|
7
|
+
* unlike base64Encode this output is safe for strict atob decoders. */
|
|
8
|
+
export function base64EncodeStandard(value) {
|
|
9
|
+
const bytes = new TextEncoder().encode(value);
|
|
10
|
+
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
11
|
+
return btoa(binary);
|
|
12
|
+
}
|
|
6
13
|
export function base64Decode(value) {
|
|
7
14
|
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
8
15
|
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
package/dist/error.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
+
export interface NamedErrorInstance<Name extends string, Data extends z.core.$ZodType> extends NamedError {
|
|
3
|
+
readonly name: Name;
|
|
4
|
+
readonly data: z.input<Data>;
|
|
5
|
+
schema(): z.ZodObject<{
|
|
6
|
+
name: z.ZodLiteral<Name>;
|
|
7
|
+
data: Data;
|
|
8
|
+
}>;
|
|
9
|
+
toObject(): {
|
|
10
|
+
name: Name;
|
|
11
|
+
data: z.input<Data>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
2
14
|
export declare abstract class NamedError extends Error {
|
|
3
15
|
abstract schema(): z.core.$ZodType;
|
|
4
16
|
abstract toObject(): {
|
|
@@ -7,6 +19,7 @@ export declare abstract class NamedError extends Error {
|
|
|
7
19
|
};
|
|
8
20
|
static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data): {
|
|
9
21
|
new (data: z.input<Data>, options?: ErrorOptions): {
|
|
22
|
+
cause?: unknown;
|
|
10
23
|
readonly name: Name;
|
|
11
24
|
readonly data: z.core.input<Data>;
|
|
12
25
|
schema(): z.ZodObject<{
|
|
@@ -19,18 +32,20 @@ export declare abstract class NamedError extends Error {
|
|
|
19
32
|
};
|
|
20
33
|
message: string;
|
|
21
34
|
stack?: string;
|
|
22
|
-
cause?: unknown;
|
|
23
35
|
};
|
|
36
|
+
isError(error: unknown): error is Error;
|
|
37
|
+
isError(value: unknown): value is Error;
|
|
24
38
|
readonly Schema: z.ZodObject<{
|
|
25
39
|
name: z.ZodLiteral<Name>;
|
|
26
40
|
data: Data;
|
|
27
41
|
}, z.core.$strip>;
|
|
28
|
-
isInstance(input: any): input is
|
|
42
|
+
isInstance(input: any): input is NamedErrorInstance<Name, Data>;
|
|
29
43
|
create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data): /*elided*/ any;
|
|
30
44
|
readonly Unknown: {
|
|
31
45
|
new (data: {
|
|
32
46
|
message: string;
|
|
33
47
|
}, options?: ErrorOptions): {
|
|
48
|
+
cause?: unknown;
|
|
34
49
|
readonly name: "UnknownError";
|
|
35
50
|
readonly data: {
|
|
36
51
|
message: string;
|
|
@@ -49,53 +64,35 @@ export declare abstract class NamedError extends Error {
|
|
|
49
64
|
};
|
|
50
65
|
message: string;
|
|
51
66
|
stack?: string;
|
|
52
|
-
cause?: unknown;
|
|
53
67
|
};
|
|
68
|
+
isError(error: unknown): error is Error;
|
|
69
|
+
isError(value: unknown): value is Error;
|
|
54
70
|
readonly Schema: z.ZodObject<{
|
|
55
71
|
name: z.ZodLiteral<"UnknownError">;
|
|
56
72
|
data: z.ZodObject<{
|
|
57
73
|
message: z.ZodString;
|
|
58
74
|
}, z.core.$strip>;
|
|
59
75
|
}, z.core.$strip>;
|
|
60
|
-
isInstance(input: any): input is {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
message: string;
|
|
64
|
-
};
|
|
65
|
-
schema(): z.ZodObject<{
|
|
66
|
-
name: z.ZodLiteral<"UnknownError">;
|
|
67
|
-
data: z.ZodObject<{
|
|
68
|
-
message: z.ZodString;
|
|
69
|
-
}, z.core.$strip>;
|
|
70
|
-
}, z.core.$strip>;
|
|
71
|
-
toObject(): {
|
|
72
|
-
name: "UnknownError";
|
|
73
|
-
data: {
|
|
74
|
-
message: string;
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
message: string;
|
|
78
|
-
stack?: string;
|
|
79
|
-
cause?: unknown;
|
|
80
|
-
};
|
|
76
|
+
isInstance(input: any): input is NamedErrorInstance<"UnknownError", z.ZodObject<{
|
|
77
|
+
message: z.ZodString;
|
|
78
|
+
}, z.core.$strip>>;
|
|
81
79
|
create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data): /*elided*/ any;
|
|
82
80
|
readonly Unknown: /*elided*/ any;
|
|
83
|
-
isError(value: unknown): value is Error;
|
|
84
81
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
85
82
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
83
|
+
prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
|
|
86
84
|
stackTraceLimit: number;
|
|
87
|
-
prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;
|
|
88
85
|
};
|
|
89
|
-
isError(value: unknown): value is Error;
|
|
90
86
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
91
87
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
88
|
+
prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
|
|
92
89
|
stackTraceLimit: number;
|
|
93
|
-
prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;
|
|
94
90
|
};
|
|
95
91
|
static readonly Unknown: {
|
|
96
92
|
new (data: {
|
|
97
93
|
message: string;
|
|
98
94
|
}, options?: ErrorOptions): {
|
|
95
|
+
cause?: unknown;
|
|
99
96
|
readonly name: "UnknownError";
|
|
100
97
|
readonly data: {
|
|
101
98
|
message: string;
|
|
@@ -114,37 +111,21 @@ export declare abstract class NamedError extends Error {
|
|
|
114
111
|
};
|
|
115
112
|
message: string;
|
|
116
113
|
stack?: string;
|
|
117
|
-
cause?: unknown;
|
|
118
114
|
};
|
|
115
|
+
isError(error: unknown): error is Error;
|
|
116
|
+
isError(value: unknown): value is Error;
|
|
119
117
|
readonly Schema: z.ZodObject<{
|
|
120
118
|
name: z.ZodLiteral<"UnknownError">;
|
|
121
119
|
data: z.ZodObject<{
|
|
122
120
|
message: z.ZodString;
|
|
123
121
|
}, z.core.$strip>;
|
|
124
122
|
}, z.core.$strip>;
|
|
125
|
-
isInstance(input: any): input is {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message: string;
|
|
129
|
-
};
|
|
130
|
-
schema(): z.ZodObject<{
|
|
131
|
-
name: z.ZodLiteral<"UnknownError">;
|
|
132
|
-
data: z.ZodObject<{
|
|
133
|
-
message: z.ZodString;
|
|
134
|
-
}, z.core.$strip>;
|
|
135
|
-
}, z.core.$strip>;
|
|
136
|
-
toObject(): {
|
|
137
|
-
name: "UnknownError";
|
|
138
|
-
data: {
|
|
139
|
-
message: string;
|
|
140
|
-
};
|
|
141
|
-
};
|
|
142
|
-
message: string;
|
|
143
|
-
stack?: string;
|
|
144
|
-
cause?: unknown;
|
|
145
|
-
};
|
|
123
|
+
isInstance(input: any): input is NamedErrorInstance<"UnknownError", z.ZodObject<{
|
|
124
|
+
message: z.ZodString;
|
|
125
|
+
}, z.core.$strip>>;
|
|
146
126
|
create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data): {
|
|
147
127
|
new (data: z.input<Data>, options?: ErrorOptions): {
|
|
128
|
+
cause?: unknown;
|
|
148
129
|
readonly name: Name;
|
|
149
130
|
readonly data: z.core.input<Data>;
|
|
150
131
|
schema(): z.ZodObject<{
|
|
@@ -157,26 +138,25 @@ export declare abstract class NamedError extends Error {
|
|
|
157
138
|
};
|
|
158
139
|
message: string;
|
|
159
140
|
stack?: string;
|
|
160
|
-
cause?: unknown;
|
|
161
141
|
};
|
|
142
|
+
isError(error: unknown): error is Error;
|
|
143
|
+
isError(value: unknown): value is Error;
|
|
162
144
|
readonly Schema: z.ZodObject<{
|
|
163
145
|
name: z.ZodLiteral<Name>;
|
|
164
146
|
data: Data;
|
|
165
147
|
}, z.core.$strip>;
|
|
166
|
-
isInstance(input: any): input is
|
|
148
|
+
isInstance(input: any): input is NamedErrorInstance<Name, Data>;
|
|
167
149
|
create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data): /*elided*/ any;
|
|
168
150
|
readonly Unknown: /*elided*/ any;
|
|
169
|
-
isError(value: unknown): value is Error;
|
|
170
151
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
171
152
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
153
|
+
prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
|
|
172
154
|
stackTraceLimit: number;
|
|
173
|
-
prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;
|
|
174
155
|
};
|
|
175
156
|
readonly Unknown: /*elided*/ any;
|
|
176
|
-
isError(value: unknown): value is Error;
|
|
177
157
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
178
158
|
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
159
|
+
prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
|
|
179
160
|
stackTraceLimit: number;
|
|
180
|
-
prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;
|
|
181
161
|
};
|
|
182
162
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { FeaturePreferences } from "./features";
|
|
2
|
+
export declare const FEATURE_CONFIG_FILENAME = "140-features.jsonc";
|
|
3
|
+
export declare function parseFeatureConfig(text: string): FeaturePreferences;
|
|
4
|
+
export declare function readFeaturePreferences(configDirectory: string): Promise<FeaturePreferences>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "jsonc-parser";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { FeaturePreferences } from "./features";
|
|
6
|
+
export const FEATURE_CONFIG_FILENAME = "140-features.jsonc";
|
|
7
|
+
const Document = z.object({ $schema: z.string().optional(), features: FeaturePreferences.optional() }).strict();
|
|
8
|
+
export function parseFeatureConfig(text) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
11
|
+
if (errors.length)
|
|
12
|
+
throw new Error("Invalid experimental feature configuration");
|
|
13
|
+
return Document.parse(value).features ?? { optIn: [] };
|
|
14
|
+
}
|
|
15
|
+
export async function readFeaturePreferences(configDirectory) {
|
|
16
|
+
const filename = path.join(configDirectory, "synergy.d", FEATURE_CONFIG_FILENAME);
|
|
17
|
+
const text = await readFile(filename, "utf8").catch((error) => {
|
|
18
|
+
if (error.code === "ENOENT")
|
|
19
|
+
return undefined;
|
|
20
|
+
throw error;
|
|
21
|
+
});
|
|
22
|
+
return text === undefined ? { optIn: [] } : parseFeatureConfig(text);
|
|
23
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const FeatureDefinition: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
owner: z.ZodString;
|
|
5
|
+
maturity: z.ZodEnum<{
|
|
6
|
+
experimental: "experimental";
|
|
7
|
+
stable: "stable";
|
|
8
|
+
}>;
|
|
9
|
+
activation: z.ZodEnum<{
|
|
10
|
+
live: "live";
|
|
11
|
+
restart: "restart";
|
|
12
|
+
}>;
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
description: z.ZodString;
|
|
15
|
+
component: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export type FeatureDefinition = z.infer<typeof FeatureDefinition>;
|
|
18
|
+
export declare const ComponentFeatures: z.ZodObject<{
|
|
19
|
+
kind: z.ZodEnum<{
|
|
20
|
+
bridge: "bridge";
|
|
21
|
+
foundation: "foundation";
|
|
22
|
+
product: "product";
|
|
23
|
+
}>;
|
|
24
|
+
definitions: z.ZodArray<z.ZodObject<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
owner: z.ZodString;
|
|
27
|
+
maturity: z.ZodEnum<{
|
|
28
|
+
experimental: "experimental";
|
|
29
|
+
stable: "stable";
|
|
30
|
+
}>;
|
|
31
|
+
activation: z.ZodEnum<{
|
|
32
|
+
live: "live";
|
|
33
|
+
restart: "restart";
|
|
34
|
+
}>;
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
description: z.ZodString;
|
|
37
|
+
component: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strict>>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
export type ComponentFeatures = z.infer<typeof ComponentFeatures>;
|
|
41
|
+
export declare const FeaturePreferences: z.ZodObject<{
|
|
42
|
+
optIn: z.ZodArray<z.ZodString>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export type FeaturePreferences = z.infer<typeof FeaturePreferences>;
|
|
45
|
+
export interface FeatureComponent {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly version: string;
|
|
48
|
+
readonly requires?: Readonly<Record<string, string>>;
|
|
49
|
+
readonly features?: {
|
|
50
|
+
readonly kind: ComponentFeatures["kind"];
|
|
51
|
+
readonly definitions: readonly FeatureDefinition[];
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare const FeaturePlan: z.ZodObject<{
|
|
55
|
+
definitions: z.ZodArray<z.ZodObject<{
|
|
56
|
+
id: z.ZodString;
|
|
57
|
+
owner: z.ZodString;
|
|
58
|
+
maturity: z.ZodEnum<{
|
|
59
|
+
experimental: "experimental";
|
|
60
|
+
stable: "stable";
|
|
61
|
+
}>;
|
|
62
|
+
activation: z.ZodEnum<{
|
|
63
|
+
live: "live";
|
|
64
|
+
restart: "restart";
|
|
65
|
+
}>;
|
|
66
|
+
name: z.ZodString;
|
|
67
|
+
description: z.ZodString;
|
|
68
|
+
component: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, z.core.$strict>>;
|
|
70
|
+
optIn: z.ZodArray<z.ZodString>;
|
|
71
|
+
selection: z.ZodArray<z.ZodObject<{
|
|
72
|
+
id: z.ZodString;
|
|
73
|
+
version: z.ZodString;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
components: z.ZodArray<z.ZodString>;
|
|
76
|
+
requirements: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
77
|
+
}, z.core.$strict>;
|
|
78
|
+
export type FeaturePlan = z.infer<typeof FeaturePlan>;
|
|
79
|
+
export declare const FeatureStatus: z.ZodObject<{
|
|
80
|
+
id: z.ZodString;
|
|
81
|
+
definition: z.ZodOptional<z.ZodObject<{
|
|
82
|
+
id: z.ZodString;
|
|
83
|
+
owner: z.ZodString;
|
|
84
|
+
maturity: z.ZodEnum<{
|
|
85
|
+
experimental: "experimental";
|
|
86
|
+
stable: "stable";
|
|
87
|
+
}>;
|
|
88
|
+
activation: z.ZodEnum<{
|
|
89
|
+
live: "live";
|
|
90
|
+
restart: "restart";
|
|
91
|
+
}>;
|
|
92
|
+
name: z.ZodString;
|
|
93
|
+
description: z.ZodString;
|
|
94
|
+
component: z.ZodOptional<z.ZodString>;
|
|
95
|
+
}, z.core.$strict>>;
|
|
96
|
+
selected: z.ZodBoolean;
|
|
97
|
+
available: z.ZodBoolean;
|
|
98
|
+
enabled: z.ZodBoolean;
|
|
99
|
+
pendingRestart: z.ZodBoolean;
|
|
100
|
+
reason: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
"component-unselected": "component-unselected";
|
|
102
|
+
"dependency-unavailable": "dependency-unavailable";
|
|
103
|
+
"not-installed": "not-installed";
|
|
104
|
+
"restart-required": "restart-required";
|
|
105
|
+
}>>;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
export type FeatureStatus = z.infer<typeof FeatureStatus>;
|
|
108
|
+
export declare namespace FeatureSelection {
|
|
109
|
+
function complete(candidates: readonly FeatureComponent[], provided?: Readonly<Record<string, string>>): {
|
|
110
|
+
definitions: {
|
|
111
|
+
id: string;
|
|
112
|
+
owner: string;
|
|
113
|
+
maturity: "experimental" | "stable";
|
|
114
|
+
activation: "live" | "restart";
|
|
115
|
+
name: string;
|
|
116
|
+
description: string;
|
|
117
|
+
component?: string | undefined;
|
|
118
|
+
}[];
|
|
119
|
+
optIn: string[];
|
|
120
|
+
selection: {
|
|
121
|
+
id: string;
|
|
122
|
+
version: string;
|
|
123
|
+
}[];
|
|
124
|
+
components: string[];
|
|
125
|
+
requirements: Record<string, string[]>;
|
|
126
|
+
};
|
|
127
|
+
interface Options {
|
|
128
|
+
optIn?: readonly string[];
|
|
129
|
+
selection?: Readonly<Record<string, string>>;
|
|
130
|
+
provided?: Readonly<Record<string, string>>;
|
|
131
|
+
}
|
|
132
|
+
function resolve(candidates: readonly FeatureComponent[], options?: Options): FeaturePlan;
|
|
133
|
+
function status(plan: FeaturePlan, requested: readonly string[], effective: readonly string[]): FeatureStatus[];
|
|
134
|
+
}
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const Id = z.string().regex(/^[a-z][a-z0-9.-]*$/);
|
|
3
|
+
export const FeatureDefinition = z
|
|
4
|
+
.object({
|
|
5
|
+
id: Id,
|
|
6
|
+
owner: Id,
|
|
7
|
+
maturity: z.enum(["experimental", "stable"]),
|
|
8
|
+
activation: z.enum(["live", "restart"]),
|
|
9
|
+
name: z.string().min(1),
|
|
10
|
+
description: z.string().min(1),
|
|
11
|
+
component: Id.optional(),
|
|
12
|
+
})
|
|
13
|
+
.strict()
|
|
14
|
+
.superRefine((value, context) => {
|
|
15
|
+
if (value.component && value.activation !== "restart")
|
|
16
|
+
context.addIssue({ code: "custom", message: "Component features require restart activation" });
|
|
17
|
+
})
|
|
18
|
+
.meta({ ref: "FeatureDefinition" });
|
|
19
|
+
export const ComponentFeatures = z
|
|
20
|
+
.object({
|
|
21
|
+
kind: z.enum(["foundation", "product", "bridge"]),
|
|
22
|
+
definitions: z.array(FeatureDefinition),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
export const FeaturePreferences = z
|
|
26
|
+
.object({ optIn: z.array(Id).refine((ids) => new Set(ids).size === ids.length, "Duplicate experimental feature") })
|
|
27
|
+
.strict();
|
|
28
|
+
export const FeaturePlan = z
|
|
29
|
+
.object({
|
|
30
|
+
definitions: z.array(FeatureDefinition),
|
|
31
|
+
optIn: FeaturePreferences.shape.optIn,
|
|
32
|
+
selection: z.array(z.object({ id: Id, version: z.string().min(1) }).strict()),
|
|
33
|
+
components: z.array(Id),
|
|
34
|
+
requirements: z.record(Id, z.array(Id)),
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
.refine((plan) => {
|
|
38
|
+
const active = activate(plan, plan.optIn);
|
|
39
|
+
return (plan.components.length === active.size &&
|
|
40
|
+
new Set(plan.components).size === active.size &&
|
|
41
|
+
plan.components.every((id) => active.has(id)));
|
|
42
|
+
}, "Active components do not match feature selection");
|
|
43
|
+
export const FeatureStatus = z
|
|
44
|
+
.object({
|
|
45
|
+
id: Id,
|
|
46
|
+
definition: FeatureDefinition.optional(),
|
|
47
|
+
selected: z.boolean(),
|
|
48
|
+
available: z.boolean(),
|
|
49
|
+
enabled: z.boolean(),
|
|
50
|
+
pendingRestart: z.boolean(),
|
|
51
|
+
reason: z.enum(["not-installed", "component-unselected", "dependency-unavailable", "restart-required"]).optional(),
|
|
52
|
+
})
|
|
53
|
+
.meta({ ref: "FeatureStatus" });
|
|
54
|
+
function activate(plan, optIn) {
|
|
55
|
+
const active = new Set(plan.selection.map((component) => component.id));
|
|
56
|
+
for (const definition of plan.definitions) {
|
|
57
|
+
if (definition.component && definition.maturity === "experimental" && !optIn.includes(definition.id))
|
|
58
|
+
active.delete(definition.component);
|
|
59
|
+
}
|
|
60
|
+
let changed = true;
|
|
61
|
+
while (changed) {
|
|
62
|
+
changed = false;
|
|
63
|
+
for (const id of active) {
|
|
64
|
+
if (!(plan.requirements[id] ?? []).some((required) => !active.has(required)))
|
|
65
|
+
continue;
|
|
66
|
+
active.delete(id);
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return active;
|
|
71
|
+
}
|
|
72
|
+
export var FeatureSelection;
|
|
73
|
+
(function (FeatureSelection) {
|
|
74
|
+
function complete(candidates, provided) {
|
|
75
|
+
return resolve(candidates, {
|
|
76
|
+
provided,
|
|
77
|
+
optIn: candidates.flatMap((component) => component.features?.definitions
|
|
78
|
+
.filter((feature) => feature.maturity === "experimental")
|
|
79
|
+
.map((feature) => feature.id) ?? []),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
FeatureSelection.complete = complete;
|
|
83
|
+
function resolve(candidates, options = {}) {
|
|
84
|
+
const optIn = FeaturePreferences.parse({ optIn: options.optIn ?? [] }).optIn;
|
|
85
|
+
const byID = new Map();
|
|
86
|
+
const definitions = new Map();
|
|
87
|
+
const gates = new Map();
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
if (byID.has(candidate.id))
|
|
90
|
+
throw new Error(`Duplicate component: ${candidate.id}`);
|
|
91
|
+
byID.set(candidate.id, candidate);
|
|
92
|
+
if (!candidate.features)
|
|
93
|
+
continue;
|
|
94
|
+
const metadata = ComponentFeatures.parse(candidate.features);
|
|
95
|
+
if (metadata.kind === "product" && !metadata.definitions.length)
|
|
96
|
+
throw new Error(`Product component ${candidate.id} needs a feature declaration`);
|
|
97
|
+
for (const definition of metadata.definitions) {
|
|
98
|
+
if (definition.owner !== candidate.id || (definition.component && definition.component !== candidate.id))
|
|
99
|
+
throw new Error(`Feature ${definition.id} must belong to ${candidate.id}`);
|
|
100
|
+
if (definitions.has(definition.id))
|
|
101
|
+
throw new Error(`Duplicate feature: ${definition.id}`);
|
|
102
|
+
definitions.set(definition.id, definition);
|
|
103
|
+
if (!definition.component)
|
|
104
|
+
continue;
|
|
105
|
+
if (gates.has(definition.component))
|
|
106
|
+
throw new Error(`Multiple feature gates for ${definition.component}`);
|
|
107
|
+
gates.set(definition.component, definition);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
if (candidate.features?.kind === "bridge" || gates.get(candidate.id)?.maturity === "experimental")
|
|
112
|
+
continue;
|
|
113
|
+
const visited = new Set();
|
|
114
|
+
const check = (required) => {
|
|
115
|
+
if (visited.has(required))
|
|
116
|
+
return;
|
|
117
|
+
visited.add(required);
|
|
118
|
+
if (gates.get(required)?.maturity === "experimental")
|
|
119
|
+
throw new Error(`Component ${candidate.id} requires experimental ${required}; split an optional integration`);
|
|
120
|
+
for (const dependency of Object.keys(byID.get(required)?.requires ?? {}))
|
|
121
|
+
check(dependency);
|
|
122
|
+
};
|
|
123
|
+
for (const required of Object.keys(candidate.requires ?? {}))
|
|
124
|
+
check(required);
|
|
125
|
+
}
|
|
126
|
+
const available = new Set();
|
|
127
|
+
const include = (id) => {
|
|
128
|
+
if (available.has(id) || options.provided?.[id])
|
|
129
|
+
return;
|
|
130
|
+
const candidate = byID.get(id);
|
|
131
|
+
if (!candidate)
|
|
132
|
+
throw new Error(`Component is not installed: ${id}`);
|
|
133
|
+
available.add(id);
|
|
134
|
+
if (candidate.features?.kind === "bridge")
|
|
135
|
+
return;
|
|
136
|
+
for (const required of Object.keys(candidate.requires ?? {}))
|
|
137
|
+
include(required);
|
|
138
|
+
};
|
|
139
|
+
for (const id of options.selection ? Object.keys(options.selection) : byID.keys()) {
|
|
140
|
+
include(id);
|
|
141
|
+
const version = byID.get(id)?.version ?? options.provided?.[id];
|
|
142
|
+
if (options.selection && options.selection[id] !== version)
|
|
143
|
+
throw new Error(`Component ${id} requires exact version ${options.selection[id]}; installed ${version}`);
|
|
144
|
+
}
|
|
145
|
+
const selected = candidates.filter((candidate) => available.has(candidate.id));
|
|
146
|
+
const plan = {
|
|
147
|
+
definitions: [...definitions.values()],
|
|
148
|
+
optIn,
|
|
149
|
+
selection: selected.map(({ id, version }) => ({ id, version })),
|
|
150
|
+
requirements: Object.fromEntries(selected.map((candidate) => [
|
|
151
|
+
candidate.id,
|
|
152
|
+
Object.keys(candidate.requires ?? {}).filter((id) => !options.provided?.[id]),
|
|
153
|
+
])),
|
|
154
|
+
};
|
|
155
|
+
const active = activate(plan, optIn);
|
|
156
|
+
for (const candidate of selected) {
|
|
157
|
+
if (active.has(candidate.id) ||
|
|
158
|
+
candidate.features?.kind === "bridge" ||
|
|
159
|
+
gates.get(candidate.id)?.maturity === "experimental")
|
|
160
|
+
continue;
|
|
161
|
+
throw new Error(`Component ${candidate.id} requires an unavailable dependency; split an optional integration`);
|
|
162
|
+
}
|
|
163
|
+
return { ...plan, components: [...active] };
|
|
164
|
+
}
|
|
165
|
+
FeatureSelection.resolve = resolve;
|
|
166
|
+
function status(plan, requested, effective) {
|
|
167
|
+
const definitions = new Map(plan.definitions.map((definition) => [definition.id, definition]));
|
|
168
|
+
const owners = new Set(plan.selection.map((component) => component.id));
|
|
169
|
+
const next = activate(plan, requested);
|
|
170
|
+
return [...new Set([...definitions.keys(), ...requested])].map((id) => {
|
|
171
|
+
const definition = definitions.get(id);
|
|
172
|
+
const selected = definition?.maturity === "stable" || requested.includes(id);
|
|
173
|
+
const ownerDisabled = !!definition && !definition.component && !next.has(definition.owner);
|
|
174
|
+
const dependencyUnavailable = !!definition && (plan.requirements[definition.owner] ?? []).some((id) => !next.has(id));
|
|
175
|
+
const available = !!definition && owners.has(definition.owner) && !dependencyUnavailable && !ownerDisabled;
|
|
176
|
+
const enabled = !!definition && effective.includes(id);
|
|
177
|
+
const nextEnabled = !!definition && next.has(definition.owner) && selected;
|
|
178
|
+
const pendingRestart = !!definition &&
|
|
179
|
+
enabled !== nextEnabled &&
|
|
180
|
+
(definition.activation === "restart" ||
|
|
181
|
+
plan.components.includes(definition.owner) !== next.has(definition.owner));
|
|
182
|
+
const reason = !definition
|
|
183
|
+
? "not-installed"
|
|
184
|
+
: !owners.has(definition.owner)
|
|
185
|
+
? "component-unselected"
|
|
186
|
+
: dependencyUnavailable || ownerDisabled
|
|
187
|
+
? "dependency-unavailable"
|
|
188
|
+
: pendingRestart
|
|
189
|
+
? "restart-required"
|
|
190
|
+
: undefined;
|
|
191
|
+
return { id, definition, selected, available, enabled, pendingRestart, ...(reason ? { reason } : {}) };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
FeatureSelection.status = status;
|
|
195
|
+
})(FeatureSelection || (FeatureSelection = {}));
|