@nexical/cli 0.11.10 → 0.11.15
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/{chunk-Q7YLW5HJ.js → chunk-GEESHGE4.js} +36 -3
- package/dist/chunk-GEESHGE4.js.map +1 -0
- package/dist/chunk-GUUPSHWC.js +70 -0
- package/dist/chunk-GUUPSHWC.js.map +1 -0
- package/dist/index.js +14 -12
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.js +11 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.js +6 -5
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/setup.js +4 -61
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/utils/git.d.ts +2 -1
- package/dist/src/utils/git.js +3 -1
- package/package.json +14 -12
- package/src/commands/init.ts +7 -0
- package/src/commands/module/add.ts +2 -2
- package/src/commands/setup.ts +6 -13
- package/src/utils/git.ts +43 -2
- package/test/e2e/lifecycle.e2e.test.ts +5 -4
- package/test/integration/commands/init.integration.test.ts +3 -0
- package/test/unit/commands/deploy.test.ts +70 -0
- package/test/unit/commands/init.test.ts +19 -1
- package/test/unit/commands/module/add.test.ts +220 -20
- package/test/unit/commands/module/list.test.ts +205 -0
- package/test/unit/commands/module/remove.test.ts +30 -0
- package/test/unit/commands/run.test.ts +7 -0
- package/test/unit/commands/setup.test.ts +43 -1
- package/test/unit/deploy/registry.test.ts +41 -0
- package/test/unit/utils/git.test.ts +88 -0
- package/test/unit/utils/git_utils.test.ts +7 -0
- package/dist/chunk-Q7YLW5HJ.js.map +0 -1
|
@@ -130,4 +130,209 @@ describe('ModuleListCommand', () => {
|
|
|
130
130
|
{ name: 'mod-a', type: 'backend', version: 'unknown', description: '' },
|
|
131
131
|
]);
|
|
132
132
|
});
|
|
133
|
+
it('should handle missing metadata files', async () => {
|
|
134
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
135
|
+
if (p.includes('apps/backend/modules')) return true; // loc.path exists
|
|
136
|
+
return false; // metadata files don't exist
|
|
137
|
+
});
|
|
138
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
139
|
+
if (p.includes('apps/backend/modules')) return ['mod-no-meta'];
|
|
140
|
+
return [];
|
|
141
|
+
});
|
|
142
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
143
|
+
isDirectory: () => true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await command.run();
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
149
|
+
{ name: 'mod-no-meta', type: 'backend', version: 'unknown', description: '' },
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should support .yml extension for module config', async () => {
|
|
154
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
155
|
+
const pStr = p.toString();
|
|
156
|
+
if (
|
|
157
|
+
pStr.endsWith('backend/modules') ||
|
|
158
|
+
pStr.endsWith('frontend/modules') ||
|
|
159
|
+
pStr.endsWith('legacy/modules')
|
|
160
|
+
)
|
|
161
|
+
return true;
|
|
162
|
+
if (pStr.endsWith('yml-mod')) return true;
|
|
163
|
+
if (pStr.endsWith('module.yml')) return true;
|
|
164
|
+
if (pStr.endsWith('module.yaml')) return false;
|
|
165
|
+
if (pStr.endsWith('package.json')) return false;
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
169
|
+
if (p.includes('apps/backend/modules')) return ['yml-mod'];
|
|
170
|
+
return [];
|
|
171
|
+
});
|
|
172
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
173
|
+
isDirectory: () => true,
|
|
174
|
+
});
|
|
175
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
176
|
+
if (p.endsWith('module.yml')) return 'name: YmlName\nversion: 1.2.3';
|
|
177
|
+
return '';
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await command.run();
|
|
181
|
+
// eslint-disable-next-line no-console
|
|
182
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
183
|
+
{ name: 'YmlName', type: 'backend', version: '1.2.3', description: '' },
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
it('should handle both .yaml and .yml existing (favoring .yaml)', async () => {
|
|
187
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
188
|
+
const pStr = p.toString();
|
|
189
|
+
if (
|
|
190
|
+
pStr.endsWith('backend/modules') ||
|
|
191
|
+
pStr.endsWith('frontend/modules') ||
|
|
192
|
+
pStr.endsWith('legacy/modules')
|
|
193
|
+
)
|
|
194
|
+
return true;
|
|
195
|
+
if (pStr.endsWith('dual-mod')) return true;
|
|
196
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
197
|
+
if (pStr.endsWith('module.yml')) return true;
|
|
198
|
+
return false;
|
|
199
|
+
});
|
|
200
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
201
|
+
if (p.includes('apps/backend/modules')) return ['dual-mod'];
|
|
202
|
+
return [];
|
|
203
|
+
});
|
|
204
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
205
|
+
isDirectory: () => true,
|
|
206
|
+
});
|
|
207
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
208
|
+
if (p.endsWith('module.yaml')) return 'name: YamlName';
|
|
209
|
+
if (p.endsWith('module.yml')) return 'name: YmlName';
|
|
210
|
+
return '';
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await command.run();
|
|
214
|
+
// eslint-disable-next-line no-console
|
|
215
|
+
expect(console.table).toHaveBeenCalledWith(
|
|
216
|
+
expect.arrayContaining([expect.objectContaining({ name: 'YamlName' })]),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
it('should handle only .yaml existing', async () => {
|
|
220
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
221
|
+
const pStr = p.toString();
|
|
222
|
+
if (pStr.endsWith('backend/modules')) return true;
|
|
223
|
+
if (pStr.endsWith('yaml-only')) return true;
|
|
224
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
225
|
+
if (pStr.endsWith('module.yml')) return false;
|
|
226
|
+
return false;
|
|
227
|
+
});
|
|
228
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
229
|
+
if (p.includes('apps/backend/modules')) return ['yaml-only'];
|
|
230
|
+
return [];
|
|
231
|
+
});
|
|
232
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
233
|
+
isDirectory: () => true,
|
|
234
|
+
});
|
|
235
|
+
(fs.readFile as any).mockResolvedValue('name: YamlOnly');
|
|
236
|
+
await command.run();
|
|
237
|
+
expect(console.table).toHaveBeenCalledWith(
|
|
238
|
+
expect.arrayContaining([expect.objectContaining({ name: 'YamlOnly' })]),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should handle both .yaml and .yml missing', async () => {
|
|
243
|
+
(fs.pathExists as any).mockResolvedValue(false);
|
|
244
|
+
(fs.readdir as any).mockImplementation((p: string) => {
|
|
245
|
+
if (p.includes('modules')) return ['no-config'];
|
|
246
|
+
return [];
|
|
247
|
+
});
|
|
248
|
+
(fs.stat as any).mockResolvedValue({ isDirectory: () => true });
|
|
249
|
+
await command.run();
|
|
250
|
+
// Should skip it and NOT call console.table since length 0
|
|
251
|
+
expect(console.table).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
it('should handle all 8 permutations of metadata existence', async () => {
|
|
254
|
+
const mods = [
|
|
255
|
+
{ id: 't-t-t', pkg: true, yaml: true, yml: true },
|
|
256
|
+
{ id: 't-t-f', pkg: true, yaml: true, yml: false },
|
|
257
|
+
{ id: 't-f-t', pkg: true, yaml: false, yml: true },
|
|
258
|
+
{ id: 't-f-f', pkg: true, yaml: false, yml: false },
|
|
259
|
+
{ id: 'f-t-t', pkg: false, yaml: true, yml: true },
|
|
260
|
+
{ id: 'f-t-f', pkg: false, yaml: true, yml: false },
|
|
261
|
+
{ id: 'f-f-t', pkg: false, yaml: false, yml: true },
|
|
262
|
+
{ id: 'f-f-f', pkg: false, yaml: false, yml: false },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
(fs.pathExists as any).mockImplementation((p: string) => {
|
|
266
|
+
const pStr = p.toString();
|
|
267
|
+
if (pStr.endsWith('modules')) return true;
|
|
268
|
+
const mod = mods.find((m) => pStr.includes(m.id));
|
|
269
|
+
if (!mod) return false;
|
|
270
|
+
if (pStr.endsWith('package.json')) return mod.pkg;
|
|
271
|
+
if (pStr.endsWith('module.yaml')) return mod.yaml;
|
|
272
|
+
if (pStr.endsWith('module.yml')) return mod.yml;
|
|
273
|
+
return true; // The directory itself
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
(fs.readdir as any).mockImplementation((p: string) => {
|
|
277
|
+
if (p.includes('apps/backend/modules')) return mods.map((m) => m.id);
|
|
278
|
+
return [];
|
|
279
|
+
});
|
|
280
|
+
(fs.stat as any).mockResolvedValue({ isDirectory: () => true });
|
|
281
|
+
|
|
282
|
+
(fs.readJson as any).mockImplementation((p: string) => {
|
|
283
|
+
const mod = mods.find((m) => p.includes(m.id));
|
|
284
|
+
if (mod?.pkg) return { version: `pkg-${mod.id}` };
|
|
285
|
+
return {};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
(fs.readFile as any).mockImplementation((p: string) => {
|
|
289
|
+
const mod = mods.find((m) => p.includes(m.id));
|
|
290
|
+
if (!mod) return '';
|
|
291
|
+
const isYaml = p.endsWith('module.yaml');
|
|
292
|
+
const name = isYaml ? `yaml-${mod.id}` : `yml-${mod.id}`;
|
|
293
|
+
const version = isYaml ? `v-yaml-${mod.id}` : `v-yml-${mod.id}`;
|
|
294
|
+
return `name: ${name}\nversion: ${version}`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await command.run();
|
|
298
|
+
|
|
299
|
+
// Verify specific precedence
|
|
300
|
+
expect(console.table).toHaveBeenCalledWith(
|
|
301
|
+
expect.arrayContaining([
|
|
302
|
+
expect.objectContaining({ name: 'yaml-t-t-t', version: 'pkg-t-t-t' }), // pkg version precedence, yaml name precedence
|
|
303
|
+
expect.objectContaining({ name: 'yml-f-f-t', version: 'v-yml-f-f-t' }), // yml used if yaml/pkg missing
|
|
304
|
+
expect.objectContaining({ name: 'f-f-f', version: 'unknown' }), // none
|
|
305
|
+
]),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should cover catch blocks and falsy returns in all paths', async () => {
|
|
310
|
+
(fs.pathExists as any).mockImplementation((p: string) => {
|
|
311
|
+
const pStr = p.toString();
|
|
312
|
+
if (pStr.includes('apps/backend/modules')) return true;
|
|
313
|
+
return true; // Every candidate exists to trigger catch blocks
|
|
314
|
+
});
|
|
315
|
+
(fs.readdir as any).mockImplementation((p: string) => {
|
|
316
|
+
if (p.includes('apps/backend/modules')) return ['fail-json', 'fail-yaml'];
|
|
317
|
+
return [];
|
|
318
|
+
});
|
|
319
|
+
(fs.stat as any).mockResolvedValue({ isDirectory: () => true });
|
|
320
|
+
|
|
321
|
+
(fs.readJson as any).mockImplementation((p: string) => {
|
|
322
|
+
if (p.includes('fail-json')) throw new Error('json fail');
|
|
323
|
+
return null;
|
|
324
|
+
});
|
|
325
|
+
(fs.readFile as any).mockImplementation((p: string) => {
|
|
326
|
+
if (p.includes('fail-yaml')) throw new Error('yaml fail');
|
|
327
|
+
return '';
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await command.run();
|
|
331
|
+
expect(console.table).toHaveBeenCalledWith(
|
|
332
|
+
expect.arrayContaining([
|
|
333
|
+
expect.objectContaining({ name: 'fail-json', version: 'unknown' }),
|
|
334
|
+
expect.objectContaining({ name: 'fail-yaml', version: 'unknown' }),
|
|
335
|
+
]),
|
|
336
|
+
);
|
|
337
|
+
});
|
|
133
338
|
});
|
|
@@ -178,4 +178,34 @@ describe('ModuleRemoveCommand', () => {
|
|
|
178
178
|
expect(fs.readFile).not.toHaveBeenCalled();
|
|
179
179
|
expect(command.success).toHaveBeenCalledWith(expect.stringContaining('removed successfully'));
|
|
180
180
|
});
|
|
181
|
+
it('should handle missing modules key in config', async () => {
|
|
182
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
183
|
+
(fs.readFile as any).mockResolvedValue('key: value');
|
|
184
|
+
|
|
185
|
+
await command.run({ name: 'test-mod' });
|
|
186
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should do nothing if module not found in config lists', async () => {
|
|
190
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
191
|
+
(fs.readFile as any).mockResolvedValue('modules:\n backend:\n - existing-mod');
|
|
192
|
+
|
|
193
|
+
await command.run({ name: 'other-mod' });
|
|
194
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle empty or null config from YAML.parse', async () => {
|
|
198
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
199
|
+
(fs.readFile as any).mockResolvedValue(''); // YAML.parse('') -> null
|
|
200
|
+
|
|
201
|
+
await command.run({ name: 'test-mod' });
|
|
202
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
it('should handle legacy array without the module to remove', async () => {
|
|
205
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
206
|
+
(fs.readFile as any).mockResolvedValue('modules:\n - other-mod');
|
|
207
|
+
|
|
208
|
+
await command.run({ name: 'test-mod' });
|
|
209
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
181
211
|
});
|
|
@@ -316,4 +316,11 @@ describe('RunCommand', () => {
|
|
|
316
316
|
expect.stringContaining('Failed to read package.json at /mock/root: String error'),
|
|
317
317
|
);
|
|
318
318
|
});
|
|
319
|
+
|
|
320
|
+
it('should error if module not found', async () => {
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
322
|
+
(fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
|
|
323
|
+
await command.run({ script: 'nonexistent:sync', args: [] });
|
|
324
|
+
expect(command.error).toHaveBeenCalledWith('Module nonexistent not found.');
|
|
325
|
+
});
|
|
319
326
|
});
|
|
@@ -103,7 +103,7 @@ describe('SetupCommand', () => {
|
|
|
103
103
|
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as unknown as any);
|
|
104
104
|
|
|
105
105
|
const error = new Error('Permission denied');
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
(error as unknown as { code: string }).code = 'EACCES';
|
|
108
108
|
vi.mocked(fs.removeSync).mockImplementationOnce(() => {
|
|
109
109
|
throw error;
|
|
@@ -114,6 +114,48 @@ describe('SetupCommand', () => {
|
|
|
114
114
|
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it('should ignore ENOENT error during removal', async () => {
|
|
118
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as unknown as any);
|
|
121
|
+
|
|
122
|
+
const error = new Error('Not found');
|
|
123
|
+
(error as unknown as { code: string }).code = 'ENOENT';
|
|
124
|
+
vi.mocked(fs.removeSync).mockImplementationOnce(() => {
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await command.run();
|
|
129
|
+
|
|
130
|
+
// Should not log error in outer block as it was re-thrown only for non-ENOENT
|
|
131
|
+
// Wait, if it's NOT re-thrown (ENOENT), it continues to symlink.
|
|
132
|
+
expect(fs.symlink).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should re-throw non-object as error during removal', async () => {
|
|
136
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as unknown as any);
|
|
139
|
+
vi.mocked(fs.removeSync).mockImplementationOnce(() => {
|
|
140
|
+
throw 'string fail';
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await command.run();
|
|
144
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('string fail'));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should re-throw error without code during removal', async () => {
|
|
148
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as unknown as any);
|
|
151
|
+
vi.mocked(fs.removeSync).mockImplementationOnce(() => {
|
|
152
|
+
throw new Error('No code');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await command.run();
|
|
156
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('No code'));
|
|
157
|
+
});
|
|
158
|
+
|
|
117
159
|
it('should log error if symlink fails with Error object', async () => {
|
|
118
160
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
119
161
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -223,5 +223,46 @@ describe('ProviderRegistry', () => {
|
|
|
223
223
|
expect.stringContaining('Failed to load local provider'),
|
|
224
224
|
);
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
it('should handle non-Error exceptions when loading local provider', async () => {
|
|
228
|
+
const mockRoot = '/mock/root';
|
|
229
|
+
vi.spyOn(fs, 'readdir').mockResolvedValue(['broken.ts'] as any);
|
|
230
|
+
mockJitiRequest.mockRejectedValue('String fail');
|
|
231
|
+
|
|
232
|
+
await registry.loadLocalProviders(mockRoot);
|
|
233
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
234
|
+
expect.stringContaining('Failed to load local provider from broken.ts: String fail'),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('Core non-Error cases', () => {
|
|
240
|
+
it('should handle non-Error in loadCoreProviders readdir', async () => {
|
|
241
|
+
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
|
242
|
+
vi.spyOn(fs, 'readdir').mockRejectedValue('Readdir string fail');
|
|
243
|
+
await registry.loadCoreProviders();
|
|
244
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Readdir string fail'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should handle non-Error during registration in loadCoreProviders', async () => {
|
|
248
|
+
vi.spyOn(fs, 'access').mockResolvedValue(undefined);
|
|
249
|
+
vi.spyOn(fs, 'readdir').mockResolvedValue(['fail-registration.js'] as any);
|
|
250
|
+
|
|
251
|
+
// Mock registerProviderFromModule to throw a string
|
|
252
|
+
const regSpy = vi
|
|
253
|
+
.spyOn(registry as any, 'registerProviderFromModule')
|
|
254
|
+
.mockImplementation(() => {
|
|
255
|
+
throw 'Registration string fail';
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Mock path.join to return a known string for vi.doMock
|
|
259
|
+
vi.spyOn(path, 'join').mockReturnValue('MOCK_REG_FAIL');
|
|
260
|
+
vi.doMock('MOCK_REG_FAIL', () => ({ default: {} }));
|
|
261
|
+
|
|
262
|
+
await registry.loadCoreProviders();
|
|
263
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Registration string fail'));
|
|
264
|
+
|
|
265
|
+
regSpy.mockRestore();
|
|
266
|
+
});
|
|
226
267
|
});
|
|
227
268
|
});
|
|
@@ -51,15 +51,59 @@ describe('git utils', () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('should clone repository', async () => {
|
|
54
|
+
// Mock anonymous to fail to test fallback
|
|
55
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
56
|
+
callback(new Error('Anonymous failed'), '', '');
|
|
57
|
+
});
|
|
58
|
+
|
|
54
59
|
await git.clone('http://repo.git', 'dest', { recursive: true });
|
|
55
60
|
expect(runCommand).toHaveBeenCalledWith('git clone --recursive http://repo.git .', 'dest');
|
|
56
61
|
});
|
|
57
62
|
|
|
58
63
|
it('should clone repository with depth', async () => {
|
|
64
|
+
// Mock anonymous to fail to test fallback
|
|
65
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
66
|
+
callback(new Error('Anonymous failed'), '', '');
|
|
67
|
+
});
|
|
68
|
+
|
|
59
69
|
await git.clone('http://repo.git', 'dest', { depth: 1 });
|
|
60
70
|
expect(runCommand).toHaveBeenCalledWith('git clone --depth 1 http://repo.git .', 'dest');
|
|
61
71
|
});
|
|
62
72
|
|
|
73
|
+
it('should try anonymous clone first', async () => {
|
|
74
|
+
// Mock anonymous to succeed
|
|
75
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
76
|
+
callback(null, 'Done', '');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await git.clone('http://repo.git', 'dest');
|
|
80
|
+
expect(mocks.exec).toHaveBeenCalledWith(
|
|
81
|
+
expect.stringContaining('-c credential.helper='),
|
|
82
|
+
expect.any(Object),
|
|
83
|
+
expect.any(Function),
|
|
84
|
+
);
|
|
85
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle anonymous clone with empty stdout', async () => {
|
|
89
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
90
|
+
callback(null, '', '');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await git.clone('http://repo.git', 'dest');
|
|
94
|
+
expect(mocks.exec).toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle anonymous clone with non-Error exception', async () => {
|
|
98
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
callback('String error' as any, '', '');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await git.clone('http://repo.git', 'dest');
|
|
104
|
+
expect(runCommand).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
63
107
|
it('should update submodules', async () => {
|
|
64
108
|
await git.updateSubmodules('dest');
|
|
65
109
|
expect(runCommand).toHaveBeenCalledWith(
|
|
@@ -111,6 +155,50 @@ describe('git utils', () => {
|
|
|
111
155
|
const url = await git.getRemoteUrl('cwd');
|
|
112
156
|
expect(url).toBe('');
|
|
113
157
|
});
|
|
158
|
+
|
|
159
|
+
it('should add submodule', async () => {
|
|
160
|
+
// Mock anonymous to fail
|
|
161
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
162
|
+
callback(new Error('Anonymous failed'), '', '');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await git.addSubmodule('url', 'path', 'cwd');
|
|
166
|
+
expect(runCommand).toHaveBeenCalledWith('git submodule add url path', 'cwd');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should try anonymous submodule add first', async () => {
|
|
170
|
+
// Mock anonymous to succeed
|
|
171
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
172
|
+
callback(null, 'Done', '');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await git.addSubmodule('url', 'path', 'cwd');
|
|
176
|
+
expect(mocks.exec).toHaveBeenCalledWith(
|
|
177
|
+
expect.stringContaining('-c credential.helper='),
|
|
178
|
+
expect.any(Object),
|
|
179
|
+
expect.any(Function),
|
|
180
|
+
);
|
|
181
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle anonymous submodule add with empty stdout', async () => {
|
|
185
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
186
|
+
callback(null, '', '');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await git.addSubmodule('url', 'path', 'cwd');
|
|
190
|
+
expect(mocks.exec).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle anonymous submodule add with non-Error exception', async () => {
|
|
194
|
+
mocks.exec.mockImplementationOnce((_cmd, _options, callback) => {
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
callback('String error' as any, '', '');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await git.addSubmodule('url', 'path', 'cwd');
|
|
200
|
+
expect(runCommand).toHaveBeenCalled();
|
|
201
|
+
});
|
|
114
202
|
});
|
|
115
203
|
|
|
116
204
|
it('should add all files', async () => {
|
|
@@ -15,6 +15,13 @@ vi.mock('node:child_process', () => ({
|
|
|
15
15
|
|
|
16
16
|
describe('git utils', () => {
|
|
17
17
|
it('should call git clone', async () => {
|
|
18
|
+
const { exec } = await import('node:child_process');
|
|
19
|
+
// Mock anonymous failure
|
|
20
|
+
vi.mocked(exec).mockImplementationOnce(((cmd: string, opts: any, callback: any) => {
|
|
21
|
+
callback(new Error('fail'), '', '');
|
|
22
|
+
return {} as any;
|
|
23
|
+
}) as any);
|
|
24
|
+
|
|
18
25
|
await git.clone('url', 'dest');
|
|
19
26
|
expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git clone'), 'dest');
|
|
20
27
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/git.ts"],"sourcesContent":["import { logger, runCommand } from '@nexical/cli-core';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst execAsync = promisify(exec);\n\nexport async function clone(\n url: string,\n destination: string,\n options: { recursive?: boolean; depth?: number } = {},\n): Promise<void> {\n const { recursive = false, depth } = options;\n const cmd = `git clone ${recursive ? '--recursive ' : ''}${depth ? `--depth ${depth} ` : ''}${url} .`;\n logger.debug(`Git clone: ${url} to ${destination}`);\n await runCommand(cmd, destination);\n}\n\nexport async function getRemoteUrl(cwd: string, remote = 'origin'): Promise<string> {\n try {\n const { stdout } = await execAsync(`git remote get-url ${remote}`, { cwd });\n return stdout.trim();\n } catch (e) {\n console.error('getRemoteUrl failed:', e);\n return '';\n }\n}\n\nexport async function updateSubmodules(cwd: string): Promise<void> {\n logger.debug(`Updating submodules in ${cwd}`);\n await runCommand(\n 'git submodule foreach --recursive \"git checkout main && git pull origin main\"',\n cwd,\n );\n}\n\nexport async function checkoutOrphan(branch: string, cwd: string): Promise<void> {\n await runCommand(`git checkout --orphan ${branch}`, cwd);\n}\n\nexport async function addAll(cwd: string): Promise<void> {\n await runCommand('git add -A', cwd);\n}\n\nexport async function commit(message: string, cwd: string): Promise<void> {\n // Escape quotes in message if needed, for now assuming simple messages\n await runCommand(`git commit -m \"${message}\"`, cwd);\n}\n\nexport async function deleteBranch(branch: string, cwd: string): Promise<void> {\n await runCommand(`git branch -D ${branch}`, cwd);\n}\n\nexport async function renameBranch(branch: string, cwd: string): Promise<void> {\n await runCommand(`git branch -m ${branch}`, cwd);\n}\n\nexport async function removeRemote(remote: string, cwd: string): Promise<void> {\n await runCommand(`git remote remove ${remote}`, cwd);\n}\n\nexport async function renameRemote(oldName: string, newName: string, cwd: string): Promise<void> {\n await runCommand(`git remote rename ${oldName} ${newName}`, cwd);\n}\n\nexport async function branchExists(branch: string, cwd: string): Promise<boolean> {\n try {\n await execAsync(`git show-ref --verify --quiet refs/heads/${branch}`, { cwd });\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,QAAQ,kBAAkB;AACnC,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAE1B,IAAM,YAAY,UAAU,IAAI;AAEhC,eAAsB,MACpB,KACA,aACA,UAAmD,CAAC,GACrC;AACf,QAAM,EAAE,YAAY,OAAO,MAAM,IAAI;AACrC,QAAM,MAAM,aAAa,YAAY,iBAAiB,EAAE,GAAG,QAAQ,WAAW,KAAK,MAAM,EAAE,GAAG,GAAG;AACjG,SAAO,MAAM,cAAc,GAAG,OAAO,WAAW,EAAE;AAClD,QAAM,WAAW,KAAK,WAAW;AACnC;AAEA,eAAsB,aAAa,KAAa,SAAS,UAA2B;AAClF,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,UAAU,sBAAsB,MAAM,IAAI,EAAE,IAAI,CAAC;AAC1E,WAAO,OAAO,KAAK;AAAA,EACrB,SAAS,GAAG;AACV,YAAQ,MAAM,wBAAwB,CAAC;AACvC,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAiB,KAA4B;AACjE,SAAO,MAAM,0BAA0B,GAAG,EAAE;AAC5C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,eAAe,QAAgB,KAA4B;AAC/E,QAAM,WAAW,yBAAyB,MAAM,IAAI,GAAG;AACzD;AAEA,eAAsB,OAAO,KAA4B;AACvD,QAAM,WAAW,cAAc,GAAG;AACpC;AAEA,eAAsB,OAAO,SAAiB,KAA4B;AAExE,QAAM,WAAW,kBAAkB,OAAO,KAAK,GAAG;AACpD;AAEA,eAAsB,aAAa,QAAgB,KAA4B;AAC7E,QAAM,WAAW,iBAAiB,MAAM,IAAI,GAAG;AACjD;AAEA,eAAsB,aAAa,QAAgB,KAA4B;AAC7E,QAAM,WAAW,iBAAiB,MAAM,IAAI,GAAG;AACjD;AAEA,eAAsB,aAAa,QAAgB,KAA4B;AAC7E,QAAM,WAAW,qBAAqB,MAAM,IAAI,GAAG;AACrD;AAEA,eAAsB,aAAa,SAAiB,SAAiB,KAA4B;AAC/F,QAAM,WAAW,qBAAqB,OAAO,IAAI,OAAO,IAAI,GAAG;AACjE;AAEA,eAAsB,aAAa,QAAgB,KAA+B;AAChF,MAAI;AACF,UAAM,UAAU,4CAA4C,MAAM,IAAI,EAAE,IAAI,CAAC;AAC7E,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|