snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
439
scripts/aggregate-changeset.js
Normal file
439
scripts/aggregate-changeset.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/* oslint-disable */
|
||||
|
||||
import { execFile as execFileCallback } from 'node:child_process'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import fg from 'fast-glob'
|
||||
import getReleasePlan from '@changesets/get-release-plan'
|
||||
|
||||
const execFile = promisify(execFileCallback)
|
||||
const require = createRequire(import.meta.url)
|
||||
const resolveFromWorkspace = request =>
|
||||
require.resolve(request, {
|
||||
paths: [process.cwd(), require.resolve('@changesets/cli/changelog')],
|
||||
})
|
||||
const semverSatisfies = require(resolveFromWorkspace('semver/functions/satisfies'))
|
||||
const validRange = require(resolveFromWorkspace('semver/ranges/valid'))
|
||||
|
||||
function normalizeChangelogOption(changelog) {
|
||||
if (changelog === false) return false
|
||||
if (typeof changelog === 'string') return [changelog, null]
|
||||
return changelog
|
||||
}
|
||||
|
||||
async function readChangesetConfig() {
|
||||
const content = await readFile('.changeset/config.json', 'utf-8')
|
||||
const json = JSON.parse(content)
|
||||
|
||||
return {
|
||||
...json,
|
||||
changelog: normalizeChangelogOption(
|
||||
json.changelog === undefined ? '@changesets/cli/changelog' : json.changelog,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveModuleFrom(fromDir, request) {
|
||||
const fromRequire = createRequire(path.join(fromDir, '__changeset_resolver__.js'))
|
||||
return fromRequire.resolve(request)
|
||||
}
|
||||
|
||||
async function loadChangelogGenerator(cwd) {
|
||||
const config = await readChangesetConfig()
|
||||
|
||||
if (!config.changelog) {
|
||||
return {
|
||||
config,
|
||||
changelogFunctions: null,
|
||||
changelogOpts: null,
|
||||
}
|
||||
}
|
||||
|
||||
const [modulePath, changelogOpts] = config.changelog
|
||||
const changesetDir = path.join(cwd, '.changeset')
|
||||
|
||||
let resolvedPath
|
||||
|
||||
try {
|
||||
resolvedPath = resolveModuleFrom(changesetDir, modulePath)
|
||||
} catch {
|
||||
resolvedPath = resolveModuleFrom(cwd, modulePath)
|
||||
}
|
||||
|
||||
let mod = await import(pathToFileURL(resolvedPath).href)
|
||||
mod = mod.default ?? mod
|
||||
mod = mod.default ?? mod
|
||||
|
||||
if (
|
||||
typeof mod.getReleaseLine !== 'function' ||
|
||||
typeof mod.getDependencyReleaseLine !== 'function'
|
||||
) {
|
||||
throw new Error(`Could not resolve changelog generation functions from ${modulePath}`)
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
changelogFunctions: mod,
|
||||
changelogOpts,
|
||||
}
|
||||
}
|
||||
|
||||
async function getChangesetCommit(cwd, changesetId) {
|
||||
const candidates = [`.changeset/${changesetId}.md`, `.changeset/${changesetId}/changes.json`]
|
||||
|
||||
for (const filePath of candidates) {
|
||||
try {
|
||||
const { stdout } = await execFile(
|
||||
'git',
|
||||
['log', '--diff-filter=A', '--format=%H', '-n', '1', '--', filePath],
|
||||
{
|
||||
cwd,
|
||||
},
|
||||
)
|
||||
const commit = stdout.trim()
|
||||
|
||||
if (commit) return commit
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function addCommitsToChangesets(cwd, changesets) {
|
||||
return Promise.all(
|
||||
changesets.map(async changeset => ({
|
||||
...changeset,
|
||||
commit: await getChangesetCommit(cwd, changeset.id),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async function loadWorkspacePackages(cwd) {
|
||||
const packageFiles = await fg(['packages/*/package.json', 'demos/package.json'], {
|
||||
cwd,
|
||||
absolute: true,
|
||||
})
|
||||
|
||||
const packages = new Map()
|
||||
|
||||
for (const packageFile of packageFiles) {
|
||||
const packageJson = JSON.parse(await readFile(packageFile, 'utf-8'))
|
||||
|
||||
if (!packageJson.name) continue
|
||||
|
||||
packages.set(packageJson.name, {
|
||||
dir: path.dirname(packageFile),
|
||||
packageJson,
|
||||
})
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
function getBumpLevel(type) {
|
||||
const level = ['none', 'patch', 'minor', 'major'].indexOf(type)
|
||||
|
||||
if (level < 0) {
|
||||
throw new Error(`Unrecognised bump type ${type}`)
|
||||
}
|
||||
|
||||
return level
|
||||
}
|
||||
|
||||
function shouldUpdateDependencyBasedOnConfig(cwd, release, dependency, config) {
|
||||
let { depVersionRange } = dependency
|
||||
const usesWorkspaceRange = depVersionRange.startsWith('workspace:')
|
||||
|
||||
if (usesWorkspaceRange) {
|
||||
depVersionRange = depVersionRange.replace(/^workspace:/, '')
|
||||
|
||||
switch (depVersionRange) {
|
||||
case '*':
|
||||
return true
|
||||
case '^':
|
||||
case '~':
|
||||
depVersionRange = `${depVersionRange}${release.oldVersion}`
|
||||
break
|
||||
default:
|
||||
if (!validRange(depVersionRange)) {
|
||||
return (
|
||||
path.posix.normalize(depVersionRange) ===
|
||||
path.relative(cwd, release.dir).replaceAll('\\', '/')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!semverSatisfies(release.version, depVersionRange)) {
|
||||
return true
|
||||
}
|
||||
|
||||
let shouldUpdate = getBumpLevel(release.type) >= getBumpLevel(config.minReleaseType)
|
||||
|
||||
if (dependency.depType === 'peerDependencies') {
|
||||
shouldUpdate = !config.onlyUpdatePeerDependentsWhenOutOfRange
|
||||
}
|
||||
|
||||
return shouldUpdate
|
||||
}
|
||||
|
||||
async function getPackageChangelogEntry(
|
||||
cwd,
|
||||
release,
|
||||
releases,
|
||||
changesets,
|
||||
changelogFunctions,
|
||||
changelogOpts,
|
||||
config,
|
||||
) {
|
||||
if (release.type === 'none') return null
|
||||
|
||||
const changelogLines = {
|
||||
major: [],
|
||||
minor: [],
|
||||
patch: [],
|
||||
}
|
||||
|
||||
for (const changeset of changesets) {
|
||||
const changesetRelease = changeset.releases.find(item => item.name === release.name)
|
||||
|
||||
if (changesetRelease && changesetRelease.type !== 'none') {
|
||||
changelogLines[changesetRelease.type].push(
|
||||
changelogFunctions.getReleaseLine(changeset, changesetRelease.type, changelogOpts),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const dependentReleases = releases.filter(otherRelease => {
|
||||
const dependencyVersionRange = release.packageJson.dependencies?.[otherRelease.name]
|
||||
const peerDependencyVersionRange = release.packageJson.peerDependencies?.[otherRelease.name]
|
||||
const versionRange = dependencyVersionRange || peerDependencyVersionRange
|
||||
const usesWorkspaceRange = versionRange?.startsWith('workspace:')
|
||||
|
||||
return (
|
||||
versionRange &&
|
||||
(usesWorkspaceRange || validRange(versionRange) !== null) &&
|
||||
shouldUpdateDependencyBasedOnConfig(
|
||||
cwd,
|
||||
{
|
||||
type: otherRelease.type,
|
||||
version: otherRelease.newVersion,
|
||||
oldVersion: otherRelease.oldVersion,
|
||||
dir: otherRelease.dir,
|
||||
},
|
||||
{
|
||||
depVersionRange: versionRange,
|
||||
depType: dependencyVersionRange ? 'dependencies' : 'peerDependencies',
|
||||
},
|
||||
{
|
||||
minReleaseType: config.updateInternalDependencies,
|
||||
onlyUpdatePeerDependentsWhenOutOfRange: config.onlyUpdatePeerDependentsWhenOutOfRange,
|
||||
},
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const relevantChangesetIds = new Set()
|
||||
|
||||
for (const dependentRelease of dependentReleases) {
|
||||
for (const changesetId of dependentRelease.changesets) {
|
||||
relevantChangesetIds.add(changesetId)
|
||||
}
|
||||
}
|
||||
|
||||
const relevantChangesets = changesets.filter(changeset => relevantChangesetIds.has(changeset.id))
|
||||
|
||||
changelogLines.patch.push(
|
||||
changelogFunctions.getDependencyReleaseLine(
|
||||
relevantChangesets,
|
||||
dependentReleases,
|
||||
changelogOpts,
|
||||
),
|
||||
)
|
||||
|
||||
const sections = []
|
||||
|
||||
for (const type of ['major', 'minor', 'patch']) {
|
||||
const lines = (await Promise.all(changelogLines[type]))
|
||||
.map(line => line?.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (lines.length > 0) {
|
||||
sections.push(`### ${type[0].toUpperCase()}${type.slice(1)} Changes\n\n${lines.join('\n')}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.length === 0) return null
|
||||
|
||||
return [`## ${release.newVersion}`, ...sections].join('\n')
|
||||
}
|
||||
|
||||
function renderPackageSection(name, entry) {
|
||||
const body = entry
|
||||
.replace(/^##\s+.*\n+/, '')
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^(#{1,5})\s/, '$1# '))
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
return `### ${name}\n\n${body}`
|
||||
}
|
||||
|
||||
function renderReleaseNotes(version, sections) {
|
||||
return (
|
||||
[
|
||||
'# Releases',
|
||||
'',
|
||||
`## v${version}`,
|
||||
'',
|
||||
...sections.flatMap((section, index) =>
|
||||
index === sections.length - 1 ? [section] : [section, ''],
|
||||
),
|
||||
].join('\n') + '\n'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing `## v<version>` section in the changelog body, or prepend it.
|
||||
* The body passed in should not include the `# Releases` heading.
|
||||
*
|
||||
* @param {string} body - existing changelog body (without top heading)
|
||||
* @param {string} newSection - the section text starting with `## v...` and ending with a newline
|
||||
* @param {string} version - semantic version string (without leading 'v')
|
||||
* @returns {string} updated changelog body including the new or replaced section
|
||||
*/
|
||||
function replaceOrPrependVersionSection(body, newSection, version) {
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
const existing = body || ''
|
||||
const newTrim = newSection.trim()
|
||||
|
||||
if (!existing.trim()) {
|
||||
return `${newTrim}\n\n`
|
||||
}
|
||||
|
||||
const headerRe = new RegExp(`^## v${escapeRegExp(version)}\\b`, 'm')
|
||||
const headerMatch = headerRe.exec(existing)
|
||||
|
||||
if (!headerMatch) {
|
||||
return `${newTrim}\n\n${existing}`
|
||||
}
|
||||
|
||||
const startIndex = headerMatch.index
|
||||
const rest = existing.slice(startIndex + headerMatch[0].length)
|
||||
const nextHeaderRe = /^#{1,2}\s.*$/m
|
||||
const nextMatch = nextHeaderRe.exec(rest)
|
||||
|
||||
const endIndex = nextMatch
|
||||
? startIndex + headerMatch[0].length + nextMatch.index
|
||||
: existing.length
|
||||
const before = existing.slice(0, startIndex)
|
||||
const after = existing.slice(endIndex)
|
||||
|
||||
return `${before}${newTrim}\n\n${after}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the release heading from the existing CHANGELOG.md file.
|
||||
*/
|
||||
async function readChangelogBody() {
|
||||
try {
|
||||
const content = await readFile('CHANGELOG.md', 'utf-8')
|
||||
return content.replace(/^# Releases\n\n/, '')
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT') return ''
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const cwd = process.cwd()
|
||||
const plan = await getReleasePlan(cwd)
|
||||
|
||||
if (!plan || !Array.isArray(plan.changesets) || plan.changesets.length === 0) {
|
||||
console.log('No changesets found — skipping changelog generation')
|
||||
return
|
||||
}
|
||||
|
||||
const version = plan.releases && plan.releases[0] ? plan.releases[0].newVersion : 'unreleased'
|
||||
const { config, changelogFunctions, changelogOpts } = await loadChangelogGenerator(cwd)
|
||||
|
||||
if (!changelogFunctions) {
|
||||
console.log(
|
||||
'Changelog generation is disabled in .changeset/config.json — skipping aggregate changelog generation',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const packageMap = await loadWorkspacePackages(cwd)
|
||||
const releasesWithPackages = plan.releases
|
||||
.filter(release => {
|
||||
const pkg = packageMap.get(release.name)
|
||||
return !!pkg
|
||||
})
|
||||
.map(release => {
|
||||
const pkg = packageMap.get(release.name)
|
||||
|
||||
return {
|
||||
...release,
|
||||
...pkg,
|
||||
}
|
||||
})
|
||||
// Ignore private packages like the demos package
|
||||
.filter(release => release.type !== 'none' && !release.packageJson.private)
|
||||
|
||||
const changesetsWithCommits = await addCommitsToChangesets(cwd, plan.changesets)
|
||||
const aggregateConfig = {
|
||||
updateInternalDependencies: config.updateInternalDependencies === 'minor' ? 'minor' : 'patch',
|
||||
onlyUpdatePeerDependentsWhenOutOfRange: Boolean(
|
||||
config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH
|
||||
?.onlyUpdatePeerDependentsWhenOutOfRange,
|
||||
),
|
||||
}
|
||||
|
||||
const sections = []
|
||||
|
||||
for (const release of releasesWithPackages) {
|
||||
const entry = await getPackageChangelogEntry(
|
||||
cwd,
|
||||
release,
|
||||
releasesWithPackages,
|
||||
changesetsWithCommits,
|
||||
changelogFunctions,
|
||||
changelogOpts,
|
||||
aggregateConfig,
|
||||
)
|
||||
|
||||
if (entry) {
|
||||
sections.push(renderPackageSection(release.name, entry))
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
console.log(
|
||||
'No package changelog entries generated — skipping aggregate changelog generation',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const output = renderReleaseNotes(version, sections)
|
||||
const existingBody = await readChangelogBody()
|
||||
const newSection = output.split('\n').slice(2).join('\n').trimEnd() + '\n'
|
||||
const updatedBody = replaceOrPrependVersionSection(existingBody, newSection, version)
|
||||
await writeFile('CHANGELOG.md', `# Releases\n\n${updatedBody}`, 'utf-8')
|
||||
|
||||
console.log('Release notes written to CHANGELOG.md')
|
||||
} catch (err) {
|
||||
console.error('Failed to aggregate changesets:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
53
scripts/check-package-dists.sh
Executable file
53
scripts/check-package-dists.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
shopt -s nullglob
|
||||
|
||||
declare -a missing_paths=()
|
||||
declare -a package_dirs=()
|
||||
declare -a empty_paths=()
|
||||
|
||||
for package_json in "$REPO_ROOT"/packages/*/package.json; do
|
||||
package_dirs+=("${package_json%/package.json}")
|
||||
done
|
||||
|
||||
if [ "${#package_dirs[@]}" -eq 0 ]; then
|
||||
echo "No package directories found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for package_dir in "${package_dirs[@]}"; do
|
||||
dist_dir="$package_dir/dist"
|
||||
|
||||
if [ ! -d "$dist_dir" ]; then
|
||||
missing_paths+=("${dist_dir#"$REPO_ROOT"/}")
|
||||
continue
|
||||
fi
|
||||
|
||||
dist_entries=("$dist_dir"/*)
|
||||
|
||||
if [ "${#dist_entries[@]}" -eq 0 ]; then
|
||||
empty_paths+=("${dist_dir#"$REPO_ROOT"/}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#missing_paths[@]}" -eq 0 ] && [ "${#empty_paths[@]}" -eq 0 ]; then
|
||||
echo "All ${#package_dirs[@]} package dist directories exist and contain build artifacts."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${#missing_paths[@]}" -gt 0 ]; then
|
||||
echo "Missing dist directories:"
|
||||
printf ' - %s\n' "${missing_paths[@]}"
|
||||
fi
|
||||
|
||||
if [ "${#empty_paths[@]}" -gt 0 ]; then
|
||||
echo "Empty dist directories:"
|
||||
printf ' - %s\n' "${empty_paths[@]}"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
54
scripts/check-package-exports.mjs
Normal file
54
scripts/check-package-exports.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
const root = resolve('.')
|
||||
const fields = ['main', 'module', 'types']
|
||||
const errors = []
|
||||
let total = 0
|
||||
|
||||
function collectTargets(value, targets = []) {
|
||||
if (typeof value === 'string') {
|
||||
targets.push(value)
|
||||
return targets
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') return targets
|
||||
|
||||
for (const nestedValue of Object.values(value)) collectTargets(nestedValue, targets)
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
const packagesRoot = join(root, 'packages')
|
||||
|
||||
for (const entry of readdirSync(packagesRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue
|
||||
|
||||
const packageRoot = join(packagesRoot, entry.name)
|
||||
const manifestPath = join(packageRoot, 'package.json')
|
||||
if (!existsSync(manifestPath)) continue
|
||||
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
total += 1
|
||||
|
||||
const targets = fields.flatMap(field =>
|
||||
typeof manifest[field] === 'string' ? [manifest[field]] : [],
|
||||
)
|
||||
targets.push(...collectTargets(manifest.exports))
|
||||
|
||||
for (const target of new Set(targets)) {
|
||||
if (target === './package.json') continue
|
||||
|
||||
// Remove leading './' from target path and join with package root
|
||||
const targetPath = join(packageRoot, target.replace(/^\.\//, ''))
|
||||
if (!existsSync(targetPath)) errors.push(`${manifest.name}: ${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`Missing package export targets in ${total} packages:`)
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`All package export targets exist in ${total} packages.`)
|
||||
}
|
||||
26
scripts/make-demo.sh
Normal file
26
scripts/make-demo.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ask user what the new Demo should be called
|
||||
read -p "What is the name of the new Demo? " DEMO_NAME
|
||||
|
||||
# remove spaces with underscores
|
||||
DEMO_NAME=${DEMO_NAME// /_}
|
||||
|
||||
# Ask user what category the new Demo should be in (options: "Dev", "Examples", "Extensions", "Experiments", "Marks", "Nodes")
|
||||
read -p "What category should the new Demo be in? (Options: Dev, Examples, Extensions, Experiments, Marks, Nodes) " DEMO_CATEGORY
|
||||
|
||||
if [[ ! "$DEMO_CATEGORY" =~ ^(Dev|Examples|Extensions|Experiments|Marks|Nodes)$ ]]; then
|
||||
echo "Invalid category. Please choose from: Dev, Examples, Extensions, Experiments, Marks, Nodes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_DEMO_DIR="demos/src/$DEMO_CATEGORY/$DEMO_NAME"
|
||||
|
||||
# Check if the directory already exists
|
||||
if [ -d "$NEW_DEMO_DIR" ]; then
|
||||
echo "Directory $NEW_DEMO_DIR already exists. Please choose a different name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy demos/src/Examples/Default to the new directory
|
||||
cp -r demos/src/Examples/Default "$NEW_DEMO_DIR"
|
||||
Reference in New Issue
Block a user