@polderlabs/bizar 3.12.1 → 3.12.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/cli/bin.mjs +101 -0
- package/cli/dev-link.mjs +175 -0
- package/cli/dev-link.test.mjs +297 -0
- package/cli/doctor.mjs +285 -0
- package/cli/doctor.test.mjs +337 -0
- package/cli/install.mjs +138 -12
- package/cli/install.test.mjs +246 -0
- package/cli/update.mjs +123 -33
- package/package.json +1 -1
package/cli/bin.mjs
CHANGED
|
@@ -88,6 +88,9 @@ function showHelp() {
|
|
|
88
88
|
service Manage the background service daemon
|
|
89
89
|
bg <subcommand> Manage background agents (list/view/kill/logs)
|
|
90
90
|
dash <subcommand> Manage the dashboard (start/stop/status/tui)
|
|
91
|
+
dev-link [src] Symlink the local plugin source into opencode's plugin dir
|
|
92
|
+
dev-unlink Remove the dev symlink and restore the deployed copy
|
|
93
|
+
doctor Check the BizarHarness install for health issues
|
|
91
94
|
|
|
92
95
|
Examples:
|
|
93
96
|
bizar install
|
|
@@ -96,6 +99,8 @@ function showHelp() {
|
|
|
96
99
|
bizar dash start --bg
|
|
97
100
|
bizar dash stop
|
|
98
101
|
bizar dash status
|
|
102
|
+
bizar doctor
|
|
103
|
+
bizar update --all --dry-run
|
|
99
104
|
|
|
100
105
|
Run \`bizar <command> --help\` for per-command help.
|
|
101
106
|
|
|
@@ -185,6 +190,7 @@ function showUpdateHelp() {
|
|
|
185
190
|
bizar update opencode bizar dash Update specific components
|
|
186
191
|
bizar update --all --yes Update everything + auto-kill running instances
|
|
187
192
|
bizar update --no-restart Don't auto-restart the dashboard after update
|
|
193
|
+
bizar update --dry-run Print what would happen, change nothing
|
|
188
194
|
bizar update --help Show this help
|
|
189
195
|
|
|
190
196
|
Components:
|
|
@@ -206,11 +212,17 @@ function showUpdateHelp() {
|
|
|
206
212
|
• If the dashboard was running and bizar or dash was updated,
|
|
207
213
|
spawns a fresh detached dashboard process with the new code
|
|
208
214
|
(skipped with --no-restart).
|
|
215
|
+
• Runs \`bizar doctor\` after a successful update to catch config
|
|
216
|
+
regressions before opencode tries to start.
|
|
217
|
+
• If ~/.config/opencode/plugins/bizar is a dev symlink (created
|
|
218
|
+
by \`bizar dev-link\`), the copy step is skipped to preserve the
|
|
219
|
+
link. Use \`bizar dev-unlink\` first, or pass --force.
|
|
209
220
|
|
|
210
221
|
Examples:
|
|
211
222
|
bizar update --all --yes Headless full update + restart
|
|
212
223
|
bizar update dash Update only the dashboard
|
|
213
224
|
bizar update plugin --no-restart Plugin-only, leave dashboard alone
|
|
225
|
+
bizar update --all --dry-run See what would change without doing it
|
|
214
226
|
`);
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -220,6 +232,69 @@ function showTestGateHelp() {
|
|
|
220
232
|
`);
|
|
221
233
|
}
|
|
222
234
|
|
|
235
|
+
function showDevLinkHelp() {
|
|
236
|
+
console.log(`
|
|
237
|
+
bizar dev-link / dev-unlink — Manage a symlink from the opencode plugin dir
|
|
238
|
+
to a local source checkout, so edits propagate to opencode on next session.
|
|
239
|
+
|
|
240
|
+
Usage:
|
|
241
|
+
bizar dev-link [source-dir] Symlink source-dir (default: ./plugins/bizar)
|
|
242
|
+
to ~/.config/opencode/plugins/bizar
|
|
243
|
+
bizar dev-link --force Replace an existing deployed copy
|
|
244
|
+
bizar dev-unlink Remove the dev symlink + restore from npm
|
|
245
|
+
bizar dev-unlink --force Remove even if not a symlink (destructive)
|
|
246
|
+
|
|
247
|
+
Description:
|
|
248
|
+
By default, opencode loads the Bizar plugin from
|
|
249
|
+
~/.config/opencode/plugins/bizar, which is a real directory copied
|
|
250
|
+
from the npm package. Edits to plugins/bizar/ in the BizarHarness
|
|
251
|
+
repo don't propagate until you re-run the installer.
|
|
252
|
+
|
|
253
|
+
\`bizar dev-link\` replaces that directory with a symlink pointing
|
|
254
|
+
at your local checkout, so source edits are picked up immediately.
|
|
255
|
+
\`bizar dev-unlink\` reverses the change by removing the symlink
|
|
256
|
+
and re-installing the deployed copy from the npm package.
|
|
257
|
+
|
|
258
|
+
While the dev link is in place, \`bizar update\` will skip the
|
|
259
|
+
plugin-copy step (and print a warning) so it doesn't clobber the
|
|
260
|
+
link. Run \`bizar dev-unlink\` first, or pass --force to overwrite.
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
bizar dev-link
|
|
264
|
+
bizar dev-link /home/me/projects/bizar/plugins/bizar
|
|
265
|
+
bizar dev-link --force
|
|
266
|
+
bizar dev-unlink
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function showDoctorHelp() {
|
|
271
|
+
console.log(`
|
|
272
|
+
bizar doctor — Check the BizarHarness install for health issues
|
|
273
|
+
|
|
274
|
+
Usage:
|
|
275
|
+
bizar doctor
|
|
276
|
+
|
|
277
|
+
Description:
|
|
278
|
+
Runs a battery of health checks against the local install:
|
|
279
|
+
• opencode CLI reachable
|
|
280
|
+
• ~/.config/opencode/opencode.json parses as JSON
|
|
281
|
+
• the Bizar plugin is registered
|
|
282
|
+
• plugin path resolves
|
|
283
|
+
• @polderlabs/bizar-plugin is installed globally
|
|
284
|
+
• core agent files are installed (odin, quick, thor, tyr)
|
|
285
|
+
• rtk / semble / skills on PATH (lenient — at least one)
|
|
286
|
+
• dashboard reachable (skipped if no port file)
|
|
287
|
+
• provider.openrouter block + MiniMax model flags are sane
|
|
288
|
+
|
|
289
|
+
Prints ✓/✗ for each check and a final summary. Exits non-zero
|
|
290
|
+
if any check fails. Use \`bizar doctor\` after a manual config
|
|
291
|
+
edit or to diagnose "why is opencode misbehaving?" questions.
|
|
292
|
+
|
|
293
|
+
Related:
|
|
294
|
+
bizar update Update + auto-run doctor on success
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
|
|
223
298
|
function showServiceHelp() {
|
|
224
299
|
const bizarConfigDir = getBizarConfigDir();
|
|
225
300
|
console.log(`
|
|
@@ -424,6 +499,32 @@ async function main() {
|
|
|
424
499
|
} else if (args[0] === 'update') {
|
|
425
500
|
if (isHelpRequest) showUpdateHelp();
|
|
426
501
|
else await runUpdate(args.slice(1));
|
|
502
|
+
} else if (args[0] === 'dev-link') {
|
|
503
|
+
if (isHelpRequest) showDevLinkHelp();
|
|
504
|
+
else {
|
|
505
|
+
const { createDevLink } = await import('./dev-link.mjs');
|
|
506
|
+
const positional = args.slice(1).filter((a) => !a.startsWith('-'));
|
|
507
|
+
const flags = args.slice(1).filter((a) => a.startsWith('-'));
|
|
508
|
+
const sourceDir = positional[0] ?? null;
|
|
509
|
+
const force = flags.includes('--force') || flags.includes('-f');
|
|
510
|
+
const ok = createDevLink(sourceDir, { force });
|
|
511
|
+
if (!ok) process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
} else if (args[0] === 'dev-unlink') {
|
|
514
|
+
if (isHelpRequest) showDevLinkHelp();
|
|
515
|
+
else {
|
|
516
|
+
const { removeDevLink } = await import('./dev-link.mjs');
|
|
517
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
518
|
+
const ok = await removeDevLink({ force });
|
|
519
|
+
if (!ok) process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
} else if (args[0] === 'doctor') {
|
|
522
|
+
if (isHelpRequest) showDoctorHelp();
|
|
523
|
+
else {
|
|
524
|
+
const { runDoctor } = await import('./doctor.mjs');
|
|
525
|
+
const result = await runDoctor();
|
|
526
|
+
if (result.failed > 0) process.exit(1);
|
|
527
|
+
}
|
|
427
528
|
} else if (args[0] === 'plan') {
|
|
428
529
|
await runPlan(args.slice(1), {});
|
|
429
530
|
} else if (args[0] === 'install') {
|
package/cli/dev-link.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/dev-link.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.12.2 — `bizar dev-link` / `bizar dev-unlink` subcommands.
|
|
5
|
+
*
|
|
6
|
+
* The Bizar opencode plugin lives at:
|
|
7
|
+
* ~/.config/opencode/plugins/bizar (the deployed copy, written by installPluginFromGlobal)
|
|
8
|
+
*
|
|
9
|
+
* When developing the plugin source under <repo>/plugins/bizar/, edits
|
|
10
|
+
* don't auto-propagate to opencode because the deployed copy is a real
|
|
11
|
+
* directory. `bizar dev-link` creates a symlink so that source edits
|
|
12
|
+
* are picked up on the next opencode session. `bizar dev-unlink`
|
|
13
|
+
* restores the deployed copy from the npm package.
|
|
14
|
+
*
|
|
15
|
+
* Symlink guard: installPluginFromGlobal refuses to overwrite a
|
|
16
|
+
* symlink (the underlying `cp -r` would dereference it and silently
|
|
17
|
+
* break the dev workflow). Combined with that guard, `bizar update` is
|
|
18
|
+
* safe to run while a dev link is in place — it prints a warning and
|
|
19
|
+
* leaves the link alone (use `--force` or `bizar dev-unlink` first).
|
|
20
|
+
*/
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
import {
|
|
23
|
+
lstatSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
symlinkSync,
|
|
26
|
+
existsSync,
|
|
27
|
+
mkdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
} from 'node:fs';
|
|
30
|
+
import { join, resolve } from 'node:path';
|
|
31
|
+
import { execSync } from 'node:child_process';
|
|
32
|
+
import { opencodeConfigDir } from './utils.mjs';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Path of the deployed plugin. Reads XDG_CONFIG_HOME / HOME at call
|
|
36
|
+
* time so tests can mock HOME.
|
|
37
|
+
*/
|
|
38
|
+
function pluginDest() {
|
|
39
|
+
return join(opencodeConfigDir(), 'plugins', 'bizar');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a symlink from ~/.config/opencode/plugins/bizar to the
|
|
44
|
+
* local source dir (default: <cwd>/plugins/bizar).
|
|
45
|
+
*
|
|
46
|
+
* Behavior:
|
|
47
|
+
* - dest does not exist → create symlink
|
|
48
|
+
* - dest is a symlink → replace it
|
|
49
|
+
* - dest is a real directory → refuse unless opts.force
|
|
50
|
+
*
|
|
51
|
+
* Returns true on success, false on refusal.
|
|
52
|
+
*/
|
|
53
|
+
export function createDevLink(sourceDir = null, opts = {}) {
|
|
54
|
+
const resolvedSource = resolve(process.cwd(), sourceDir ?? 'plugins/bizar');
|
|
55
|
+
const dest = pluginDest();
|
|
56
|
+
const parentDir = join(dest, '..');
|
|
57
|
+
|
|
58
|
+
// Make sure ~/.config/opencode/plugins/ exists.
|
|
59
|
+
if (!existsSync(parentDir)) {
|
|
60
|
+
mkdirSync(parentDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Probe existing dest state. Use lstat so we see the symlink itself,
|
|
64
|
+
// not its target.
|
|
65
|
+
let existing = null;
|
|
66
|
+
let isSymlink = false;
|
|
67
|
+
try {
|
|
68
|
+
existing = lstatSync(dest);
|
|
69
|
+
isSymlink = existing.isSymbolicLink();
|
|
70
|
+
} catch {
|
|
71
|
+
// doesn't exist — we will create.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (existing && !isSymlink) {
|
|
75
|
+
if (!opts.force) {
|
|
76
|
+
console.log(chalk.yellow(` ⚠ ${dest} exists and is not a symlink.`));
|
|
77
|
+
console.log(chalk.yellow(` Refusing to overwrite the deployed copy.`));
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.dim(
|
|
80
|
+
` Use --force to replace it, or remove it manually before running \`bizar dev-link\`.`,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
console.log(chalk.yellow(` ⚠ Replacing existing deployed copy at ${dest}`));
|
|
86
|
+
rmSync(dest, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isSymlink) {
|
|
90
|
+
// Existing symlink (possibly stale) — remove before re-creating.
|
|
91
|
+
rmSync(dest, { force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
symlinkSync(resolvedSource, dest);
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.green(` ✓ dev link created: ${dest} → ${resolvedSource}`),
|
|
97
|
+
);
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.dim(
|
|
100
|
+
` Edits to the source dir will now propagate to opencode on next session.`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove the dev symlink and restore the deployed plugin from npm.
|
|
108
|
+
* Refuses to remove a real directory (would destroy the deployed copy)
|
|
109
|
+
* unless opts.force is set.
|
|
110
|
+
*
|
|
111
|
+
* Returns true on success, false on refusal or error.
|
|
112
|
+
*/
|
|
113
|
+
export async function removeDevLink(opts = {}) {
|
|
114
|
+
const dest = pluginDest();
|
|
115
|
+
let isSymlink = false;
|
|
116
|
+
let exists = false;
|
|
117
|
+
try {
|
|
118
|
+
const st = lstatSync(dest);
|
|
119
|
+
exists = true;
|
|
120
|
+
isSymlink = st.isSymbolicLink();
|
|
121
|
+
} catch {
|
|
122
|
+
// doesn't exist
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!exists) {
|
|
126
|
+
console.log(chalk.red(` ✗ ${dest} does not exist — nothing to unlink.`));
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isSymlink) {
|
|
131
|
+
if (!opts.force) {
|
|
132
|
+
console.log(
|
|
133
|
+
chalk.red(
|
|
134
|
+
` ✗ ${dest} is not a symlink. Refusing to remove a real directory.`,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
console.log(chalk.dim(` Use --force if you really mean it.`));
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
console.log(chalk.yellow(` ⚠ Force-removing real directory at ${dest}`));
|
|
141
|
+
rmSync(dest, { recursive: true, force: true });
|
|
142
|
+
} else {
|
|
143
|
+
rmSync(dest, { force: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Restore from npm. installPluginFromGlobal is silent because we
|
|
147
|
+
// already announced the unlink above; it will print its own brief
|
|
148
|
+
// success message on the next line if a real copy was installed.
|
|
149
|
+
try {
|
|
150
|
+
const { installPluginFromGlobal } = await import('./install.mjs');
|
|
151
|
+
await installPluginFromGlobal({ silent: true });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(chalk.yellow(` ⚠ restore from npm failed: ${err.message}`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Read the installed version for the success message.
|
|
157
|
+
let version = 'unknown';
|
|
158
|
+
try {
|
|
159
|
+
const root = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
160
|
+
const pkgPath = join(root, '@polderlabs', 'bizar-plugin', 'package.json');
|
|
161
|
+
if (existsSync(pkgPath)) {
|
|
162
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
163
|
+
version = pkg.version ?? 'unknown';
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// informational only
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(
|
|
170
|
+
chalk.green(
|
|
171
|
+
` ✓ dev link removed; deployed plugin restored from @polderlabs/bizar-plugin@${version}`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/dev-link.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for the `bizar dev-link` / `bizar dev-unlink` subcommands.
|
|
5
|
+
* Uses Node's built-in node:test (no external test framework).
|
|
6
|
+
*
|
|
7
|
+
* Strategy: mock HOME (and XDG_CONFIG_HOME) so opencodeConfigDir() in
|
|
8
|
+
* cli/utils.mjs returns a path inside a tmpdir, and exercise the
|
|
9
|
+
* create/remove symlink behavior against a controlled filesystem.
|
|
10
|
+
*
|
|
11
|
+
* Note: We mock HOME *before* importing dev-link.mjs because the module
|
|
12
|
+
* imports opencodeConfigDir transitively from utils.mjs. The path is
|
|
13
|
+
* resolved at call time, though, so the mock just needs to be in place
|
|
14
|
+
* when the functions run.
|
|
15
|
+
*/
|
|
16
|
+
import { test, describe, before, after, beforeEach, afterEach } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import {
|
|
19
|
+
mkdtempSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
symlinkSync,
|
|
24
|
+
lstatSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
readlinkSync,
|
|
27
|
+
} from 'node:fs';
|
|
28
|
+
import { tmpdir } from 'node:os';
|
|
29
|
+
import { join, dirname } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const PROJECT_ROOT = join(__dirname, '..');
|
|
34
|
+
|
|
35
|
+
const { createDevLink, removeDevLink } = await import('./dev-link.mjs');
|
|
36
|
+
|
|
37
|
+
// ── HOME mocking ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const ORIG_HOME = process.env.HOME;
|
|
40
|
+
const ORIG_XDG = process.env.XDG_CONFIG_HOME;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Point HOME at a fresh tmpdir so the module's opencodeConfigDir()
|
|
44
|
+
* resolves inside it (via the fallback `<HOME>/.config/opencode`).
|
|
45
|
+
* Returns the tmpdir path.
|
|
46
|
+
*
|
|
47
|
+
* Note: we deliberately do NOT set XDG_CONFIG_HOME here. The
|
|
48
|
+
* opencodeConfigDir() helper treats a set XDG_CONFIG_HOME as the
|
|
49
|
+
* direct parent (so `XDG_CONFIG_HOME=~/.config` → `~/.config/opencode`,
|
|
50
|
+
* matching the standard layout). Setting it to a raw tmpdir would
|
|
51
|
+
* produce `<tmpdir>/opencode` instead of the expected
|
|
52
|
+
* `<tmpdir>/.config/opencode` and break path alignment with the test.
|
|
53
|
+
*/
|
|
54
|
+
function freshHome() {
|
|
55
|
+
const home = mkdtempSync(join(tmpdir(), 'bizar-devlink-'));
|
|
56
|
+
process.env.HOME = home;
|
|
57
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
58
|
+
return home;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Path to the deployed plugin dir under the mocked HOME. */
|
|
62
|
+
function pluginDest(home) {
|
|
63
|
+
return join(home, '.config', 'opencode', 'plugins', 'bizar');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
after(() => {
|
|
67
|
+
// Restore env at the very end (individual tests have already done this).
|
|
68
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
69
|
+
else process.env.HOME = ORIG_HOME;
|
|
70
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
71
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── createDevLink ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('createDevLink()', () => {
|
|
77
|
+
let home;
|
|
78
|
+
let sourceDir;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
home = freshHome();
|
|
82
|
+
// The "source" can be any directory — it doesn't need real plugin
|
|
83
|
+
// contents for the symlink test. Use the actual repo plugins/bizar
|
|
84
|
+
// so the test doubles as a smoke test of the real path.
|
|
85
|
+
sourceDir = join(PROJECT_ROOT, 'plugins', 'bizar');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
|
|
90
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
91
|
+
else process.env.HOME = ORIG_HOME;
|
|
92
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
93
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('creates a symlink when dest does not exist', () => {
|
|
97
|
+
const dest = pluginDest(home);
|
|
98
|
+
assert.equal(existsSync(dest), false, 'precondition: dest should not exist');
|
|
99
|
+
|
|
100
|
+
const ok = createDevLink(sourceDir);
|
|
101
|
+
assert.equal(ok, true);
|
|
102
|
+
assert.equal(existsSync(dest), true);
|
|
103
|
+
const st = lstatSync(dest);
|
|
104
|
+
assert.equal(st.isSymbolicLink(), true, 'dest should be a symlink');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('refuses to overwrite a real directory without --force', () => {
|
|
108
|
+
const dest = pluginDest(home);
|
|
109
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
110
|
+
mkdirSync(dest, { recursive: true });
|
|
111
|
+
writeFileSync(join(dest, 'index.ts'), 'not a symlink');
|
|
112
|
+
|
|
113
|
+
const ok = createDevLink(sourceDir);
|
|
114
|
+
assert.equal(ok, false, 'should refuse without force');
|
|
115
|
+
assert.equal(existsSync(dest), true);
|
|
116
|
+
const st = lstatSync(dest);
|
|
117
|
+
assert.equal(st.isSymbolicLink(), false, 'dest should still be a real dir');
|
|
118
|
+
assert.equal(existsSync(join(dest, 'index.ts')), true, 'contents preserved');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('overwrites a real directory with opts.force=true', () => {
|
|
122
|
+
const dest = pluginDest(home);
|
|
123
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
124
|
+
mkdirSync(dest, { recursive: true });
|
|
125
|
+
writeFileSync(join(dest, 'index.ts'), 'not a symlink');
|
|
126
|
+
|
|
127
|
+
const ok = createDevLink(sourceDir, { force: true });
|
|
128
|
+
assert.equal(ok, true);
|
|
129
|
+
const st = lstatSync(dest);
|
|
130
|
+
assert.equal(st.isSymbolicLink(), true, 'dest should now be a symlink');
|
|
131
|
+
// Confirm the symlink points at the requested source. We can't
|
|
132
|
+
// look up `dest/index.ts` to verify the old contents are gone —
|
|
133
|
+
// that path now resolves through the symlink to the source's own
|
|
134
|
+
// index.ts, which may exist. Instead, verify the link target.
|
|
135
|
+
const linkTarget = readlinkSync(dest);
|
|
136
|
+
assert.equal(linkTarget, sourceDir);
|
|
137
|
+
// And confirm the previously-existing real directory is no longer
|
|
138
|
+
// present as a real directory at `dest`.
|
|
139
|
+
assert.equal(st.isDirectory(), false, 'dest should no longer be a directory');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('replaces an existing symlink', () => {
|
|
143
|
+
const dest = pluginDest(home);
|
|
144
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
145
|
+
|
|
146
|
+
const otherTarget = join(home, 'other-source');
|
|
147
|
+
mkdirSync(otherTarget, { recursive: true });
|
|
148
|
+
symlinkSync(otherTarget, dest);
|
|
149
|
+
|
|
150
|
+
const ok = createDevLink(sourceDir);
|
|
151
|
+
assert.equal(ok, true);
|
|
152
|
+
const st = lstatSync(dest);
|
|
153
|
+
assert.equal(st.isSymbolicLink(), true);
|
|
154
|
+
// Confirm the symlink now points at the requested source.
|
|
155
|
+
const linkTarget = readlinkSync(dest);
|
|
156
|
+
assert.equal(linkTarget, sourceDir);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('resolves a relative source dir against cwd', () => {
|
|
160
|
+
const dest = pluginDest(home);
|
|
161
|
+
const ok = createDevLink('plugins/bizar');
|
|
162
|
+
assert.equal(ok, true);
|
|
163
|
+
const linkTarget = readlinkSync(dest);
|
|
164
|
+
// The resolved path should be an absolute path under PROJECT_ROOT.
|
|
165
|
+
assert.ok(linkTarget.startsWith('/'), `expected absolute path, got: ${linkTarget}`);
|
|
166
|
+
assert.ok(linkTarget.endsWith('/plugins/bizar'));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('creates the opencode config parent if missing', () => {
|
|
170
|
+
const dest = pluginDest(home);
|
|
171
|
+
assert.equal(existsSync(dirname(dest)), false, 'precondition');
|
|
172
|
+
const ok = createDevLink(sourceDir);
|
|
173
|
+
assert.equal(ok, true);
|
|
174
|
+
assert.equal(existsSync(dirname(dest)), true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── removeDevLink ───────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe('removeDevLink()', () => {
|
|
181
|
+
let home;
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
home = freshHome();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
|
|
189
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
190
|
+
else process.env.HOME = ORIG_HOME;
|
|
191
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
192
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('errors when dest does not exist', async () => {
|
|
196
|
+
const dest = pluginDest(home);
|
|
197
|
+
assert.equal(existsSync(dest), false);
|
|
198
|
+
|
|
199
|
+
const ok = await removeDevLink();
|
|
200
|
+
assert.equal(ok, false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('errors when dest is a real directory and no --force', async () => {
|
|
204
|
+
const dest = pluginDest(home);
|
|
205
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
206
|
+
mkdirSync(dest, { recursive: true });
|
|
207
|
+
writeFileSync(join(dest, 'index.ts'), 'precious data');
|
|
208
|
+
|
|
209
|
+
const ok = await removeDevLink();
|
|
210
|
+
assert.equal(ok, false);
|
|
211
|
+
assert.equal(existsSync(join(dest, 'index.ts')), true, 'data preserved');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('removes a symlink and restores from npm', async () => {
|
|
215
|
+
const dest = pluginDest(home);
|
|
216
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
217
|
+
|
|
218
|
+
const sourceDir = join(PROJECT_ROOT, 'plugins', 'bizar');
|
|
219
|
+
symlinkSync(sourceDir, dest);
|
|
220
|
+
assert.equal(lstatSync(dest).isSymbolicLink(), true);
|
|
221
|
+
|
|
222
|
+
const ok = await removeDevLink();
|
|
223
|
+
assert.equal(ok, true);
|
|
224
|
+
// The symlink should be gone. Whether the npm-restore step
|
|
225
|
+
// recreated a real directory depends on whether @polderlabs/bizar-plugin
|
|
226
|
+
// is installed globally in the test environment. Both outcomes are
|
|
227
|
+
// acceptable — what matters is that the symlink was removed.
|
|
228
|
+
if (existsSync(dest)) {
|
|
229
|
+
const st = lstatSync(dest);
|
|
230
|
+
assert.equal(
|
|
231
|
+
st.isSymbolicLink(),
|
|
232
|
+
false,
|
|
233
|
+
'symlink should have been replaced with a real directory (npm restore)',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('refuses to unlink a real dir without --force', async () => {
|
|
239
|
+
const dest = pluginDest(home);
|
|
240
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
241
|
+
mkdirSync(dest, { recursive: true });
|
|
242
|
+
|
|
243
|
+
const ok = await removeDevLink();
|
|
244
|
+
assert.equal(ok, false);
|
|
245
|
+
assert.equal(existsSync(dest), true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('force-removes a real dir with opts.force=true', async () => {
|
|
249
|
+
const dest = pluginDest(home);
|
|
250
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
251
|
+
mkdirSync(dest, { recursive: true });
|
|
252
|
+
writeFileSync(join(dest, 'marker'), 'x');
|
|
253
|
+
|
|
254
|
+
const ok = await removeDevLink({ force: true });
|
|
255
|
+
assert.equal(ok, true);
|
|
256
|
+
// The npm-restore may or may not recreate the directory. If it
|
|
257
|
+
// does, the marker should be gone.
|
|
258
|
+
if (existsSync(dest)) {
|
|
259
|
+
assert.equal(existsSync(join(dest, 'marker')), false);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── Symlink detection ───────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
describe('symlink detection', () => {
|
|
267
|
+
let home;
|
|
268
|
+
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
home = freshHome();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
afterEach(() => {
|
|
274
|
+
if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
|
|
275
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
276
|
+
else process.env.HOME = ORIG_HOME;
|
|
277
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
278
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('lstatSync reports isSymbolicLink()=true after createDevLink', () => {
|
|
282
|
+
const dest = pluginDest(home);
|
|
283
|
+
createDevLink(join(PROJECT_ROOT, 'plugins', 'bizar'));
|
|
284
|
+
const st = lstatSync(dest);
|
|
285
|
+
assert.equal(st.isSymbolicLink(), true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('lstatSync reports isSymbolicLink()=false for a real dir', () => {
|
|
289
|
+
const dest = pluginDest(home);
|
|
290
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
291
|
+
mkdirSync(dest, { recursive: true });
|
|
292
|
+
const st = lstatSync(dest);
|
|
293
|
+
assert.equal(st.isSymbolicLink(), false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
console.log(' dev-link.mjs tests loaded — run with: node --test cli/dev-link.test.mjs');
|