@jackwener/opencli 1.5.0 → 1.5.1
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/dist/browser/cdp.js +5 -0
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/cli-manifest.json +465 -5
- package/dist/cli.js +34 -3
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/xiaohongshu/publish.js +78 -42
- package/dist/clis/xiaohongshu/publish.test.js +20 -8
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/discovery.js +40 -28
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +2 -2
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/execution.js +45 -7
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/main.js +1 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +55 -17
- package/dist/plugin.js +706 -154
- package/dist/plugin.test.js +836 -38
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1 -1
- package/dist/types.d.ts +2 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/guide/plugins.md +10 -0
- package/package.json +1 -1
- package/src/browser/cdp.ts +6 -0
- package/src/browser/page.ts +24 -1
- package/src/cli.ts +34 -3
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/xiaohongshu/publish.test.ts +22 -8
- package/src/clis/xiaohongshu/publish.ts +93 -52
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/discovery.ts +41 -33
- package/src/doctor.ts +2 -3
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +6 -2
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +39 -6
- package/src/external.ts +6 -1
- package/src/main.ts +1 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +881 -38
- package/src/plugin.ts +871 -158
- package/src/runtime.ts +2 -2
- package/src/types.ts +2 -0
- package/tests/e2e/browser-public.test.ts +1 -1
package/dist/plugin.test.js
CHANGED
|
@@ -5,17 +5,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
8
9
|
import { PLUGINS_DIR } from './discovery.js';
|
|
9
10
|
import * as pluginModule from './plugin.js';
|
|
10
11
|
const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
|
|
11
12
|
mockExecFileSync: vi.fn(),
|
|
12
13
|
mockExecSync: vi.fn(),
|
|
13
14
|
}));
|
|
14
|
-
const {
|
|
15
|
+
const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, _promoteDir, _replaceDir, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
|
|
15
16
|
describe('parseSource', () => {
|
|
16
17
|
it('parses github:user/repo format', () => {
|
|
17
18
|
const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
|
|
18
19
|
expect(result).toEqual({
|
|
20
|
+
type: 'git',
|
|
19
21
|
cloneUrl: 'https://github.com/ByteYue/opencli-plugin-github-trending.git',
|
|
20
22
|
name: 'github-trending',
|
|
21
23
|
});
|
|
@@ -23,6 +25,7 @@ describe('parseSource', () => {
|
|
|
23
25
|
it('parses https URL format', () => {
|
|
24
26
|
const result = _parseSource('https://github.com/ByteYue/opencli-plugin-hot-digest');
|
|
25
27
|
expect(result).toEqual({
|
|
28
|
+
type: 'git',
|
|
26
29
|
cloneUrl: 'https://github.com/ByteYue/opencli-plugin-hot-digest.git',
|
|
27
30
|
name: 'hot-digest',
|
|
28
31
|
});
|
|
@@ -39,6 +42,88 @@ describe('parseSource', () => {
|
|
|
39
42
|
expect(_parseSource('invalid')).toBeNull();
|
|
40
43
|
expect(_parseSource('npm:some-package')).toBeNull();
|
|
41
44
|
});
|
|
45
|
+
it('parses file:// local plugin directories', () => {
|
|
46
|
+
const localDir = path.join(os.tmpdir(), 'opencli-plugin-test');
|
|
47
|
+
const fileUrl = pathToFileURL(localDir).href;
|
|
48
|
+
const result = _parseSource(fileUrl);
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
type: 'local',
|
|
51
|
+
localPath: localDir,
|
|
52
|
+
name: 'test',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('parses plain absolute local plugin directories', () => {
|
|
56
|
+
const localDir = path.join(os.tmpdir(), 'my-plugin');
|
|
57
|
+
const result = _parseSource(localDir);
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
type: 'local',
|
|
60
|
+
localPath: localDir,
|
|
61
|
+
name: 'my-plugin',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('strips opencli-plugin- prefix for local paths', () => {
|
|
65
|
+
const localDir = path.join(os.tmpdir(), 'opencli-plugin-foo');
|
|
66
|
+
const result = _parseSource(localDir);
|
|
67
|
+
expect(result.name).toBe('foo');
|
|
68
|
+
});
|
|
69
|
+
// ── Generic git URL support ──
|
|
70
|
+
it('parses ssh:// URLs', () => {
|
|
71
|
+
const result = _parseSource('ssh://git@gitlab.com/team/opencli-plugin-tools.git');
|
|
72
|
+
expect(result).toEqual({
|
|
73
|
+
type: 'git',
|
|
74
|
+
cloneUrl: 'ssh://git@gitlab.com/team/opencli-plugin-tools.git',
|
|
75
|
+
name: 'tools',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('parses ssh:// URLs without .git suffix', () => {
|
|
79
|
+
const result = _parseSource('ssh://git@gitlab.com/team/my-plugin');
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
type: 'git',
|
|
82
|
+
cloneUrl: 'ssh://git@gitlab.com/team/my-plugin',
|
|
83
|
+
name: 'my-plugin',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it('parses git@ SCP-style URLs', () => {
|
|
87
|
+
const result = _parseSource('git@gitlab.com:team/my-plugin.git');
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
type: 'git',
|
|
90
|
+
cloneUrl: 'git@gitlab.com:team/my-plugin.git',
|
|
91
|
+
name: 'my-plugin',
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('parses git@ SCP-style URLs and strips opencli-plugin- prefix', () => {
|
|
95
|
+
const result = _parseSource('git@github.com:user/opencli-plugin-awesome.git');
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
type: 'git',
|
|
98
|
+
cloneUrl: 'git@github.com:user/opencli-plugin-awesome.git',
|
|
99
|
+
name: 'awesome',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('parses generic HTTPS git URLs (non-GitHub)', () => {
|
|
103
|
+
const result = _parseSource('https://codehub.example.com/Team/App/opencli-plugins-app.git');
|
|
104
|
+
expect(result).toEqual({
|
|
105
|
+
type: 'git',
|
|
106
|
+
cloneUrl: 'https://codehub.example.com/Team/App/opencli-plugins-app.git',
|
|
107
|
+
name: 'opencli-plugins-app',
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
it('parses generic HTTPS git URLs without .git suffix', () => {
|
|
111
|
+
const result = _parseSource('https://gitlab.example.com/org/my-plugin');
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
type: 'git',
|
|
114
|
+
cloneUrl: 'https://gitlab.example.com/org/my-plugin.git',
|
|
115
|
+
name: 'my-plugin',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('still prefers GitHub shorthand over generic HTTPS for github.com', () => {
|
|
119
|
+
const result = _parseSource('https://github.com/user/repo');
|
|
120
|
+
// Should be handled by the GitHub-specific matcher (normalizes URL)
|
|
121
|
+
expect(result).toEqual({
|
|
122
|
+
type: 'git',
|
|
123
|
+
cloneUrl: 'https://github.com/user/repo.git',
|
|
124
|
+
name: 'repo',
|
|
125
|
+
});
|
|
126
|
+
});
|
|
42
127
|
});
|
|
43
128
|
describe('validatePluginStructure', () => {
|
|
44
129
|
const testDir = path.join(PLUGINS_DIR, '__test-validate__');
|
|
@@ -95,29 +180,29 @@ describe('validatePluginStructure', () => {
|
|
|
95
180
|
});
|
|
96
181
|
});
|
|
97
182
|
describe('lock file', () => {
|
|
98
|
-
const backupPath = `${
|
|
183
|
+
const backupPath = `${getLockFilePath()}.test-backup`;
|
|
99
184
|
let hadOriginal = false;
|
|
100
185
|
beforeEach(() => {
|
|
101
|
-
hadOriginal = fs.existsSync(
|
|
186
|
+
hadOriginal = fs.existsSync(getLockFilePath());
|
|
102
187
|
if (hadOriginal) {
|
|
103
188
|
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
104
|
-
fs.copyFileSync(
|
|
189
|
+
fs.copyFileSync(getLockFilePath(), backupPath);
|
|
105
190
|
}
|
|
106
191
|
});
|
|
107
192
|
afterEach(() => {
|
|
108
193
|
if (hadOriginal) {
|
|
109
|
-
fs.copyFileSync(backupPath,
|
|
194
|
+
fs.copyFileSync(backupPath, getLockFilePath());
|
|
110
195
|
fs.unlinkSync(backupPath);
|
|
111
196
|
return;
|
|
112
197
|
}
|
|
113
198
|
try {
|
|
114
|
-
fs.unlinkSync(
|
|
199
|
+
fs.unlinkSync(getLockFilePath());
|
|
115
200
|
}
|
|
116
201
|
catch { }
|
|
117
202
|
});
|
|
118
203
|
it('reads empty lock when file does not exist', () => {
|
|
119
204
|
try {
|
|
120
|
-
fs.unlinkSync(
|
|
205
|
+
fs.unlinkSync(getLockFilePath());
|
|
121
206
|
}
|
|
122
207
|
catch { }
|
|
123
208
|
expect(_readLockFile()).toEqual({});
|
|
@@ -125,12 +210,12 @@ describe('lock file', () => {
|
|
|
125
210
|
it('round-trips lock entries', () => {
|
|
126
211
|
const entries = {
|
|
127
212
|
'test-plugin': {
|
|
128
|
-
source: 'https://github.com/user/repo.git',
|
|
213
|
+
source: { kind: 'git', url: 'https://github.com/user/repo.git' },
|
|
129
214
|
commitHash: 'abc1234567890def',
|
|
130
215
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
131
216
|
},
|
|
132
217
|
'another-plugin': {
|
|
133
|
-
source: 'https://github.com/user/another.git',
|
|
218
|
+
source: { kind: 'git', url: 'https://github.com/user/another.git' },
|
|
134
219
|
commitHash: 'def4567890123abc',
|
|
135
220
|
installedAt: '2025-02-01T00:00:00.000Z',
|
|
136
221
|
updatedAt: '2025-03-01T00:00:00.000Z',
|
|
@@ -140,10 +225,97 @@ describe('lock file', () => {
|
|
|
140
225
|
expect(_readLockFile()).toEqual(entries);
|
|
141
226
|
});
|
|
142
227
|
it('handles malformed lock file gracefully', () => {
|
|
143
|
-
fs.mkdirSync(path.dirname(
|
|
144
|
-
fs.writeFileSync(
|
|
228
|
+
fs.mkdirSync(path.dirname(getLockFilePath()), { recursive: true });
|
|
229
|
+
fs.writeFileSync(getLockFilePath(), 'not valid json');
|
|
145
230
|
expect(_readLockFile()).toEqual({});
|
|
146
231
|
});
|
|
232
|
+
it('keeps the previous lockfile contents when atomic rewrite fails', () => {
|
|
233
|
+
const existing = {
|
|
234
|
+
stable: {
|
|
235
|
+
source: { kind: 'git', url: 'https://github.com/user/stable.git' },
|
|
236
|
+
commitHash: 'stable1234567890',
|
|
237
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
_writeLockFile(existing);
|
|
241
|
+
const renameSync = vi.fn(() => {
|
|
242
|
+
throw new Error('rename failed');
|
|
243
|
+
});
|
|
244
|
+
const rmSync = vi.fn(() => undefined);
|
|
245
|
+
expect(() => _writeLockFileWithFs({
|
|
246
|
+
broken: {
|
|
247
|
+
source: { kind: 'git', url: 'https://github.com/user/broken.git' },
|
|
248
|
+
commitHash: 'broken1234567890',
|
|
249
|
+
installedAt: '2025-02-01T00:00:00.000Z',
|
|
250
|
+
},
|
|
251
|
+
}, {
|
|
252
|
+
mkdirSync: fs.mkdirSync,
|
|
253
|
+
writeFileSync: fs.writeFileSync,
|
|
254
|
+
renameSync,
|
|
255
|
+
rmSync,
|
|
256
|
+
})).toThrow('rename failed');
|
|
257
|
+
expect(_readLockFile()).toEqual(existing);
|
|
258
|
+
expect(rmSync).toHaveBeenCalledTimes(1);
|
|
259
|
+
});
|
|
260
|
+
it('migrates legacy string sources to structured sources on read', () => {
|
|
261
|
+
const legacyLocalPath = path.resolve(path.join(os.tmpdir(), 'opencli-legacy-local-plugin'));
|
|
262
|
+
fs.mkdirSync(path.dirname(getLockFilePath()), { recursive: true });
|
|
263
|
+
fs.writeFileSync(getLockFilePath(), JSON.stringify({
|
|
264
|
+
alpha: {
|
|
265
|
+
source: 'https://github.com/user/opencli-plugins.git',
|
|
266
|
+
commitHash: 'abc1234567890def',
|
|
267
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
268
|
+
monorepo: { name: 'opencli-plugins', subPath: 'packages/alpha' },
|
|
269
|
+
},
|
|
270
|
+
beta: {
|
|
271
|
+
source: `local:${legacyLocalPath}`,
|
|
272
|
+
commitHash: 'local',
|
|
273
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
274
|
+
},
|
|
275
|
+
}, null, 2));
|
|
276
|
+
expect(_readLockFile()).toEqual({
|
|
277
|
+
alpha: {
|
|
278
|
+
source: {
|
|
279
|
+
kind: 'monorepo',
|
|
280
|
+
url: 'https://github.com/user/opencli-plugins.git',
|
|
281
|
+
repoName: 'opencli-plugins',
|
|
282
|
+
subPath: 'packages/alpha',
|
|
283
|
+
},
|
|
284
|
+
commitHash: 'abc1234567890def',
|
|
285
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
286
|
+
},
|
|
287
|
+
beta: {
|
|
288
|
+
source: { kind: 'local', path: legacyLocalPath },
|
|
289
|
+
commitHash: 'local',
|
|
290
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
it('returns normalized entries even when migration rewrite fails', () => {
|
|
295
|
+
fs.mkdirSync(path.dirname(getLockFilePath()), { recursive: true });
|
|
296
|
+
fs.writeFileSync(getLockFilePath(), JSON.stringify({
|
|
297
|
+
alpha: {
|
|
298
|
+
source: 'https://github.com/user/opencli-plugins.git',
|
|
299
|
+
commitHash: 'abc1234567890def',
|
|
300
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
301
|
+
monorepo: { name: 'opencli-plugins', subPath: 'packages/alpha' },
|
|
302
|
+
},
|
|
303
|
+
}, null, 2));
|
|
304
|
+
expect(_readLockFileWithWriter(() => {
|
|
305
|
+
throw new Error('disk full');
|
|
306
|
+
})).toEqual({
|
|
307
|
+
alpha: {
|
|
308
|
+
source: {
|
|
309
|
+
kind: 'monorepo',
|
|
310
|
+
url: 'https://github.com/user/opencli-plugins.git',
|
|
311
|
+
repoName: 'opencli-plugins',
|
|
312
|
+
subPath: 'packages/alpha',
|
|
313
|
+
},
|
|
314
|
+
commitHash: 'abc1234567890def',
|
|
315
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
147
319
|
});
|
|
148
320
|
describe('getCommitHash', () => {
|
|
149
321
|
it('returns a hash for a git repo', () => {
|
|
@@ -161,7 +333,6 @@ describe('resolveEsbuildBin', () => {
|
|
|
161
333
|
expect(binPath).not.toBeNull();
|
|
162
334
|
expect(typeof binPath).toBe('string');
|
|
163
335
|
expect(fs.existsSync(binPath)).toBe(true);
|
|
164
|
-
// On Windows the resolved path ends with 'esbuild.cmd', on Unix 'esbuild'
|
|
165
336
|
expect(binPath).toMatch(/esbuild(\.cmd)?$/);
|
|
166
337
|
});
|
|
167
338
|
});
|
|
@@ -186,7 +357,7 @@ describe('listPlugins', () => {
|
|
|
186
357
|
fs.writeFileSync(path.join(testDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
187
358
|
const lock = _readLockFile();
|
|
188
359
|
lock['__test-list-plugin__'] = {
|
|
189
|
-
source: 'https://github.com/user/repo.git',
|
|
360
|
+
source: { kind: 'git', url: 'https://github.com/user/repo.git' },
|
|
190
361
|
commitHash: 'abcdef1234567890abcdef1234567890abcdef12',
|
|
191
362
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
192
363
|
};
|
|
@@ -200,10 +371,44 @@ describe('listPlugins', () => {
|
|
|
200
371
|
_writeLockFile(lock);
|
|
201
372
|
});
|
|
202
373
|
it('returns empty array when no plugins dir', () => {
|
|
203
|
-
// listPlugins should handle missing dir gracefully
|
|
204
374
|
const plugins = listPlugins();
|
|
205
375
|
expect(Array.isArray(plugins)).toBe(true);
|
|
206
376
|
});
|
|
377
|
+
it('prefers lockfile source for local symlink plugins', () => {
|
|
378
|
+
const localTarget = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-local-list-'));
|
|
379
|
+
const linkPath = path.join(PLUGINS_DIR, '__test-list-plugin__');
|
|
380
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
381
|
+
fs.writeFileSync(path.join(localTarget, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
382
|
+
try {
|
|
383
|
+
fs.unlinkSync(linkPath);
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
try {
|
|
387
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
388
|
+
}
|
|
389
|
+
catch { }
|
|
390
|
+
fs.symlinkSync(localTarget, linkPath, 'dir');
|
|
391
|
+
const lock = _readLockFile();
|
|
392
|
+
lock['__test-list-plugin__'] = {
|
|
393
|
+
source: { kind: 'local', path: localTarget },
|
|
394
|
+
commitHash: 'local',
|
|
395
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
396
|
+
};
|
|
397
|
+
_writeLockFile(lock);
|
|
398
|
+
const plugins = listPlugins();
|
|
399
|
+
const found = plugins.find(p => p.name === '__test-list-plugin__');
|
|
400
|
+
expect(found?.source).toBe(`local:${localTarget}`);
|
|
401
|
+
try {
|
|
402
|
+
fs.unlinkSync(linkPath);
|
|
403
|
+
}
|
|
404
|
+
catch { }
|
|
405
|
+
try {
|
|
406
|
+
fs.rmSync(localTarget, { recursive: true, force: true });
|
|
407
|
+
}
|
|
408
|
+
catch { }
|
|
409
|
+
delete lock['__test-list-plugin__'];
|
|
410
|
+
_writeLockFile(lock);
|
|
411
|
+
});
|
|
207
412
|
});
|
|
208
413
|
describe('uninstallPlugin', () => {
|
|
209
414
|
const testDir = path.join(PLUGINS_DIR, '__test-uninstall__');
|
|
@@ -224,7 +429,7 @@ describe('uninstallPlugin', () => {
|
|
|
224
429
|
fs.writeFileSync(path.join(testDir, 'test.yaml'), 'site: test');
|
|
225
430
|
const lock = _readLockFile();
|
|
226
431
|
lock['__test-uninstall__'] = {
|
|
227
|
-
source: 'https://github.com/user/repo.git',
|
|
432
|
+
source: { kind: 'git', url: 'https://github.com/user/repo.git' },
|
|
228
433
|
commitHash: 'abc123',
|
|
229
434
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
230
435
|
};
|
|
@@ -240,6 +445,40 @@ describe('updatePlugin', () => {
|
|
|
240
445
|
it('throws for non-existent plugin', () => {
|
|
241
446
|
expect(() => updatePlugin('__nonexistent__')).toThrow('not installed');
|
|
242
447
|
});
|
|
448
|
+
it('refreshes local plugins without running git pull', () => {
|
|
449
|
+
const localTarget = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-local-update-'));
|
|
450
|
+
const linkPath = path.join(PLUGINS_DIR, '__test-local-update__');
|
|
451
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
452
|
+
fs.writeFileSync(path.join(localTarget, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
453
|
+
fs.symlinkSync(localTarget, linkPath, 'dir');
|
|
454
|
+
const lock = _readLockFile();
|
|
455
|
+
lock['__test-local-update__'] = {
|
|
456
|
+
source: { kind: 'local', path: localTarget },
|
|
457
|
+
commitHash: 'local',
|
|
458
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
459
|
+
};
|
|
460
|
+
_writeLockFile(lock);
|
|
461
|
+
mockExecFileSync.mockClear();
|
|
462
|
+
updatePlugin('__test-local-update__');
|
|
463
|
+
expect(mockExecFileSync.mock.calls.some(([cmd, args, opts]) => cmd === 'git'
|
|
464
|
+
&& Array.isArray(args)
|
|
465
|
+
&& args[0] === 'pull'
|
|
466
|
+
&& opts?.cwd === linkPath)).toBe(false);
|
|
467
|
+
const updated = _readLockFile()['__test-local-update__'];
|
|
468
|
+
expect(updated?.source).toEqual({ kind: 'local', path: path.resolve(localTarget) });
|
|
469
|
+
expect(updated?.updatedAt).toBeDefined();
|
|
470
|
+
try {
|
|
471
|
+
fs.unlinkSync(linkPath);
|
|
472
|
+
}
|
|
473
|
+
catch { }
|
|
474
|
+
try {
|
|
475
|
+
fs.rmSync(localTarget, { recursive: true, force: true });
|
|
476
|
+
}
|
|
477
|
+
catch { }
|
|
478
|
+
const finalLock = _readLockFile();
|
|
479
|
+
delete finalLock['__test-local-update__'];
|
|
480
|
+
_writeLockFile(finalLock);
|
|
481
|
+
});
|
|
243
482
|
});
|
|
244
483
|
vi.mock('node:child_process', () => {
|
|
245
484
|
return {
|
|
@@ -310,6 +549,23 @@ describe('updateAllPlugins', () => {
|
|
|
310
549
|
fs.writeFileSync(path.join(testDirA, 'cmd.yaml'), 'site: a');
|
|
311
550
|
fs.writeFileSync(path.join(testDirB, 'cmd.yaml'), 'site: b');
|
|
312
551
|
fs.writeFileSync(path.join(testDirC, 'cmd.yaml'), 'site: c');
|
|
552
|
+
const lock = _readLockFile();
|
|
553
|
+
lock['plugin-a'] = {
|
|
554
|
+
source: { kind: 'git', url: 'https://github.com/user/plugin-a.git' },
|
|
555
|
+
commitHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
556
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
557
|
+
};
|
|
558
|
+
lock['plugin-b'] = {
|
|
559
|
+
source: { kind: 'git', url: 'https://github.com/user/plugin-b.git' },
|
|
560
|
+
commitHash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
561
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
562
|
+
};
|
|
563
|
+
lock['plugin-c'] = {
|
|
564
|
+
source: { kind: 'git', url: 'https://github.com/user/plugin-c.git' },
|
|
565
|
+
commitHash: 'cccccccccccccccccccccccccccccccccccccccc',
|
|
566
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
567
|
+
};
|
|
568
|
+
_writeLockFile(lock);
|
|
313
569
|
});
|
|
314
570
|
afterEach(() => {
|
|
315
571
|
try {
|
|
@@ -324,9 +580,33 @@ describe('updateAllPlugins', () => {
|
|
|
324
580
|
fs.rmSync(testDirC, { recursive: true });
|
|
325
581
|
}
|
|
326
582
|
catch { }
|
|
583
|
+
const lock = _readLockFile();
|
|
584
|
+
delete lock['plugin-a'];
|
|
585
|
+
delete lock['plugin-b'];
|
|
586
|
+
delete lock['plugin-c'];
|
|
587
|
+
_writeLockFile(lock);
|
|
327
588
|
vi.clearAllMocks();
|
|
328
589
|
});
|
|
329
590
|
it('collects successes and failures without throwing', () => {
|
|
591
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
592
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
593
|
+
const cloneUrl = String(args[3]);
|
|
594
|
+
const cloneDir = String(args[4]);
|
|
595
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
596
|
+
fs.writeFileSync(path.join(cloneDir, 'cmd.yaml'), 'site: test\nname: hello\n');
|
|
597
|
+
if (cloneUrl.includes('plugin-b')) {
|
|
598
|
+
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({ name: 'plugin-b' }));
|
|
599
|
+
}
|
|
600
|
+
return '';
|
|
601
|
+
}
|
|
602
|
+
if (cmd === 'npm' && Array.isArray(args) && args[0] === 'install') {
|
|
603
|
+
throw new Error('Network error');
|
|
604
|
+
}
|
|
605
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
606
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
607
|
+
}
|
|
608
|
+
return '';
|
|
609
|
+
});
|
|
330
610
|
const results = _updateAllPlugins();
|
|
331
611
|
const resA = results.find(r => r.name === 'plugin-a');
|
|
332
612
|
const resB = results.find(r => r.name === 'plugin-b');
|
|
@@ -345,6 +625,7 @@ describe('parseSource with monorepo subplugin', () => {
|
|
|
345
625
|
it('parses github:user/repo/subplugin format', () => {
|
|
346
626
|
const result = _parseSource('github:ByteYue/opencli-plugins/polymarket');
|
|
347
627
|
expect(result).toEqual({
|
|
628
|
+
type: 'git',
|
|
348
629
|
cloneUrl: 'https://github.com/ByteYue/opencli-plugins.git',
|
|
349
630
|
name: 'opencli-plugins',
|
|
350
631
|
subPlugin: 'polymarket',
|
|
@@ -358,6 +639,7 @@ describe('parseSource with monorepo subplugin', () => {
|
|
|
358
639
|
it('still parses github:user/repo without subplugin', () => {
|
|
359
640
|
const result = _parseSource('github:user/my-repo');
|
|
360
641
|
expect(result).toEqual({
|
|
642
|
+
type: 'git',
|
|
361
643
|
cloneUrl: 'https://github.com/user/my-repo.git',
|
|
362
644
|
name: 'my-repo',
|
|
363
645
|
});
|
|
@@ -394,23 +676,23 @@ describe('monorepo uninstall with symlink', () => {
|
|
|
394
676
|
let monoDir;
|
|
395
677
|
beforeEach(() => {
|
|
396
678
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-mono-uninstall-'));
|
|
397
|
-
// We need to use the real PLUGINS_DIR for uninstallPlugin() to work
|
|
398
679
|
pluginDir = path.join(PLUGINS_DIR, '__test-mono-sub__');
|
|
399
680
|
monoDir = path.join(_getMonoreposDir(), '__test-mono__');
|
|
400
|
-
// Set up monorepo structure
|
|
401
681
|
const subDir = path.join(monoDir, 'packages', 'sub');
|
|
402
682
|
fs.mkdirSync(subDir, { recursive: true });
|
|
403
683
|
fs.writeFileSync(path.join(subDir, 'cmd.yaml'), 'site: test');
|
|
404
|
-
// Create symlink in plugins dir
|
|
405
684
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
406
685
|
fs.symlinkSync(subDir, pluginDir, 'dir');
|
|
407
|
-
// Set up lock file with monorepo entry
|
|
408
686
|
const lock = _readLockFile();
|
|
409
687
|
lock['__test-mono-sub__'] = {
|
|
410
|
-
source:
|
|
688
|
+
source: {
|
|
689
|
+
kind: 'monorepo',
|
|
690
|
+
url: 'https://github.com/user/test.git',
|
|
691
|
+
repoName: '__test-mono__',
|
|
692
|
+
subPath: 'packages/sub',
|
|
693
|
+
},
|
|
411
694
|
commitHash: 'abc123',
|
|
412
695
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
413
|
-
monorepo: { name: '__test-mono__', subPath: 'packages/sub' },
|
|
414
696
|
};
|
|
415
697
|
_writeLockFile(lock);
|
|
416
698
|
});
|
|
@@ -427,42 +709,36 @@ describe('monorepo uninstall with symlink', () => {
|
|
|
427
709
|
fs.rmSync(monoDir, { recursive: true, force: true });
|
|
428
710
|
}
|
|
429
711
|
catch { }
|
|
430
|
-
// Clean up lock entry
|
|
431
712
|
const lock = _readLockFile();
|
|
432
713
|
delete lock['__test-mono-sub__'];
|
|
433
714
|
_writeLockFile(lock);
|
|
434
715
|
});
|
|
435
716
|
it('removes symlink but keeps monorepo if other sub-plugins reference it', () => {
|
|
436
|
-
// Add another sub-plugin referencing the same monorepo
|
|
437
717
|
const lock = _readLockFile();
|
|
438
718
|
lock['__test-mono-other__'] = {
|
|
439
|
-
source:
|
|
719
|
+
source: {
|
|
720
|
+
kind: 'monorepo',
|
|
721
|
+
url: 'https://github.com/user/test.git',
|
|
722
|
+
repoName: '__test-mono__',
|
|
723
|
+
subPath: 'packages/other',
|
|
724
|
+
},
|
|
440
725
|
commitHash: 'abc123',
|
|
441
726
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
442
|
-
monorepo: { name: '__test-mono__', subPath: 'packages/other' },
|
|
443
727
|
};
|
|
444
728
|
_writeLockFile(lock);
|
|
445
729
|
uninstallPlugin('__test-mono-sub__');
|
|
446
|
-
// Symlink removed
|
|
447
730
|
expect(fs.existsSync(pluginDir)).toBe(false);
|
|
448
|
-
// Monorepo dir still exists (other sub-plugin references it)
|
|
449
731
|
expect(fs.existsSync(monoDir)).toBe(true);
|
|
450
|
-
// Lock entry removed
|
|
451
732
|
expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
|
|
452
|
-
// Other lock entry still present
|
|
453
733
|
expect(_readLockFile()['__test-mono-other__']).toBeDefined();
|
|
454
|
-
// Clean up the other entry
|
|
455
734
|
const finalLock = _readLockFile();
|
|
456
735
|
delete finalLock['__test-mono-other__'];
|
|
457
736
|
_writeLockFile(finalLock);
|
|
458
737
|
});
|
|
459
738
|
it('removes symlink AND monorepo dir when last sub-plugin is uninstalled', () => {
|
|
460
739
|
uninstallPlugin('__test-mono-sub__');
|
|
461
|
-
// Symlink removed
|
|
462
740
|
expect(fs.existsSync(pluginDir)).toBe(false);
|
|
463
|
-
// Monorepo dir also removed (no more references)
|
|
464
741
|
expect(fs.existsSync(monoDir)).toBe(false);
|
|
465
|
-
// Lock entry removed
|
|
466
742
|
expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
|
|
467
743
|
});
|
|
468
744
|
});
|
|
@@ -470,23 +746,24 @@ describe('listPlugins with monorepo metadata', () => {
|
|
|
470
746
|
const testSymlinkTarget = path.join(os.tmpdir(), 'opencli-list-mono-target');
|
|
471
747
|
const testLink = path.join(PLUGINS_DIR, '__test-mono-list__');
|
|
472
748
|
beforeEach(() => {
|
|
473
|
-
// Create a target dir with a command file
|
|
474
749
|
fs.mkdirSync(testSymlinkTarget, { recursive: true });
|
|
475
750
|
fs.writeFileSync(path.join(testSymlinkTarget, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
476
|
-
// Create symlink
|
|
477
751
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
478
752
|
try {
|
|
479
753
|
fs.unlinkSync(testLink);
|
|
480
754
|
}
|
|
481
755
|
catch { }
|
|
482
756
|
fs.symlinkSync(testSymlinkTarget, testLink, 'dir');
|
|
483
|
-
// Set up lock file with monorepo entry
|
|
484
757
|
const lock = _readLockFile();
|
|
485
758
|
lock['__test-mono-list__'] = {
|
|
486
|
-
source:
|
|
759
|
+
source: {
|
|
760
|
+
kind: 'monorepo',
|
|
761
|
+
url: 'https://github.com/user/test-mono.git',
|
|
762
|
+
repoName: 'test-mono',
|
|
763
|
+
subPath: 'packages/list',
|
|
764
|
+
},
|
|
487
765
|
commitHash: 'def456def456def456def456def456def456def4',
|
|
488
766
|
installedAt: '2025-01-01T00:00:00.000Z',
|
|
489
|
-
monorepo: { name: 'test-mono', subPath: 'packages/list' },
|
|
490
767
|
};
|
|
491
768
|
_writeLockFile(lock);
|
|
492
769
|
});
|
|
@@ -512,3 +789,524 @@ describe('listPlugins with monorepo metadata', () => {
|
|
|
512
789
|
expect(found.source).toBe('https://github.com/user/test-mono.git');
|
|
513
790
|
});
|
|
514
791
|
});
|
|
792
|
+
describe('installLocalPlugin', () => {
|
|
793
|
+
let tmpDir;
|
|
794
|
+
const pluginName = '__test-local-plugin__';
|
|
795
|
+
beforeEach(() => {
|
|
796
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-local-install-'));
|
|
797
|
+
fs.writeFileSync(path.join(tmpDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
798
|
+
});
|
|
799
|
+
afterEach(() => {
|
|
800
|
+
const linkPath = path.join(PLUGINS_DIR, pluginName);
|
|
801
|
+
try {
|
|
802
|
+
fs.unlinkSync(linkPath);
|
|
803
|
+
}
|
|
804
|
+
catch { }
|
|
805
|
+
try {
|
|
806
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
const lock = _readLockFile();
|
|
810
|
+
delete lock[pluginName];
|
|
811
|
+
_writeLockFile(lock);
|
|
812
|
+
});
|
|
813
|
+
it('creates a symlink to the local directory', () => {
|
|
814
|
+
const result = _installLocalPlugin(tmpDir, pluginName);
|
|
815
|
+
expect(result).toBe(pluginName);
|
|
816
|
+
const linkPath = path.join(PLUGINS_DIR, pluginName);
|
|
817
|
+
expect(fs.existsSync(linkPath)).toBe(true);
|
|
818
|
+
expect(_isSymlinkSync(linkPath)).toBe(true);
|
|
819
|
+
});
|
|
820
|
+
it('records local: source in lockfile', () => {
|
|
821
|
+
_installLocalPlugin(tmpDir, pluginName);
|
|
822
|
+
const lock = _readLockFile();
|
|
823
|
+
expect(lock[pluginName]).toBeDefined();
|
|
824
|
+
expect(lock[pluginName].source).toEqual({ kind: 'local', path: path.resolve(tmpDir) });
|
|
825
|
+
});
|
|
826
|
+
it('lists the recorded local source', () => {
|
|
827
|
+
_installLocalPlugin(tmpDir, pluginName);
|
|
828
|
+
const plugins = listPlugins();
|
|
829
|
+
const found = plugins.find(p => p.name === pluginName);
|
|
830
|
+
expect(found).toBeDefined();
|
|
831
|
+
expect(found.source).toBe(`local:${path.resolve(tmpDir)}`);
|
|
832
|
+
});
|
|
833
|
+
it('throws for non-existent path', () => {
|
|
834
|
+
expect(() => _installLocalPlugin('/does/not/exist', 'x')).toThrow('does not exist');
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
describe('isLocalPluginSource', () => {
|
|
838
|
+
it('detects lockfile local sources', () => {
|
|
839
|
+
expect(_isLocalPluginSource('local:/tmp/plugin')).toBe(true);
|
|
840
|
+
expect(_isLocalPluginSource('https://github.com/user/repo.git')).toBe(false);
|
|
841
|
+
expect(_isLocalPluginSource(undefined)).toBe(false);
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
describe('plugin source helpers', () => {
|
|
845
|
+
it('formats local plugin sources consistently', () => {
|
|
846
|
+
const dir = path.join(os.tmpdir(), 'opencli-plugin-source');
|
|
847
|
+
expect(_toLocalPluginSource(dir)).toBe(`local:${path.resolve(dir)}`);
|
|
848
|
+
});
|
|
849
|
+
it('serializes structured local sources consistently', () => {
|
|
850
|
+
const dir = path.join(os.tmpdir(), 'opencli-plugin-source');
|
|
851
|
+
expect(_toStoredPluginSource({ kind: 'local', path: dir })).toBe(`local:${path.resolve(dir)}`);
|
|
852
|
+
});
|
|
853
|
+
it('prefers lockfile source over git remote lookup', () => {
|
|
854
|
+
const dir = path.join(os.tmpdir(), 'opencli-plugin-source');
|
|
855
|
+
const localPath = path.resolve(path.join(os.tmpdir(), 'opencli-plugin-source-local'));
|
|
856
|
+
const source = _resolveStoredPluginSource({
|
|
857
|
+
source: { kind: 'local', path: localPath },
|
|
858
|
+
commitHash: 'local',
|
|
859
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
860
|
+
}, dir);
|
|
861
|
+
expect(source).toBe(`local:${localPath}`);
|
|
862
|
+
});
|
|
863
|
+
it('returns structured monorepo sources unchanged', () => {
|
|
864
|
+
const dir = path.join(os.tmpdir(), 'opencli-plugin-source');
|
|
865
|
+
const source = _resolvePluginSource({
|
|
866
|
+
source: {
|
|
867
|
+
kind: 'monorepo',
|
|
868
|
+
url: 'https://github.com/user/opencli-plugins.git',
|
|
869
|
+
repoName: 'opencli-plugins',
|
|
870
|
+
subPath: 'packages/alpha',
|
|
871
|
+
},
|
|
872
|
+
commitHash: 'abcdef1234567890abcdef1234567890abcdef12',
|
|
873
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
874
|
+
}, dir);
|
|
875
|
+
expect(source).toEqual({
|
|
876
|
+
kind: 'monorepo',
|
|
877
|
+
url: 'https://github.com/user/opencli-plugins.git',
|
|
878
|
+
repoName: 'opencli-plugins',
|
|
879
|
+
subPath: 'packages/alpha',
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
describe('moveDir', () => {
|
|
884
|
+
it('cleans up destination when EXDEV fallback copy fails', () => {
|
|
885
|
+
const src = path.join(os.tmpdir(), 'opencli-move-src');
|
|
886
|
+
const dest = path.join(os.tmpdir(), 'opencli-move-dest');
|
|
887
|
+
const renameErr = Object.assign(new Error('cross-device link not permitted'), { code: 'EXDEV' });
|
|
888
|
+
const copyErr = new Error('copy failed');
|
|
889
|
+
const renameSync = vi.fn(() => { throw renameErr; });
|
|
890
|
+
const cpSync = vi.fn(() => { throw copyErr; });
|
|
891
|
+
const rmSync = vi.fn(() => undefined);
|
|
892
|
+
expect(() => _moveDir(src, dest, { renameSync, cpSync, rmSync })).toThrow(copyErr);
|
|
893
|
+
expect(renameSync).toHaveBeenCalledWith(src, dest);
|
|
894
|
+
expect(cpSync).toHaveBeenCalledWith(src, dest, { recursive: true });
|
|
895
|
+
expect(rmSync).toHaveBeenCalledWith(dest, { recursive: true, force: true });
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
describe('promoteDir', () => {
|
|
899
|
+
it('cleans up temporary publish dir when final rename fails', () => {
|
|
900
|
+
const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
|
|
901
|
+
const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
|
|
902
|
+
const publishErr = new Error('publish failed');
|
|
903
|
+
const existsSync = vi.fn(() => false);
|
|
904
|
+
const mkdirSync = vi.fn(() => undefined);
|
|
905
|
+
const cpSync = vi.fn(() => undefined);
|
|
906
|
+
const rmSync = vi.fn(() => undefined);
|
|
907
|
+
const renameSync = vi.fn((src, _target) => {
|
|
908
|
+
if (String(src) === staging)
|
|
909
|
+
return;
|
|
910
|
+
throw publishErr;
|
|
911
|
+
});
|
|
912
|
+
expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
913
|
+
const tempDest = renameSync.mock.calls[0][1];
|
|
914
|
+
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
915
|
+
expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
|
|
916
|
+
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
describe('replaceDir', () => {
|
|
920
|
+
it('rolls back the original destination when swap fails', () => {
|
|
921
|
+
const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
|
|
922
|
+
const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
|
|
923
|
+
const publishErr = new Error('swap failed');
|
|
924
|
+
const existingPaths = new Set([dest]);
|
|
925
|
+
const existsSync = vi.fn((p) => existingPaths.has(String(p)));
|
|
926
|
+
const mkdirSync = vi.fn(() => undefined);
|
|
927
|
+
const cpSync = vi.fn(() => undefined);
|
|
928
|
+
const rmSync = vi.fn(() => undefined);
|
|
929
|
+
const renameSync = vi.fn((src, target) => {
|
|
930
|
+
if (String(src) === staging) {
|
|
931
|
+
existingPaths.add(String(target));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (String(src) === dest) {
|
|
935
|
+
existingPaths.delete(dest);
|
|
936
|
+
existingPaths.add(String(target));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (String(target) === dest)
|
|
940
|
+
throw publishErr;
|
|
941
|
+
if (existingPaths.has(String(src))) {
|
|
942
|
+
existingPaths.delete(String(src));
|
|
943
|
+
existingPaths.add(String(target));
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
947
|
+
const tempDest = renameSync.mock.calls[0][1];
|
|
948
|
+
const backupDest = renameSync.mock.calls[1][1];
|
|
949
|
+
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
950
|
+
expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
|
|
951
|
+
expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
|
|
952
|
+
expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
|
|
953
|
+
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
describe('installPlugin transactional staging', () => {
|
|
957
|
+
const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
|
|
958
|
+
const standaloneName = '__test-transactional-standalone__';
|
|
959
|
+
const standaloneDir = path.join(PLUGINS_DIR, standaloneName);
|
|
960
|
+
const monorepoSource = 'github:user/opencli-plugins-__test-transactional__';
|
|
961
|
+
const monorepoRepoDir = path.join(_getMonoreposDir(), 'opencli-plugins-__test-transactional__');
|
|
962
|
+
const monorepoLink = path.join(PLUGINS_DIR, 'alpha');
|
|
963
|
+
beforeEach(() => {
|
|
964
|
+
mockExecFileSync.mockClear();
|
|
965
|
+
mockExecSync.mockClear();
|
|
966
|
+
});
|
|
967
|
+
afterEach(() => {
|
|
968
|
+
try {
|
|
969
|
+
fs.unlinkSync(monorepoLink);
|
|
970
|
+
}
|
|
971
|
+
catch { }
|
|
972
|
+
try {
|
|
973
|
+
fs.rmSync(monorepoLink, { recursive: true, force: true });
|
|
974
|
+
}
|
|
975
|
+
catch { }
|
|
976
|
+
try {
|
|
977
|
+
fs.rmSync(monorepoRepoDir, { recursive: true, force: true });
|
|
978
|
+
}
|
|
979
|
+
catch { }
|
|
980
|
+
try {
|
|
981
|
+
fs.rmSync(standaloneDir, { recursive: true, force: true });
|
|
982
|
+
}
|
|
983
|
+
catch { }
|
|
984
|
+
const lock = _readLockFile();
|
|
985
|
+
delete lock[standaloneName];
|
|
986
|
+
delete lock.alpha;
|
|
987
|
+
_writeLockFile(lock);
|
|
988
|
+
vi.clearAllMocks();
|
|
989
|
+
});
|
|
990
|
+
it('does not expose the final standalone plugin dir when lifecycle fails in staging', () => {
|
|
991
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
992
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
993
|
+
const cloneDir = String(args[args.length - 1]);
|
|
994
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
995
|
+
fs.writeFileSync(path.join(cloneDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
996
|
+
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({ name: standaloneName }));
|
|
997
|
+
return '';
|
|
998
|
+
}
|
|
999
|
+
if (cmd === 'npm' && Array.isArray(args) && args[0] === 'install') {
|
|
1000
|
+
throw new Error('boom');
|
|
1001
|
+
}
|
|
1002
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1003
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1004
|
+
}
|
|
1005
|
+
return '';
|
|
1006
|
+
});
|
|
1007
|
+
expect(() => installPlugin(standaloneSource)).toThrow(`npm install failed`);
|
|
1008
|
+
expect(fs.existsSync(standaloneDir)).toBe(false);
|
|
1009
|
+
expect(_readLockFile()[standaloneName]).toBeUndefined();
|
|
1010
|
+
});
|
|
1011
|
+
it('does not expose monorepo links or repo dir when lifecycle fails in staging', () => {
|
|
1012
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1013
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1014
|
+
const cloneDir = String(args[args.length - 1]);
|
|
1015
|
+
const alphaDir = path.join(cloneDir, 'packages', 'alpha');
|
|
1016
|
+
fs.mkdirSync(alphaDir, { recursive: true });
|
|
1017
|
+
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({
|
|
1018
|
+
name: 'opencli-plugins-__test-transactional__',
|
|
1019
|
+
private: true,
|
|
1020
|
+
}));
|
|
1021
|
+
fs.writeFileSync(path.join(cloneDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1022
|
+
plugins: {
|
|
1023
|
+
alpha: { path: 'packages/alpha' },
|
|
1024
|
+
},
|
|
1025
|
+
}));
|
|
1026
|
+
fs.writeFileSync(path.join(alphaDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1027
|
+
return '';
|
|
1028
|
+
}
|
|
1029
|
+
if (cmd === 'npm' && Array.isArray(args) && args[0] === 'install') {
|
|
1030
|
+
throw new Error('boom');
|
|
1031
|
+
}
|
|
1032
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1033
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1034
|
+
}
|
|
1035
|
+
return '';
|
|
1036
|
+
});
|
|
1037
|
+
expect(() => installPlugin(monorepoSource)).toThrow(`npm install failed`);
|
|
1038
|
+
expect(fs.existsSync(monorepoRepoDir)).toBe(false);
|
|
1039
|
+
expect(fs.existsSync(monorepoLink)).toBe(false);
|
|
1040
|
+
expect(_readLockFile().alpha).toBeUndefined();
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
describe('installPlugin with existing monorepo', () => {
|
|
1044
|
+
const repoName = '__test-existing-monorepo__';
|
|
1045
|
+
const repoDir = path.join(_getMonoreposDir(), repoName);
|
|
1046
|
+
const pluginName = 'beta';
|
|
1047
|
+
const pluginLink = path.join(PLUGINS_DIR, pluginName);
|
|
1048
|
+
beforeEach(() => {
|
|
1049
|
+
mockExecFileSync.mockClear();
|
|
1050
|
+
mockExecSync.mockClear();
|
|
1051
|
+
});
|
|
1052
|
+
afterEach(() => {
|
|
1053
|
+
try {
|
|
1054
|
+
fs.unlinkSync(pluginLink);
|
|
1055
|
+
}
|
|
1056
|
+
catch { }
|
|
1057
|
+
try {
|
|
1058
|
+
fs.rmSync(pluginLink, { recursive: true, force: true });
|
|
1059
|
+
}
|
|
1060
|
+
catch { }
|
|
1061
|
+
try {
|
|
1062
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
1063
|
+
}
|
|
1064
|
+
catch { }
|
|
1065
|
+
const lock = _readLockFile();
|
|
1066
|
+
delete lock[pluginName];
|
|
1067
|
+
_writeLockFile(lock);
|
|
1068
|
+
vi.clearAllMocks();
|
|
1069
|
+
});
|
|
1070
|
+
it('reinstalls root dependencies when adding a sub-plugin from an existing monorepo', () => {
|
|
1071
|
+
const subDir = path.join(repoDir, 'packages', pluginName);
|
|
1072
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
1073
|
+
fs.writeFileSync(path.join(repoDir, 'package.json'), JSON.stringify({
|
|
1074
|
+
name: repoName,
|
|
1075
|
+
private: true,
|
|
1076
|
+
workspaces: ['packages/*'],
|
|
1077
|
+
}));
|
|
1078
|
+
fs.writeFileSync(path.join(repoDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1079
|
+
plugins: {
|
|
1080
|
+
[pluginName]: { path: `packages/${pluginName}` },
|
|
1081
|
+
},
|
|
1082
|
+
}));
|
|
1083
|
+
fs.writeFileSync(path.join(subDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1084
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1085
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1086
|
+
const cloneDir = String(args[4]);
|
|
1087
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
1088
|
+
fs.writeFileSync(path.join(cloneDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1089
|
+
plugins: {
|
|
1090
|
+
[pluginName]: { path: `packages/${pluginName}` },
|
|
1091
|
+
},
|
|
1092
|
+
}));
|
|
1093
|
+
return '';
|
|
1094
|
+
}
|
|
1095
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1096
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1097
|
+
}
|
|
1098
|
+
return '';
|
|
1099
|
+
});
|
|
1100
|
+
installPlugin(`github:user/${repoName}/${pluginName}`);
|
|
1101
|
+
const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
|
|
1102
|
+
expect(npmCalls.some(([, , opts]) => opts?.cwd === repoDir)).toBe(true);
|
|
1103
|
+
expect(fs.realpathSync(pluginLink)).toBe(fs.realpathSync(subDir));
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
describe('updatePlugin transactional staging', () => {
|
|
1107
|
+
const standaloneName = '__test-transactional-update__';
|
|
1108
|
+
const standaloneDir = path.join(PLUGINS_DIR, standaloneName);
|
|
1109
|
+
const monorepoName = '__test-transactional-mono-update__';
|
|
1110
|
+
const monorepoRepoDir = path.join(_getMonoreposDir(), monorepoName);
|
|
1111
|
+
const monorepoPluginName = 'alpha-update';
|
|
1112
|
+
const monorepoLink = path.join(PLUGINS_DIR, monorepoPluginName);
|
|
1113
|
+
beforeEach(() => {
|
|
1114
|
+
mockExecFileSync.mockClear();
|
|
1115
|
+
mockExecSync.mockClear();
|
|
1116
|
+
});
|
|
1117
|
+
afterEach(() => {
|
|
1118
|
+
try {
|
|
1119
|
+
fs.unlinkSync(monorepoLink);
|
|
1120
|
+
}
|
|
1121
|
+
catch { }
|
|
1122
|
+
try {
|
|
1123
|
+
fs.rmSync(monorepoLink, { recursive: true, force: true });
|
|
1124
|
+
}
|
|
1125
|
+
catch { }
|
|
1126
|
+
try {
|
|
1127
|
+
fs.rmSync(monorepoRepoDir, { recursive: true, force: true });
|
|
1128
|
+
}
|
|
1129
|
+
catch { }
|
|
1130
|
+
try {
|
|
1131
|
+
fs.rmSync(standaloneDir, { recursive: true, force: true });
|
|
1132
|
+
}
|
|
1133
|
+
catch { }
|
|
1134
|
+
const lock = _readLockFile();
|
|
1135
|
+
delete lock[standaloneName];
|
|
1136
|
+
delete lock[monorepoPluginName];
|
|
1137
|
+
_writeLockFile(lock);
|
|
1138
|
+
vi.clearAllMocks();
|
|
1139
|
+
});
|
|
1140
|
+
it('keeps the existing standalone plugin when staged update preparation fails', () => {
|
|
1141
|
+
fs.mkdirSync(standaloneDir, { recursive: true });
|
|
1142
|
+
fs.writeFileSync(path.join(standaloneDir, 'old.yaml'), 'site: old\nname: old\n');
|
|
1143
|
+
const lock = _readLockFile();
|
|
1144
|
+
lock[standaloneName] = {
|
|
1145
|
+
source: {
|
|
1146
|
+
kind: 'git',
|
|
1147
|
+
url: 'https://github.com/user/opencli-plugin-__test-transactional-update__.git',
|
|
1148
|
+
},
|
|
1149
|
+
commitHash: 'oldhasholdhasholdhasholdhasholdhasholdh',
|
|
1150
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
1151
|
+
};
|
|
1152
|
+
_writeLockFile(lock);
|
|
1153
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1154
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1155
|
+
const cloneDir = String(args[4]);
|
|
1156
|
+
fs.mkdirSync(cloneDir, { recursive: true });
|
|
1157
|
+
fs.writeFileSync(path.join(cloneDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1158
|
+
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({ name: standaloneName }));
|
|
1159
|
+
return '';
|
|
1160
|
+
}
|
|
1161
|
+
if (cmd === 'npm' && Array.isArray(args) && args[0] === 'install') {
|
|
1162
|
+
throw new Error('boom');
|
|
1163
|
+
}
|
|
1164
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1165
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1166
|
+
}
|
|
1167
|
+
return '';
|
|
1168
|
+
});
|
|
1169
|
+
expect(() => updatePlugin(standaloneName)).toThrow('npm install failed');
|
|
1170
|
+
expect(fs.existsSync(standaloneDir)).toBe(true);
|
|
1171
|
+
expect(fs.readFileSync(path.join(standaloneDir, 'old.yaml'), 'utf-8')).toContain('site: old');
|
|
1172
|
+
expect(_readLockFile()[standaloneName]?.commitHash).toBe('oldhasholdhasholdhasholdhasholdhasholdh');
|
|
1173
|
+
});
|
|
1174
|
+
it('keeps the existing monorepo repo and link when staged update preparation fails', () => {
|
|
1175
|
+
const subDir = path.join(monorepoRepoDir, 'packages', monorepoPluginName);
|
|
1176
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
1177
|
+
fs.writeFileSync(path.join(subDir, 'old.yaml'), 'site: old\nname: old\n');
|
|
1178
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
1179
|
+
fs.symlinkSync(subDir, monorepoLink, 'dir');
|
|
1180
|
+
const lock = _readLockFile();
|
|
1181
|
+
lock[monorepoPluginName] = {
|
|
1182
|
+
source: {
|
|
1183
|
+
kind: 'monorepo',
|
|
1184
|
+
url: 'https://github.com/user/opencli-plugins-__test-transactional-mono-update__.git',
|
|
1185
|
+
repoName: monorepoName,
|
|
1186
|
+
subPath: `packages/${monorepoPluginName}`,
|
|
1187
|
+
},
|
|
1188
|
+
commitHash: 'oldmonooldmonooldmonooldmonooldmonoold',
|
|
1189
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
1190
|
+
};
|
|
1191
|
+
_writeLockFile(lock);
|
|
1192
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1193
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1194
|
+
const cloneDir = String(args[4]);
|
|
1195
|
+
const alphaDir = path.join(cloneDir, 'packages', monorepoPluginName);
|
|
1196
|
+
fs.mkdirSync(alphaDir, { recursive: true });
|
|
1197
|
+
fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({
|
|
1198
|
+
name: 'opencli-plugins-__test-transactional-mono-update__',
|
|
1199
|
+
private: true,
|
|
1200
|
+
}));
|
|
1201
|
+
fs.writeFileSync(path.join(cloneDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1202
|
+
plugins: {
|
|
1203
|
+
[monorepoPluginName]: { path: `packages/${monorepoPluginName}` },
|
|
1204
|
+
},
|
|
1205
|
+
}));
|
|
1206
|
+
fs.writeFileSync(path.join(alphaDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1207
|
+
return '';
|
|
1208
|
+
}
|
|
1209
|
+
if (cmd === 'npm' && Array.isArray(args) && args[0] === 'install') {
|
|
1210
|
+
throw new Error('boom');
|
|
1211
|
+
}
|
|
1212
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1213
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1214
|
+
}
|
|
1215
|
+
return '';
|
|
1216
|
+
});
|
|
1217
|
+
expect(() => updatePlugin(monorepoPluginName)).toThrow('npm install failed');
|
|
1218
|
+
expect(fs.existsSync(monorepoRepoDir)).toBe(true);
|
|
1219
|
+
expect(fs.existsSync(monorepoLink)).toBe(true);
|
|
1220
|
+
expect(fs.readFileSync(path.join(subDir, 'old.yaml'), 'utf-8')).toContain('site: old');
|
|
1221
|
+
expect(_readLockFile()[monorepoPluginName]?.commitHash).toBe('oldmonooldmonooldmonooldmonooldmonoold');
|
|
1222
|
+
});
|
|
1223
|
+
it('relinks monorepo plugins when the updated manifest moves their subPath', () => {
|
|
1224
|
+
const oldSubDir = path.join(monorepoRepoDir, 'packages', 'old-alpha');
|
|
1225
|
+
fs.mkdirSync(oldSubDir, { recursive: true });
|
|
1226
|
+
fs.writeFileSync(path.join(oldSubDir, 'old.yaml'), 'site: old\nname: old\n');
|
|
1227
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
1228
|
+
fs.symlinkSync(oldSubDir, monorepoLink, 'dir');
|
|
1229
|
+
const lock = _readLockFile();
|
|
1230
|
+
lock[monorepoPluginName] = {
|
|
1231
|
+
source: {
|
|
1232
|
+
kind: 'monorepo',
|
|
1233
|
+
url: 'https://github.com/user/opencli-plugins-__test-transactional-mono-update__.git',
|
|
1234
|
+
repoName: monorepoName,
|
|
1235
|
+
subPath: 'packages/old-alpha',
|
|
1236
|
+
},
|
|
1237
|
+
commitHash: 'oldmonooldmonooldmonooldmonooldmonoold',
|
|
1238
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
1239
|
+
};
|
|
1240
|
+
_writeLockFile(lock);
|
|
1241
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1242
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1243
|
+
const cloneDir = String(args[4]);
|
|
1244
|
+
const movedDir = path.join(cloneDir, 'packages', 'moved-alpha');
|
|
1245
|
+
fs.mkdirSync(movedDir, { recursive: true });
|
|
1246
|
+
fs.writeFileSync(path.join(cloneDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1247
|
+
plugins: {
|
|
1248
|
+
[monorepoPluginName]: { path: 'packages/moved-alpha' },
|
|
1249
|
+
},
|
|
1250
|
+
}));
|
|
1251
|
+
fs.writeFileSync(path.join(movedDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1252
|
+
return '';
|
|
1253
|
+
}
|
|
1254
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1255
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1256
|
+
}
|
|
1257
|
+
return '';
|
|
1258
|
+
});
|
|
1259
|
+
updatePlugin(monorepoPluginName);
|
|
1260
|
+
const expectedTarget = path.join(monorepoRepoDir, 'packages', 'moved-alpha');
|
|
1261
|
+
expect(fs.realpathSync(monorepoLink)).toBe(fs.realpathSync(expectedTarget));
|
|
1262
|
+
expect(_readLockFile()[monorepoPluginName]?.source).toMatchObject({
|
|
1263
|
+
kind: 'monorepo',
|
|
1264
|
+
subPath: 'packages/moved-alpha',
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
it('rolls back the monorepo repo swap when relinking fails', () => {
|
|
1268
|
+
const oldSubDir = path.join(monorepoRepoDir, 'packages', 'old-alpha');
|
|
1269
|
+
fs.mkdirSync(oldSubDir, { recursive: true });
|
|
1270
|
+
fs.writeFileSync(path.join(oldSubDir, 'old.yaml'), 'site: old\nname: old\n');
|
|
1271
|
+
fs.mkdirSync(monorepoLink, { recursive: true });
|
|
1272
|
+
fs.writeFileSync(path.join(monorepoLink, 'blocker.txt'), 'not a symlink');
|
|
1273
|
+
const lock = _readLockFile();
|
|
1274
|
+
lock[monorepoPluginName] = {
|
|
1275
|
+
source: {
|
|
1276
|
+
kind: 'monorepo',
|
|
1277
|
+
url: 'https://github.com/user/opencli-plugins-__test-transactional-mono-update__.git',
|
|
1278
|
+
repoName: monorepoName,
|
|
1279
|
+
subPath: 'packages/old-alpha',
|
|
1280
|
+
},
|
|
1281
|
+
commitHash: 'oldmonooldmonooldmonooldmonooldmonoold',
|
|
1282
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
1283
|
+
};
|
|
1284
|
+
_writeLockFile(lock);
|
|
1285
|
+
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
1286
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
1287
|
+
const cloneDir = String(args[4]);
|
|
1288
|
+
const movedDir = path.join(cloneDir, 'packages', 'moved-alpha');
|
|
1289
|
+
fs.mkdirSync(movedDir, { recursive: true });
|
|
1290
|
+
fs.writeFileSync(path.join(cloneDir, 'opencli-plugin.json'), JSON.stringify({
|
|
1291
|
+
plugins: {
|
|
1292
|
+
[monorepoPluginName]: { path: 'packages/moved-alpha' },
|
|
1293
|
+
},
|
|
1294
|
+
}));
|
|
1295
|
+
fs.writeFileSync(path.join(movedDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
1296
|
+
return '';
|
|
1297
|
+
}
|
|
1298
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
1299
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
1300
|
+
}
|
|
1301
|
+
return '';
|
|
1302
|
+
});
|
|
1303
|
+
expect(() => updatePlugin(monorepoPluginName)).toThrow('to be a symlink');
|
|
1304
|
+
expect(fs.existsSync(path.join(monorepoRepoDir, 'packages', 'old-alpha', 'old.yaml'))).toBe(true);
|
|
1305
|
+
expect(fs.existsSync(path.join(monorepoRepoDir, 'packages', 'moved-alpha'))).toBe(false);
|
|
1306
|
+
expect(fs.readFileSync(path.join(monorepoLink, 'blocker.txt'), 'utf-8')).toBe('not a symlink');
|
|
1307
|
+
expect(_readLockFile()[monorepoPluginName]?.source).toMatchObject({
|
|
1308
|
+
kind: 'monorepo',
|
|
1309
|
+
subPath: 'packages/old-alpha',
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
});
|