borgmcp 2.6.1 → 2.7.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.
@@ -33,6 +33,17 @@ export interface RepositoryCubeCreation {
33
33
  cube: RepositoryCubeDetail;
34
34
  }
35
35
 
36
+ type NewCubeTemplate = Exclude<CubeTemplate, 'default'>;
37
+
38
+ const NEW_CUBE_TEMPLATE_NAMES: readonly NewCubeTemplate[] =
39
+ NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name);
40
+ const NEW_CUBE_TEMPLATE_OPTIONS = NEW_CUBE_TEMPLATE_NAMES.join('|');
41
+ const NEW_CUBE_TEMPLATE_LIST = NEW_CUBE_TEMPLATE_NAMES.join(', ');
42
+
43
+ function parseNewCubeTemplate(value: string): NewCubeTemplate | undefined {
44
+ return NEW_CUBE_TEMPLATE_PRESENTATIONS.find(({ name }) => name === value)?.name;
45
+ }
46
+
36
47
  export type RepositoryCubeResolution = ResolveRepositoryCubeResponse;
37
48
 
38
49
  export interface RepositoryCubeInitFlags {
@@ -63,7 +74,7 @@ export interface RepositoryCubeInitDeps {
63
74
  name: string;
64
75
  workingRepoName: string;
65
76
  repository: CreateCubeRepository;
66
- template: Exclude<CubeTemplate, 'default'>;
77
+ template: NewCubeTemplate;
67
78
  }): Promise<RepositoryCubeCreation>;
68
79
  }
69
80
 
@@ -158,7 +169,7 @@ function renderResult(
158
169
  `Next: borg assimilate --host ${shellEscape(input.serverOrigin)}`,
159
170
  );
160
171
  } else {
161
- lines.push('Continuing with role and seat setup...');
172
+ lines.push('Continuing with role and seat setup');
162
173
  }
163
174
  resultWriter(`${lines.join('\n')}\n`);
164
175
  }
@@ -176,6 +187,7 @@ export async function initializeRepositoryCube(input: {
176
187
  input.flags.template !== undefined || input.flags.noTemplate === true || input.flags.yes === true;
177
188
  const creationOptionsUnused = association !== null && creationOptionsRequested;
178
189
  if (association) {
190
+ deps.write(`Checking for this repository's cube on ${input.serverOrigin}…\n`);
179
191
  const cube = await deps.getCube(association.cubeId);
180
192
  const response: CreateCubeResponse = {
181
193
  result: 'resolved',
@@ -241,6 +253,7 @@ export async function initializeRepositoryCube(input: {
241
253
  return { kind: 'success', creation: { response, cube }, existing: true };
242
254
  };
243
255
 
256
+ deps.write(`Checking for this repository's cube on ${input.serverOrigin}…\n`);
244
257
  const serverAssociation = await deps.resolveAssociation(repository, input.context.derivedName);
245
258
  if (serverAssociation.result === 'resolved') {
246
259
  return saveResolvedAssociation(serverAssociation, {
@@ -263,9 +276,14 @@ export async function initializeRepositoryCube(input: {
263
276
  return { kind: 'stop', code: 1 };
264
277
  }
265
278
  if (!deps.isTTY() || input.flags.yes) {
279
+ const retryCommand = input.mode === 'cube-init'
280
+ ? `borg server cube init --host ${shellEscape(input.serverOrigin)}`
281
+ : `borg assimilate --host ${shellEscape(input.serverOrigin)}`;
266
282
  deps.write(
267
- 'Adopting an existing cube requires interactive confirmation; --yes is not accepted here.\n' +
268
- 'Rerun without --yes in an interactive terminal. No cube, repository binding, or drone was created.\n',
283
+ `Found existing cube '${matches[0].name}' on ${input.serverOrigin}.\n` +
284
+ 'Linking a repository to an existing cube requires one interactive confirmation.\n' +
285
+ `Run ${retryCommand} --cube-name ${shellEscape(matches[0].name)} once in an interactive terminal to link it; scripted runs work from then on.\n` +
286
+ 'No cube, repository binding, or drone was created.\n',
269
287
  );
270
288
  return { kind: 'stop', code: 1 };
271
289
  }
@@ -276,16 +294,17 @@ export async function initializeRepositoryCube(input: {
276
294
  ` server: ${input.serverOrigin}\n`,
277
295
  );
278
296
  while (true) {
279
- const answer = await ask(deps, 'Link this repository to that cube? [y/N]: ', 'adoption');
297
+ const answer = await ask(deps, 'Link this repository to that cube? [Y/n]: ', 'adoption');
280
298
  if ('stop' in answer) return { kind: 'stop', code: answer.stop };
281
299
  const confirmation = answer.value.trim().toLowerCase();
282
- if (confirmation === 'y' || confirmation === 'yes') break;
283
- if (confirmation === '' || confirmation === 'n' || confirmation === 'no') {
300
+ if (confirmation === '' || confirmation === 'y' || confirmation === 'yes') break;
301
+ if (confirmation === 'n' || confirmation === 'no') {
284
302
  deps.write('No cube, repository binding, or drone was created.\n');
285
303
  return { kind: 'stop', code: 0 };
286
304
  }
287
305
  deps.write('Enter y or n.\n');
288
306
  }
307
+ deps.write(`Linking this repository to cube '${matches[0].name}'…\n`);
289
308
  const associated = await deps.associateCube({
290
309
  cubeId: matches[0].id,
291
310
  workingRepoName: input.context.derivedName,
@@ -299,7 +318,11 @@ export async function initializeRepositoryCube(input: {
299
318
  if (confirmed.result !== 'resolved' || confirmed.cube_id !== associated.cube_id) {
300
319
  throw new RepositoryAssociationConfirmationError();
301
320
  }
302
- return await saveResolvedAssociation(confirmed, { adopted: true, creationOptionsUnused: creationOptionsRequested });
321
+ return await saveResolvedAssociation(confirmed, {
322
+ adopted: true,
323
+ creationOptionsUnused: input.flags.template !== undefined ||
324
+ input.flags.noTemplate === true,
325
+ });
303
326
  } catch (error) {
304
327
  if (error instanceof RepositoryAssociationSaveError) throw error;
305
328
  throw new RepositoryAssociationConfirmationError();
@@ -320,16 +343,19 @@ export async function initializeRepositoryCube(input: {
320
343
  }
321
344
 
322
345
  if (input.flags.noTemplate) {
323
- deps.write('--no-template is not supported for repository cube creation. Use --template software-dev or --template starter.\n');
346
+ deps.write(`--no-template is not supported for repository cube creation. Use --template ${NEW_CUBE_TEMPLATE_OPTIONS}.\n`);
324
347
  return { kind: 'stop', code: 1 };
325
348
  }
326
- if (input.flags.template !== undefined && input.flags.template !== 'software-dev' && input.flags.template !== 'starter') {
349
+ const requestedTemplate = input.flags.template === undefined
350
+ ? undefined
351
+ : parseNewCubeTemplate(input.flags.template);
352
+ if (input.flags.template !== undefined && requestedTemplate === undefined) {
327
353
  const safe = input.flags.template.replace(/[\u0000-\u001f\u007f]/g, '?').slice(0, 120);
328
- deps.write(`Unknown template '${safe}'. Use software-dev or starter.\n`);
354
+ deps.write(`Unknown template '${safe}'. Available templates: ${NEW_CUBE_TEMPLATE_LIST}.\n`);
329
355
  return { kind: 'stop', code: 1 };
330
356
  }
331
357
  if (!deps.isTTY() && !input.flags.yes && (!input.flags.cubeName || !input.flags.template)) {
332
- deps.write('Non-interactive cube creation requires --cube-name <name> and --template software-dev|starter, or --yes to use repository defaults.\n');
358
+ deps.write(`Non-interactive cube creation requires --cube-name <name> and --template ${NEW_CUBE_TEMPLATE_OPTIONS}, or --yes to use repository defaults.\n`);
333
359
  return { kind: 'stop', code: 1 };
334
360
  }
335
361
 
@@ -358,7 +384,7 @@ export async function initializeRepositoryCube(input: {
358
384
  if (editedNameAdoption) return editedNameAdoption;
359
385
  }
360
386
 
361
- let template = input.flags.template as 'software-dev' | 'starter' | undefined;
387
+ let template = requestedTemplate;
362
388
  if (!template && deps.isTTY() && !input.flags.yes) {
363
389
  let menu = 'Choose a template:\n';
364
390
  for (let i = 0; i < NEW_CUBE_TEMPLATE_PRESENTATIONS.length; i += 1) {
@@ -372,24 +398,23 @@ export async function initializeRepositoryCube(input: {
372
398
  const answer = await ask(deps, 'Template [1]: ');
373
399
  if ('stop' in answer) return { kind: 'stop', code: answer.stop };
374
400
  const selected = answer.value.trim();
375
- if (selected === '' || selected === '1') template = 'software-dev';
376
- else if (selected === '2') template = 'starter';
377
- else deps.write('Choose 1 or 2.\n');
401
+ const selectedIndex = selected === ''
402
+ ? 0
403
+ : /^[1-9]\d*$/.test(selected) ? Number(selected) - 1 : -1;
404
+ const selectedPresentation = NEW_CUBE_TEMPLATE_PRESENTATIONS[selectedIndex];
405
+ if (selectedPresentation) template = selectedPresentation.name;
406
+ else deps.write(`Choose 1-${NEW_CUBE_TEMPLATE_PRESENTATIONS.length}.\n`);
378
407
  }
379
408
  }
380
- template ??= 'software-dev';
409
+ template ??= NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
381
410
 
382
411
  if (deps.isTTY() && !input.flags.yes) {
383
- deps.write(
384
- `Create this cube?\n` +
385
- ` Name: ${name}\n` +
386
- ` Template: ${presentation(template).label}\n` +
387
- ` Repository: ${input.context.root}\n` +
388
- ` Server: ${input.serverOrigin}\n`,
389
- );
390
412
  let confirmed = false;
391
413
  while (!confirmed) {
392
- const answer = await ask(deps, 'Create cube? [Y/n]: ');
414
+ const answer = await ask(
415
+ deps,
416
+ `Create cube '${name}' (${presentation(template).label}) on ${input.serverOrigin}? [Y/n]: `,
417
+ );
393
418
  if ('stop' in answer) return { kind: 'stop', code: answer.stop };
394
419
  const confirmation = answer.value.trim().toLowerCase();
395
420
  if (confirmation === '' || confirmation === 'y' || confirmation === 'yes') {
@@ -404,7 +429,7 @@ export async function initializeRepositoryCube(input: {
404
429
  }
405
430
  }
406
431
 
407
- deps.write('Creating cube...\n');
432
+ deps.write(`Creating cube '${name}'…\n`);
408
433
  const workingRepoName = input.context.derivedName;
409
434
  let creation: RepositoryCubeCreation;
410
435
  try {
@@ -2,7 +2,7 @@ import { createHmac, randomBytes, randomUUID } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { basename, isAbsolute, join } from 'node:path';
4
4
  import { realpath } from 'node:fs/promises';
5
- import type { CreateCubeRepository, CubeTemplate } from 'borgmcp-shared/protocol';
5
+ import { CUBE_TEMPLATES, type CreateCubeRepository, type CubeTemplate } from 'borgmcp-shared/protocol';
6
6
  import { canonicalizeWorkingRepoIdentity } from './working-repo.js';
7
7
  import { normalizeCubeName } from './cube-name.js';
8
8
  import { borgConfigRoot, ensurePrivateBorgConfigRoot } from './private-root.js';
@@ -118,7 +118,7 @@ function parseState(raw: string | null): RepositoryIdentityState {
118
118
  !DISPLAY_NAME_RE.test(value.name) ||
119
119
  typeof value.workingRepoName !== 'string' || Buffer.byteLength(value.workingRepoName, 'utf8') > 120 ||
120
120
  !DISPLAY_NAME_RE.test(value.workingRepoName) ||
121
- (value.template !== 'software-dev' && value.template !== 'starter' && value.template !== 'default')
121
+ !CUBE_TEMPLATES.some((template) => template === value.template)
122
122
  ) {
123
123
  throw new Error('Borg repository identity store is malformed or unsupported');
124
124
  }