opencode-landstrip 0.18.34 → 0.18.36

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 (6) hide show
  1. package/README.md +23 -70
  2. package/index.ts +56 -467
  3. package/package.json +2 -2
  4. package/shared.ts +100 -37
  5. package/tui.ts +290 -154
  6. package/LICENSE +0 -201
package/shared.ts CHANGED
@@ -9,14 +9,19 @@ import {
9
9
 
10
10
  import {
11
11
  allowsAllDomains,
12
+ canonicalizeGlobPattern,
13
+ canonicalizePath,
12
14
  controlResponseLine,
13
15
  decodeLandstripTrap,
14
16
  domainMatchesAny,
15
17
  domainMatchesPattern,
16
18
  expandHomePath,
19
+ expandPath,
20
+ globToRegExp,
17
21
  formatLandstripTrap,
18
22
  formatLandstripTraps,
19
23
  isRecord,
24
+ normalizePathSeparators,
20
25
  parseLandstripTraps,
21
26
  pathUnderDirectory,
22
27
  sessionAllows,
@@ -25,14 +30,19 @@ import {
25
30
 
26
31
  export {
27
32
  allowsAllDomains,
33
+ canonicalizeGlobPattern,
34
+ canonicalizePath,
28
35
  controlResponseLine,
29
36
  decodeLandstripTrap,
30
37
  domainMatchesAny,
31
38
  domainMatchesPattern,
32
39
  expandHomePath,
40
+ expandPath,
41
+ globToRegExp,
33
42
  formatLandstripTrap,
34
43
  formatLandstripTraps,
35
44
  isRecord,
45
+ normalizePathSeparators,
36
46
  parseLandstripTraps,
37
47
  pathUnderDirectory,
38
48
  sessionAllows,
@@ -93,57 +103,97 @@ export function list(values: string[]): string {
93
103
  return values.join(', ') || '(none)';
94
104
  }
95
105
 
96
- function stringArray(value: unknown): string[] | undefined {
97
- if (!Array.isArray(value)) return undefined;
98
- return value.every((item) => typeof item === 'string') ? [...value] : undefined;
106
+ function rejectUnknownFields(
107
+ value: Record<string, unknown>,
108
+ fields: readonly string[],
109
+ prefix = '',
110
+ ): void {
111
+ for (const field of Object.keys(value)) {
112
+ if (!fields.includes(field)) throw new Error(`unknown sandbox field ${prefix}${field}`);
113
+ }
114
+ }
115
+
116
+ function booleanValue(value: unknown, field: string): boolean | undefined {
117
+ if (value === undefined) return undefined;
118
+ if (typeof value !== 'boolean') throw new Error(`${field} must be a boolean`);
119
+ return value;
120
+ }
121
+
122
+ function stringArray(value: unknown, field: string): string[] | undefined {
123
+ if (value === undefined) return undefined;
124
+ if (!Array.isArray(value) || [...value].some((item) => typeof item !== 'string')) {
125
+ throw new Error(`${field} must be an array of strings`);
126
+ }
127
+ return [...value];
99
128
  }
100
129
 
101
130
  function normalizeNetworkConfig(value: unknown): Partial<SandboxNetworkConfig> | undefined {
102
- if (!isRecord(value)) return undefined;
131
+ if (value === undefined) return undefined;
132
+ if (!isRecord(value)) throw new Error('network must be an object');
133
+ rejectUnknownFields(
134
+ value,
135
+ [
136
+ 'allowNetwork',
137
+ 'allowLocalBinding',
138
+ 'allowAllUnixSockets',
139
+ 'allowUnixSockets',
140
+ 'allowedDomains',
141
+ 'deniedDomains',
142
+ ],
143
+ 'network.',
144
+ );
103
145
 
104
146
  const config: Partial<SandboxNetworkConfig> = {};
105
- if (typeof value.allowNetwork === 'boolean') config.allowNetwork = value.allowNetwork;
106
- if (typeof value.allowLocalBinding === 'boolean')
107
- config.allowLocalBinding = value.allowLocalBinding;
108
- if (typeof value.allowAllUnixSockets === 'boolean')
109
- config.allowAllUnixSockets = value.allowAllUnixSockets;
147
+ const allowNetwork = booleanValue(value.allowNetwork, 'network.allowNetwork');
148
+ if (allowNetwork !== undefined) config.allowNetwork = allowNetwork;
149
+ const allowLocalBinding = booleanValue(value.allowLocalBinding, 'network.allowLocalBinding');
150
+ if (allowLocalBinding !== undefined) config.allowLocalBinding = allowLocalBinding;
151
+ const allowAllUnixSockets = booleanValue(
152
+ value.allowAllUnixSockets,
153
+ 'network.allowAllUnixSockets',
154
+ );
155
+ if (allowAllUnixSockets !== undefined) config.allowAllUnixSockets = allowAllUnixSockets;
110
156
 
111
- const allowUnixSockets = stringArray(value.allowUnixSockets);
157
+ const allowUnixSockets = stringArray(value.allowUnixSockets, 'network.allowUnixSockets');
112
158
  if (allowUnixSockets) config.allowUnixSockets = allowUnixSockets;
113
159
 
114
- const allowedDomains = stringArray(value.allowedDomains);
160
+ const allowedDomains = stringArray(value.allowedDomains, 'network.allowedDomains');
115
161
  if (allowedDomains) config.allowedDomains = allowedDomains;
116
162
 
117
- const deniedDomains = stringArray(value.deniedDomains);
163
+ const deniedDomains = stringArray(value.deniedDomains, 'network.deniedDomains');
118
164
  if (deniedDomains) config.deniedDomains = deniedDomains;
119
165
 
120
166
  return config;
121
167
  }
122
168
 
123
169
  function normalizeFilesystemConfig(value: unknown): Partial<SandboxFilesystemConfig> | undefined {
124
- if (!isRecord(value)) return undefined;
170
+ if (value === undefined) return undefined;
171
+ if (!isRecord(value)) throw new Error('filesystem must be an object');
172
+ rejectUnknownFields(value, ['denyRead', 'allowRead', 'allowWrite', 'denyWrite'], 'filesystem.');
125
173
 
126
174
  const config: Partial<SandboxFilesystemConfig> = {};
127
- const denyRead = stringArray(value.denyRead);
175
+ const denyRead = stringArray(value.denyRead, 'filesystem.denyRead');
128
176
  if (denyRead) config.denyRead = denyRead;
129
177
 
130
- const allowRead = stringArray(value.allowRead);
178
+ const allowRead = stringArray(value.allowRead, 'filesystem.allowRead');
131
179
  if (allowRead) config.allowRead = allowRead;
132
180
 
133
- const allowWrite = stringArray(value.allowWrite);
181
+ const allowWrite = stringArray(value.allowWrite, 'filesystem.allowWrite');
134
182
  if (allowWrite) config.allowWrite = allowWrite;
135
183
 
136
- const denyWrite = stringArray(value.denyWrite);
184
+ const denyWrite = stringArray(value.denyWrite, 'filesystem.denyWrite');
137
185
  if (denyWrite) config.denyWrite = denyWrite;
138
186
 
139
187
  return config;
140
188
  }
141
189
 
142
190
  export function normalizeConfig(value: unknown): SandboxConfigOverrides {
143
- if (!isRecord(value)) return {};
191
+ if (!isRecord(value)) throw new Error('sandbox config must be an object');
192
+ rejectUnknownFields(value, ['enabled', 'network', 'filesystem']);
144
193
 
145
194
  const config: SandboxConfigOverrides = {};
146
- if (typeof value.enabled === 'boolean') config.enabled = value.enabled;
195
+ const enabled = booleanValue(value.enabled, 'enabled');
196
+ if (enabled !== undefined) config.enabled = enabled;
147
197
 
148
198
  const network = normalizeNetworkConfig(value.network);
149
199
  if (network) config.network = network;
@@ -156,7 +206,9 @@ export function normalizeConfig(value: unknown): SandboxConfigOverrides {
156
206
 
157
207
  export function normalizeOptions(options: unknown): SandboxConfigOverrides {
158
208
  if (!isRecord(options)) return {};
159
- return normalizeConfig(isRecord(options.config) ? options.config : options);
209
+ if (options.config === undefined) return normalizeConfig(options);
210
+ if (!isRecord(options.config)) throw new Error('config must be an object');
211
+ return normalizeConfig(options.config);
160
212
  }
161
213
 
162
214
  function mergeArray(base: string[], override?: string[]): string[] {
@@ -194,15 +246,16 @@ export function getConfigPaths(baseDirectory: string): { globalPath: string; pro
194
246
  };
195
247
  }
196
248
 
197
- // Returns `{}` when the file is absent and `null` when it exists but cannot be
198
- // parsed, so callers can refuse to overwrite a corrupted config.
199
- export function readConfigFile(configPath: string): SandboxConfigOverrides | null {
249
+ export function readConfigFile(configPath: string): SandboxConfigOverrides {
200
250
  if (!existsSync(configPath)) return {};
201
251
 
202
252
  try {
203
253
  return normalizeConfig(JSON.parse(readFileSync(configPath, 'utf-8')));
204
- } catch {
205
- return null;
254
+ } catch (error) {
255
+ throw new Error(
256
+ `Could not load ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
257
+ { cause: error },
258
+ );
206
259
  }
207
260
  }
208
261
 
@@ -219,17 +272,14 @@ export function loadConfig(
219
272
  }
220
273
 
221
274
  const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
222
- const globalOverrides = readConfigFile(globalPath) ?? {};
275
+ const globalOverrides = readConfigFile(globalPath);
223
276
  const baseConfig = deepMerge(templateConfig, globalOverrides);
224
277
 
225
- return deepMerge(deepMerge(baseConfig, readConfigFile(projectPath) ?? {}), optionOverrides);
278
+ return deepMerge(deepMerge(baseConfig, readConfigFile(projectPath)), optionOverrides);
226
279
  }
227
280
 
228
281
  export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
229
282
  const current = readConfigFile(configPath);
230
- if (current === null) {
231
- throw new Error(`Config file ${configPath} is corrupted; refusing to overwrite`);
232
- }
233
283
 
234
284
  const templateConfig: SandboxConfig = JSON.parse(
235
285
  readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
@@ -482,19 +532,32 @@ export function discoveryFilePath(baseDirectory: string): string {
482
532
  return join(discoveryDir(), `port-${directoryHash(baseDirectory)}.json`);
483
533
  }
484
534
 
485
- // /sandbox toggles the persisted `enabled` flag. Write it where the setting
535
+ // /landstrip toggles the persisted `enabled` flag. Write it where the setting
486
536
  // already lives — the project config if it sets `enabled`, otherwise the global
487
537
  // config — and return the scope written so the UI can report it.
538
+ export function sandboxConfigTarget(baseDirectory: string): {
539
+ scope: 'project' | 'global';
540
+ path: string;
541
+ } {
542
+ const { globalPath, projectPath } = getConfigPaths(baseDirectory);
543
+ const useProject = readConfigFile(projectPath).enabled !== undefined;
544
+ return useProject
545
+ ? { scope: 'project', path: projectPath }
546
+ : { scope: 'global', path: globalPath };
547
+ }
548
+
488
549
  export function setSandboxConfigEnabled(
489
550
  baseDirectory: string,
490
551
  enabled: boolean,
552
+ optionOverrides: SandboxConfigOverrides = {},
491
553
  ): 'project' | 'global' {
492
- const { globalPath, projectPath } = getConfigPaths(baseDirectory);
493
- const projectConfig = readConfigFile(projectPath);
494
- const useProject = projectConfig !== null && projectConfig.enabled !== undefined;
495
- const target = useProject ? projectPath : globalPath;
496
- writeConfigFile(target, { enabled });
497
- return useProject ? 'project' : 'global';
554
+ if (optionOverrides.enabled !== undefined) {
555
+ throw new Error('Sandbox state is managed by plugin options');
556
+ }
557
+
558
+ const target = sandboxConfigTarget(baseDirectory);
559
+ writeConfigFile(target.path, { enabled });
560
+ return target.scope;
498
561
  }
499
562
 
500
563
  export function writeDiscoveryPort(baseDirectory: string, port: number): void {