From 427b58db6f10f1df379eb0ff97e2b15703a53c37 Mon Sep 17 00:00:00 2001 From: Chris Manson Date: Mon, 7 Jul 2025 10:51:19 +0100 Subject: [PATCH] add glitch support --- cli.js | 16 +++- package.json | 2 + pnpm-lock.yaml | 81 +++++++++++++++++++ projects/glitch.js | 117 ++++++++++++++++++++++++++++ projects/lib/dry-execa.js | 3 +- projects/lib/ensure-one-password.js | 25 ++++++ 6 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 projects/glitch.js create mode 100644 projects/lib/ensure-one-password.js diff --git a/cli.js b/cli.js index b21444b..f615057 100755 --- a/cli.js +++ b/cli.js @@ -11,6 +11,7 @@ const pkg = JSON.parse( import guides from './projects/guides.js'; import guidesSearch from './projects/guides-search.js'; +import glitch from './projects/glitch.js'; program .name(pkg.name) @@ -27,7 +28,7 @@ program .action((args, commandOptions) => guides(args, { ...program.opts(), - ...commandOptions, + ...commandOptions.opts(), }), ); @@ -37,8 +38,19 @@ program .action((args, commandOptions) => guidesSearch(args, { ...program.opts(), - ...commandOptions, + ...commandOptions.opts(), }), ); +program + .command('glitch') + .description('Update the Glitch starter pack') + .requiredOption('-v, --version ', `Set this to be the version you're trying to release`) + .action((args, commandOptions) => + glitch(args, { + ...program.opts(), + ...commandOptions.opts(), + }) + ); + program.parse(); diff --git a/package.json b/package.json index d5b5b33..86e80a8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "commander": "^12.1.0", "enquirer": "^2.4.1", "execa": "^9.5.2", + "extract-zip": "^2.0.1", "semver": "^7.6.3", + "tmp-promise": "^3.0.3", "yaml": "^2.6.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9518e9d..772568c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,15 @@ importers: execa: specifier: ^9.5.2 version: 9.5.2 + extract-zip: + specifier: ^2.0.1 + version: 2.0.1 semver: specifier: ^7.6.3 version: 7.6.3 + tmp-promise: + specifier: ^3.0.3 + version: 3.0.3 yaml: specifier: ^2.6.1 version: 2.6.1 @@ -240,6 +246,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -310,6 +322,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + cacache@15.3.0: resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} engines: {node: '>= 10'} @@ -471,6 +486,11 @@ packages: resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -487,6 +507,9 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -963,6 +986,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -1165,6 +1191,13 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1183,6 +1216,9 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -1269,6 +1305,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1517,6 +1556,16 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.10.2 + optional: true + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -1580,6 +1629,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-crc32@0.2.13: {} + cacache@15.3.0: dependencies: '@npmcli/fs': 1.1.1 @@ -1807,6 +1858,16 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.1 + extract-zip@2.0.1: + dependencies: + debug: 4.4.0 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -1825,6 +1886,10 @@ snapshots: dependencies: reusify: 1.0.4 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -2292,6 +2357,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pend@1.2.0: {} + picomatch@2.3.1: {} prelude-ls@1.2.1: {} @@ -2485,6 +2552,12 @@ snapshots: dependencies: any-promise: 1.3.0 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.2.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -2499,6 +2572,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 + undici-types@6.20.0: + optional: true + unicorn-magic@0.3.0: {} unique-filename@1.1.1: @@ -2585,6 +2661,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yocto-queue@0.1.0: {} yoctocolors@2.1.1: {} diff --git a/projects/glitch.js b/projects/glitch.js new file mode 100644 index 0000000..138fdd6 --- /dev/null +++ b/projects/glitch.js @@ -0,0 +1,117 @@ +import { execa } from 'execa'; + +import { readFile, cp } from 'node:fs/promises'; +import fs from 'fs'; + +import ensureOnePassword from './lib/ensure-one-password.js'; +import { automated } from './lib/log.js'; +import { dryExeca } from './lib/dry-execa.js'; +import dryWrite from './lib/dry-write.js'; + +import tmp from 'tmp-promise'; + +import { basename, join } from 'node:path'; +import extract from 'extract-zip'; +import { finished } from 'stream/promises'; +import { Readable } from 'node:stream'; + +tmp.setGracefulCleanup(); + +async function dryDownload(url, dest, dryRun) { + if(dryRun) { + console.log(`🌵 Downloading ${url} to ${dest}`); + return + } + + automated(`Downloading ${url}`); + + var file = fs.createWriteStream(dest); + const res = await fetch(url); + await finished(Readable.fromWeb(res.body).pipe(file)); + +} + +function dryExtract(path, dir, dryRun) { + if(dryRun) { + console.log(`🌵 Extracting ${path} into ${dir}`) + return + } + + automated(`Extracting ${basename(path)}`) + return extract(path, { dir }) +} + +function dryCopy(from, to, dryRun) { + if(dryRun) { + console.log(`🌵 Copying ${from} to ${to}`); + return + } + automated(`Copying ${from} to ${to}`); + return cp(from, to, {recursive: true}); +} + + +export default async function guides(args, options) { + const dryRun = options.dryRun ?? false; + + await ensureOnePassword(); + + const { stdout: glitchRepo } = await execa`op read ${"op://Ember Learning Team/Glitch/password"}`; + const tmpDirectory = await tmp.dir({ + unsafeCleanup: true + }); + + // we need to make sure to clean up the temp directory since the remote url + // has the password embedded in it and it's a security risk to keep it around + try { + await dryExeca(`git clone ${glitchRepo}`, dryRun, {cwd: tmpDirectory.path }); + + await dryDownload(`https://github.com/ember-cli/ember-new-output/archive/v${options.version}.zip`, join(tmpDirectory.path, 'version.zip'), dryRun); + + await dryExtract(join(tmpDirectory.path, 'version.zip'), tmpDirectory.path, dryRun); + + await dryCopy(join(tmpDirectory.path, `ember-new-output-${options.version}`), join(tmpDirectory.path, 'emberjs'), dryRun); + + let pkgJsonPath = join(tmpDirectory.path, 'emberjs', 'package.json'); + let pkgJson; + + if(dryRun) { + pkgJson = `{ "name": "fake-content" }`; + } else { + pkgJson = await readFile(pkgJsonPath, 'utf-8'); + pkgJson = pkgJson.replace('ember serve', 'npx ember serve -p 4200') + } + + await dryWrite(pkgJsonPath, pkgJson, dryRun); + + await dryExeca(`npm install`, dryRun, { + cwd: join(tmpDirectory.path, 'emberjs') + }); + + + let indexHtmlPath = join(tmpDirectory.path, 'emberjs', 'app', 'index.html'); + let indexHtml; + + if(dryRun) { + indexHtml = ` fake content `; + } else { + indexHtml = await readFile(indexHtmlPath, "utf-8"); + indexHtml = indexHtml.replace(` `, ` + +
+ + `) + } + + await dryWrite(indexHtmlPath, indexHtml, dryRun); + + await dryExeca(`git add .`, dryRun, {cwd: join(tmpDirectory.path, 'emberjs')}) + await dryExeca(`git commit -m ${options.version}`, dryRun, {cwd: join(tmpDirectory.path, 'emberjs')}) + await dryExeca(`git push`, dryRun, {cwd: join(tmpDirectory.path, 'emberjs')}) + } finally { + await tmpDirectory.cleanup(); + } + + console.log('success'); +} diff --git a/projects/lib/dry-execa.js b/projects/lib/dry-execa.js index 7ce66f4..e9beab8 100644 --- a/projects/lib/dry-execa.js +++ b/projects/lib/dry-execa.js @@ -5,7 +5,7 @@ import { execaCommand } from 'execa'; * @param {string} command * @param {boolean} dryRun */ -export function dryExeca(command, dryRun = true) { +export function dryExeca(command, dryRun = true, options) { if (dryRun) { console.log(`🌵 Dry run: '${command}'`); } else { @@ -14,6 +14,7 @@ export function dryExeca(command, dryRun = true) { preferLocal: true, stdout: 'inherit', stdin: 'inherit', + ...options, }); } } diff --git a/projects/lib/ensure-one-password.js b/projects/lib/ensure-one-password.js new file mode 100644 index 0000000..95b7a6d --- /dev/null +++ b/projects/lib/ensure-one-password.js @@ -0,0 +1,25 @@ +import {execa } from 'execa'; +import { fatalError } from './log.js'; + +function signIn() { + return execa`op signin --account ember-cli.1password.com` +} + +export default async function ensureOnePassword() { + try { + await execa`op whoami`; + } catch (err) { + try { + if (err.message.includes('account is not signed in')) { + return signIn(); + } else if (err.message.includes('ENOENT')) { + await execa({stdio: 'inherit'})`brew install --cask 1password/tap/1password-cli` + return signIn(); + } else { + fatalError(`error checking one-password commandline: ${err.message}`) + } + } catch (err) { + fatalError(`unknown error occored while checking one-password commandline: ${err.message}`); + } + } +} \ No newline at end of file