@git.zone/cli 2.19.8 → 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.
- package/assets/templates/asset_deno_binary_installer/install.sh +506 -0
- package/assets/templates/asset_deno_binary_postinstall/bin/wrapper.js +37 -0
- package/assets/templates/asset_deno_binary_postinstall/scripts/install-binary.js +123 -0
- package/assets/templates/asset_deno_binary_release_gitea/.gitea/workflows/release.yml +154 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/helpers.workflow.d.ts +1 -0
- package/dist_ts/helpers.workflow.js +2 -1
- package/dist_ts/mod_commit/index.js +10 -3
- package/dist_ts/mod_config/index.js +91 -1
- package/dist_ts/mod_format/classes.baseformatter.d.ts +5 -2
- package/dist_ts/mod_format/classes.baseformatter.js +49 -7
- package/dist_ts/mod_format/formatters/assets.formatter.d.ts +14 -0
- package/dist_ts/mod_format/formatters/assets.formatter.js +384 -0
- package/dist_ts/mod_format/formatters/packagejson.formatter.js +40 -2
- package/dist_ts/mod_format/index.js +4 -1
- package/dist_ts/mod_format/interfaces.format.d.ts +3 -0
- package/dist_ts/mod_format/interfaces.format.js +2 -1
- package/package.json +4 -4
- package/readme.md +95 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers.workflow.ts +2 -0
- package/ts/mod_commit/index.ts +12 -2
- package/ts/mod_config/index.ts +95 -0
- package/ts/mod_format/classes.baseformatter.ts +47 -6
- package/ts/mod_format/formatters/assets.formatter.ts +451 -0
- package/ts/mod_format/formatters/packagejson.formatter.ts +43 -1
- package/ts/mod_format/index.ts +3 -0
- package/ts/mod_format/interfaces.format.ts +4 -0
|
@@ -62,7 +62,31 @@ export abstract class BaseFormatter {
|
|
|
62
62
|
// Override in subclasses if needed
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
protected
|
|
65
|
+
protected normalizeFileMode(mode: string | undefined): string | undefined {
|
|
66
|
+
if (!mode) return undefined;
|
|
67
|
+
const normalizedMode = mode.trim();
|
|
68
|
+
if (!/^[0-7]{3,4}$/.test(normalizedMode)) {
|
|
69
|
+
throw new Error(`Invalid file mode ${mode}`);
|
|
70
|
+
}
|
|
71
|
+
return normalizedMode.padStart(4, '0');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected async getFileMode(filepath: string): Promise<string | undefined> {
|
|
75
|
+
try {
|
|
76
|
+
const stats = await plugins.fs.stat(filepath);
|
|
77
|
+
return (stats.mode & 0o777).toString(8).padStart(4, '0');
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected async setFileMode(filepath: string, mode: string): Promise<void> {
|
|
84
|
+
const normalizedMode = this.normalizeFileMode(mode);
|
|
85
|
+
if (!normalizedMode) return;
|
|
86
|
+
await plugins.fs.chmod(filepath, Number.parseInt(normalizedMode, 8));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected async modifyFile(filepath: string, content: string, mode?: string): Promise<void> {
|
|
66
90
|
if (!filepath || filepath.trim() === '') {
|
|
67
91
|
throw new Error(`Invalid empty filepath in modifyFile`);
|
|
68
92
|
}
|
|
@@ -73,9 +97,12 @@ export abstract class BaseFormatter {
|
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
|
100
|
+
if (mode) {
|
|
101
|
+
await this.setFileMode(normalizedPath, mode);
|
|
102
|
+
}
|
|
76
103
|
}
|
|
77
104
|
|
|
78
|
-
protected async createFile(filepath: string, content: string): Promise<void> {
|
|
105
|
+
protected async createFile(filepath: string, content: string, mode?: string): Promise<void> {
|
|
79
106
|
let normalizedPath = filepath;
|
|
80
107
|
if (!plugins.path.parse(filepath).dir) {
|
|
81
108
|
normalizedPath = './' + filepath;
|
|
@@ -88,6 +115,9 @@ export abstract class BaseFormatter {
|
|
|
88
115
|
}
|
|
89
116
|
|
|
90
117
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
|
118
|
+
if (mode) {
|
|
119
|
+
await this.setFileMode(normalizedPath, mode);
|
|
120
|
+
}
|
|
91
121
|
}
|
|
92
122
|
|
|
93
123
|
protected async deleteFile(filepath: string): Promise<void> {
|
|
@@ -115,13 +145,19 @@ export abstract class BaseFormatter {
|
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
const newContent = change.content;
|
|
148
|
+
const modeAfter = this.normalizeFileMode(change.mode);
|
|
149
|
+
const modeBefore = modeAfter ? await this.getFileMode(change.path) : undefined;
|
|
150
|
+
const hasContentDiff = currentContent !== newContent && newContent !== undefined;
|
|
151
|
+
const hasModeDiff = modeAfter !== undefined && modeBefore !== modeAfter;
|
|
118
152
|
|
|
119
|
-
if (
|
|
153
|
+
if (hasContentDiff || hasModeDiff) {
|
|
120
154
|
diffs.push({
|
|
121
155
|
path: change.path,
|
|
122
156
|
type: change.type,
|
|
123
157
|
before: currentContent,
|
|
124
158
|
after: newContent,
|
|
159
|
+
modeBefore,
|
|
160
|
+
modeAfter,
|
|
125
161
|
});
|
|
126
162
|
}
|
|
127
163
|
} else if (change.type === 'delete') {
|
|
@@ -147,21 +183,26 @@ export abstract class BaseFormatter {
|
|
|
147
183
|
|
|
148
184
|
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
|
149
185
|
console.log(`\n--- ${diff.path}`);
|
|
150
|
-
if (diff.before && diff.after) {
|
|
186
|
+
if (diff.before !== undefined && diff.after !== undefined) {
|
|
151
187
|
console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, {
|
|
152
188
|
originalFileName: diff.path,
|
|
153
189
|
revisedFileName: diff.path,
|
|
154
190
|
context: 3,
|
|
155
191
|
}));
|
|
156
|
-
} else if (diff.after &&
|
|
192
|
+
} else if (diff.after !== undefined && diff.before === undefined) {
|
|
157
193
|
console.log(' (new file)');
|
|
158
194
|
const lines = diff.after.split('\n').slice(0, 10);
|
|
159
195
|
lines.forEach(line => console.log(` + ${line}`));
|
|
160
196
|
if (diff.after.split('\n').length > 10) {
|
|
161
197
|
console.log(' ... (truncated)');
|
|
162
198
|
}
|
|
163
|
-
} else if (diff.
|
|
199
|
+
} else if (diff.type === 'delete') {
|
|
164
200
|
console.log(' (file will be deleted)');
|
|
201
|
+
} else if (diff.modeAfter) {
|
|
202
|
+
console.log(' (file content unchanged)');
|
|
203
|
+
}
|
|
204
|
+
if (diff.modeBefore !== diff.modeAfter && diff.modeAfter) {
|
|
205
|
+
console.log(` mode: ${diff.modeBefore || '(missing)'} -> ${diff.modeAfter}`);
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
|
|
@@ -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
|
-
|
|
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
|
package/ts/mod_format/index.ts
CHANGED
|
@@ -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] || '📁';
|