purgetss 7.5.3 → 7.6.1
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/README.md +38 -17
- package/bin/purgetss +140 -1
- package/dist/purgetss.ui.js +23 -26
- package/dist/utilities.tss +13 -1
- package/lib/completions/titanium/completions-v3.json +62 -1
- package/lib/templates/purgetss.config.js.cjs +15 -1
- package/lib/templates/purgetss.ui.js.cjs +22 -25
- package/package.json +3 -1
- package/src/cli/commands/brand.js +69 -0
- package/src/cli/commands/create.js +11 -7
- package/src/cli/commands/fonts.js +9 -9
- package/src/cli/commands/icon-library.js +18 -16
- package/src/cli/commands/images.js +116 -0
- package/src/cli/commands/init.js +4 -0
- package/src/cli/commands/module.js +4 -2
- package/src/cli/commands/purge.js +48 -98
- package/src/cli/commands/semantic.js +180 -0
- package/src/cli/commands/shades.js +332 -13
- package/src/cli/utils/project-detection.js +4 -2
- package/src/core/analyzers/class-extractor.js +110 -3
- package/src/core/branding/brand-config.js +111 -0
- package/src/core/branding/branding-logger.js +40 -0
- package/src/core/branding/cleanup-legacy.js +220 -0
- package/src/core/branding/ensure-brand-section.js +80 -0
- package/src/core/branding/gen-android-adaptive.js +116 -0
- package/src/core/branding/gen-android-legacy.js +63 -0
- package/src/core/branding/gen-ic-launcher-xml.js +29 -0
- package/src/core/branding/gen-ios-dark.js +70 -0
- package/src/core/branding/gen-ios-tinted.js +55 -0
- package/src/core/branding/gen-ios.js +69 -0
- package/src/core/branding/gen-marketplace.js +71 -0
- package/src/core/branding/gen-notification.js +76 -0
- package/src/core/branding/gen-splash.js +64 -0
- package/src/core/branding/index.js +336 -0
- package/src/core/branding/post-gen-notes.js +145 -0
- package/src/core/branding/prepare-master.js +108 -0
- package/src/core/branding/tiapp-reader.js +110 -0
- package/src/core/images/ensure-images-section.js +57 -0
- package/src/core/images/gen-scales.js +181 -0
- package/src/core/images/index.js +171 -0
- package/src/shared/config-manager.js +46 -0
- package/src/shared/config-writer.js +84 -0
- package/src/shared/constants.js +3 -0
- package/src/shared/logger.js +69 -4
- package/src/shared/prompt.js +64 -0
- package/src/shared/svg-utils.js +80 -0
- package/src/shared/utils.js +8 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-ios
|
|
3
|
+
*
|
|
4
|
+
* Produces Titanium's two root-level iOS/Android icons:
|
|
5
|
+
*
|
|
6
|
+
* DefaultIcon.png 1024×1024, alpha preserved (universal fallback)
|
|
7
|
+
* DefaultIcon-ios.png 1024×1024, alpha flattened on bg-color (iOS)
|
|
8
|
+
*
|
|
9
|
+
* Two distinct paddings — `DefaultIcon.png` is the universal fallback (iOS +
|
|
10
|
+
* Android when no adaptive icons exist), so it uses the Android safe-zone
|
|
11
|
+
* padding to stay inside launcher masks. `DefaultIcon-ios.png` is iOS-only
|
|
12
|
+
* (no launcher mask), so it uses the looser aesthetic iOS padding.
|
|
13
|
+
*
|
|
14
|
+
* Apple rejects alpha on App Store icon uploads, so DefaultIcon-ios.png is
|
|
15
|
+
* always flattened onto the bg-color.
|
|
16
|
+
*
|
|
17
|
+
* @fileoverview Root iOS icons for Titanium projects
|
|
18
|
+
* @author César Estrada
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs'
|
|
22
|
+
import path from 'path'
|
|
23
|
+
import sharp from 'sharp'
|
|
24
|
+
|
|
25
|
+
const CANVAS = 1024
|
|
26
|
+
|
|
27
|
+
export async function genIos(tightMaster, bgColor, androidPadding, iosPadding, outRoot) {
|
|
28
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
29
|
+
|
|
30
|
+
const defaultIconPath = path.join(outRoot, 'DefaultIcon.png')
|
|
31
|
+
const defaultIconIosPath = path.join(outRoot, 'DefaultIcon-ios.png')
|
|
32
|
+
|
|
33
|
+
// DefaultIcon.png — alpha preserved, Android safe-zone padding so it stays
|
|
34
|
+
// launcher-mask-safe when Android falls back to it.
|
|
35
|
+
await renderSquare(tightMaster, androidPadding, null, defaultIconPath)
|
|
36
|
+
|
|
37
|
+
// DefaultIcon-ios.png — flattened on bg-color, iOS aesthetic padding.
|
|
38
|
+
await renderSquare(tightMaster, iosPadding, bgColor, defaultIconIosPath)
|
|
39
|
+
|
|
40
|
+
return { defaultIcon: defaultIconPath, defaultIconIos: defaultIconIosPath }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function renderSquare(tightMaster, paddingPct, flattenBg, outPath) {
|
|
44
|
+
const inner = Math.floor((CANVAS * (100 - 2 * paddingPct)) / 100)
|
|
45
|
+
|
|
46
|
+
const resized = await sharp(tightMaster)
|
|
47
|
+
.resize({
|
|
48
|
+
width: inner,
|
|
49
|
+
height: inner,
|
|
50
|
+
fit: 'inside',
|
|
51
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
52
|
+
})
|
|
53
|
+
.toBuffer()
|
|
54
|
+
|
|
55
|
+
const pipeline = sharp({
|
|
56
|
+
create: {
|
|
57
|
+
width: CANVAS,
|
|
58
|
+
height: CANVAS,
|
|
59
|
+
channels: 4,
|
|
60
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
61
|
+
}
|
|
62
|
+
}).composite([{ input: resized, gravity: 'center' }])
|
|
63
|
+
|
|
64
|
+
if (flattenBg) {
|
|
65
|
+
await pipeline.flatten({ background: flattenBg }).removeAlpha().png({ compressionLevel: 9 }).toFile(outPath)
|
|
66
|
+
} else {
|
|
67
|
+
await pipeline.png({ compressionLevel: 9 }).toFile(outPath)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-marketplace
|
|
3
|
+
*
|
|
4
|
+
* Store-submission artwork:
|
|
5
|
+
*
|
|
6
|
+
* iTunesConnect.png 1024×1024 (App Store)
|
|
7
|
+
* MarketplaceArtwork.png 512×512 (Google Play)
|
|
8
|
+
*
|
|
9
|
+
* Alpha handling depends on whether --bg-color was explicitly provided:
|
|
10
|
+
* - not provided → alpha preserved (matches `ti create` default template)
|
|
11
|
+
* - provided → alpha flattened onto bg-color (safer for dark-mode stores)
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Marketplace artwork for Titanium branding
|
|
14
|
+
* @author César Estrada
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import sharp from 'sharp'
|
|
20
|
+
|
|
21
|
+
export async function genMarketplace(tightMaster, paddingPct, outRoot, opts = {}) {
|
|
22
|
+
const { flatten = false, bgColor = '#FFFFFF' } = opts
|
|
23
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
24
|
+
|
|
25
|
+
const itunesConnect = await renderSquare(
|
|
26
|
+
tightMaster,
|
|
27
|
+
paddingPct,
|
|
28
|
+
1024,
|
|
29
|
+
path.join(outRoot, 'iTunesConnect.png'),
|
|
30
|
+
{ flatten, bgColor }
|
|
31
|
+
)
|
|
32
|
+
const marketplaceArtwork = await renderSquare(
|
|
33
|
+
tightMaster,
|
|
34
|
+
paddingPct,
|
|
35
|
+
512,
|
|
36
|
+
path.join(outRoot, 'MarketplaceArtwork.png'),
|
|
37
|
+
{ flatten, bgColor }
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return { itunesConnect, marketplaceArtwork }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function renderSquare(tight, paddingPct, canvasSize, outPath, { flatten, bgColor }) {
|
|
44
|
+
const inner = Math.floor((canvasSize * (100 - 2 * paddingPct)) / 100)
|
|
45
|
+
|
|
46
|
+
const resized = await sharp(tight)
|
|
47
|
+
.resize({
|
|
48
|
+
width: inner,
|
|
49
|
+
height: inner,
|
|
50
|
+
fit: 'inside',
|
|
51
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
52
|
+
})
|
|
53
|
+
.toBuffer()
|
|
54
|
+
|
|
55
|
+
let pipeline = sharp({
|
|
56
|
+
create: {
|
|
57
|
+
width: canvasSize,
|
|
58
|
+
height: canvasSize,
|
|
59
|
+
channels: 4,
|
|
60
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.composite([{ input: resized, gravity: 'center' }])
|
|
64
|
+
|
|
65
|
+
if (flatten) {
|
|
66
|
+
pipeline = pipeline.flatten({ background: bgColor }).removeAlpha()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await pipeline.png({ compressionLevel: 9 }).toFile(outPath)
|
|
70
|
+
return outPath
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-notification
|
|
3
|
+
*
|
|
4
|
+
* White-on-transparent notification icons × 5 densities.
|
|
5
|
+
*
|
|
6
|
+
* Android status bar applies runtime tint based on the notification's color
|
|
7
|
+
* property, so all non-transparent pixels become white. Color info is discarded.
|
|
8
|
+
*
|
|
9
|
+
* Sizes: mdpi=24, hdpi=36, xhdpi=48, xxhdpi=72, xxxhdpi=96.
|
|
10
|
+
* Output path: drawable-<density>/ic_stat_notify.png
|
|
11
|
+
*
|
|
12
|
+
* Notification icons are NOT masked by the launcher, so they render as drawn.
|
|
13
|
+
* Trim any transparent padding in the master first, then scale the logo to
|
|
14
|
+
* fill the canvas edge-to-edge with a 1px anti-aliasing margin per side.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview Status-bar notification icons × 5 densities
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import sharp from 'sharp'
|
|
23
|
+
|
|
24
|
+
export const NOTIFICATION_DENSITIES = [
|
|
25
|
+
{ name: 'mdpi', size: 24 },
|
|
26
|
+
{ name: 'hdpi', size: 36 },
|
|
27
|
+
{ name: 'xhdpi', size: 48 },
|
|
28
|
+
{ name: 'xxhdpi', size: 72 },
|
|
29
|
+
{ name: 'xxxhdpi', size: 96 }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const EDGE_MARGIN = 2
|
|
33
|
+
|
|
34
|
+
export async function genNotification(tightMaster, resRoot) {
|
|
35
|
+
const generated = []
|
|
36
|
+
|
|
37
|
+
const trimmedMaster = await sharp(tightMaster)
|
|
38
|
+
.trim()
|
|
39
|
+
.png()
|
|
40
|
+
.toBuffer()
|
|
41
|
+
|
|
42
|
+
for (const { name, size } of NOTIFICATION_DENSITIES) {
|
|
43
|
+
const inner = Math.max(1, size - EDGE_MARGIN)
|
|
44
|
+
const dir = path.join(resRoot, `drawable-${name}`)
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
46
|
+
|
|
47
|
+
const outPath = path.join(dir, 'ic_stat_notify.png')
|
|
48
|
+
|
|
49
|
+
const whitened = await sharp(trimmedMaster)
|
|
50
|
+
.resize({
|
|
51
|
+
width: inner,
|
|
52
|
+
height: inner,
|
|
53
|
+
fit: 'inside',
|
|
54
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
55
|
+
})
|
|
56
|
+
.ensureAlpha()
|
|
57
|
+
.linear([0, 0, 0, 1], [255, 255, 255, 0])
|
|
58
|
+
.toBuffer()
|
|
59
|
+
|
|
60
|
+
await sharp({
|
|
61
|
+
create: {
|
|
62
|
+
width: size,
|
|
63
|
+
height: size,
|
|
64
|
+
channels: 4,
|
|
65
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.composite([{ input: whitened, gravity: 'center' }])
|
|
69
|
+
.png({ compressionLevel: 9 })
|
|
70
|
+
.toFile(outPath)
|
|
71
|
+
|
|
72
|
+
generated.push(outPath)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return generated
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-splash
|
|
3
|
+
*
|
|
4
|
+
* Android 12+ SplashScreen API icon × 5 densities.
|
|
5
|
+
*
|
|
6
|
+
* Spec: 288dp canvas, icon occupies central 192dp (~67% of canvas).
|
|
7
|
+
* The OS applies a circular mask automatically — keep pixels transparent
|
|
8
|
+
* outside the 192dp safe-zone.
|
|
9
|
+
*
|
|
10
|
+
* Densities: mdpi=288, hdpi=432, xhdpi=576, xxhdpi=864, xxxhdpi=1152.
|
|
11
|
+
* Output path: drawable-<density>/splash_icon.png
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Android 12+ splash icons × 5 densities
|
|
14
|
+
* @author César Estrada
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import sharp from 'sharp'
|
|
20
|
+
|
|
21
|
+
export const SPLASH_DENSITIES = [
|
|
22
|
+
{ name: 'mdpi', size: 288 },
|
|
23
|
+
{ name: 'hdpi', size: 432 },
|
|
24
|
+
{ name: 'xhdpi', size: 576 },
|
|
25
|
+
{ name: 'xxhdpi', size: 864 },
|
|
26
|
+
{ name: 'xxxhdpi', size: 1152 }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export async function genSplash(tightMaster, resRoot) {
|
|
30
|
+
const generated = []
|
|
31
|
+
|
|
32
|
+
for (const { name, size } of SPLASH_DENSITIES) {
|
|
33
|
+
const inner = Math.floor((size * 192) / 288)
|
|
34
|
+
const dir = path.join(resRoot, `drawable-${name}`)
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
36
|
+
|
|
37
|
+
const outPath = path.join(dir, 'splash_icon.png')
|
|
38
|
+
|
|
39
|
+
const innerLogo = await sharp(tightMaster)
|
|
40
|
+
.resize({
|
|
41
|
+
width: inner,
|
|
42
|
+
height: inner,
|
|
43
|
+
fit: 'inside',
|
|
44
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
45
|
+
})
|
|
46
|
+
.toBuffer()
|
|
47
|
+
|
|
48
|
+
await sharp({
|
|
49
|
+
create: {
|
|
50
|
+
width: size,
|
|
51
|
+
height: size,
|
|
52
|
+
channels: 4,
|
|
53
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.composite([{ input: innerLogo, gravity: 'center' }])
|
|
57
|
+
.png({ compressionLevel: 9 })
|
|
58
|
+
.toFile(outPath)
|
|
59
|
+
|
|
60
|
+
generated.push(outPath)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return generated
|
|
64
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Branding pipeline orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Composes the branding pipeline for Titanium projects.
|
|
5
|
+
*
|
|
6
|
+
* Every invocation (kitchen-sink) generates:
|
|
7
|
+
*
|
|
8
|
+
* iOS & marketplace:
|
|
9
|
+
* - DefaultIcon.png (alpha preserved)
|
|
10
|
+
* - DefaultIcon-ios.png (flattened on bg-color)
|
|
11
|
+
* - DefaultIcon-Dark.png (iOS 18+, transparent by default)
|
|
12
|
+
* - DefaultIcon-Tinted.png (iOS 18+, grayscale on black)
|
|
13
|
+
* - iTunesConnect.png (1024²)
|
|
14
|
+
* - MarketplaceArtwork.png (512²)
|
|
15
|
+
*
|
|
16
|
+
* Android:
|
|
17
|
+
* - ic_launcher_foreground.png × 5 densities
|
|
18
|
+
* - ic_launcher_background.png × 5 densities
|
|
19
|
+
* - ic_launcher_monochrome.png × 5 densities (themed icons / dark mode)
|
|
20
|
+
* - ic_launcher.png × 5 densities (legacy pre-adaptive)
|
|
21
|
+
* - mipmap-anydpi-v26/ic_launcher.xml
|
|
22
|
+
*
|
|
23
|
+
* Optional (opt-in):
|
|
24
|
+
* - ic_stat_notify.png × 5 (--notification)
|
|
25
|
+
* - splash_icon.png × 5 (--splash)
|
|
26
|
+
*
|
|
27
|
+
* Opt-out:
|
|
28
|
+
* - DefaultIcon-Dark.png (--no-dark)
|
|
29
|
+
* - DefaultIcon-Tinted.png (--no-tinted)
|
|
30
|
+
*
|
|
31
|
+
* @fileoverview Titanium branding pipeline orchestrator
|
|
32
|
+
* @author César Estrada
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import fs from 'fs'
|
|
36
|
+
import os from 'os'
|
|
37
|
+
import path from 'path'
|
|
38
|
+
import { logger } from './branding-logger.js'
|
|
39
|
+
import { logger as mainLogger } from '../../shared/logger.js'
|
|
40
|
+
import { confirmWithAlways } from '../../shared/prompt.js'
|
|
41
|
+
import { setConfigProperty } from '../../shared/config-writer.js'
|
|
42
|
+
import { prepareMaster } from './prepare-master.js'
|
|
43
|
+
import { genIos } from './gen-ios.js'
|
|
44
|
+
import { genIosDark } from './gen-ios-dark.js'
|
|
45
|
+
import { genIosTinted } from './gen-ios-tinted.js'
|
|
46
|
+
import { genAndroidAdaptive } from './gen-android-adaptive.js'
|
|
47
|
+
import { genAndroidLegacy } from './gen-android-legacy.js'
|
|
48
|
+
import { genMarketplace } from './gen-marketplace.js'
|
|
49
|
+
import { genNotification } from './gen-notification.js'
|
|
50
|
+
import { genSplash } from './gen-splash.js'
|
|
51
|
+
import { genIcLauncherXml } from './gen-ic-launcher-xml.js'
|
|
52
|
+
import { detectProjectType } from './tiapp-reader.js'
|
|
53
|
+
import { cleanupLegacy } from './cleanup-legacy.js'
|
|
54
|
+
import { printPostGenNotes } from './post-gen-notes.js'
|
|
55
|
+
|
|
56
|
+
export async function runBranding(opts) {
|
|
57
|
+
const {
|
|
58
|
+
logo,
|
|
59
|
+
monochromeLogo = null,
|
|
60
|
+
darkLogo = null,
|
|
61
|
+
darkBgColor = null,
|
|
62
|
+
withDark = true,
|
|
63
|
+
withTinted = true,
|
|
64
|
+
tintedLogo = null,
|
|
65
|
+
bgColor = '#FFFFFF',
|
|
66
|
+
bgColorExplicit = false,
|
|
67
|
+
padding = 15,
|
|
68
|
+
iosPadding = 4,
|
|
69
|
+
notification = false,
|
|
70
|
+
splash = false,
|
|
71
|
+
cleanupLegacy: runCleanup = false,
|
|
72
|
+
aggressive = false,
|
|
73
|
+
projectRoot = process.cwd(),
|
|
74
|
+
output,
|
|
75
|
+
dryRun = false,
|
|
76
|
+
inPlace = false,
|
|
77
|
+
notes = false,
|
|
78
|
+
yes = false,
|
|
79
|
+
confirmOverwrites = true
|
|
80
|
+
} = opts
|
|
81
|
+
|
|
82
|
+
validateOptions({ logo, bgColor, darkBgColor, padding, iosPadding, cleanupLegacy: runCleanup })
|
|
83
|
+
|
|
84
|
+
const projectType = detectProjectType(projectRoot)
|
|
85
|
+
const isInPlace = inPlace && !output
|
|
86
|
+
const stagingRoot = output || (isInPlace ? projectRoot : path.join(projectRoot, '.ti-branding'))
|
|
87
|
+
|
|
88
|
+
console.log()
|
|
89
|
+
mainLogger.info('Generating branding assets...')
|
|
90
|
+
console.log()
|
|
91
|
+
logger.property('Project: ', `${projectRoot} (${projectType})`)
|
|
92
|
+
if (logo) {
|
|
93
|
+
logger.property('Logo: ', logo)
|
|
94
|
+
logger.property('Background: ', bgColor)
|
|
95
|
+
logger.property('Padding: ', `Android ${padding}% / iOS ${iosPadding}% per side`)
|
|
96
|
+
console.log()
|
|
97
|
+
logger.property(isInPlace ? 'Writing IN PLACE to: ' : 'Staging: ', isInPlace ? projectRoot : stagingRoot)
|
|
98
|
+
}
|
|
99
|
+
if (isInPlace && !dryRun && confirmOverwrites) {
|
|
100
|
+
logger.warning(`⚠ In-place mode will OVERWRITE files in ${projectRoot}.`)
|
|
101
|
+
logger.warning(` Commit first if you want a rollback.`)
|
|
102
|
+
const choice = await confirmWithAlways('Continue? [y/N/a]', { yes })
|
|
103
|
+
if (choice === 'no') {
|
|
104
|
+
logger.info('Aborted.')
|
|
105
|
+
// eslint-disable-next-line n/no-process-exit
|
|
106
|
+
process.exit(0)
|
|
107
|
+
}
|
|
108
|
+
if (choice === 'always') {
|
|
109
|
+
const saved = setConfigProperty('brand', 'confirmOverwrites', false)
|
|
110
|
+
if (saved) {
|
|
111
|
+
logger.success('Saved brand.confirmOverwrites = false to purgetss/config.cjs — you won\'t be asked again.')
|
|
112
|
+
} else {
|
|
113
|
+
logger.warning('Could not persist preference (config.cjs missing or unreadable). Proceeding anyway.')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (dryRun) logger.warning('DRY RUN — no files will be written')
|
|
118
|
+
|
|
119
|
+
const generated = []
|
|
120
|
+
|
|
121
|
+
// Cleanup-only mode
|
|
122
|
+
if (!logo && runCleanup) {
|
|
123
|
+
logger.info('Cleanup-only mode')
|
|
124
|
+
await cleanupLegacy({ projectRoot, projectType, aggressive, dryRun })
|
|
125
|
+
return { stagingRoot, generated }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!logo) {
|
|
129
|
+
throw new Error('Logo image is required (unless running --cleanup-legacy alone).')
|
|
130
|
+
}
|
|
131
|
+
if (!fs.existsSync(logo)) {
|
|
132
|
+
throw new Error(`Logo image not found: ${logo}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (projectType === 'unknown') {
|
|
136
|
+
logger.warning(`Could not detect project layout. Expected 'app/' (Alloy) or 'Resources/' (Classic).`)
|
|
137
|
+
logger.warning(`Assets will be staged under ${stagingRoot}/standalone/ — copy manually.`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const androidResStaging = getStagingAndroidResRoot(stagingRoot, projectType)
|
|
141
|
+
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
const lines = [
|
|
144
|
+
`${stagingRoot}/DefaultIcon.png + DefaultIcon-ios.png`
|
|
145
|
+
]
|
|
146
|
+
if (withDark) {
|
|
147
|
+
const darkSrc = darkLogo
|
|
148
|
+
? `from ${darkLogo}`
|
|
149
|
+
: (darkBgColor ? `opaque bg ${darkBgColor}` : 'transparent per Apple HIG')
|
|
150
|
+
lines.push(`${stagingRoot}/DefaultIcon-Dark.png (${darkSrc})`)
|
|
151
|
+
}
|
|
152
|
+
if (withTinted) {
|
|
153
|
+
const tintedSrc = tintedLogo ? `from ${tintedLogo}` : 'grayscale of logo, flattened on black'
|
|
154
|
+
lines.push(`${stagingRoot}/DefaultIcon-Tinted.png (${tintedSrc})`)
|
|
155
|
+
}
|
|
156
|
+
lines.push(`${stagingRoot}/iTunesConnect.png + MarketplaceArtwork.png`)
|
|
157
|
+
lines.push(`${androidResStaging}/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher_{foreground,background,monochrome}.png`)
|
|
158
|
+
lines.push(`${androidResStaging}/mipmap-{...}/ic_launcher.png (legacy)`)
|
|
159
|
+
lines.push(`${androidResStaging}/mipmap-anydpi-v26/ic_launcher.xml`)
|
|
160
|
+
if (notification) lines.push(`${androidResStaging}/drawable-*/ic_stat_notify.png × 5`)
|
|
161
|
+
if (splash) lines.push(`${androidResStaging}/drawable-*/splash_icon.png × 5`)
|
|
162
|
+
mainLogger.block('[dry-run] Would generate:', ...lines)
|
|
163
|
+
if (runCleanup) {
|
|
164
|
+
await cleanupLegacy({ projectRoot, projectType, aggressive, dryRun })
|
|
165
|
+
}
|
|
166
|
+
return { stagingRoot, generated }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Route temp logos through the OS temp dir in --in-place mode so the
|
|
170
|
+
// project tree (and VSCode's file explorer) stays clean. Using a unique
|
|
171
|
+
// subdir per run avoids clashes between parallel invocations.
|
|
172
|
+
const tempDir = isInPlace
|
|
173
|
+
? path.join(os.tmpdir(), `pt-branding-${process.pid}-${Date.now()}`)
|
|
174
|
+
: stagingRoot
|
|
175
|
+
const weCreatedTempDir = isInPlace && !fs.existsSync(tempDir)
|
|
176
|
+
if (weCreatedTempDir) fs.mkdirSync(tempDir, { recursive: true })
|
|
177
|
+
|
|
178
|
+
// ---- Section: Logos ---------------------------------------------------
|
|
179
|
+
logger.section('Logos')
|
|
180
|
+
logger.bullet('Dual logos (square + tight)')
|
|
181
|
+
const logoBase = path.join(tempDir, '_logo')
|
|
182
|
+
const { tight } = await prepareMaster(logo, logoBase)
|
|
183
|
+
|
|
184
|
+
let monoTight = null
|
|
185
|
+
if (monochromeLogo) {
|
|
186
|
+
if (!fs.existsSync(monochromeLogo)) {
|
|
187
|
+
throw new Error(`Monochrome logo not found: ${monochromeLogo}`)
|
|
188
|
+
}
|
|
189
|
+
logger.bullet(`Monochrome logo: ${monochromeLogo}`)
|
|
190
|
+
const monoBase = path.join(tempDir, '_logo_mono')
|
|
191
|
+
const monoResult = await prepareMaster(monochromeLogo, monoBase)
|
|
192
|
+
monoTight = monoResult.tight
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- Section: iOS & marketplace ----------------------------------------
|
|
196
|
+
logger.section('iOS & marketplace')
|
|
197
|
+
logger.bullet(`DefaultIcon.png (Android-safe padding ${padding}%) + DefaultIcon-ios.png (iOS padding ${iosPadding}%)`)
|
|
198
|
+
const ios = await genIos(tight, bgColor, padding, iosPadding, stagingRoot)
|
|
199
|
+
generated.push(ios.defaultIcon, ios.defaultIconIos)
|
|
200
|
+
|
|
201
|
+
if (withDark) {
|
|
202
|
+
let darkSource = tight
|
|
203
|
+
if (darkLogo) {
|
|
204
|
+
if (!fs.existsSync(darkLogo)) throw new Error(`Dark logo not found: ${darkLogo}`)
|
|
205
|
+
const darkBase = path.join(tempDir, '_logo_dark')
|
|
206
|
+
const darkResult = await prepareMaster(darkLogo, darkBase)
|
|
207
|
+
darkSource = darkResult.tight
|
|
208
|
+
}
|
|
209
|
+
const darkSrcLabel = darkLogo ? 'from --dark-logo, ' : ''
|
|
210
|
+
const darkBgLabel = darkBgColor ? `opaque bg ${darkBgColor}` : 'transparent per Apple HIG'
|
|
211
|
+
logger.bullet(`DefaultIcon-Dark.png (${darkSrcLabel}${darkBgLabel})`)
|
|
212
|
+
const darkPath = await genIosDark(darkSource, darkBgColor, iosPadding, stagingRoot)
|
|
213
|
+
generated.push(darkPath)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (withTinted) {
|
|
217
|
+
let tintedSource = tight
|
|
218
|
+
if (tintedLogo) {
|
|
219
|
+
if (!fs.existsSync(tintedLogo)) throw new Error(`Tinted logo not found: ${tintedLogo}`)
|
|
220
|
+
const tintedBase = path.join(tempDir, '_logo_tinted')
|
|
221
|
+
const tintedResult = await prepareMaster(tintedLogo, tintedBase)
|
|
222
|
+
tintedSource = tintedResult.tight
|
|
223
|
+
}
|
|
224
|
+
const tintedSrcLabel = tintedLogo ? 'from --tinted-logo' : 'grayscale of logo'
|
|
225
|
+
logger.bullet(`DefaultIcon-Tinted.png (${tintedSrcLabel}, flattened on black)`)
|
|
226
|
+
const tintedPath = await genIosTinted(tintedSource, iosPadding, stagingRoot)
|
|
227
|
+
generated.push(tintedPath)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const alphaMode = bgColorExplicit ? `flattened on ${bgColor}` : 'alpha preserved'
|
|
231
|
+
logger.bullet(`iTunesConnect.png + MarketplaceArtwork.png (${alphaMode})`)
|
|
232
|
+
const mkt = await genMarketplace(tight, iosPadding, stagingRoot, {
|
|
233
|
+
flatten: bgColorExplicit,
|
|
234
|
+
bgColor
|
|
235
|
+
})
|
|
236
|
+
generated.push(mkt.itunesConnect, mkt.marketplaceArtwork)
|
|
237
|
+
|
|
238
|
+
// ---- Section: Android --------------------------------------------------
|
|
239
|
+
logger.section('Android')
|
|
240
|
+
|
|
241
|
+
const monoLabel = monoTight ? ', monochrome from --monochrome-logo' : ''
|
|
242
|
+
logger.bullet(`Adaptive icons (foreground + background + monochrome${monoLabel}) × 5`)
|
|
243
|
+
const adaptiveFiles = await genAndroidAdaptive(tight, bgColor, padding, androidResStaging, { monoTight })
|
|
244
|
+
generated.push(...adaptiveFiles)
|
|
245
|
+
|
|
246
|
+
logger.bullet('Legacy ic_launcher.png × 5')
|
|
247
|
+
const legacyFiles = await genAndroidLegacy(tight, bgColor, padding, androidResStaging)
|
|
248
|
+
generated.push(...legacyFiles)
|
|
249
|
+
|
|
250
|
+
const xmlPath = genIcLauncherXml(androidResStaging)
|
|
251
|
+
generated.push(xmlPath)
|
|
252
|
+
logger.bullet(`Adaptive icon XML: ${xmlPath}`)
|
|
253
|
+
|
|
254
|
+
if (notification) {
|
|
255
|
+
const monoLabelNotif = monoTight ? ' from --monochrome-logo' : ' whitened from logo'
|
|
256
|
+
logger.bullet(`Notification icons (white+alpha, edge-to-edge${monoLabelNotif}) × 5`)
|
|
257
|
+
const notifFiles = await genNotification(monoTight || tight, androidResStaging)
|
|
258
|
+
generated.push(...notifFiles)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (splash) {
|
|
262
|
+
logger.bullet('Splash icons × 5')
|
|
263
|
+
const splashFiles = await genSplash(tight, androidResStaging)
|
|
264
|
+
generated.push(...splashFiles)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (runCleanup) {
|
|
268
|
+
logger.info('Cleanup legacy artifacts')
|
|
269
|
+
await cleanupLegacy({ projectRoot, projectType, aggressive, dryRun })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Clean up temp _logo_* files in --in-place mode
|
|
273
|
+
if (isInPlace) {
|
|
274
|
+
if (weCreatedTempDir) {
|
|
275
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
276
|
+
} else {
|
|
277
|
+
const tmpFiles = [
|
|
278
|
+
path.join(tempDir, '_logo_square.png'),
|
|
279
|
+
path.join(tempDir, '_logo_tight.png'),
|
|
280
|
+
path.join(tempDir, '_logo_mono_square.png'),
|
|
281
|
+
path.join(tempDir, '_logo_mono_tight.png'),
|
|
282
|
+
path.join(tempDir, '_logo_dark_square.png'),
|
|
283
|
+
path.join(tempDir, '_logo_dark_tight.png'),
|
|
284
|
+
path.join(tempDir, '_logo_tinted_square.png'),
|
|
285
|
+
path.join(tempDir, '_logo_tinted_tight.png')
|
|
286
|
+
]
|
|
287
|
+
for (const tmp of tmpFiles) {
|
|
288
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
logger.info('')
|
|
292
|
+
logger.success(`All assets written IN PLACE at: ${projectRoot}`)
|
|
293
|
+
} else {
|
|
294
|
+
logger.info('')
|
|
295
|
+
logger.success(`All assets staged at: ${stagingRoot}`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
printPostGenNotes({
|
|
299
|
+
projectType,
|
|
300
|
+
projectRoot,
|
|
301
|
+
stagingRoot,
|
|
302
|
+
bgColor,
|
|
303
|
+
padding,
|
|
304
|
+
iosPadding,
|
|
305
|
+
withSplash: splash,
|
|
306
|
+
withNotification: notification,
|
|
307
|
+
inPlace: isInPlace,
|
|
308
|
+
fullNotes: notes
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return { stagingRoot, generated }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getStagingAndroidResRoot(stagingRoot, projectType) {
|
|
315
|
+
if (projectType === 'alloy') return path.join(stagingRoot, 'app', 'platform', 'android', 'res')
|
|
316
|
+
if (projectType === 'classic') return path.join(stagingRoot, 'platform', 'android', 'res')
|
|
317
|
+
return path.join(stagingRoot, 'standalone', 'platform', 'android', 'res')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validateOptions({ logo, bgColor, darkBgColor, padding, iosPadding, cleanupLegacy }) {
|
|
321
|
+
if (!logo && !cleanupLegacy) {
|
|
322
|
+
throw new Error('Logo image path is required (unless using --cleanup-legacy alone).')
|
|
323
|
+
}
|
|
324
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(bgColor)) {
|
|
325
|
+
throw new Error(`--bg-color must be a 6-digit hex like #0B1326 (got: ${bgColor}).`)
|
|
326
|
+
}
|
|
327
|
+
if (darkBgColor && !/^#[0-9A-Fa-f]{6}$/.test(darkBgColor)) {
|
|
328
|
+
throw new Error(`--dark-bg-color must be a 6-digit hex like #1C1C1E (got: ${darkBgColor}).`)
|
|
329
|
+
}
|
|
330
|
+
if (padding < 0 || padding > 40) {
|
|
331
|
+
throw new Error(`--padding must be between 0 and 40 (got: ${padding}).`)
|
|
332
|
+
}
|
|
333
|
+
if (iosPadding < 0 || iosPadding > 40) {
|
|
334
|
+
throw new Error(`--ios-padding must be between 0 and 40 (got: ${iosPadding}).`)
|
|
335
|
+
}
|
|
336
|
+
}
|