@git.zone/cli 2.20.0 → 2.21.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.
@@ -0,0 +1,451 @@
1
+ import { BaseFormatter } from '../classes.baseformatter.js';
2
+ import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
3
+ import * as plugins from '../mod.plugins.js';
4
+ import * as paths from '../../paths.js';
5
+ import { logger, logVerbose } from '../../gitzone.logging.js';
6
+ import { getCliConfigValue, readSmartconfigFile } from '../../helpers.smartconfig.js';
7
+
8
+ interface IAssetFileMapping {
9
+ from: string;
10
+ to: string;
11
+ mode?: string;
12
+ }
13
+
14
+ interface IAssetTemplateEntry {
15
+ template: string;
16
+ files: IAssetFileMapping[];
17
+ enabled?: boolean;
18
+ variables?: Record<string, any>;
19
+ }
20
+
21
+ interface IBinaryTargetRenderData {
22
+ name: string;
23
+ assetName: string;
24
+ os: string;
25
+ arch: string;
26
+ denoTarget: string;
27
+ nodeKey: string;
28
+ label: string;
29
+ }
30
+
31
+ const isPlainObject = (value: unknown): value is Record<string, any> => {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ };
34
+
35
+ const readJsonFile = async (filePath: string): Promise<Record<string, any>> => {
36
+ if (!(await plugins.smartfs.file(filePath).exists())) {
37
+ return {};
38
+ }
39
+ const content = (await plugins.smartfs.file(filePath).encoding('utf8').read()) as string;
40
+ return JSON.parse(content);
41
+ };
42
+
43
+ const uniqueStringArray = (value: unknown, fallback: string[] = []): string[] => {
44
+ if (!Array.isArray(value)) {
45
+ return fallback;
46
+ }
47
+ const result: string[] = [];
48
+ for (const item of value) {
49
+ if (typeof item !== 'string' || !item.trim()) {
50
+ continue;
51
+ }
52
+ if (!result.includes(item)) {
53
+ result.push(item);
54
+ }
55
+ }
56
+ return result;
57
+ };
58
+
59
+ const normalizeRepository = (assetsConfig: Record<string, any>, moduleConfig: Record<string, any>) => {
60
+ const repository = isPlainObject(assetsConfig.repository) ? assetsConfig.repository : {};
61
+ const host = repository.host || moduleConfig.githost || 'code.foss.global';
62
+ const path = repository.path || [moduleConfig.gitscope, moduleConfig.gitrepo].filter(Boolean).join('/');
63
+ const branch = repository.branch || repository.rawBranch || assetsConfig.rawBranch || 'main';
64
+ const baseUrl = `https://${host}/${path}`;
65
+ return {
66
+ host,
67
+ path,
68
+ branch,
69
+ baseUrl,
70
+ apiBaseUrl: `https://${host}/api/v1/repos/${path}`,
71
+ gitUrl: `${baseUrl}.git`,
72
+ };
73
+ };
74
+
75
+ const normalizeOs = (target: Record<string, any>): string => {
76
+ const rawValue = `${target.os || ''} ${target.target || ''} ${target.name || ''}`.toLowerCase();
77
+ if (rawValue.includes('apple') || rawValue.includes('darwin') || rawValue.includes('macos')) return 'macos';
78
+ if (rawValue.includes('windows') || rawValue.includes('pc-windows') || rawValue.includes('-win')) return 'windows';
79
+ return 'linux';
80
+ };
81
+
82
+ const normalizeArch = (target: Record<string, any>): string => {
83
+ const rawValue = `${target.arch || ''} ${target.target || ''} ${target.name || ''}`.toLowerCase();
84
+ if (rawValue.includes('aarch64') || rawValue.includes('arm64')) return 'arm64';
85
+ return 'x64';
86
+ };
87
+
88
+ const getNodePlatform = (os: string): string => {
89
+ if (os === 'macos') return 'darwin';
90
+ if (os === 'windows') return 'win32';
91
+ return 'linux';
92
+ };
93
+
94
+ const getPlatformLabel = (target: IBinaryTargetRenderData): string => {
95
+ const osLabel = target.os === 'macos' ? 'macOS' : target.os === 'windows' ? 'Windows' : 'Linux';
96
+ const archLabel = target.arch === 'arm64' ? 'ARM64' : 'x64';
97
+ return `${osLabel} ${archLabel}`;
98
+ };
99
+
100
+ const normalizeBinaryTargets = (
101
+ assetsConfig: Record<string, any>,
102
+ smartconfigData: Record<string, any>,
103
+ ): IBinaryTargetRenderData[] => {
104
+ const binaryConfig = isPlainObject(assetsConfig.binary) ? assetsConfig.binary : {};
105
+ const tsdenoConfig = smartconfigData['@git.zone/tsdeno'];
106
+ const targets = Array.isArray(binaryConfig.targets)
107
+ ? binaryConfig.targets
108
+ : isPlainObject(tsdenoConfig) && Array.isArray(tsdenoConfig.compileTargets)
109
+ ? tsdenoConfig.compileTargets
110
+ : [];
111
+
112
+ return targets
113
+ .filter((target) => isPlainObject(target) && typeof target.name === 'string')
114
+ .map((target) => {
115
+ const os = normalizeOs(target);
116
+ const arch = normalizeArch(target);
117
+ const isWindows = os === 'windows';
118
+ const assetName = isWindows && !target.name.endsWith('.exe') ? `${target.name}.exe` : target.name;
119
+ const result: IBinaryTargetRenderData = {
120
+ name: target.name,
121
+ assetName,
122
+ os,
123
+ arch,
124
+ denoTarget: target.target || target.denoTarget || '',
125
+ nodeKey: `${getNodePlatform(os)}:${arch}`,
126
+ label: '',
127
+ };
128
+ result.label = getPlatformLabel(result);
129
+ return result;
130
+ });
131
+ };
132
+
133
+ const buildDefaultEntries = (assetsConfig: Record<string, any>, cliName: string): IAssetTemplateEntry[] => {
134
+ if (assetsConfig.kind !== 'denoBinaryCli') {
135
+ return [];
136
+ }
137
+
138
+ const entries: IAssetTemplateEntry[] = [];
139
+ const installerConfig = isPlainObject(assetsConfig.installer) ? assetsConfig.installer : {};
140
+ const npmWrapperConfig = isPlainObject(assetsConfig.npmWrapper) ? assetsConfig.npmWrapper : {};
141
+ const releaseWorkflowConfig = isPlainObject(assetsConfig.releaseWorkflow)
142
+ ? assetsConfig.releaseWorkflow
143
+ : isPlainObject(assetsConfig.releaseAssets)
144
+ ? assetsConfig.releaseAssets
145
+ : {};
146
+
147
+ if (installerConfig.enabled !== false) {
148
+ entries.push({
149
+ template: 'asset_deno_binary_installer',
150
+ files: [{ from: 'install.sh', to: installerConfig.file || 'install.sh', mode: '0755' }],
151
+ });
152
+ }
153
+
154
+ if (npmWrapperConfig.enabled === true) {
155
+ entries.push({
156
+ template: 'asset_deno_binary_postinstall',
157
+ files: [
158
+ {
159
+ from: 'scripts/install-binary.js',
160
+ to: npmWrapperConfig.postinstallFile || 'scripts/install-binary.js',
161
+ mode: '0755',
162
+ },
163
+ {
164
+ from: 'bin/wrapper.js',
165
+ to: npmWrapperConfig.binFile || `bin/${cliName}-wrapper.js`,
166
+ mode: '0755',
167
+ },
168
+ ],
169
+ });
170
+ }
171
+
172
+ if (releaseWorkflowConfig.enabled !== false) {
173
+ entries.push({
174
+ template: 'asset_deno_binary_release_gitea',
175
+ files: [{ from: '.gitea/workflows/release.yml', to: releaseWorkflowConfig.file || '.gitea/workflows/release.yml' }],
176
+ });
177
+ }
178
+
179
+ return entries;
180
+ };
181
+
182
+ export class AssetsFormatter extends BaseFormatter {
183
+ get name(): string {
184
+ return 'assets';
185
+ }
186
+
187
+ async analyze(): Promise<IPlannedChange[]> {
188
+ const assetsConfig = await getCliConfigValue<Record<string, any> | undefined>('assets', undefined);
189
+ if (!isPlainObject(assetsConfig) || assetsConfig.enabled === false) {
190
+ logVerbose('No @git.zone/cli.assets config found, skipping');
191
+ return [];
192
+ }
193
+
194
+ const smartconfigData = await readSmartconfigFile(paths.cwd);
195
+ const renderData = await this.buildRenderData(assetsConfig, smartconfigData);
196
+ const entries = this.getTemplateEntries(assetsConfig, renderData.cliName);
197
+ const changes: IPlannedChange[] = [];
198
+
199
+ for (const entry of entries) {
200
+ if (entry.enabled === false) {
201
+ continue;
202
+ }
203
+ const renderedFiles = await this.renderTemplate(entry.template, {
204
+ ...renderData,
205
+ ...(entry.variables || {}),
206
+ });
207
+
208
+ for (const file of entry.files || []) {
209
+ const content = renderedFiles.get(file.from) || renderedFiles.get(file.to);
210
+ if (content === undefined) {
211
+ throw new Error(`Managed asset ${entry.template}/${file.from} did not render a file`);
212
+ }
213
+
214
+ const destExists = await plugins.smartfs.file(file.to).exists();
215
+ const currentContent = destExists
216
+ ? ((await plugins.smartfs.file(file.to).encoding('utf8').read()) as string)
217
+ : '';
218
+ const normalizedMode = this.normalizeFileMode(file.mode);
219
+ const currentMode = normalizedMode ? await this.getFileMode(file.to) : undefined;
220
+ const needsContentChange = content !== currentContent;
221
+ const needsModeChange = normalizedMode !== undefined && currentMode !== normalizedMode;
222
+
223
+ if (needsContentChange || needsModeChange) {
224
+ changes.push({
225
+ type: destExists ? 'modify' : 'create',
226
+ path: file.to,
227
+ module: this.name,
228
+ description: `Apply managed asset ${entry.template}/${file.from}`,
229
+ content: needsContentChange ? content : undefined,
230
+ mode: normalizedMode,
231
+ });
232
+ }
233
+ }
234
+ }
235
+
236
+ return changes;
237
+ }
238
+
239
+ async validate(): Promise<IFormatWarning[]> {
240
+ const assetsConfig = await getCliConfigValue<Record<string, any> | undefined>('assets', undefined);
241
+ if (!isPlainObject(assetsConfig) || assetsConfig.enabled === false) {
242
+ return [];
243
+ }
244
+ if (assetsConfig.kind && assetsConfig.kind !== 'denoBinaryCli') {
245
+ return [{ level: 'error', module: this.name, message: `Unsupported managed asset kind: ${assetsConfig.kind}` }];
246
+ }
247
+ const installer = isPlainObject(assetsConfig.installer) ? assetsConfig.installer : {};
248
+ if (installer.distribution && !['rawBranch', 'releaseAsset'].includes(installer.distribution)) {
249
+ return [
250
+ {
251
+ level: 'error',
252
+ module: this.name,
253
+ message: `Unsupported installer distribution: ${installer.distribution}`,
254
+ },
255
+ ];
256
+ }
257
+ return [];
258
+ }
259
+
260
+ async applyChange(change: IPlannedChange): Promise<void> {
261
+ if (change.type === 'create') {
262
+ await this.createFile(change.path, change.content || '', change.mode);
263
+ } else if (change.type === 'modify') {
264
+ if (change.content !== undefined) {
265
+ await this.modifyFile(change.path, change.content, change.mode);
266
+ } else if (change.mode) {
267
+ await this.setFileMode(change.path, change.mode);
268
+ }
269
+ }
270
+ logger.log('info', `Applied managed asset to ${change.path}`);
271
+ }
272
+
273
+ private getTemplateEntries(assetsConfig: Record<string, any>, cliName: string): IAssetTemplateEntry[] {
274
+ if (Array.isArray(assetsConfig.managed)) {
275
+ return assetsConfig.managed
276
+ .filter((entry) => isPlainObject(entry) && typeof entry.template === 'string')
277
+ .map((entry) => ({
278
+ template: entry.template,
279
+ enabled: entry.enabled,
280
+ variables: isPlainObject(entry.variables) ? entry.variables : undefined,
281
+ files: Array.isArray(entry.files)
282
+ ? entry.files
283
+ .filter((file) => isPlainObject(file) && typeof file.from === 'string' && typeof file.to === 'string')
284
+ .map((file) => ({ from: file.from, to: file.to, mode: file.mode }))
285
+ : [],
286
+ }));
287
+ }
288
+ return buildDefaultEntries(assetsConfig, cliName);
289
+ }
290
+
291
+ private async renderTemplate(templateName: string, variables: Record<string, any>): Promise<Map<string, string>> {
292
+ const templateDir = plugins.path.join(paths.templatesDir, templateName);
293
+ if (!(await plugins.smartfs.directory(templateDir).exists())) {
294
+ throw new Error(`Managed asset template does not exist: ${templateName}`);
295
+ }
296
+
297
+ const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir);
298
+ await scafTemplate.readTemplateFromDir();
299
+ await scafTemplate.supplyVariables(variables);
300
+
301
+ const renderedFiles = await scafTemplate.renderToMemory();
302
+ const fileMap = new Map<string, string>();
303
+ for (const file of renderedFiles) {
304
+ fileMap.set(file.path, file.contents.toString());
305
+ }
306
+ return fileMap;
307
+ }
308
+
309
+ private async buildRenderData(
310
+ assetsConfig: Record<string, any>,
311
+ smartconfigData: Record<string, any>,
312
+ ): Promise<Record<string, any>> {
313
+ const packageJson = await readJsonFile(plugins.path.join(paths.cwd, 'package.json'));
314
+ const denoJson = await readJsonFile(plugins.path.join(paths.cwd, 'deno.json'));
315
+ const moduleConfig = this.project.gitzoneConfig?.data?.module || {};
316
+ const cliName = assetsConfig.cliName || packageJson.name?.split('/').pop() || moduleConfig.gitrepo || 'app';
317
+ const displayName = assetsConfig.displayName || cliName.toUpperCase();
318
+ const repository = normalizeRepository(assetsConfig, moduleConfig);
319
+ const npmPackageName = (moduleConfig as any).npmPackagename || moduleConfig.npmPackageName;
320
+ const binaryTargets = normalizeBinaryTargets(assetsConfig, smartconfigData);
321
+ const binaryConfig = isPlainObject(assetsConfig.binary) ? assetsConfig.binary : {};
322
+ const installerConfig = this.normalizeInstallerConfig(assetsConfig, cliName, repository);
323
+ const npmWrapperConfig = this.normalizeNpmWrapperConfig(assetsConfig, cliName);
324
+ const releaseWorkflowConfig = this.normalizeReleaseWorkflowConfig(assetsConfig, binaryConfig, installerConfig);
325
+
326
+ return {
327
+ module: moduleConfig,
328
+ projectType: this.project.gitzoneConfig?.data?.projectType,
329
+ packageJson,
330
+ denoJson,
331
+ assets: assetsConfig,
332
+ cliName,
333
+ cliNameUpper: cliName.toUpperCase(),
334
+ displayName,
335
+ packageName: assetsConfig.packageName || packageJson.name || npmPackageName || cliName,
336
+ versionFallback: packageJson.version || denoJson.version || '0.0.0',
337
+ repository,
338
+ binary: {
339
+ ...binaryConfig,
340
+ outDir: binaryConfig.outDir || 'dist/binaries',
341
+ installFileName: binaryConfig.installFileName || cliName,
342
+ targets: binaryTargets,
343
+ supportedPlatformText: binaryTargets.map((target) => target.label).join(', '),
344
+ hasTargets: binaryTargets.length > 0,
345
+ },
346
+ installer: installerConfig,
347
+ npmWrapper: npmWrapperConfig,
348
+ releaseWorkflow: releaseWorkflowConfig,
349
+ };
350
+ }
351
+
352
+ private normalizeInstallerConfig(
353
+ assetsConfig: Record<string, any>,
354
+ cliName: string,
355
+ repository: Record<string, any>,
356
+ ): Record<string, any> {
357
+ const installer = isPlainObject(assetsConfig.installer) ? assetsConfig.installer : {};
358
+ const modes = isPlainObject(installer.modes) ? installer.modes : {};
359
+ const source = isPlainObject(modes.source) ? modes.source : isPlainObject(installer.source) ? installer.source : {};
360
+ const service = isPlainObject(installer.service) ? installer.service : {};
361
+ const file = installer.file || 'install.sh';
362
+ const releaseAssetName = plugins.path.basename(file);
363
+ const distribution = installer.distribution || installer.delivery || 'rawBranch';
364
+ const versionSelection = uniqueStringArray(installer.versionSelection, ['latest', 'version']);
365
+ const detectNames = uniqueStringArray(service.detectNames, service.name ? [service.name] : [cliName]);
366
+ const removeLegacyUnits = uniqueStringArray(service.removeLegacyUnits || service.legacyUnitNames || []);
367
+ const ensureDirs = uniqueStringArray(installer.ensureDirs);
368
+ const preservePaths = uniqueStringArray(installer.preservePaths);
369
+ const successHints = uniqueStringArray(installer.successHints, [`${cliName} --version`, `${cliName} --help`]);
370
+ const rawBranchInstallUrl = `${repository.baseUrl}/raw/branch/${repository.branch}/${file}`;
371
+ const releaseAssetInstallUrl = `${repository.baseUrl}/releases/download/<version>/${releaseAssetName}`;
372
+ const releaseNotesInstallUrl = `${repository.baseUrl}/releases/download/$VERSION/${releaseAssetName}`;
373
+
374
+ return {
375
+ ...installer,
376
+ enabled: installer.enabled !== false,
377
+ file,
378
+ distribution,
379
+ releaseAssetName,
380
+ publicInstallUrl: distribution === 'releaseAsset' ? releaseAssetInstallUrl : rawBranchInstallUrl,
381
+ releaseNotesInstallUrl: distribution === 'releaseAsset' ? releaseNotesInstallUrl : rawBranchInstallUrl,
382
+ installDir: installer.installDir || `/opt/${cliName}`,
383
+ binDir: installer.binDir || '/usr/local/bin',
384
+ defaultMode: modes.default || installer.defaultMode || 'binary',
385
+ versionSelection,
386
+ supportsMajor: versionSelection.includes('major'),
387
+ source: {
388
+ ...source,
389
+ enabled: source.enabled === true,
390
+ commands: uniqueStringArray(source.commands),
391
+ executable: source.executable || 'cli.js',
392
+ executableFiles: uniqueStringArray(source.executableFiles, source.executable ? [source.executable] : ['cli.js']),
393
+ validateCommand: source.validate || source.validateCommand || `node ${source.executable || 'cli.js'} --version`,
394
+ },
395
+ service: {
396
+ ...service,
397
+ detectNames,
398
+ removeLegacyUnits,
399
+ hasLegacyUnitRemoval: removeLegacyUnits.length > 0,
400
+ restartName: service.restartName || detectNames[0] || cliName,
401
+ hasRefreshCommand: typeof service.refreshCommand === 'string' && service.refreshCommand.trim().length > 0,
402
+ },
403
+ ensureDirs,
404
+ preservePaths,
405
+ primaryPreservePath: preservePaths[0],
406
+ hasPreservePaths: preservePaths.length > 0,
407
+ successHints,
408
+ };
409
+ }
410
+
411
+ private normalizeNpmWrapperConfig(assetsConfig: Record<string, any>, cliName: string): Record<string, any> {
412
+ const npmWrapper = isPlainObject(assetsConfig.npmWrapper) ? assetsConfig.npmWrapper : {};
413
+ return {
414
+ ...npmWrapper,
415
+ enabled: npmWrapper.enabled === true,
416
+ binFile: npmWrapper.binFile || `bin/${cliName}-wrapper.js`,
417
+ postinstallFile: npmWrapper.postinstallFile || 'scripts/install-binary.js',
418
+ binariesDir: npmWrapper.binariesDir || 'dist/binaries',
419
+ };
420
+ }
421
+
422
+ private normalizeReleaseWorkflowConfig(
423
+ assetsConfig: Record<string, any>,
424
+ binaryConfig: Record<string, any>,
425
+ installerConfig: Record<string, any>,
426
+ ): Record<string, any> {
427
+ const releaseWorkflow = isPlainObject(assetsConfig.releaseWorkflow)
428
+ ? assetsConfig.releaseWorkflow
429
+ : isPlainObject(assetsConfig.releaseAssets)
430
+ ? assetsConfig.releaseAssets
431
+ : {};
432
+ const binaryOutDir = binaryConfig.outDir || 'dist/binaries';
433
+ const assetGlobs = uniqueStringArray(releaseWorkflow.assetGlobs || releaseWorkflow.include, [`${binaryOutDir}/*`]);
434
+ const includeInstallerAsset =
435
+ typeof releaseWorkflow.includeInstallerAsset === 'boolean'
436
+ ? releaseWorkflow.includeInstallerAsset
437
+ : installerConfig.enabled !== false && installerConfig.distribution === 'releaseAsset';
438
+ return {
439
+ ...releaseWorkflow,
440
+ enabled: releaseWorkflow.enabled !== false,
441
+ file: releaseWorkflow.file || '.gitea/workflows/release.yml',
442
+ installCommand: releaseWorkflow.installCommand || 'pnpm install --ignore-scripts',
443
+ compileCommand: releaseWorkflow.compileCommand || 'pnpm exec tsdeno compile',
444
+ packNpmArtifact: releaseWorkflow.packNpmArtifact === true,
445
+ includeInstallerAsset,
446
+ assetGlobs,
447
+ keepReleases: releaseWorkflow.keepReleases ?? 0,
448
+ binaryOutDir,
449
+ };
450
+ }
451
+ }
@@ -70,8 +70,41 @@ export class PackageJsonFormatter extends BaseFormatter {
70
70
  packageJson.scripts.build = `echo "Not needed for now"`;
71
71
  }
72
72
 
73
+ const rawAssetsConfig = gitzoneData.assets;
74
+ const assetsConfig = rawAssetsConfig?.enabled === false ? undefined : rawAssetsConfig;
75
+ const managedAssetConfig = rawAssetsConfig?.kind === 'denoBinaryCli' ? rawAssetsConfig : undefined;
76
+ const managedCliName = managedAssetConfig?.cliName || packageJson.name?.split('/').pop();
77
+ const managedBinFile = managedAssetConfig?.npmWrapper?.binFile || `bin/${managedCliName}-wrapper.js`;
78
+ const managedPostinstallFile = managedAssetConfig?.npmWrapper?.postinstallFile || 'scripts/install-binary.js';
79
+ const managedPostinstallCommand = `node ${managedPostinstallFile}`;
80
+
81
+ if (assetsConfig?.kind === 'denoBinaryCli' && assetsConfig.npmWrapper?.enabled) {
82
+ if (managedCliName) {
83
+ packageJson.bin = typeof packageJson.bin === 'object' && !Array.isArray(packageJson.bin)
84
+ ? packageJson.bin
85
+ : {};
86
+ packageJson.bin[managedCliName] = `./${managedBinFile}`;
87
+ }
88
+ packageJson.scripts.postinstall = managedPostinstallCommand;
89
+ } else if (managedAssetConfig) {
90
+ if (
91
+ managedCliName &&
92
+ typeof packageJson.bin === 'object' &&
93
+ !Array.isArray(packageJson.bin) &&
94
+ packageJson.bin[managedCliName] === `./${managedBinFile}`
95
+ ) {
96
+ delete packageJson.bin[managedCliName];
97
+ if (Object.keys(packageJson.bin).length === 0) {
98
+ delete packageJson.bin;
99
+ }
100
+ }
101
+ if (packageJson.scripts.postinstall === managedPostinstallCommand) {
102
+ delete packageJson.scripts.postinstall;
103
+ }
104
+ }
105
+
73
106
  // Set files array
74
- packageJson.files = [
107
+ const files = [
75
108
  'ts/**/*',
76
109
  'ts_web/**/*',
77
110
  'dist/**/*',
@@ -84,6 +117,15 @@ export class PackageJsonFormatter extends BaseFormatter {
84
117
  'readme.md',
85
118
  ];
86
119
 
120
+ if (assetsConfig?.kind === 'denoBinaryCli' && assetsConfig.installer?.enabled !== false) {
121
+ files.push(assetsConfig.installer?.file || 'install.sh');
122
+ }
123
+ if (assetsConfig?.kind === 'denoBinaryCli' && assetsConfig.npmWrapper?.enabled) {
124
+ files.push('bin/');
125
+ files.push(assetsConfig.npmWrapper.postinstallFile || 'scripts/install-binary.js');
126
+ }
127
+ packageJson.files = [...new Set(files)];
128
+
87
129
  // Set pnpm overrides from assets
88
130
  try {
89
131
  const overridesContent = (await plugins.smartfs
@@ -17,6 +17,7 @@ import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
17
17
  import { LicenseFormatter } from "./formatters/license.formatter.js";
18
18
  import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
19
19
  import { TemplatesFormatter } from "./formatters/templates.formatter.js";
20
+ import { AssetsFormatter } from "./formatters/assets.formatter.js";
20
21
  import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
21
22
  import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
22
23
  import { PrettierFormatter } from "./formatters/prettier.formatter.js";
@@ -61,6 +62,7 @@ const formatterMap: Record<
61
62
  license: LicenseFormatter,
62
63
  packagejson: PackageJsonFormatter,
63
64
  templates: TemplatesFormatter,
65
+ assets: AssetsFormatter,
64
66
  gitignore: GitignoreFormatter,
65
67
  tsconfig: TsconfigFormatter,
66
68
  prettier: PrettierFormatter,
@@ -206,6 +208,7 @@ const serializePlan = (plan: any) => {
206
208
  path: change.path,
207
209
  module: change.module,
208
210
  description: change.description,
211
+ mode: change.mode,
209
212
  })),
210
213
  };
211
214
  };
@@ -21,6 +21,7 @@ export type IPlannedChange = {
21
21
  module: string;
22
22
  description: string;
23
23
  content?: string; // New content for create/modify operations
24
+ mode?: string; // Optional file mode, e.g. 0755 for generated executables
24
25
  };
25
26
 
26
27
  export interface ICheckResult {
@@ -30,6 +31,8 @@ export interface ICheckResult {
30
31
  type: 'create' | 'modify' | 'delete';
31
32
  before?: string;
32
33
  after?: string;
34
+ modeBefore?: string;
35
+ modeAfter?: string;
33
36
  }>;
34
37
  }
35
38
 
@@ -44,6 +47,7 @@ export function getModuleIcon(module: string): string {
44
47
  readme: '📖',
45
48
  templates: '📄',
46
49
  smartconfig: '⚙️',
50
+ assets: '🧩',
47
51
  copy: '📋',
48
52
  };
49
53
  return icons[module] || '📁';