create-meith 0.30.0 → 0.31.0
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/bin.mjs +576 -39
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/cli.ts +17 -2
- package/src/index.ts +19 -0
- package/src/scaffold.ts +195 -26
- package/src/update.ts +489 -0
package/dist/bin.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
4
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
5
|
+
import { mkdir as mkdir2, readdir, writeFile as writeFile2 } from "node:fs/promises";
|
|
6
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
7
|
+
import { promisify as promisify2 } from "node:util";
|
|
8
8
|
|
|
9
9
|
// src/scaffold.ts
|
|
10
10
|
var DEFAULT_REPOSITORY_URL = "https://github.com/meith-dev/meith";
|
|
@@ -125,6 +125,18 @@ var ENV_SMTP_MAIL_BLOCK = `# Mail. Leave these alone and the installer asks for
|
|
|
125
125
|
# MAIL_SMTP_USERNAME=
|
|
126
126
|
# MAIL_SMTP_PASSWORD=
|
|
127
127
|
# MAIL_FROM=noreply@yourdomain.com`;
|
|
128
|
+
var ENV_BACKUP_BLOCK = `# The off-site backup destination: an S3-compatible bucket \`meith backup\`
|
|
129
|
+
# ships every bundle to, pruned there to the newest --keep (7 unless set).
|
|
130
|
+
# All four required together \u2014 a partial set fails the backup, never the board.
|
|
131
|
+
# BACKUP_S3_ENDPOINT is for anything S3-compatible (R2, MinIO, Spaces;
|
|
132
|
+
# BACKUP_S3_REGION=auto for R2); BACKUP_S3_PREFIX shares one bucket between
|
|
133
|
+
# boards. Use a bucket of its own, never the bucket the uploads live in.
|
|
134
|
+
# BACKUP_S3_BUCKET=
|
|
135
|
+
# BACKUP_S3_REGION=
|
|
136
|
+
# BACKUP_S3_ACCESS_KEY_ID=
|
|
137
|
+
# BACKUP_S3_SECRET_ACCESS_KEY=
|
|
138
|
+
# BACKUP_S3_ENDPOINT=
|
|
139
|
+
# BACKUP_S3_PREFIX=`;
|
|
128
140
|
function selfHostEnvExample(name) {
|
|
129
141
|
return `# ${name} \u2014 environment.
|
|
130
142
|
#
|
|
@@ -155,6 +167,8 @@ APP_URL=
|
|
|
155
167
|
|
|
156
168
|
${ENV_SMTP_MAIL_BLOCK}
|
|
157
169
|
|
|
170
|
+
${ENV_BACKUP_BLOCK}
|
|
171
|
+
|
|
158
172
|
`;
|
|
159
173
|
}
|
|
160
174
|
function vercelEnvExample(name) {
|
|
@@ -323,7 +337,6 @@ function envExample(name, target) {
|
|
|
323
337
|
}
|
|
324
338
|
var SELF_HOST_DEPLOY_KIT = [
|
|
325
339
|
".dockerignore",
|
|
326
|
-
".github/dependabot.yml",
|
|
327
340
|
".github/workflows/build.yml",
|
|
328
341
|
"Dockerfile",
|
|
329
342
|
"Dockerfile.prebuilt",
|
|
@@ -508,9 +521,10 @@ export function installedPluginDefinitions() {
|
|
|
508
521
|
files.set(
|
|
509
522
|
".github/dependabot.yml",
|
|
510
523
|
`# Keeps this board's GitHub Actions current: one weekly pull request that
|
|
511
|
-
# bumps the actions pinned in .github/workflows.
|
|
512
|
-
#
|
|
513
|
-
# \`next\` pin has to move together with \`@meith/web\`
|
|
524
|
+
# bumps the actions pinned in .github/workflows. Meith itself is updated
|
|
525
|
+
# separately, by .github/workflows/update.yml \u2014 see README.md, "Upgrading" \u2014
|
|
526
|
+
# because the board's \`next\` pin has to move together with \`@meith/web\`
|
|
527
|
+
# rather than on its own.
|
|
514
528
|
version: 2
|
|
515
529
|
updates:
|
|
516
530
|
- package-ecosystem: github-actions
|
|
@@ -520,6 +534,82 @@ updates:
|
|
|
520
534
|
groups:
|
|
521
535
|
actions:
|
|
522
536
|
patterns: ['*']
|
|
537
|
+
`
|
|
538
|
+
);
|
|
539
|
+
files.set(
|
|
540
|
+
".github/workflows/update.yml",
|
|
541
|
+
`# Opens a pull request whenever a new Meith release is out \u2014 see README.md,
|
|
542
|
+
# "Upgrading". Once a week, and on the Run workflow button, it runs the same
|
|
543
|
+
# updater you can run by hand, \`npx create-meith@latest update\`: every
|
|
544
|
+
# @meith/* pin and next moved together in package.json, and the deploy files
|
|
545
|
+
# the scaffold owns rewritten to the new release's shape. A file you have
|
|
546
|
+
# edited yourself is never rewritten; the run's log names any it left alone.
|
|
547
|
+
#
|
|
548
|
+
# One-time setup: under Settings \u2192 Actions \u2192 General, enable "Allow GitHub
|
|
549
|
+
# Actions to create and approve pull requests" \u2014 without it the last step
|
|
550
|
+
# fails, saying GitHub Actions is not permitted to create pull requests.
|
|
551
|
+
#
|
|
552
|
+
# Merging is still an upgrade, not a formality: read the release notes the
|
|
553
|
+
# pull request links, take a backup first, and once the new version serves,
|
|
554
|
+
# run \`meith upgrade\` against it. Migrations are forward-only \u2014 the backup
|
|
555
|
+
# is the rollback.
|
|
556
|
+
name: Meith update
|
|
557
|
+
|
|
558
|
+
on:
|
|
559
|
+
schedule:
|
|
560
|
+
- cron: '30 4 * * 1'
|
|
561
|
+
workflow_dispatch:
|
|
562
|
+
|
|
563
|
+
permissions:
|
|
564
|
+
contents: write
|
|
565
|
+
pull-requests: write
|
|
566
|
+
|
|
567
|
+
jobs:
|
|
568
|
+
update:
|
|
569
|
+
name: Update Meith
|
|
570
|
+
runs-on: ubuntu-latest
|
|
571
|
+
steps:
|
|
572
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
573
|
+
|
|
574
|
+
- name: Run the updater
|
|
575
|
+
run: npx --yes create-meith@latest update
|
|
576
|
+
|
|
577
|
+
- name: Open a pull request
|
|
578
|
+
env:
|
|
579
|
+
GH_TOKEN: \${{ github.token }}
|
|
580
|
+
run: |
|
|
581
|
+
if [ -z "$(git status --porcelain)" ]; then
|
|
582
|
+
echo "Already up to date."
|
|
583
|
+
exit 0
|
|
584
|
+
fi
|
|
585
|
+
VERSION=$(node -p "require('./package.json').dependencies['@meith/web']")
|
|
586
|
+
BODY="$RUNNER_TEMP/meith-update-body.md"
|
|
587
|
+
{
|
|
588
|
+
echo "Updates this board to Meith $VERSION \u2014 every @meith/* package and"
|
|
589
|
+
echo "next moved together, and the scaffold-owned deploy files rewritten"
|
|
590
|
+
echo "to this release's shape. Files with local edits were left alone;"
|
|
591
|
+
echo "this workflow run's log names any it skipped."
|
|
592
|
+
echo
|
|
593
|
+
echo "Before merging and deploying:"
|
|
594
|
+
echo
|
|
595
|
+
echo "1. Read the release notes \u2014 their Migrations line says what this"
|
|
596
|
+
echo " release does to the schema:"
|
|
597
|
+
echo " https://github.com/meith-dev/meith/releases/tag/v$VERSION"
|
|
598
|
+
echo "2. Take a backup. Migrations are forward-only; the backup is the"
|
|
599
|
+
echo " rollback."
|
|
600
|
+
echo
|
|
601
|
+
echo "After the new version deploys, run 'meith upgrade' once against the"
|
|
602
|
+
echo "board \u2014 the admin panel shows a notice until it has run. See"
|
|
603
|
+
echo "README.md, \\"Upgrading\\"."
|
|
604
|
+
} > "$BODY"
|
|
605
|
+
git config user.name "github-actions[bot]"
|
|
606
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
607
|
+
git checkout -B meith-update
|
|
608
|
+
git add -A
|
|
609
|
+
git commit -m "Update Meith to $VERSION"
|
|
610
|
+
git push -f origin meith-update
|
|
611
|
+
gh pr create --title "Update Meith to $VERSION" --body-file "$BODY" \\
|
|
612
|
+
|| gh pr edit meith-update --title "Update Meith to $VERSION" --body-file "$BODY"
|
|
523
613
|
`
|
|
524
614
|
);
|
|
525
615
|
files.set(
|
|
@@ -934,8 +1024,24 @@ services:
|
|
|
934
1024
|
- MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
|
|
935
1025
|
- MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
|
|
936
1026
|
- MAIL_FROM=\${MAIL_FROM:-}
|
|
1027
|
+
# The off-site backup destination, unset until the four required values
|
|
1028
|
+
# are filled in on the Coolify resource. Coolify only hands a compose
|
|
1029
|
+
# service the variables the file names, so a Scheduled Task running
|
|
1030
|
+
# \`meith backup\` in this container would never see them without these
|
|
1031
|
+
# lines \u2014 see the meith repository's
|
|
1032
|
+
# docs/getting-started/deployment/coolify.md, "Set up backups".
|
|
1033
|
+
- BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
|
|
1034
|
+
- BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
|
|
1035
|
+
- BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
|
|
1036
|
+
- BACKUP_S3_SECRET_ACCESS_KEY=\${BACKUP_S3_SECRET_ACCESS_KEY:-}
|
|
1037
|
+
- BACKUP_S3_ENDPOINT=\${BACKUP_S3_ENDPOINT:-}
|
|
1038
|
+
- BACKUP_S3_PREFIX=\${BACKUP_S3_PREFIX:-}
|
|
937
1039
|
volumes:
|
|
938
1040
|
- uploads:/app/.uploads
|
|
1041
|
+
# Where \`meith backup --dir /backups\` writes its ring of bundles. A
|
|
1042
|
+
# named volume rather than the container filesystem, so the ring survives
|
|
1043
|
+
# every redeploy.
|
|
1044
|
+
- backups:/backups
|
|
939
1045
|
depends_on:
|
|
940
1046
|
postgres:
|
|
941
1047
|
condition: service_healthy
|
|
@@ -971,6 +1077,7 @@ services:
|
|
|
971
1077
|
volumes:
|
|
972
1078
|
pgdata:
|
|
973
1079
|
uploads:
|
|
1080
|
+
backups:
|
|
974
1081
|
`
|
|
975
1082
|
);
|
|
976
1083
|
files.set(
|
|
@@ -1066,8 +1173,24 @@ services:
|
|
|
1066
1173
|
- MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
|
|
1067
1174
|
- MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
|
|
1068
1175
|
- MAIL_FROM=\${MAIL_FROM:-}
|
|
1176
|
+
# The off-site backup destination, unset until the four required values
|
|
1177
|
+
# are filled in on the Coolify resource. Coolify only hands a compose
|
|
1178
|
+
# service the variables the file names, so a Scheduled Task running
|
|
1179
|
+
# \`meith backup\` in this container would never see them without these
|
|
1180
|
+
# lines \u2014 see the meith repository's
|
|
1181
|
+
# docs/getting-started/deployment/coolify.md, "Set up backups".
|
|
1182
|
+
- BACKUP_S3_BUCKET=\${BACKUP_S3_BUCKET:-}
|
|
1183
|
+
- BACKUP_S3_REGION=\${BACKUP_S3_REGION:-}
|
|
1184
|
+
- BACKUP_S3_ACCESS_KEY_ID=\${BACKUP_S3_ACCESS_KEY_ID:-}
|
|
1185
|
+
- BACKUP_S3_SECRET_ACCESS_KEY=\${BACKUP_S3_SECRET_ACCESS_KEY:-}
|
|
1186
|
+
- BACKUP_S3_ENDPOINT=\${BACKUP_S3_ENDPOINT:-}
|
|
1187
|
+
- BACKUP_S3_PREFIX=\${BACKUP_S3_PREFIX:-}
|
|
1069
1188
|
volumes:
|
|
1070
1189
|
- uploads:/app/.uploads
|
|
1190
|
+
# Where \`meith backup --dir /backups\` writes its ring of bundles. A
|
|
1191
|
+
# named volume rather than the container filesystem, so the ring survives
|
|
1192
|
+
# every redeploy.
|
|
1193
|
+
- backups:/backups
|
|
1071
1194
|
depends_on:
|
|
1072
1195
|
postgres:
|
|
1073
1196
|
condition: service_healthy
|
|
@@ -1103,6 +1226,7 @@ services:
|
|
|
1103
1226
|
volumes:
|
|
1104
1227
|
pgdata:
|
|
1105
1228
|
uploads:
|
|
1229
|
+
backups:
|
|
1106
1230
|
`
|
|
1107
1231
|
);
|
|
1108
1232
|
files.set(
|
|
@@ -1288,27 +1412,52 @@ for the full guide.
|
|
|
1288
1412
|
|
|
1289
1413
|
## Upgrading
|
|
1290
1414
|
|
|
1415
|
+
\`.github/workflows/update.yml\` does this for you: once a week \u2014 and
|
|
1416
|
+
whenever you press **Run workflow** on the Actions tab \u2014 it checks for a new
|
|
1417
|
+
Meith release and opens a pull request that moves every \`@meith/*\` package
|
|
1418
|
+
and \`next\` together, and rewrites the deploy files this scaffold owns
|
|
1419
|
+
(\`Dockerfile\`, the compose files, the workflows) to the new release's
|
|
1420
|
+
shape. A file you have edited yourself is never rewritten; the run's log
|
|
1421
|
+
names any it left for you. One-time setup: under
|
|
1422
|
+
**Settings \u2192 Actions \u2192 General**, enable **Allow GitHub Actions to create
|
|
1423
|
+
and approve pull requests**, or the workflow cannot open one.
|
|
1424
|
+
|
|
1425
|
+
Merging that pull request is still an upgrade, not a formality: read the
|
|
1426
|
+
release notes it links, take a backup first, and press **Redeploy** in
|
|
1427
|
+
Coolify after the merge \u2014 pushing alone does not rebuild. Once the new
|
|
1428
|
+
version serves, run \`npm run meith -- upgrade\` against it for the plugin
|
|
1429
|
+
migrations.
|
|
1430
|
+
|
|
1431
|
+
The same update, by hand and without waiting for the schedule:
|
|
1432
|
+
|
|
1433
|
+
\`\`\`sh
|
|
1434
|
+
npx create-meith@latest update
|
|
1435
|
+
git commit -am "Update Meith"
|
|
1436
|
+
git push
|
|
1437
|
+
\`\`\`
|
|
1438
|
+
|
|
1439
|
+
Under the hood, the version move is these two commands, plus the deploy-file
|
|
1440
|
+
rewrite neither of them can do:
|
|
1441
|
+
|
|
1291
1442
|
\`\`\`sh
|
|
1292
1443
|
npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
|
|
1293
1444
|
npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
|
|
1294
|
-
git commit -am "Upgrade Meith and the Next.js version it builds with"
|
|
1295
|
-
git push
|
|
1296
1445
|
\`\`\`
|
|
1297
1446
|
|
|
1298
1447
|
The second command is not optional. This board pins \`next\` itself, and
|
|
1299
|
-
|
|
1300
|
-
board's own pin on the old Next while \`@meith/web\` depends on the
|
|
1301
|
-
which npm resolves by installing both \u2014 the build then runs on one
|
|
1302
|
-
while everything reading \`package.json\` sees the other. Reading the
|
|
1303
|
-
out of the freshly installed \`@meith/web\` is what keeps the two the
|
|
1304
|
-
without anybody having to know the number.
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
this repository's own GitHub Actions \u2014
|
|
1309
|
-
weekly pull request bumping the actions
|
|
1310
|
-
\`.github/workflows
|
|
1311
|
-
|
|
1448
|
+
the npm commands alone never bump it: upgrading only the \`@meith/*\` packages
|
|
1449
|
+
leaves the board's own pin on the old Next while \`@meith/web\` depends on the
|
|
1450
|
+
new one, which npm resolves by installing both \u2014 the build then runs on one
|
|
1451
|
+
version while everything reading \`package.json\` sees the other. Reading the
|
|
1452
|
+
version out of the freshly installed \`@meith/web\` is what keeps the two the
|
|
1453
|
+
same without anybody having to know the number.
|
|
1454
|
+
|
|
1455
|
+
\`next\` and \`@meith/web\` move together or not at all, which is why one
|
|
1456
|
+
updater owns the whole move and no dependency bot bumps either on its own.
|
|
1457
|
+
What Dependabot *does* keep current is this repository's own GitHub Actions \u2014
|
|
1458
|
+
\`.github/dependabot.yml\` opens a weekly pull request bumping the actions
|
|
1459
|
+
pinned under \`.github/workflows\`, a safe, independent update the updater
|
|
1460
|
+
leaves to it.
|
|
1312
1461
|
|
|
1313
1462
|
On the quick-start path there is no version to keep in sync by hand:
|
|
1314
1463
|
\`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
|
|
@@ -1542,20 +1691,38 @@ each retry into another attempt against whatever it is failing against.
|
|
|
1542
1691
|
|
|
1543
1692
|
## Upgrading
|
|
1544
1693
|
|
|
1694
|
+
\`.github/workflows/update.yml\` does this for you: once a week \u2014 and
|
|
1695
|
+
whenever you press **Run workflow** on the Actions tab \u2014 it checks for a new
|
|
1696
|
+
Meith release and opens a pull request that moves every \`@meith/*\` package
|
|
1697
|
+
and \`next\` together. A file you have edited yourself is never rewritten; the
|
|
1698
|
+
run's log names any it left for you. One-time setup: under
|
|
1699
|
+
**Settings \u2192 Actions \u2192 General**, enable **Allow GitHub Actions to create and
|
|
1700
|
+
approve pull requests**, or the workflow cannot open one. Read the release
|
|
1701
|
+
notes the pull request links and take a backup before merging; Vercel
|
|
1702
|
+
rebuilds on the merge, and the build command applies the new migrations
|
|
1703
|
+
before it builds.
|
|
1704
|
+
|
|
1705
|
+
The same update, by hand and without waiting for the schedule:
|
|
1706
|
+
|
|
1707
|
+
\`\`\`sh
|
|
1708
|
+
npx create-meith@latest update
|
|
1709
|
+
git commit -am "Update Meith"
|
|
1710
|
+
git push
|
|
1711
|
+
\`\`\`
|
|
1712
|
+
|
|
1713
|
+
Under the hood, the version move is these two commands:
|
|
1714
|
+
|
|
1545
1715
|
\`\`\`sh
|
|
1546
1716
|
npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
|
|
1547
1717
|
npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
|
|
1548
|
-
git commit -am "Upgrade Meith and the Next.js version it builds with"
|
|
1549
|
-
git push
|
|
1550
1718
|
\`\`\`
|
|
1551
1719
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
everything else installed here.
|
|
1720
|
+
\`--save-exact\` matters and \`.npmrc\` already sets it for everything else
|
|
1721
|
+
installed here.
|
|
1555
1722
|
|
|
1556
1723
|
The second command is not optional. This board pins \`next\` itself \u2014 Vercel
|
|
1557
|
-
reads that pin to pick its Next.js builder \u2014 and
|
|
1558
|
-
Upgrading only the \`@meith/*\` packages leaves two versions of Next
|
|
1724
|
+
reads that pin to pick its Next.js builder \u2014 and the npm commands alone never
|
|
1725
|
+
bump it. Upgrading only the \`@meith/*\` packages leaves two versions of Next
|
|
1559
1726
|
installed, the board built with one and the platform configured for the
|
|
1560
1727
|
other. Reading the version out of the freshly installed \`@meith/web\` keeps
|
|
1561
1728
|
them the same without anybody having to know the number.
|
|
@@ -2098,8 +2265,369 @@ function extensionNextSteps(name) {
|
|
|
2098
2265
|
return [`cd ${name}`, "npm install", "npm test"];
|
|
2099
2266
|
}
|
|
2100
2267
|
|
|
2101
|
-
// src/
|
|
2268
|
+
// src/update.ts
|
|
2269
|
+
import { execFile } from "node:child_process";
|
|
2270
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2271
|
+
import { dirname, join } from "node:path";
|
|
2272
|
+
import { promisify } from "node:util";
|
|
2273
|
+
import { gunzipSync } from "node:zlib";
|
|
2102
2274
|
var execFileAsync = promisify(execFile);
|
|
2275
|
+
var TEMPLATE_BOARD_NAME = "meith-board";
|
|
2276
|
+
var TEMPLATE_REPOSITORIES = {
|
|
2277
|
+
"self-host": "meith-dev/template",
|
|
2278
|
+
vercel: "meith-dev/vercel-template"
|
|
2279
|
+
};
|
|
2280
|
+
var RELEASE_NOTES_URL = "https://github.com/meith-dev/meith/releases/tag";
|
|
2281
|
+
function templateTarballUrl(target, version) {
|
|
2282
|
+
return `https://codeload.github.com/${TEMPLATE_REPOSITORIES[target]}/tar.gz/refs/tags/v${version}`;
|
|
2283
|
+
}
|
|
2284
|
+
function parseExactVersion(value) {
|
|
2285
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
2286
|
+
if (match === null) return null;
|
|
2287
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2288
|
+
}
|
|
2289
|
+
function compareExactVersions(left, right) {
|
|
2290
|
+
const a = parseExactVersion(left);
|
|
2291
|
+
const b = parseExactVersion(right);
|
|
2292
|
+
if (a === null || b === null) return 0;
|
|
2293
|
+
for (let part = 0; part < 3; part++) {
|
|
2294
|
+
if (a[part] !== b[part])
|
|
2295
|
+
return a[part] - b[part];
|
|
2296
|
+
}
|
|
2297
|
+
return 0;
|
|
2298
|
+
}
|
|
2299
|
+
function substituteBoardName(content, name) {
|
|
2300
|
+
if (name === TEMPLATE_BOARD_NAME) return content;
|
|
2301
|
+
return content.split(TEMPLATE_BOARD_NAME).join(name);
|
|
2302
|
+
}
|
|
2303
|
+
function normalizeActionPins(content) {
|
|
2304
|
+
return content.split("\n").map((line) => /^\s*(- )?uses:\s/.test(line) ? line.replace(/@\S+(\s*#.*)?$/, "@pin") : line).join("\n");
|
|
2305
|
+
}
|
|
2306
|
+
function comparable(path, content) {
|
|
2307
|
+
return path.startsWith(".github/workflows/") ? normalizeActionPins(content) : content;
|
|
2308
|
+
}
|
|
2309
|
+
function tarText(block, start, length) {
|
|
2310
|
+
const slice = block.subarray(start, start + length);
|
|
2311
|
+
const end = slice.indexOf(0);
|
|
2312
|
+
return new TextDecoder().decode(end === -1 ? slice : slice.subarray(0, end));
|
|
2313
|
+
}
|
|
2314
|
+
function unpackTemplateTarball(data, name) {
|
|
2315
|
+
const tar = gunzipSync(data);
|
|
2316
|
+
const files = /* @__PURE__ */ new Map();
|
|
2317
|
+
for (let at = 0; at + 512 <= tar.length; ) {
|
|
2318
|
+
const header = tar.subarray(at, at + 512);
|
|
2319
|
+
at += 512;
|
|
2320
|
+
if (header.every((byte) => byte === 0)) break;
|
|
2321
|
+
const size = Number.parseInt(tarText(header, 124, 12).trim() || "0", 8);
|
|
2322
|
+
const type = header[156] ?? 0;
|
|
2323
|
+
const prefix = tarText(header, 345, 155);
|
|
2324
|
+
const path = prefix === "" ? tarText(header, 0, 100) : `${prefix}/${tarText(header, 0, 100)}`;
|
|
2325
|
+
if (type === 48 || type === 0) {
|
|
2326
|
+
const relative = path.split("/").slice(1).join("/");
|
|
2327
|
+
if (relative !== "") {
|
|
2328
|
+
const content = new TextDecoder().decode(tar.subarray(at, at + size));
|
|
2329
|
+
files.set(relative, substituteBoardName(content, name));
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
at += Math.ceil(size / 512) * 512;
|
|
2333
|
+
}
|
|
2334
|
+
return files;
|
|
2335
|
+
}
|
|
2336
|
+
async function fetchPreviousTree(target, version, name) {
|
|
2337
|
+
try {
|
|
2338
|
+
const response = await fetch(templateTarballUrl(target, version), {
|
|
2339
|
+
redirect: "follow",
|
|
2340
|
+
signal: AbortSignal.timeout(3e4)
|
|
2341
|
+
});
|
|
2342
|
+
if (!response.ok) return null;
|
|
2343
|
+
return unpackTemplateTarball(new Uint8Array(await response.arrayBuffer()), name);
|
|
2344
|
+
} catch {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function mergeDependencies(current, next, newVersion) {
|
|
2349
|
+
const merged = {};
|
|
2350
|
+
for (const [name, range] of Object.entries(current)) {
|
|
2351
|
+
merged[name] = next[name] ?? (name.startsWith("@meith/") ? newVersion : range);
|
|
2352
|
+
}
|
|
2353
|
+
for (const [name, range] of Object.entries(next)) {
|
|
2354
|
+
if (!(name in merged)) merged[name] = range;
|
|
2355
|
+
}
|
|
2356
|
+
return merged;
|
|
2357
|
+
}
|
|
2358
|
+
function mergeScripts(current, previous, next, degraded) {
|
|
2359
|
+
const merged = { ...current ?? {} };
|
|
2360
|
+
for (const [key, value] of Object.entries(next)) {
|
|
2361
|
+
const held = current?.[key];
|
|
2362
|
+
if (held === void 0) {
|
|
2363
|
+
if (degraded || previous?.[key] === void 0) merged[key] = value;
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
if (held === value || held === previous?.[key]) merged[key] = value;
|
|
2367
|
+
}
|
|
2368
|
+
return merged;
|
|
2369
|
+
}
|
|
2370
|
+
function mergeManifest(currentText, previousText, nextText, newVersion) {
|
|
2371
|
+
const current = JSON.parse(currentText);
|
|
2372
|
+
const previous = previousText === null ? null : JSON.parse(previousText);
|
|
2373
|
+
const next = JSON.parse(nextText);
|
|
2374
|
+
const merged = { ...current };
|
|
2375
|
+
merged.dependencies = mergeDependencies(
|
|
2376
|
+
current.dependencies ?? {},
|
|
2377
|
+
next.dependencies ?? {},
|
|
2378
|
+
newVersion
|
|
2379
|
+
);
|
|
2380
|
+
if (current.devDependencies !== void 0) {
|
|
2381
|
+
merged.devDependencies = mergeDependencies(current.devDependencies, {}, newVersion);
|
|
2382
|
+
}
|
|
2383
|
+
if (next.scripts !== void 0) {
|
|
2384
|
+
merged.scripts = mergeScripts(
|
|
2385
|
+
current.scripts,
|
|
2386
|
+
previous?.scripts,
|
|
2387
|
+
next.scripts,
|
|
2388
|
+
previous === null
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
for (const field of ["type", "engines"]) {
|
|
2392
|
+
if (!(field in next)) continue;
|
|
2393
|
+
const untouched = previous !== null && JSON.stringify(current[field]) === JSON.stringify(previous[field]);
|
|
2394
|
+
if (!(field in current) || untouched) merged[field] = next[field];
|
|
2395
|
+
}
|
|
2396
|
+
return `${JSON.stringify(merged, null, 2)}
|
|
2397
|
+
`;
|
|
2398
|
+
}
|
|
2399
|
+
function planUpdate({ current, previous, next, newVersion }) {
|
|
2400
|
+
const writes = /* @__PURE__ */ new Map();
|
|
2401
|
+
const deletes = [];
|
|
2402
|
+
const created = [];
|
|
2403
|
+
const updated = [];
|
|
2404
|
+
const skipped = [];
|
|
2405
|
+
const review = [];
|
|
2406
|
+
const currentManifest = current.get("package.json");
|
|
2407
|
+
const nextManifest = next.get("package.json");
|
|
2408
|
+
if (currentManifest !== void 0 && nextManifest !== void 0) {
|
|
2409
|
+
const merged = mergeManifest(
|
|
2410
|
+
currentManifest,
|
|
2411
|
+
previous?.get("package.json") ?? null,
|
|
2412
|
+
nextManifest,
|
|
2413
|
+
newVersion
|
|
2414
|
+
);
|
|
2415
|
+
if (merged !== currentManifest) {
|
|
2416
|
+
writes.set("package.json", merged);
|
|
2417
|
+
updated.push("package.json");
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (previous === null) {
|
|
2421
|
+
for (const [path, content] of next) {
|
|
2422
|
+
if (path === "package.json") continue;
|
|
2423
|
+
if (current.get(path) !== content) review.push(path);
|
|
2424
|
+
}
|
|
2425
|
+
return { writes, deletes, created, updated, skipped, review: review.sort() };
|
|
2426
|
+
}
|
|
2427
|
+
const paths = /* @__PURE__ */ new Set([...next.keys(), ...previous.keys()]);
|
|
2428
|
+
paths.delete("package.json");
|
|
2429
|
+
for (const path of [...paths].sort()) {
|
|
2430
|
+
const held = current.get(path);
|
|
2431
|
+
const before = previous.get(path);
|
|
2432
|
+
const after = next.get(path);
|
|
2433
|
+
if (after !== void 0) {
|
|
2434
|
+
if (held === void 0) {
|
|
2435
|
+
if (before === void 0) {
|
|
2436
|
+
writes.set(path, after);
|
|
2437
|
+
created.push(path);
|
|
2438
|
+
}
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (held === after) continue;
|
|
2442
|
+
if (before !== void 0 && comparable(path, held) === comparable(path, before)) {
|
|
2443
|
+
writes.set(path, after);
|
|
2444
|
+
updated.push(path);
|
|
2445
|
+
} else {
|
|
2446
|
+
skipped.push(path);
|
|
2447
|
+
}
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (held === void 0 || before === void 0) continue;
|
|
2451
|
+
if (comparable(path, held) === comparable(path, before)) deletes.push(path);
|
|
2452
|
+
else skipped.push(path);
|
|
2453
|
+
}
|
|
2454
|
+
return { writes, deletes, created, updated, skipped: skipped.sort(), review };
|
|
2455
|
+
}
|
|
2456
|
+
async function exists(path) {
|
|
2457
|
+
try {
|
|
2458
|
+
await access(path);
|
|
2459
|
+
return true;
|
|
2460
|
+
} catch {
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async function readIfPresent(path) {
|
|
2465
|
+
try {
|
|
2466
|
+
return await readFile(path, "utf8");
|
|
2467
|
+
} catch {
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
async function refreshNpmLockfile(cwd) {
|
|
2472
|
+
try {
|
|
2473
|
+
await execFileAsync(
|
|
2474
|
+
"npm",
|
|
2475
|
+
["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
2476
|
+
{ cwd }
|
|
2477
|
+
);
|
|
2478
|
+
return null;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return "package-lock.json could not be refreshed \u2014 run `npm install` here to bring it up to date.";
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function runUpdate(newVersion, options = {}) {
|
|
2484
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2485
|
+
const manifestText = await readIfPresent(join(cwd, "package.json"));
|
|
2486
|
+
if (manifestText === null) {
|
|
2487
|
+
return {
|
|
2488
|
+
code: 1,
|
|
2489
|
+
lines: [
|
|
2490
|
+
"create-meith update: no package.json here.",
|
|
2491
|
+
"Run this inside the board directory \u2014 the one `npx create-meith` scaffolded,",
|
|
2492
|
+
"or the clone of your board repository."
|
|
2493
|
+
]
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
let manifest;
|
|
2497
|
+
try {
|
|
2498
|
+
manifest = JSON.parse(manifestText);
|
|
2499
|
+
} catch {
|
|
2500
|
+
return { code: 1, lines: ["create-meith update: package.json here is not valid JSON."] };
|
|
2501
|
+
}
|
|
2502
|
+
const currentVersion = manifest.dependencies?.["@meith/web"];
|
|
2503
|
+
if (currentVersion === void 0) {
|
|
2504
|
+
return {
|
|
2505
|
+
code: 1,
|
|
2506
|
+
lines: [
|
|
2507
|
+
"create-meith update: this package.json does not depend on @meith/web,",
|
|
2508
|
+
"so this does not look like a Meith board."
|
|
2509
|
+
]
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const parsedCurrent = parseExactVersion(currentVersion);
|
|
2513
|
+
if (parsedCurrent === null) {
|
|
2514
|
+
return {
|
|
2515
|
+
code: 1,
|
|
2516
|
+
lines: [
|
|
2517
|
+
`create-meith update: @meith/web is pinned to '${currentVersion}', not an exact X.Y.Z version.`,
|
|
2518
|
+
"Pin it first \u2014 `npm install --save-exact @meith/web@<version>` \u2014 so the updater",
|
|
2519
|
+
"can tell which release this board is on."
|
|
2520
|
+
]
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
const parsedNew = parseExactVersion(newVersion);
|
|
2524
|
+
if (parsedNew === null) {
|
|
2525
|
+
return {
|
|
2526
|
+
code: 1,
|
|
2527
|
+
lines: [`create-meith update: cannot update to '${newVersion}' \u2014 not an exact version.`]
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
if (compareExactVersions(currentVersion, newVersion) === 0) {
|
|
2531
|
+
return { code: 0, lines: [`Already at ${newVersion} \u2014 nothing to update.`] };
|
|
2532
|
+
}
|
|
2533
|
+
if (compareExactVersions(currentVersion, newVersion) > 0) {
|
|
2534
|
+
return {
|
|
2535
|
+
code: 1,
|
|
2536
|
+
lines: [
|
|
2537
|
+
`create-meith update: this board is on ${currentVersion}, newer than ${newVersion}.`,
|
|
2538
|
+
"Downgrades are refused \u2014 migrations are forward-only. To update, run the",
|
|
2539
|
+
"newest updater: `npx create-meith@latest update`."
|
|
2540
|
+
]
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
if (parsedNew[0] - parsedCurrent[0] > 2) {
|
|
2544
|
+
const stage = parsedCurrent[0] + 2;
|
|
2545
|
+
return {
|
|
2546
|
+
code: 1,
|
|
2547
|
+
lines: [
|
|
2548
|
+
`create-meith update: ${currentVersion} to ${newVersion} jumps more than two majors,`,
|
|
2549
|
+
"which is further than upgrades are tested to span. Update in stages instead \u2014",
|
|
2550
|
+
`\`npx create-meith@${stage} update\` first, deploy and run \`meith upgrade\`,`,
|
|
2551
|
+
"then come back to `npx create-meith@latest update`."
|
|
2552
|
+
]
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
const name = typeof manifest.name === "string" && manifest.name !== "" ? manifest.name : TEMPLATE_BOARD_NAME;
|
|
2556
|
+
const target = await exists(join(cwd, "vercel.json")) ? "vercel" : "self-host";
|
|
2557
|
+
const next = scaffold({
|
|
2558
|
+
name,
|
|
2559
|
+
version: newVersion,
|
|
2560
|
+
repositoryUrl: DEFAULT_REPOSITORY_URL,
|
|
2561
|
+
target
|
|
2562
|
+
});
|
|
2563
|
+
const previous = await (options.loadPrevious ?? fetchPreviousTree)(target, currentVersion, name);
|
|
2564
|
+
const paths = /* @__PURE__ */ new Set(["package.json", ...next.keys(), ...previous?.keys() ?? []]);
|
|
2565
|
+
const current = /* @__PURE__ */ new Map();
|
|
2566
|
+
for (const path of paths) {
|
|
2567
|
+
const content = await readIfPresent(join(cwd, path));
|
|
2568
|
+
if (content !== null) current.set(path, content);
|
|
2569
|
+
}
|
|
2570
|
+
const plan = planUpdate({ current, previous, next, newVersion });
|
|
2571
|
+
for (const [path, content] of plan.writes) {
|
|
2572
|
+
const absolute = join(cwd, path);
|
|
2573
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
2574
|
+
await writeFile(absolute, content, "utf8");
|
|
2575
|
+
}
|
|
2576
|
+
for (const path of plan.deletes) {
|
|
2577
|
+
await rm(join(cwd, path), { force: true });
|
|
2578
|
+
}
|
|
2579
|
+
const lockfileWarning = plan.writes.size > 0 && await exists(join(cwd, "package-lock.json")) ? await (options.refreshLockfile ?? refreshNpmLockfile)(cwd) : null;
|
|
2580
|
+
const templateUrl = `https://github.com/${TEMPLATE_REPOSITORIES[target]}`;
|
|
2581
|
+
const nextManifest = JSON.parse(next.get("package.json") ?? "{}");
|
|
2582
|
+
const nextPin = nextManifest.dependencies?.next;
|
|
2583
|
+
const lines = [];
|
|
2584
|
+
const changed = plan.writes.size + plan.deletes.length;
|
|
2585
|
+
if (changed === 0) {
|
|
2586
|
+
lines.push(`Nothing to write \u2014 every file already matches ${newVersion}.`);
|
|
2587
|
+
} else {
|
|
2588
|
+
lines.push(
|
|
2589
|
+
`Updated ${name} from ${currentVersion} to ${newVersion} \u2014 ${changed} file${changed === 1 ? "" : "s"} changed.`,
|
|
2590
|
+
""
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
for (const path of plan.updated) {
|
|
2594
|
+
lines.push(
|
|
2595
|
+
path === "package.json" ? ` updated package.json \u2014 every @meith/* pin to ${newVersion}${nextPin === void 0 ? "" : `, next to ${nextPin}`}` : ` updated ${path}`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
for (const path of plan.created) lines.push(` added ${path}`);
|
|
2599
|
+
for (const path of plan.deletes) lines.push(` removed ${path} \u2014 this release no longer ships it`);
|
|
2600
|
+
for (const path of plan.skipped) {
|
|
2601
|
+
lines.push(
|
|
2602
|
+
` kept ${path} \u2014 it differs from the ${currentVersion} scaffold; compare it with ${templateUrl} at v${newVersion}`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
if (previous === null) {
|
|
2606
|
+
lines.push(
|
|
2607
|
+
"",
|
|
2608
|
+
`Could not read the v${currentVersion} template from ${templateUrl}, so only`,
|
|
2609
|
+
"package.json was updated. Review these files against that repository at",
|
|
2610
|
+
`v${newVersion} \u2014 this release may have changed them:`,
|
|
2611
|
+
...plan.review.map((path) => ` ${path}`)
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
if (lockfileWarning !== null) lines.push("", lockfileWarning);
|
|
2615
|
+
if (changed > 0) {
|
|
2616
|
+
lines.push(
|
|
2617
|
+
"",
|
|
2618
|
+
"Next:",
|
|
2619
|
+
" 1. Review the diff, commit, and push.",
|
|
2620
|
+
" 2. Take a backup, then deploy. The release notes name the migrations:",
|
|
2621
|
+
` ${RELEASE_NOTES_URL}/v${newVersion}`,
|
|
2622
|
+
" 3. Once the new version serves, run `meith upgrade` against the board \u2014",
|
|
2623
|
+
" the admin panel shows a notice until it has run."
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
return { code: 0, lines };
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/cli.ts
|
|
2630
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
2103
2631
|
async function isSafeTarget(target) {
|
|
2104
2632
|
try {
|
|
2105
2633
|
return (await readdir(target)).length === 0;
|
|
@@ -2109,14 +2637,14 @@ async function isSafeTarget(target) {
|
|
|
2109
2637
|
}
|
|
2110
2638
|
async function initGit(target) {
|
|
2111
2639
|
try {
|
|
2112
|
-
await
|
|
2113
|
-
await
|
|
2640
|
+
await execFileAsync2("git", ["init", "-q", "-b", "main"], { cwd: target });
|
|
2641
|
+
await execFileAsync2("git", ["add", "-A"], { cwd: target });
|
|
2114
2642
|
return true;
|
|
2115
2643
|
} catch {
|
|
2116
2644
|
return false;
|
|
2117
2645
|
}
|
|
2118
2646
|
}
|
|
2119
|
-
async function run(argv, version) {
|
|
2647
|
+
async function run(argv, version, updateOptions = {}) {
|
|
2120
2648
|
const positional = [];
|
|
2121
2649
|
for (let i = 0; i < argv.length; i++) {
|
|
2122
2650
|
const arg = argv[i];
|
|
@@ -2131,21 +2659,30 @@ async function run(argv, version) {
|
|
|
2131
2659
|
return {
|
|
2132
2660
|
code: 0,
|
|
2133
2661
|
lines: [
|
|
2134
|
-
"create-meith \u2014 scaffold a forum project, a plugin or a theme.",
|
|
2662
|
+
"create-meith \u2014 scaffold a forum project, a plugin or a theme, or update one.",
|
|
2135
2663
|
"",
|
|
2136
2664
|
" npx create-meith <name> [--repo <url>] [--no-git]",
|
|
2137
2665
|
" npx create-meith --plugin <name> [--repo <url>] [--no-git]",
|
|
2138
2666
|
" npx create-meith --theme <name> [--repo <url>] [--no-git]",
|
|
2667
|
+
" npx create-meith@latest update",
|
|
2139
2668
|
"",
|
|
2140
2669
|
"The first form writes a deployable board into ./<name>, then tells you",
|
|
2141
2670
|
"what to run. --plugin and --theme write an extension workspace instead:",
|
|
2142
2671
|
"source and a passing test copied from the meith repository\u2019s worked",
|
|
2143
2672
|
"examples, plus a README and a marketplace listing.json.",
|
|
2144
2673
|
"",
|
|
2674
|
+
"`update`, run inside a board, moves it to this release: every @meith/*",
|
|
2675
|
+
"pin and next together in package.json, and the deploy files the scaffold",
|
|
2676
|
+
"owns rewritten to the new shape \u2014 files you have edited are left alone",
|
|
2677
|
+
"and named. `update` is a reserved word, so a board cannot take it as a name.",
|
|
2678
|
+
"",
|
|
2145
2679
|
"--no-git skips initializing a git repository in the new directory."
|
|
2146
2680
|
]
|
|
2147
2681
|
};
|
|
2148
2682
|
}
|
|
2683
|
+
if (name === "update") {
|
|
2684
|
+
return await runUpdate(version, updateOptions);
|
|
2685
|
+
}
|
|
2149
2686
|
const wantsPlugin = argv.includes("--plugin");
|
|
2150
2687
|
const wantsTheme = argv.includes("--theme");
|
|
2151
2688
|
if (wantsPlugin && wantsTheme) {
|
|
@@ -2178,9 +2715,9 @@ async function run(argv, version) {
|
|
|
2178
2715
|
repositoryUrl: repoIndex === -1 ? void 0 : repositoryUrl
|
|
2179
2716
|
});
|
|
2180
2717
|
for (const [relative, contents] of files) {
|
|
2181
|
-
const path =
|
|
2182
|
-
await
|
|
2183
|
-
await
|
|
2718
|
+
const path = join2(target, relative);
|
|
2719
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2720
|
+
await writeFile2(path, contents, "utf8");
|
|
2184
2721
|
}
|
|
2185
2722
|
const gitReady = argv.includes("--no-git") ? false : await initGit(target);
|
|
2186
2723
|
return {
|
|
@@ -2219,7 +2756,7 @@ async function run(argv, version) {
|
|
|
2219
2756
|
}
|
|
2220
2757
|
|
|
2221
2758
|
// src/bin.ts
|
|
2222
|
-
var result = await run(process.argv.slice(2), "0.
|
|
2759
|
+
var result = await run(process.argv.slice(2), "0.31.0");
|
|
2223
2760
|
for (const line of result.lines) {
|
|
2224
2761
|
if (result.code === 0) console.log(line);
|
|
2225
2762
|
else console.error(line);
|