pantheon-opencode 1.6.0-beta.11 → 1.6.0-beta.13
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/AGENTS.global.md +3 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +85 -31
- package/README.pt-BR.md +7 -4
- package/bin/pantheon-init.mjs +278 -102
- package/package.json +5 -3
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +77 -13
- package/scripts/check-plugin-v2-coverage.mjs +7 -7
- package/scripts/doctor.mjs +242 -19
- package/scripts/install/backup.mjs +191 -18
- package/scripts/install/cli-ui.mjs +73 -0
- package/scripts/install/file-inventory.mjs +555 -0
- package/scripts/install/interactive.mjs +69 -391
- package/scripts/install/migrate.mjs +45 -38
- package/scripts/install/model-picker.mjs +63 -22
- package/scripts/install/opencode-version.mjs +9 -1
- package/scripts/install/opencode.mjs +279 -79
- package/scripts/install/plan-artifact.mjs +517 -0
- package/scripts/install/plan.mjs +165 -3
- package/scripts/install/plugin.mjs +118 -14
- package/scripts/install/prompt-result.mjs +2 -0
- package/scripts/install/shared.mjs +188 -65
- package/scripts/install/state.mjs +63 -8
- package/scripts/install/venv.mjs +15 -6
- package/scripts/install/verify.mjs +128 -0
- package/scripts/install/vision-prompt.mjs +2 -0
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/manifest-inventory.mjs +49 -3
- package/scripts/node-engine-policy.mjs +85 -0
- package/scripts/package-evidence.mjs +6 -0
- package/scripts/postinstall.mjs +26 -26
- package/scripts/reconcile.mjs +325 -0
- package/scripts/sync-tui.mjs +5 -1
- package/scripts/uninstall.mjs +258 -34
- package/scripts/update.mjs +164 -0
- package/scripts/validate-node-engine-policy.mjs +13 -0
- package/scripts/version-check.mjs +34 -1
- package/scripts/versioning.mjs +9 -1
- package/skills-lock.json +3 -3
- package/src/agents/mnemosyne.md +7 -2
- package/src/instructions/memory-protocol.instructions.md +2 -2
- package/src/instructions/zeus-memory-operations.instructions.md +4 -3
- package/src/pantheon/v2-unsupported.mjs +2 -3
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +3 -3
- package/src/plugins/tui/package.json +2 -2
- package/src/skills/artifact-management/SKILL.md +5 -1
- package/src/skills/memory-bank/SKILL.md +5 -1
- package/src/skills/worktrees/SKILL.md +30 -11
|
@@ -41,11 +41,13 @@ import {
|
|
|
41
41
|
mkdirSync,
|
|
42
42
|
readdirSync,
|
|
43
43
|
readFileSync,
|
|
44
|
+
realpathSync,
|
|
44
45
|
rmSync,
|
|
45
46
|
statSync,
|
|
46
47
|
writeFileSync,
|
|
47
48
|
} from 'node:fs'
|
|
48
49
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
50
|
+
import { assertNoSymlinkAncestors, verifyRegularFile, writeFileAtomically } from './verify.mjs'
|
|
49
51
|
|
|
50
52
|
/** Directory (relative to the config dir) that holds the backup sets. */
|
|
51
53
|
export const BACKUP_SUBDIR = join('.pantheon', 'backups')
|
|
@@ -179,13 +181,37 @@ function chmodBestEffort(path, mode) {
|
|
|
179
181
|
* the exact backup (see `notRevertible`).
|
|
180
182
|
*/
|
|
181
183
|
function ensureDir(path, mode) {
|
|
182
|
-
|
|
184
|
+
assertNoSymlinkAncestors(path)
|
|
185
|
+
let info = null
|
|
183
186
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
info = lstatSync(path)
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error
|
|
190
|
+
}
|
|
191
|
+
if (info) {
|
|
192
|
+
if (!info.isDirectory()) throw new Error(`Backup path is not a directory: ${path}`)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
mkdirSync(path, { recursive: true, mode })
|
|
196
|
+
assertNoSymlinkAncestors(path)
|
|
197
|
+
info = lstatSync(path)
|
|
198
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
199
|
+
throw new Error(`Backup path is not a regular directory: ${path}`)
|
|
188
200
|
}
|
|
201
|
+
chmodBestEffort(path, mode)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assertBackupEntry(configDir, relativePath) {
|
|
205
|
+
const rel = String(relativePath)
|
|
206
|
+
if (!rel || isAbsolute(rel) || rel.split(/[\\/]/).some((part) => part === '..' || part === '')) {
|
|
207
|
+
throw new Error(`Unsafe backup entry path: ${rel}`)
|
|
208
|
+
}
|
|
209
|
+
const source = resolve(configDir, rel)
|
|
210
|
+
const fromRoot = relative(resolve(configDir), source)
|
|
211
|
+
if (fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
|
|
212
|
+
throw new Error(`Backup entry escapes config directory: ${rel}`)
|
|
213
|
+
}
|
|
214
|
+
return { rel, source }
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
/**
|
|
@@ -216,6 +242,7 @@ export function createBackupSet({
|
|
|
216
242
|
clock = () => new Date(),
|
|
217
243
|
keep = DEFAULT_KEEP,
|
|
218
244
|
} = {}) {
|
|
245
|
+
assertNoSymlinkAncestors(configDir)
|
|
219
246
|
const base = clock()
|
|
220
247
|
const name = nextBackupName(configDir, base)
|
|
221
248
|
const dir = backupSetPath(configDir, name)
|
|
@@ -225,10 +252,10 @@ export function createBackupSet({
|
|
|
225
252
|
const seen = new Set()
|
|
226
253
|
const manifestEntries = []
|
|
227
254
|
for (const relativePath of entries ?? []) {
|
|
228
|
-
const rel =
|
|
255
|
+
const { rel, source } = assertBackupEntry(configDir, relativePath)
|
|
229
256
|
if (rel === '' || seen.has(rel)) continue
|
|
230
257
|
seen.add(rel)
|
|
231
|
-
|
|
258
|
+
assertNoSymlinkAncestors(source, { includeLeaf: false })
|
|
232
259
|
let stat = null
|
|
233
260
|
try {
|
|
234
261
|
stat = lstatSync(source)
|
|
@@ -239,8 +266,10 @@ export function createBackupSet({
|
|
|
239
266
|
const buffer = readFileSync(source)
|
|
240
267
|
const dest = join(dir, rel)
|
|
241
268
|
ensureDir(dirname(dest), 0o700)
|
|
242
|
-
|
|
269
|
+
assertNoSymlinkAncestors(dest)
|
|
270
|
+
writeFileSync(dest, buffer, { mode: 0o600, flag: 'wx' })
|
|
243
271
|
chmodBestEffort(dest, 0o600)
|
|
272
|
+
verifyRegularFile(dest, buffer)
|
|
244
273
|
manifestEntries.push({
|
|
245
274
|
relativePath: rel,
|
|
246
275
|
existed: true,
|
|
@@ -267,13 +296,82 @@ export function createBackupSet({
|
|
|
267
296
|
entries: manifestEntries,
|
|
268
297
|
}
|
|
269
298
|
const manifestPath = join(dir, 'manifest.json')
|
|
270
|
-
|
|
299
|
+
assertNoSymlinkAncestors(manifestPath)
|
|
300
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
301
|
+
mode: 0o600,
|
|
302
|
+
flag: 'wx',
|
|
303
|
+
})
|
|
271
304
|
chmodBestEffort(manifestPath, 0o600)
|
|
305
|
+
verifyRegularFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
272
306
|
|
|
273
307
|
const pruned = keep > 0 ? pruneBackups(configDir, keep) : { removed: [], failures: [] }
|
|
274
308
|
return { dir, name, manifest, pruned }
|
|
275
309
|
}
|
|
276
310
|
|
|
311
|
+
/** Build the exact backup outputs without writing files. */
|
|
312
|
+
export function previewBackupSet({
|
|
313
|
+
configDir,
|
|
314
|
+
entries,
|
|
315
|
+
notRevertible = [],
|
|
316
|
+
packageVersion = 'unknown',
|
|
317
|
+
clock = () => new Date(),
|
|
318
|
+
} = {}) {
|
|
319
|
+
const base = clock()
|
|
320
|
+
const name = nextBackupName(configDir, base)
|
|
321
|
+
const dir = backupSetPath(configDir, name)
|
|
322
|
+
const seen = new Set()
|
|
323
|
+
const manifestEntries = []
|
|
324
|
+
const files = []
|
|
325
|
+
for (const relativePath of entries ?? []) {
|
|
326
|
+
const rel = String(relativePath)
|
|
327
|
+
if (rel === '' || seen.has(rel)) continue
|
|
328
|
+
seen.add(rel)
|
|
329
|
+
const source = join(configDir, rel)
|
|
330
|
+
let stat = null
|
|
331
|
+
try {
|
|
332
|
+
stat = lstatSync(source)
|
|
333
|
+
} catch {}
|
|
334
|
+
if (stat?.isFile()) {
|
|
335
|
+
const buffer = readFileSync(source)
|
|
336
|
+
files.push({
|
|
337
|
+
path: join(dir, rel),
|
|
338
|
+
bytes: buffer.length,
|
|
339
|
+
outputFingerprint: sha256(buffer),
|
|
340
|
+
})
|
|
341
|
+
manifestEntries.push({
|
|
342
|
+
relativePath: rel,
|
|
343
|
+
existed: true,
|
|
344
|
+
sha256: sha256(buffer),
|
|
345
|
+
bytes: buffer.length,
|
|
346
|
+
mode: stat.mode & 0o777,
|
|
347
|
+
})
|
|
348
|
+
} else {
|
|
349
|
+
manifestEntries.push({
|
|
350
|
+
relativePath: rel,
|
|
351
|
+
existed: false,
|
|
352
|
+
sha256: null,
|
|
353
|
+
bytes: 0,
|
|
354
|
+
mode: null,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const manifest = {
|
|
359
|
+
createdAt: base.toISOString(),
|
|
360
|
+
packageVersion,
|
|
361
|
+
configDir,
|
|
362
|
+
notRevertible: [...new Set((notRevertible ?? []).map(String))],
|
|
363
|
+
entries: manifestEntries,
|
|
364
|
+
}
|
|
365
|
+
const manifestContent = `${JSON.stringify(manifest, null, 2)}\n`
|
|
366
|
+
return {
|
|
367
|
+
dir,
|
|
368
|
+
files,
|
|
369
|
+
manifestPath: join(dir, 'manifest.json'),
|
|
370
|
+
manifestBytes: Buffer.byteLength(manifestContent, 'utf8'),
|
|
371
|
+
manifestFingerprint: sha256(Buffer.from(manifestContent)),
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
277
375
|
/**
|
|
278
376
|
* A manifest entry path must be a plain relative path inside the config dir —
|
|
279
377
|
* no absolute path and no `..` segment (the backup mirrors the config dir).
|
|
@@ -285,7 +383,52 @@ export function createBackupSet({
|
|
|
285
383
|
function isSafeRelativePath(rel) {
|
|
286
384
|
if (typeof rel !== 'string' || rel === '') return false
|
|
287
385
|
if (isAbsolute(rel)) return false
|
|
288
|
-
return !rel.split(/[\\/]/).
|
|
386
|
+
return !rel.split(/[\\/]/).some((part) => part === '' || part === '.' || part === '..')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function assertRestoreRoot(configDir) {
|
|
390
|
+
const root = resolve(configDir)
|
|
391
|
+
assertNoSymlinkAncestors(root)
|
|
392
|
+
const info = lstatSync(root)
|
|
393
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
394
|
+
throw new Error('Restore config root is not a regular directory')
|
|
395
|
+
}
|
|
396
|
+
return { root, canonicalRoot: realpathSync(root) }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function assertRestoreTarget(root, canonicalRoot, relativePath) {
|
|
400
|
+
if (!isSafeRelativePath(relativePath)) throw new Error('Unsafe restore target path')
|
|
401
|
+
const target = resolve(root, relativePath)
|
|
402
|
+
const lexical = relative(root, target)
|
|
403
|
+
if (!lexical || lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
|
|
404
|
+
throw new Error('Restore target escapes config directory')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
assertNoSymlinkAncestors(target)
|
|
408
|
+
|
|
409
|
+
let existing = target
|
|
410
|
+
let canonicalExisting
|
|
411
|
+
while (true) {
|
|
412
|
+
try {
|
|
413
|
+
canonicalExisting = realpathSync(existing)
|
|
414
|
+
break
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error
|
|
417
|
+
const parent = dirname(existing)
|
|
418
|
+
if (parent === existing) throw error
|
|
419
|
+
existing = parent
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const canonicalTarget = resolve(canonicalExisting, relative(existing, target))
|
|
423
|
+
const canonicalRelative = relative(canonicalRoot, canonicalTarget)
|
|
424
|
+
if (
|
|
425
|
+
canonicalRelative === '..' ||
|
|
426
|
+
canonicalRelative.startsWith(`..${sep}`) ||
|
|
427
|
+
isAbsolute(canonicalRelative)
|
|
428
|
+
) {
|
|
429
|
+
throw new Error('Restore target escapes config directory')
|
|
430
|
+
}
|
|
431
|
+
return target
|
|
289
432
|
}
|
|
290
433
|
|
|
291
434
|
/**
|
|
@@ -359,19 +502,35 @@ function restoreOrder(a, b) {
|
|
|
359
502
|
* @returns {{ entries: Array<{relativePath: string, action: string}>, manifest: object } | { error: string }}
|
|
360
503
|
*/
|
|
361
504
|
export function planRestore(dir, configDir) {
|
|
505
|
+
let root
|
|
506
|
+
let canonicalRoot
|
|
507
|
+
try {
|
|
508
|
+
;({ root, canonicalRoot } = assertRestoreRoot(configDir))
|
|
509
|
+
} catch {
|
|
510
|
+
return { error: 'unsafe-target-root' }
|
|
511
|
+
}
|
|
362
512
|
const read = readManifest(dir)
|
|
363
513
|
if (!read.ok) return { error: read.reason }
|
|
364
514
|
const verify = verifyBackupSet(dir)
|
|
365
515
|
if (!verify.ok) return { error: verify.reason ?? 'integrity', mismatches: verify.mismatches }
|
|
366
516
|
|
|
517
|
+
try {
|
|
518
|
+
for (const entry of read.manifest.entries) {
|
|
519
|
+
assertRestoreTarget(root, canonicalRoot, entry.relativePath)
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
return { error: 'unsafe-target-path' }
|
|
523
|
+
}
|
|
524
|
+
|
|
367
525
|
const entries = []
|
|
368
526
|
for (const entry of [...read.manifest.entries].sort(restoreOrder)) {
|
|
369
|
-
const target =
|
|
527
|
+
const target = assertRestoreTarget(root, canonicalRoot, entry.relativePath)
|
|
370
528
|
if (entry.existed) {
|
|
371
529
|
let action = 'create'
|
|
372
530
|
try {
|
|
373
531
|
const st = lstatSync(target)
|
|
374
532
|
if (st.isFile()) {
|
|
533
|
+
assertRestoreTarget(root, canonicalRoot, entry.relativePath)
|
|
375
534
|
const current = readFileSync(target)
|
|
376
535
|
action = sha256(current) === entry.sha256 ? 'unchanged' : 'update'
|
|
377
536
|
} else {
|
|
@@ -382,6 +541,7 @@ export function planRestore(dir, configDir) {
|
|
|
382
541
|
}
|
|
383
542
|
entries.push({ relativePath: entry.relativePath, action })
|
|
384
543
|
} else {
|
|
544
|
+
assertRestoreTarget(root, canonicalRoot, entry.relativePath)
|
|
385
545
|
const exists = existsSync(target)
|
|
386
546
|
entries.push({ relativePath: entry.relativePath, action: exists ? 'remove' : 'unchanged' })
|
|
387
547
|
}
|
|
@@ -404,34 +564,47 @@ export function restoreBackupSet(dir, configDir, { dryRun = false } = {}) {
|
|
|
404
564
|
const detail = plan.mismatches?.length ? ` (${plan.mismatches.length} mismatch(es))` : ''
|
|
405
565
|
throw new Error(`Backup is not restorable: ${plan.error}${detail}`)
|
|
406
566
|
}
|
|
567
|
+
const { root, canonicalRoot } = assertRestoreRoot(configDir)
|
|
568
|
+
for (const item of plan.entries) {
|
|
569
|
+
assertRestoreTarget(root, canonicalRoot, item.relativePath)
|
|
570
|
+
}
|
|
407
571
|
if (dryRun) return { entries: plan.entries }
|
|
408
572
|
|
|
409
573
|
for (const item of plan.entries) {
|
|
410
574
|
const backUpFile = join(dir, item.relativePath)
|
|
411
|
-
const target =
|
|
575
|
+
const target = assertRestoreTarget(root, canonicalRoot, item.relativePath)
|
|
412
576
|
if (item.action === 'unchanged') continue
|
|
413
577
|
|
|
414
|
-
// Refuse to write through/over a symlink (mirrors writeIfChanged).
|
|
415
578
|
let linkStat = null
|
|
416
579
|
try {
|
|
417
580
|
linkStat = lstatSync(target)
|
|
418
581
|
} catch {
|
|
419
582
|
linkStat = null
|
|
420
583
|
}
|
|
421
|
-
if (linkStat?.isSymbolicLink())
|
|
584
|
+
if (linkStat?.isSymbolicLink())
|
|
422
585
|
throw new Error(`Refusing to restore through symlink: ${item.relativePath}`)
|
|
423
|
-
}
|
|
424
586
|
|
|
425
587
|
if (item.action === 'remove') {
|
|
426
|
-
if (linkStat?.isFile())
|
|
588
|
+
if (linkStat?.isFile()) {
|
|
589
|
+
assertRestoreTarget(root, canonicalRoot, item.relativePath)
|
|
590
|
+
const current = lstatSync(target)
|
|
591
|
+
if (current.isSymbolicLink()) {
|
|
592
|
+
throw new Error(`Refusing to restore through symlink: ${item.relativePath}`)
|
|
593
|
+
}
|
|
594
|
+
if (current.isFile()) rmSync(target, { force: true })
|
|
595
|
+
}
|
|
427
596
|
continue
|
|
428
597
|
}
|
|
429
598
|
|
|
430
599
|
const buffer = readFileSync(backUpFile)
|
|
431
600
|
ensureDir(dirname(target), 0o700)
|
|
432
|
-
|
|
601
|
+
assertRestoreTarget(root, canonicalRoot, item.relativePath)
|
|
602
|
+
writeFileAtomically(target, buffer)
|
|
433
603
|
const recorded = plan.manifest.entries.find((e) => e.relativePath === item.relativePath)
|
|
434
|
-
if (recorded && recorded.mode !== null)
|
|
604
|
+
if (recorded && recorded.mode !== null) {
|
|
605
|
+
assertRestoreTarget(root, canonicalRoot, item.relativePath)
|
|
606
|
+
chmodBestEffort(target, recorded.mode)
|
|
607
|
+
}
|
|
435
608
|
}
|
|
436
609
|
return { entries: plan.entries }
|
|
437
610
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
1
2
|
import { strings } from './strings.mjs'
|
|
2
3
|
|
|
3
4
|
export const icons = {
|
|
@@ -196,3 +197,75 @@ export function printSummary(target, _platforms, stats) {
|
|
|
196
197
|
console.log(tag(S.summaryNextMcp))
|
|
197
198
|
console.log(tag(S.summaryNextDoctor))
|
|
198
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build a persistent worktree location outside the project directory.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} projectRoot - Project directory whose exact basename, including spaces, is retained.
|
|
205
|
+
* @returns {{ pattern: string, example: string }} Recommended paths for future worktrees.
|
|
206
|
+
*/
|
|
207
|
+
export function getWorktreeRecommendation(projectRoot = process.cwd()) {
|
|
208
|
+
const resolvedProject = resolve(projectRoot)
|
|
209
|
+
const projectName = basename(resolvedProject)
|
|
210
|
+
const repositoryWorktreeRoot = resolve(
|
|
211
|
+
dirname(resolvedProject),
|
|
212
|
+
'.pantheon-worktrees',
|
|
213
|
+
projectName || 'project',
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
pattern: join(repositoryWorktreeRoot, '<task>'),
|
|
218
|
+
example: join(repositoryWorktreeRoot, 'feature-name'),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Show the suggested external worktree location in a real interactive TTY setup.
|
|
224
|
+
* Pantheon provides guidance only; it never creates a worktree or its directory.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} [options]
|
|
227
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project path for the recommendation.
|
|
228
|
+
* @param {boolean} [options.interactive=false] - Whether setup is interactive.
|
|
229
|
+
* @param {boolean} [options.dryRun=false] - Whether setup is preview-only.
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
export function printWorktreeGuidance({
|
|
233
|
+
projectRoot = process.cwd(),
|
|
234
|
+
interactive = false,
|
|
235
|
+
dryRun = false,
|
|
236
|
+
} = {}) {
|
|
237
|
+
if (!interactive || dryRun || !tty()) return
|
|
238
|
+
|
|
239
|
+
const recommendation = getWorktreeRecommendation(projectRoot)
|
|
240
|
+
const rows = [
|
|
241
|
+
{ label: 'Default: ', value: recommendation.pattern, color: colors.green },
|
|
242
|
+
{ label: 'Example: ', value: recommendation.example, color: colors.cyan },
|
|
243
|
+
{
|
|
244
|
+
label: 'Use: ',
|
|
245
|
+
value:
|
|
246
|
+
'Persistent location; preserve the exact project directory basename, including spaces.',
|
|
247
|
+
color: colors.dim,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
label: 'Note: ',
|
|
251
|
+
value: 'Pantheon does not create Git worktrees or directories automatically.',
|
|
252
|
+
color: colors.yellow,
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
const contentWidth = Math.max(
|
|
256
|
+
'Worktrees'.length,
|
|
257
|
+
...rows.map(({ label, value }) => label.length + value.length),
|
|
258
|
+
)
|
|
259
|
+
const border = colors.cyan
|
|
260
|
+
|
|
261
|
+
console.log('')
|
|
262
|
+
console.log(` ${border(`┌${'─'.repeat(contentWidth + 2)}┐`)}`)
|
|
263
|
+
console.log(` ${border('│')} ${colors.bold('Worktrees'.padEnd(contentWidth))} ${border('│')}`)
|
|
264
|
+
console.log(` ${border(`├${'─'.repeat(contentWidth + 2)}┤`)}`)
|
|
265
|
+
for (const { label, value, color } of rows) {
|
|
266
|
+
const padding = ' '.repeat(contentWidth - label.length - value.length)
|
|
267
|
+
console.log(` ${border('│')} ${colors.bold(label)}${color(value)}${padding} ${border('│')}`)
|
|
268
|
+
}
|
|
269
|
+
console.log(` ${border(`└${'─'.repeat(contentWidth + 2)}┘`)}`)
|
|
270
|
+
console.log('')
|
|
271
|
+
}
|