also looking at this
feat(operations): fleet inventory, attach preflight, and the exports that were missing
#192
11 files
+1313
-5
| @@ -358,3 +358,66 @@ function findMatchingBrace(text: string, start: number): number { | ||
| 358 | 358 | |
| 359 | 359 | throw new Error('Could not find the closing brace for sites: { ... } in cloud.config.ts') |
| 360 | 360 | } |
| 361 | ||
| 362 | export interface SetAttachToInput { | |
| 363 | configText: string | |
| 364 | /** Slug of the project that owns the box this one is joining. */ | |
| 365 | owner: string | |
| 366 | } | |
| 367 | ||
| 368 | /** | |
| 369 | * Set `cloud.attachTo`, joining this project to a server another project owns. | |
| 370 | * | |
| 371 | * Deliberately narrow. This edits TypeScript source with text, which is only | |
| 372 | * defensible while it refuses everything it does not certainly understand, so | |
| 373 | * it handles exactly the shape the templates generate: | |
| 374 | * | |
| 375 | * cloud: { | |
| 376 | * provider: 'hetzner', | |
| 377 | * }, | |
| 378 | * | |
| 379 | * Anything else - two `cloud:` blocks, a nested object inside one, no block at | |
| 380 | * all - throws with what it saw, and the caller prints the edit for a person to | |
| 381 | * make. A config mangled by a clever regex is a far worse outcome than a config | |
| 382 | * the tool declined to touch. | |
| 383 | * | |
| 384 | * Idempotent: a config already attached to `owner` comes back byte for byte. | |
| 385 | * | |
| 386 | * @see https://github.com/stacksjs/ts-cloud/issues/167 | |
| 387 | */ | |
| 388 | export function setAttachToInCloudConfig(input: SetAttachToInput): string { | |
| 389 | const { configText, owner } = input | |
| 390 | const blocks = [...configText.matchAll(/\n( {2})cloud: \{\n([\s\S]*?)\n\1\},\n/g)] | |
| 391 | ||
| 392 | const [match] = blocks | |
| 393 | if (!match) throw new Error('No `cloud: { ... }` block found in the cloud config') | |
| 394 | if (blocks.length > 1) { | |
| 395 | throw new Error(`Found ${blocks.length} \`cloud: { ... }\` blocks in the cloud config, so which one to edit is ambiguous`) | |
| 396 | } | |
| 397 | ||
| 398 | const [whole, indent, body] = match | |
| 399 | if (indent === undefined || body === undefined) { | |
| 400 | throw new Error('The `cloud: { ... }` block did not parse into an indent and a body') | |
| 401 | } | |
| 402 | ||
| 403 | if (body.includes('{')) { | |
| 404 | throw new Error('The `cloud: { ... }` block holds a nested object, which this edit does not attempt to rewrite') | |
| 405 | } | |
| 406 | ||
| 407 | const existing = body.match(/^\s*attachTo:\s*(['"])([^'"]*)\1\s*,?\s*$/m) | |
| 408 | if (existing) { | |
| 409 | const [line, quote = '\'', current = ''] = existing | |
| 410 | if (current === owner) return configText | |
| 411 | ||
| 412 | return configText.replace(whole, whole.replace(line, line.replace(`${quote}${current}${quote}`, `'${owner}'`))) | |
| 413 | } | |
| 414 | ||
| 415 | const inner = `${indent} ` | |
| 416 | return configText.replace( | |
| 417 | whole, | |
| 418 | whole.replace( | |
| 419 | `\n${indent}},\n`, | |
| 420 | `\n${inner}// Deploy onto the box '${owner}' owns rather than provisioning one.\n${inner}attachTo: '${owner}',\n${indent}},\n`, | |
| 421 | ), | |
| 422 | ) | |
| 423 | } | |