@frontendxlab/opencode-computer-use 0.1.2 → 0.1.3

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 CHANGED
@@ -111,8 +111,9 @@ npx opencode-computer-use-setup --global --v1
111
111
  ```
112
112
 
113
113
  Use `--dry-run` to inspect the action without changing files. Set
114
- `OPENCODE_COMPUTER_USE_SKIP_SETUP=1` to disable the postinstall hook. Restart
115
- OpenCode after a successful setup so the plugin is loaded.
114
+ `OPENCODE_COMPUTER_USE_SKIP_SETUP=1` to disable the postinstall hook. Setup also
115
+ skips automatically when `CI=1` or `CI=true`. Restart OpenCode after a
116
+ successful setup so the plugin is loaded.
116
117
 
117
118
  Recent pnpm releases may require explicit approval before running lifecycle
118
119
  scripts from dependencies. If pnpm reports `ERR_PNPM_IGNORED_BUILDS`, approve
package/SECURITY.md CHANGED
@@ -26,7 +26,8 @@ sandbox.
26
26
  - The postinstall setup only adds this package to OpenCode's plugin list, keeps
27
27
  JSONC comments, creates a `.bak` before replacement, and never edits a
28
28
  conflicting or malformed configuration. Ambiguous setup is non-destructive
29
- and prints manual instructions.
29
+ and prints manual instructions. CI and explicit skip environments bypass
30
+ postinstall mutation.
30
31
 
31
32
  ## Operator responsibilities
32
33
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { randomUUID } from "node:crypto";
4
5
  import { homedir } from "node:os";
5
6
  import { delimiter, dirname, join, resolve } from "node:path";
6
7
  import { spawnSync } from "node:child_process";
@@ -189,6 +190,7 @@ function configFlavor(text) {
189
190
  }
190
191
 
191
192
  function existingConfigFiles(root, scope, env = process.env) {
193
+ if (env.OPENCODE_CONFIG) return [resolve(env.OPENCODE_CONFIG)];
192
194
  const bases = scope === "global"
193
195
  ? [globalConfigDir(env)]
194
196
  : [root, join(root, ".opencode")];
@@ -290,15 +292,29 @@ export function updateConfigText(text, flavor) {
290
292
  function writeConfig(file, content, dryRun) {
291
293
  if (dryRun) return;
292
294
  mkdirSync(dirname(file), { recursive: true });
295
+ const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
293
296
  if (existsSync(file)) {
294
- copyFileSync(file, `${file}.bak`);
297
+ const backup = `${file}.bak`;
298
+ copyFileSync(file, backup);
299
+ chmodSync(backup, mode);
295
300
  }
296
- const temporary = `${file}.tmp-${process.pid}`;
297
- writeFileSync(temporary, content, "utf8");
301
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
302
+ writeFileSync(temporary, content, { encoding: "utf8", mode, flag: "wx" });
303
+ chmodSync(temporary, mode);
298
304
  if (process.platform === "win32" && existsSync(file)) rmSync(file, { force: true });
299
305
  renameSync(temporary, file);
300
306
  }
301
307
 
308
+ function validateConfigFiles(root, scope, env) {
309
+ for (const file of existingConfigFiles(root, scope, env)) {
310
+ const text = readFileSync(file, "utf8");
311
+ const data = parse(text);
312
+ if (text.trim() && (data === undefined || data === null || typeof data !== "object" || Array.isArray(data))) {
313
+ throw new Error(`configuration is not valid JSON or JSONC: ${file}`);
314
+ }
315
+ }
316
+ }
317
+
302
318
  function packageAlreadyConfigured(root, scope, env) {
303
319
  for (const file of existingConfigFiles(root, scope, env)) {
304
320
  try {
@@ -312,6 +328,12 @@ function packageAlreadyConfigured(root, scope, env) {
312
328
  return undefined;
313
329
  }
314
330
 
331
+ function redactMessage(value) {
332
+ return String(value)
333
+ .replace(/\b(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*(?:bearer|basic)?[^\n\r,;]*/giu, "$1=[redacted]")
334
+ .slice(0, 500);
335
+ }
336
+
315
337
  function runPluginCli(flavor, scope, root, env) {
316
338
  const command = flavor === "v1" ? "opencode" : "opencode2";
317
339
  const args = ["plugin", "add", PACKAGE_NAME];
@@ -325,7 +347,7 @@ function runPluginCli(flavor, scope, root, env) {
325
347
  timeout: 120_000,
326
348
  });
327
349
  if (result.error || result.status !== 0) {
328
- const detail = `${result.stdout || ""}${result.stderr || ""}`.trim().slice(0, 500);
350
+ const detail = redactMessage(`${result.stdout || ""}${result.stderr || ""}`.trim());
329
351
  throw new Error(`${command} plugin add failed${detail ? `: ${detail}` : ""}`);
330
352
  }
331
353
  }
@@ -388,12 +410,16 @@ export async function setup(options = {}, env = process.env, io = print) {
388
410
  const requestedFlavor = options.flavor || "auto";
389
411
  const scope = inferScope(requestedScope, env);
390
412
  const root = chooseRoot(scope, env);
413
+ if (env.OPENCODE_CONFIG_CONTENT) {
414
+ throw new Error("OPENCODE_CONFIG_CONTENT is immutable; add the plugin to that configuration manually");
415
+ }
391
416
  const detection = detectOpenCode(env);
392
417
  const config = detectConfigFlavor(root, scope, env);
393
418
  const flavor = chooseFlavor(requestedFlavor, detection, config);
394
419
  const dryRun = Boolean(options.dryRun);
395
420
  let result;
396
421
  if (scope === "global" && !dryRun) {
422
+ validateConfigFiles(root, scope, env);
397
423
  const existing = packageAlreadyConfigured(root, scope, env);
398
424
  if (existing) {
399
425
  result = { file: existing, changed: false };
@@ -406,9 +432,10 @@ export async function setup(options = {}, env = process.env, io = print) {
406
432
  } else {
407
433
  result = configForSetup(root, scope, flavor, env, dryRun);
408
434
  }
409
- io(
410
- `OpenCode Computer Use setup ${result.changed ? "configured" : "already configured"} for ${flavor.toUpperCase()} (${scope}).`,
411
- );
435
+ const action = options.dryRun
436
+ ? (result.changed ? "would configure" : "already configured")
437
+ : (result.changed ? "configured" : "already configured");
438
+ io(`OpenCode Computer Use setup ${action} for ${flavor.toUpperCase()} (${scope}).`);
412
439
  if (result.changed) io(`Configuration: ${result.file}`);
413
440
  return { ...result, scope, flavor, detection };
414
441
  }
@@ -423,14 +450,6 @@ function isDevelopmentInstall(env) {
423
450
  }
424
451
 
425
452
  export async function main(argv = process.argv.slice(2), env = process.env, io = print) {
426
- if (isDevelopmentInstall(env)) {
427
- io("OpenCode Computer Use setup skipped for a package-development install.");
428
- return 0;
429
- }
430
- if (env.OPENCODE_COMPUTER_USE_SKIP_SETUP === "1") {
431
- io("OpenCode Computer Use setup skipped by OPENCODE_COMPUTER_USE_SKIP_SETUP.");
432
- return 0;
433
- }
434
453
  let options;
435
454
  try {
436
455
  options = parseArguments(argv);
@@ -443,6 +462,18 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
443
462
  printHelp();
444
463
  return 0;
445
464
  }
465
+ if (env.CI === "true" || env.CI === "1") {
466
+ io("OpenCode Computer Use setup skipped in CI.");
467
+ return 0;
468
+ }
469
+ if (isDevelopmentInstall(env)) {
470
+ io("OpenCode Computer Use setup skipped for a package-development install.");
471
+ return 0;
472
+ }
473
+ if (env.OPENCODE_COMPUTER_USE_SKIP_SETUP === "1") {
474
+ io("OpenCode Computer Use setup skipped by OPENCODE_COMPUTER_USE_SKIP_SETUP.");
475
+ return 0;
476
+ }
446
477
  return setup(options, env, io)
447
478
  .then(() => 0)
448
479
  .catch((error) => {
package/docs/index.html CHANGED
@@ -459,7 +459,7 @@ npm install</code></pre><button class="copy" type="button" data-copy="#clone">Co
459
459
  <div class="code"><pre><code id="npm-install">npm install @frontendxlab/opencode-computer-use
460
460
  # pnpm install @frontendxlab/opencode-computer-use also works</code></pre><button class="copy" type="button" data-copy="#npm-install">Copy</button></div>
461
461
  <p>The package postinstall step detects OpenCode 1 versus OpenCode2 and local versus global installation. It edits JSONC without discarding comments, avoids duplicate entries, and prints both manual formats when the choice is ambiguous. Run it explicitly with <code>npx opencode-computer-use-setup</code>, or add <code>--local</code>, <code>--global</code>, <code>--v1</code>, or <code>--v2</code> when you want to choose explicitly.</p>
462
- <p>Recent pnpm releases can require explicit lifecycle-script approval. If pnpm reports <code>ERR_PNPM_IGNORED_BUILDS</code>, run <code>pnpm approve-builds @frontendxlab/opencode-computer-use</code> once, then rerun <code>pnpm install</code>. If scripts are disabled, run <code>pnpm exec opencode-computer-use-setup</code> manually.</p>
462
+ <p>Recent pnpm releases can require explicit lifecycle-script approval. If pnpm reports <code>ERR_PNPM_IGNORED_BUILDS</code>, run <code>pnpm approve-builds @frontendxlab/opencode-computer-use</code> once, then rerun <code>pnpm install</code>. If scripts are disabled, run <code>pnpm exec opencode-computer-use-setup</code> manually. CI environments skip setup automatically.</p>
463
463
  <div class="tabs" role="tablist" aria-label="OpenCode generation">
464
464
  <button role="tab" id="tab-v1" aria-controls="panel-v1" aria-selected="true">OpenCode 1</button>
465
465
  <button role="tab" id="tab-v2" aria-controls="panel-v2" aria-selected="false" tabindex="-1">OpenCode2</button>
@@ -90,7 +90,7 @@ manual instructions. Run it explicitly with npx opencode-computer-use-setup;
90
90
  use --local, --global, --v1, or --v2 to choose explicitly. If pnpm reports
91
91
  ERR_PNPM_IGNORED_BUILDS, approve the package once with pnpm approve-builds
92
92
  @frontendxlab/opencode-computer-use, or run pnpm exec
93
- opencode-computer-use-setup manually.
93
+ opencode-computer-use-setup manually. CI environments skip setup.
94
94
  OpenCode 1 (opencode.json or opencode.jsonc, plugin list):
95
95
  { "$schema": "https://opencode.ai/config.json",
96
96
  "plugin": [["@frontendxlab/opencode-computer-use", { "backend": "auto" }]] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontendxlab/opencode-computer-use",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A dual OpenCode and OpenCode2 plugin for local cross-platform desktop control.",
5
5
  "license": "MIT",
6
6
  "repository": {