Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7eaa238f | |||
| 601e0d1063 | |||
| 4bb1a2f8c7 | |||
| b506bf8785 | |||
| d5fbeb3fc6 | |||
| 2ecdeff3dc | |||
| 5a663ae767 | |||
| 218c84a39b | |||
| 27d5cdca35 | |||
| 3ebf072bfb |
30
changelog.md
30
changelog.md
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-15 - 2.11.0 - feat(mod_format)
|
||||
feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff
|
||||
|
||||
- Replaced plugins.smartdiff.formatLineDiffForConsole(...) with plugins.smartdiff.formatUnifiedDiffForConsole(...) when both before and after are present.
|
||||
- Passes originalFileName and revisedFileName as diff.path and sets context to 3 to show a unified diff with surrounding lines.
|
||||
- Improves console output for multi-line diffs by using unified diff format and including file names.
|
||||
|
||||
## 2025-12-15 - 2.10.0 - feat(mod_format)
|
||||
Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic
|
||||
|
||||
- Replace generic LegacyFormatter with explicit BaseFormatter implementations for formatters: copy, gitignore, license, npmextra, packagejson, prettier, readme, templates, tsconfig (legacy.formatter.ts removed).
|
||||
- Copy formatter: implemented pattern-based copying, template-preserve path handling, content equality check and planned change generation/apply.
|
||||
- Gitignore formatter: canonical template with preservation of custom section when updating/creating .gitignore.
|
||||
- License formatter: added runtime license check against node_modules for incompatible licenses and reporting (no file changes).
|
||||
- Npmextra formatter: automatic migrations for old namespace keys to package-scoped keys and migration of npmAccessLevel -> @git.zone/cli.release.accessLevel; reformatting and interactive prompting to fill missing repo metadata.
|
||||
- Package.json formatter: enforces repository/metadata, sets module type/private/license/scripts/files, ensures/updates dependencies (including fetching latest via registry), and applies pnpm overrides from assets.
|
||||
- Prettier formatter: added check() to compute diffs by running Prettier and returning per-file before/after diffs.
|
||||
- Readme formatter: create readme.md and readme.hints.md when missing with default content.
|
||||
- Templates formatter: apply templates from templatesDir based on project type (vscode, CI, docker, website/service/wcc), compare template vs destination and create/modify files as needed; ensures dest directories exist.
|
||||
- Tsconfig formatter: sets compilerOptions.baseUrl and computes path mappings from @git.zone/tspublish modules.
|
||||
- General: extensive use of plugins (smartfs, path, smartnpm, smartinteract, smartobject, smartlegal), improved logging and verbose messages.
|
||||
|
||||
## 2025-12-15 - 2.9.0 - feat(format)
|
||||
Add --diff option to format command to display file diffs; pass flag through CLI and show formatter diffs. Bump @git.zone/tsdoc to ^1.11.0.
|
||||
|
||||
- Add a diff boolean option to mod_format to enable showing file diffs during format operations.
|
||||
- CLI change: pass argvArg.diff into the options so the --diff flag is honored by the format command.
|
||||
- When diff is enabled, run formatter.check() for each active formatter and call displayAllDiffs() for those with differences, with informational logging.
|
||||
- Update dependency @git.zone/tsdoc from ^1.10.2 to ^1.11.0.
|
||||
|
||||
## 2025-12-15 - 2.8.0 - feat(commit)
|
||||
Add commit configuration and automatic pre-commit tests
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@git.zone/cli",
|
||||
"private": false,
|
||||
"version": "2.8.0",
|
||||
"version": "2.11.0",
|
||||
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
||||
"main": "dist_ts/index.ts",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@types/node": "^25.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@git.zone/tsdoc": "^1.10.2",
|
||||
"@git.zone/tsdoc": "^1.11.3",
|
||||
"@git.zone/tspublish": "^1.10.3",
|
||||
"@push.rocks/commitinfo": "^1.0.12",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
|
||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@git.zone/tsdoc':
|
||||
specifier: ^1.10.2
|
||||
version: 1.10.2(ws@8.18.3)(zod@3.25.76)
|
||||
specifier: ^1.11.3
|
||||
version: 1.11.3(ws@8.18.3)(zod@3.25.76)
|
||||
'@git.zone/tspublish':
|
||||
specifier: ^1.10.3
|
||||
version: 1.10.3
|
||||
@@ -520,8 +520,8 @@ packages:
|
||||
resolution: {integrity: sha512-YD1qMYA/4eOuF57V0ccR+xo6ww1+QOYFA2K5gBPFBDNh9VdfvWxxDhOUybja8lT9PVMoli8PHG5WA5tKJkdXIQ==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tsdoc@1.10.2':
|
||||
resolution: {integrity: sha512-r4pKv74CH0KtzRvGdLioJd3DznSKmr8ZVE43QPFfGSNftH5P2eLAe5lc5nK8gCWb8mgEkb8WNfqtTL3Lkg+XyQ==}
|
||||
'@git.zone/tsdoc@1.11.3':
|
||||
resolution: {integrity: sha512-U6X9laKv9CTZiqtQpqVMZ2x3qKH1ucey3y16T5UQ70j7wza2GV9rwdkTIHgpYrWFBMSoh909T+ELH8qWbRqimw==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tspublish@1.10.3':
|
||||
@@ -999,12 +999,18 @@ packages:
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||
|
||||
'@push.rocks/smartagent@1.2.5':
|
||||
resolution: {integrity: sha512-qV7zyHbp5p5ySg16uipjIdYzKM85fn5/l97pKlZz9awRZhOcvYblmypQRKHlMc+O2mVevxLY4Q/6pzYwI8UXvw==}
|
||||
|
||||
'@push.rocks/smartai@0.8.0':
|
||||
resolution: {integrity: sha512-guzi28meUDc3mydC8kpoA+4pzExRQqygXYFDD4qQSWPpIRHQ7qhpeNqJzrrGezT1yOH5Gb9taPEGwT56hI+nwQ==}
|
||||
|
||||
'@push.rocks/smartarchive@4.2.4':
|
||||
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
||||
|
||||
'@push.rocks/smartarchive@5.0.1':
|
||||
resolution: {integrity: sha512-x4bie9IIdL9BZqBZLc8Pemp8xZOJGa6mXSVgKJRL4/Rw+E5N4rVHjQOYGRV75nC2mAMJh9GIbixuxLnWjj77ag==}
|
||||
|
||||
'@push.rocks/smartarray@1.1.0':
|
||||
resolution: {integrity: sha512-b5YgBmUdglOJH8zeUf2ZWdPCoqySgwvkycRi2BhA9zVZHkpASh39Ej0q0fxFJetlUVyYqGfVoMVjbVrLFfFV7g==}
|
||||
|
||||
@@ -1042,6 +1048,9 @@ packages:
|
||||
'@push.rocks/smartdelay@3.0.5':
|
||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||
|
||||
'@push.rocks/smartdeno@1.2.0':
|
||||
resolution: {integrity: sha512-6S1plCaMUVOZiRSflfoz9Fqk9phACCuKmc7Z6SfTvfl+p9VcPUmewKgaa/0QiLOpiI6ksfxdfmkS5Rw5HpYeIA==}
|
||||
|
||||
'@push.rocks/smartdiff@1.1.0':
|
||||
resolution: {integrity: sha512-AAz/unmko0C+g+60odOoK32PE3Ci3YLoB+zfg1LGLyVRCthcdzjqa1C2Km0MfG7IyJQKPdj8J5HPubtpm3ZeaQ==}
|
||||
|
||||
@@ -2704,9 +2713,6 @@ packages:
|
||||
resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
gpt-tokenizer@3.4.0:
|
||||
resolution: {integrity: sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==}
|
||||
|
||||
graceful-fs@4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||
|
||||
@@ -2905,8 +2911,8 @@ packages:
|
||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isomorphic-git@1.36.0:
|
||||
resolution: {integrity: sha512-22tU165ptowHYoDEwYJy5EKRzpHiuLMliaR01fH9ZwaUj1z/IqE++tGpjw/pD6eCWoxiOp6TPWX434aJ9zA4Lg==}
|
||||
isomorphic-git@1.36.1:
|
||||
resolution: {integrity: sha512-fC8SRT8MwoaXDK8G4z5biPEbqf2WyEJUb2MJ2ftSd39/UIlsnoZxLGux+lae0poLZO4AEcx6aUVOh5bV+P8zFA==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5060,12 +5066,13 @@ snapshots:
|
||||
- '@swc/helpers'
|
||||
- supports-color
|
||||
|
||||
'@git.zone/tsdoc@1.10.2(ws@8.18.3)(zod@3.25.76)':
|
||||
'@git.zone/tsdoc@1.11.3(ws@8.18.3)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@git.zone/tspublish': 1.10.3
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/npmextra': 5.3.3
|
||||
'@push.rocks/qenv': 6.1.3
|
||||
'@push.rocks/smartagent': 1.2.5(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
|
||||
'@push.rocks/smartai': 0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
|
||||
'@push.rocks/smartcli': 4.0.19
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -5078,7 +5085,6 @@ snapshots:
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
gpt-tokenizer: 3.4.0
|
||||
typedoc: 0.28.15(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -5877,6 +5883,30 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
'@push.rocks/smartagent@1.2.5(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@push.rocks/smartai': 0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
|
||||
'@push.rocks/smartbrowser': 2.0.8(typescript@5.9.3)
|
||||
'@push.rocks/smartdeno': 1.2.0
|
||||
'@push.rocks/smartfs': 1.2.0
|
||||
'@push.rocks/smartrequest': 5.0.1
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
minimatch: 10.1.1
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- bufferutil
|
||||
- react
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
- vue
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@push.rocks/smartai@0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.65.0(zod@3.25.76)
|
||||
@@ -5923,6 +5953,26 @@ snapshots:
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartarchive@5.0.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 4.4.2
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstream': 3.2.5
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@types/tar-stream': 3.1.4
|
||||
fflate: 0.8.2
|
||||
file-type: 21.1.1
|
||||
tar-stream: 3.1.7
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartarray@1.1.0': {}
|
||||
|
||||
'@push.rocks/smartbrowser@2.0.8(typescript@5.9.3)':
|
||||
@@ -6046,6 +6096,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
|
||||
'@push.rocks/smartdeno@1.2.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartarchive': 5.0.1
|
||||
'@push.rocks/smartfs': 1.2.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartdiff@1.1.0':
|
||||
dependencies:
|
||||
diff: 8.0.2
|
||||
@@ -6177,7 +6239,7 @@ snapshots:
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@types/diff': 8.0.0
|
||||
diff: 8.0.2
|
||||
isomorphic-git: 1.36.0
|
||||
isomorphic-git: 1.36.1
|
||||
minimatch: 10.1.1
|
||||
|
||||
'@push.rocks/smartguard@3.1.0':
|
||||
@@ -8359,8 +8421,6 @@ snapshots:
|
||||
p-cancelable: 3.0.0
|
||||
responselike: 3.0.0
|
||||
|
||||
gpt-tokenizer@3.4.0: {}
|
||||
|
||||
graceful-fs@4.2.10: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
@@ -8573,7 +8633,7 @@ snapshots:
|
||||
|
||||
isexe@3.1.1: {}
|
||||
|
||||
isomorphic-git@1.36.0:
|
||||
isomorphic-git@1.36.1:
|
||||
dependencies:
|
||||
async-lock: 1.4.1
|
||||
clean-git-ref: 2.0.1
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/cli',
|
||||
version: '2.8.0',
|
||||
version: '2.11.0',
|
||||
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ export let run = async () => {
|
||||
interactive: argvArg.interactive !== false,
|
||||
parallel: argvArg.parallel !== false,
|
||||
verbose: argvArg.verbose,
|
||||
diff: argvArg.diff,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -143,7 +143,11 @@ export abstract class BaseFormatter {
|
||||
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
||||
console.log(`\n--- ${diff.path}`);
|
||||
if (diff.before && diff.after) {
|
||||
console.log(plugins.smartdiff.formatLineDiffForConsole(diff.before, diff.after));
|
||||
console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, {
|
||||
originalFileName: diff.path,
|
||||
revisedFileName: diff.path,
|
||||
context: 3,
|
||||
}));
|
||||
} else if (diff.after && !diff.before) {
|
||||
console.log(' (new file)');
|
||||
// Show first few lines of new content
|
||||
|
||||
@@ -1,8 +1,117 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatCopy from '../format.copy.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
export class CopyFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'copy', formatCopy);
|
||||
interface ICopyPattern {
|
||||
from: string;
|
||||
to: string;
|
||||
preservePath?: boolean;
|
||||
}
|
||||
|
||||
export class CopyFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'copy';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
|
||||
// Get copy configuration from npmextra.json
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const copyConfig = npmextraConfig.dataFor<{ patterns: ICopyPattern[] }>(
|
||||
'gitzone.format.copy',
|
||||
{ patterns: [] },
|
||||
);
|
||||
|
||||
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
||||
logVerbose('No copy patterns configured in npmextra.json');
|
||||
return changes;
|
||||
}
|
||||
|
||||
for (const pattern of copyConfig.patterns) {
|
||||
if (!pattern.from || !pattern.to) {
|
||||
logVerbose('Invalid copy pattern - missing "from" or "to" field');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle glob patterns
|
||||
const entries = await plugins.smartfs
|
||||
.directory('.')
|
||||
.recursive()
|
||||
.filter(pattern.from)
|
||||
.list();
|
||||
const files = entries.map((entry) => entry.path);
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = file;
|
||||
let destPath = pattern.to;
|
||||
|
||||
// If destination is a directory, preserve filename
|
||||
if (pattern.to.endsWith('/')) {
|
||||
const filename = plugins.path.basename(file);
|
||||
destPath = plugins.path.join(pattern.to, filename);
|
||||
}
|
||||
|
||||
// Handle template variables in destination path
|
||||
if (pattern.preservePath) {
|
||||
const relativePath = plugins.path.relative(
|
||||
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
||||
file,
|
||||
);
|
||||
destPath = plugins.path.join(pattern.to, relativePath);
|
||||
}
|
||||
|
||||
// Read source content
|
||||
const content = (await plugins.smartfs
|
||||
.file(sourcePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Check if destination exists and has same content
|
||||
let needsCopy = true;
|
||||
const destExists = await plugins.smartfs.file(destPath).exists();
|
||||
if (destExists) {
|
||||
const existingContent = (await plugins.smartfs
|
||||
.file(destPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
if (existingContent === content) {
|
||||
needsCopy = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsCopy) {
|
||||
changes.push({
|
||||
type: destExists ? 'modify' : 'create',
|
||||
path: destPath,
|
||||
module: this.name,
|
||||
description: `Copy from ${sourcePath}`,
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logVerbose(`Failed to process pattern ${pattern.from}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (!change.content) return;
|
||||
|
||||
// Ensure destination directory exists
|
||||
const destDir = plugins.path.dirname(change.path);
|
||||
await plugins.smartfs.directory(destDir).recursive().create();
|
||||
|
||||
if (change.type === 'create') {
|
||||
await this.createFile(change.path, change.content);
|
||||
} else {
|
||||
await this.modifyFile(change.path, change.content);
|
||||
}
|
||||
logger.log('info', `Copied to ${change.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,111 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatGitignore from '../format.gitignore.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import { logger } from '../../gitzone.logging.js';
|
||||
|
||||
export class GitignoreFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'gitignore', formatGitignore);
|
||||
// Standard gitignore template content (without front-matter)
|
||||
const GITIGNORE_TEMPLATE = `.nogit/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.serena/
|
||||
|
||||
#------# custom`;
|
||||
|
||||
export class GitignoreFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'gitignore';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const gitignorePath = '.gitignore';
|
||||
|
||||
// Check if file exists and extract custom content
|
||||
let customContent = '';
|
||||
const exists = await plugins.smartfs.file(gitignorePath).exists();
|
||||
|
||||
if (exists) {
|
||||
const existingContent = (await plugins.smartfs
|
||||
.file(gitignorePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Extract custom section content
|
||||
const customMarkers = ['#------# custom', '# custom'];
|
||||
for (const marker of customMarkers) {
|
||||
const splitResult = existingContent.split(marker);
|
||||
if (splitResult.length > 1) {
|
||||
customContent = splitResult[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute new content
|
||||
let newContent = GITIGNORE_TEMPLATE;
|
||||
if (customContent) {
|
||||
newContent = GITIGNORE_TEMPLATE + '\n' + customContent + '\n';
|
||||
} else {
|
||||
newContent = GITIGNORE_TEMPLATE + '\n';
|
||||
}
|
||||
|
||||
// Read current content to compare
|
||||
let currentContent = '';
|
||||
if (exists) {
|
||||
currentContent = (await plugins.smartfs
|
||||
.file(gitignorePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
}
|
||||
|
||||
// Determine change type
|
||||
if (!exists) {
|
||||
changes.push({
|
||||
type: 'create',
|
||||
path: gitignorePath,
|
||||
module: this.name,
|
||||
description: 'Create .gitignore',
|
||||
content: newContent,
|
||||
});
|
||||
} else if (newContent !== currentContent) {
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: gitignorePath,
|
||||
module: this.name,
|
||||
description: 'Update .gitignore (preserving custom section)',
|
||||
content: newContent,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (!change.content) return;
|
||||
|
||||
if (change.type === 'create') {
|
||||
await this.createFile(change.path, change.content);
|
||||
logger.log('info', 'Created .gitignore');
|
||||
} else if (change.type === 'modify') {
|
||||
await this.modifyFile(change.path, change.content);
|
||||
logger.log('info', 'Updated .gitignore (preserved custom section)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import { Project } from '../../classes.project.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
|
||||
// This is a wrapper for existing format modules
|
||||
export class LegacyFormatter extends BaseFormatter {
|
||||
private moduleName: string;
|
||||
private formatModule: any;
|
||||
|
||||
constructor(
|
||||
context: any,
|
||||
project: Project,
|
||||
moduleName: string,
|
||||
formatModule: any,
|
||||
) {
|
||||
super(context, project);
|
||||
this.moduleName = moduleName;
|
||||
this.formatModule = formatModule;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.moduleName;
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
// For legacy modules, we can't easily predict changes
|
||||
// So we'll return a generic change that indicates the module will run
|
||||
return [
|
||||
{
|
||||
type: 'modify',
|
||||
path: '<various files>',
|
||||
module: this.name,
|
||||
description: `Run ${this.name} formatter`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
// Run the legacy format module
|
||||
await this.formatModule.run(this.project);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,62 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatLicense from '../format.license.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { logger } from '../../gitzone.logging.js';
|
||||
|
||||
export class LicenseFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'license', formatLicense);
|
||||
const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
|
||||
|
||||
export class LicenseFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'license';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
// License formatter only checks for incompatible licenses
|
||||
// It does not modify any files, so return empty array
|
||||
// The actual check happens in execute() for reporting purposes
|
||||
return [];
|
||||
}
|
||||
|
||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||
const startTime = this.stats.moduleStartTime(this.name);
|
||||
this.stats.startModule(this.name);
|
||||
|
||||
try {
|
||||
// Check if node_modules exists
|
||||
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||
const nodeModulesExists = await plugins.smartfs
|
||||
.directory(nodeModulesPath)
|
||||
.exists();
|
||||
|
||||
if (!nodeModulesExists) {
|
||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||
return;
|
||||
}
|
||||
|
||||
// Run license check
|
||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
||||
paths.cwd,
|
||||
INCOMPATIBLE_LICENSES,
|
||||
);
|
||||
|
||||
if (licenseCheckResult.failingModules.length === 0) {
|
||||
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||
} else {
|
||||
logger.log('error', 'License check failed - incompatible licenses found:');
|
||||
for (const failedModule of licenseCheckResult.failingModules) {
|
||||
console.log(
|
||||
` ${failedModule.name} has license ${failedModule.license}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.stats.endModule(this.name, startTime);
|
||||
}
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
// No file changes for license formatter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,165 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatNpmextra from '../format.npmextra.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
export class NpmextraFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'npmextra', formatNpmextra);
|
||||
/**
|
||||
* Migrates npmextra.json from old namespace keys to new package-scoped keys
|
||||
*/
|
||||
const migrateNamespaceKeys = (npmextraJson: any): boolean => {
|
||||
let migrated = false;
|
||||
const migrations = [
|
||||
{ oldKey: 'gitzone', newKey: '@git.zone/cli' },
|
||||
{ oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' },
|
||||
{ oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' },
|
||||
{ oldKey: 'npmci', newKey: '@ship.zone/szci' },
|
||||
{ oldKey: 'szci', newKey: '@ship.zone/szci' },
|
||||
];
|
||||
for (const { oldKey, newKey } of migrations) {
|
||||
if (npmextraJson[oldKey] && !npmextraJson[newKey]) {
|
||||
npmextraJson[newKey] = npmextraJson[oldKey];
|
||||
delete npmextraJson[oldKey];
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
return migrated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
|
||||
*/
|
||||
const migrateAccessLevel = (npmextraJson: any): boolean => {
|
||||
const szciConfig = npmextraJson['@ship.zone/szci'];
|
||||
|
||||
if (!szciConfig?.npmAccessLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gitzoneConfig = npmextraJson['@git.zone/cli'] || {};
|
||||
if (gitzoneConfig?.release?.accessLevel) {
|
||||
delete szciConfig.npmAccessLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!npmextraJson['@git.zone/cli']) {
|
||||
npmextraJson['@git.zone/cli'] = {};
|
||||
}
|
||||
if (!npmextraJson['@git.zone/cli'].release) {
|
||||
npmextraJson['@git.zone/cli'].release = {};
|
||||
}
|
||||
|
||||
npmextraJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel;
|
||||
delete szciConfig.npmAccessLevel;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export class NpmextraFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'npmextra';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const npmextraPath = 'npmextra.json';
|
||||
|
||||
// Check if file exists
|
||||
const exists = await plugins.smartfs.file(npmextraPath).exists();
|
||||
if (!exists) {
|
||||
logVerbose('npmextra.json does not exist, skipping');
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Read current content
|
||||
const currentContent = (await plugins.smartfs
|
||||
.file(npmextraPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Parse and compute new content
|
||||
const npmextraJson = JSON.parse(currentContent);
|
||||
|
||||
// Apply migrations (these are automatic, non-interactive)
|
||||
migrateNamespaceKeys(npmextraJson);
|
||||
migrateAccessLevel(npmextraJson);
|
||||
|
||||
// Ensure namespaces exist
|
||||
if (!npmextraJson['@git.zone/cli']) {
|
||||
npmextraJson['@git.zone/cli'] = {};
|
||||
}
|
||||
if (!npmextraJson['@ship.zone/szci']) {
|
||||
npmextraJson['@ship.zone/szci'] = {};
|
||||
}
|
||||
|
||||
const newContent = JSON.stringify(npmextraJson, null, 2);
|
||||
|
||||
// Only add change if content differs
|
||||
if (newContent !== currentContent) {
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: npmextraPath,
|
||||
module: this.name,
|
||||
description: 'Migrate and format npmextra.json',
|
||||
content: newContent,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (change.type !== 'modify' || !change.content) return;
|
||||
|
||||
// Parse the content to check for missing required fields
|
||||
const npmextraJson = JSON.parse(change.content);
|
||||
|
||||
// Check for missing required module information
|
||||
const expectedRepoInformation: string[] = [
|
||||
'projectType',
|
||||
'module.githost',
|
||||
'module.gitscope',
|
||||
'module.gitrepo',
|
||||
'module.description',
|
||||
'module.npmPackagename',
|
||||
'module.license',
|
||||
];
|
||||
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||
if (
|
||||
!plugins.smartobject.smartGet(
|
||||
npmextraJson['@git.zone/cli'],
|
||||
expectedRepoInformationItem,
|
||||
)
|
||||
) {
|
||||
interactInstance.addQuestions([
|
||||
{
|
||||
message: `What is the value of ${expectedRepoInformationItem}`,
|
||||
name: expectedRepoInformationItem,
|
||||
type: 'input',
|
||||
default: 'undefined variable',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const answerbucket = await interactInstance.runQueue();
|
||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||
const cliProvidedValue = answerbucket.getAnswerFor(
|
||||
expectedRepoInformationItem,
|
||||
);
|
||||
if (cliProvidedValue) {
|
||||
plugins.smartobject.smartAdd(
|
||||
npmextraJson['@git.zone/cli'],
|
||||
expectedRepoInformationItem,
|
||||
cliProvidedValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the final content
|
||||
const finalContent = JSON.stringify(npmextraJson, null, 2);
|
||||
await this.modifyFile(change.path, finalContent);
|
||||
logger.log('info', 'Updated npmextra.json');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,204 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatPackageJson from '../format.packagejson.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
export class PackageJsonFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'packagejson', formatPackageJson);
|
||||
/**
|
||||
* Ensures a certain dependency exists or is excluded
|
||||
*/
|
||||
const ensureDependency = async (
|
||||
packageJsonObject: any,
|
||||
position: 'dep' | 'devDep' | 'everywhere',
|
||||
constraint: 'exclude' | 'include' | 'latest',
|
||||
dependencyArg: string,
|
||||
): Promise<void> => {
|
||||
const [packageName, version] = dependencyArg.includes('@')
|
||||
? dependencyArg.split('@').filter(Boolean)
|
||||
: [dependencyArg, 'latest'];
|
||||
|
||||
const targetSections: string[] = [];
|
||||
|
||||
switch (position) {
|
||||
case 'dep':
|
||||
targetSections.push('dependencies');
|
||||
break;
|
||||
case 'devDep':
|
||||
targetSections.push('devDependencies');
|
||||
break;
|
||||
case 'everywhere':
|
||||
targetSections.push('dependencies', 'devDependencies');
|
||||
break;
|
||||
}
|
||||
|
||||
for (const section of targetSections) {
|
||||
if (!packageJsonObject[section]) {
|
||||
packageJsonObject[section] = {};
|
||||
}
|
||||
|
||||
switch (constraint) {
|
||||
case 'exclude':
|
||||
delete packageJsonObject[section][packageName];
|
||||
break;
|
||||
case 'include':
|
||||
if (!packageJsonObject[section][packageName]) {
|
||||
packageJsonObject[section][packageName] =
|
||||
version === 'latest' ? '^1.0.0' : version;
|
||||
}
|
||||
break;
|
||||
case 'latest':
|
||||
try {
|
||||
const registry = new plugins.smartnpm.NpmRegistry();
|
||||
const packageInfo = await registry.getPackageInfo(packageName);
|
||||
const latestVersion = packageInfo['dist-tags'].latest;
|
||||
packageJsonObject[section][packageName] = `^${latestVersion}`;
|
||||
} catch (error) {
|
||||
logVerbose(
|
||||
`Could not fetch latest version for ${packageName}, using existing or default`,
|
||||
);
|
||||
if (!packageJsonObject[section][packageName]) {
|
||||
packageJsonObject[section][packageName] =
|
||||
version === 'latest' ? '^1.0.0' : version;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class PackageJsonFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'packagejson';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const packageJsonPath = 'package.json';
|
||||
|
||||
// Check if file exists
|
||||
const exists = await plugins.smartfs.file(packageJsonPath).exists();
|
||||
if (!exists) {
|
||||
logVerbose('package.json does not exist, skipping');
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Read current content
|
||||
const currentContent = (await plugins.smartfs
|
||||
.file(packageJsonPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Parse and compute new content
|
||||
const packageJson = JSON.parse(currentContent);
|
||||
|
||||
// Get gitzone config from npmextra
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
||||
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
|
||||
|
||||
// Set metadata from gitzone config
|
||||
if (gitzoneData.module) {
|
||||
packageJson.repository = {
|
||||
type: 'git',
|
||||
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
|
||||
};
|
||||
packageJson.bugs = {
|
||||
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
|
||||
};
|
||||
packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`;
|
||||
}
|
||||
|
||||
// Ensure module type
|
||||
if (!packageJson.type) {
|
||||
packageJson.type = 'module';
|
||||
}
|
||||
|
||||
// Ensure private field exists
|
||||
if (packageJson.private === undefined) {
|
||||
packageJson.private = true;
|
||||
}
|
||||
|
||||
// Ensure license field exists
|
||||
if (!packageJson.license) {
|
||||
packageJson.license = 'UNLICENSED';
|
||||
}
|
||||
|
||||
// Ensure scripts object exists
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
|
||||
// Ensure build script exists
|
||||
if (!packageJson.scripts.build) {
|
||||
packageJson.scripts.build = `echo "Not needed for now"`;
|
||||
}
|
||||
|
||||
// Ensure buildDocs script exists
|
||||
if (!packageJson.scripts.buildDocs) {
|
||||
packageJson.scripts.buildDocs = `tsdoc`;
|
||||
}
|
||||
|
||||
// Set files array
|
||||
packageJson.files = [
|
||||
'ts/**/*',
|
||||
'ts_web/**/*',
|
||||
'dist/**/*',
|
||||
'dist_*/**/*',
|
||||
'dist_ts/**/*',
|
||||
'dist_ts_web/**/*',
|
||||
'assets/**/*',
|
||||
'cli.js',
|
||||
'npmextra.json',
|
||||
'readme.md',
|
||||
];
|
||||
|
||||
// Handle dependencies
|
||||
await ensureDependency(
|
||||
packageJson,
|
||||
'devDep',
|
||||
'exclude',
|
||||
'@push.rocks/tapbundle',
|
||||
);
|
||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
|
||||
await ensureDependency(
|
||||
packageJson,
|
||||
'devDep',
|
||||
'latest',
|
||||
'@git.zone/tsbuild',
|
||||
);
|
||||
|
||||
// Set pnpm overrides from assets
|
||||
try {
|
||||
const overridesContent = (await plugins.smartfs
|
||||
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
const overrides = JSON.parse(overridesContent);
|
||||
packageJson.pnpm = packageJson.pnpm || {};
|
||||
packageJson.pnpm.overrides = overrides;
|
||||
} catch (error) {
|
||||
logVerbose(`Could not read overrides.json: ${error.message}`);
|
||||
}
|
||||
|
||||
const newContent = JSON.stringify(packageJson, null, 2);
|
||||
|
||||
// Only add change if content differs
|
||||
if (newContent !== currentContent) {
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: packageJsonPath,
|
||||
module: this.name,
|
||||
description: 'Format package.json',
|
||||
content: newContent,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (change.type !== 'modify' || !change.content) return;
|
||||
|
||||
await this.modifyFile(change.path, change.content);
|
||||
logger.log('info', 'Updated package.json');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import type { IPlannedChange, ICheckResult } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
@@ -40,6 +40,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
|
||||
// Add files from TypeScript directories
|
||||
for (const dir of includeDirs) {
|
||||
try {
|
||||
const globPattern = `${dir}/**/*.${extensions}`;
|
||||
const dirEntries = await plugins.smartfs
|
||||
.directory('.')
|
||||
@@ -47,20 +48,32 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
.filter(globPattern)
|
||||
.list();
|
||||
const dirFiles = dirEntries.map((entry) => entry.path);
|
||||
allFiles.push(...dirFiles);
|
||||
// Filter out files in excluded directories
|
||||
const filteredFiles = dirFiles.filter((f) =>
|
||||
!f.includes('node_modules/') &&
|
||||
!f.includes('.nogit/') &&
|
||||
!f.includes('.git/')
|
||||
);
|
||||
allFiles.push(...filteredFiles);
|
||||
} catch (error) {
|
||||
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add root config files
|
||||
// Add root config files (only check root level, no recursive needed)
|
||||
for (const pattern of rootConfigFiles) {
|
||||
try {
|
||||
const rootEntries = await plugins.smartfs
|
||||
.directory('.')
|
||||
.recursive()
|
||||
.filter(pattern)
|
||||
.list();
|
||||
const rootFiles = rootEntries.map((entry) => entry.path);
|
||||
// Only include files at root level (no slashes in path)
|
||||
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
||||
allFiles.push(...rootLevelFiles);
|
||||
} catch (error) {
|
||||
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
@@ -230,4 +243,53 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
arrowParens: 'always',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override check() to compute diffs on-the-fly by running prettier
|
||||
*/
|
||||
async check(): Promise<ICheckResult> {
|
||||
const changes = await this.analyze();
|
||||
const diffs: ICheckResult['diffs'] = [];
|
||||
|
||||
for (const change of changes) {
|
||||
if (change.type !== 'modify') continue;
|
||||
|
||||
try {
|
||||
// Read current content
|
||||
const currentContent = (await plugins.smartfs
|
||||
.file(change.path)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Skip files without extension (prettier can't infer parser)
|
||||
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
||||
if (!fileExt) continue;
|
||||
|
||||
// Format with prettier to get what it would produce
|
||||
const prettier = await import('prettier');
|
||||
const formatted = await prettier.format(currentContent, {
|
||||
filepath: change.path,
|
||||
...(await this.getPrettierConfig()),
|
||||
});
|
||||
|
||||
// Only add to diffs if content differs
|
||||
if (formatted !== currentContent) {
|
||||
diffs.push({
|
||||
path: change.path,
|
||||
type: 'modify',
|
||||
before: currentContent,
|
||||
after: formatted,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be processed
|
||||
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasDiff: diffs.length > 0,
|
||||
diffs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as formatReadme from '../format.readme.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import { logger } from '../../gitzone.logging.js';
|
||||
|
||||
const DEFAULT_README_CONTENT = `# Project Readme
|
||||
|
||||
This is the initial readme file.`;
|
||||
|
||||
const DEFAULT_README_HINTS_CONTENT = `# Project Readme Hints
|
||||
|
||||
This is the initial readme hints file.`;
|
||||
|
||||
export class ReadmeFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
@@ -8,17 +17,39 @@ export class ReadmeFormatter extends BaseFormatter {
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
return [
|
||||
{
|
||||
type: 'modify',
|
||||
const changes: IPlannedChange[] = [];
|
||||
|
||||
// Check readme.md
|
||||
const readmeExists = await plugins.smartfs.file('readme.md').exists();
|
||||
if (!readmeExists) {
|
||||
changes.push({
|
||||
type: 'create',
|
||||
path: 'readme.md',
|
||||
module: this.name,
|
||||
description: 'Ensure readme files exist',
|
||||
},
|
||||
];
|
||||
description: 'Create readme.md',
|
||||
content: DEFAULT_README_CONTENT,
|
||||
});
|
||||
}
|
||||
|
||||
// Check readme.hints.md
|
||||
const hintsExists = await plugins.smartfs.file('readme.hints.md').exists();
|
||||
if (!hintsExists) {
|
||||
changes.push({
|
||||
type: 'create',
|
||||
path: 'readme.hints.md',
|
||||
module: this.name,
|
||||
description: 'Create readme.hints.md',
|
||||
content: DEFAULT_README_HINTS_CONTENT,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
await formatReadme.run();
|
||||
if (change.type !== 'create' || !change.content) return;
|
||||
|
||||
await this.createFile(change.path, change.content);
|
||||
logger.log('info', `Created ${change.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,155 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatTemplates from '../format.templates.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
export class TemplatesFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'templates', formatTemplates);
|
||||
export class TemplatesFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'templates';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const project = this.project;
|
||||
const projectType = project.gitzoneConfig?.data?.projectType;
|
||||
|
||||
// VSCode template - for all projects
|
||||
const vscodeChanges = await this.analyzeTemplate('vscode', [
|
||||
{ templatePath: '.vscode/settings.json', destPath: '.vscode/settings.json' },
|
||||
{ templatePath: '.vscode/launch.json', destPath: '.vscode/launch.json' },
|
||||
]);
|
||||
changes.push(...vscodeChanges);
|
||||
|
||||
// CI and other templates based on projectType
|
||||
switch (projectType) {
|
||||
case 'npm':
|
||||
case 'wcc':
|
||||
const accessLevel = project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
|
||||
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
|
||||
const ciChanges = await this.analyzeTemplate(ciTemplate, [
|
||||
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
|
||||
{ templatePath: '.gitea/workflows/default_tags.yaml', destPath: '.gitea/workflows/default_tags.yaml' },
|
||||
]);
|
||||
changes.push(...ciChanges);
|
||||
break;
|
||||
|
||||
case 'service':
|
||||
case 'website':
|
||||
const dockerCiChanges = await this.analyzeTemplate('ci_docker', [
|
||||
{ templatePath: '.gitea/workflows/docker_nottags.yaml', destPath: '.gitea/workflows/docker_nottags.yaml' },
|
||||
{ templatePath: '.gitea/workflows/docker_tags.yaml', destPath: '.gitea/workflows/docker_tags.yaml' },
|
||||
]);
|
||||
changes.push(...dockerCiChanges);
|
||||
|
||||
const dockerfileChanges = await this.analyzeTemplate('dockerfile_service', [
|
||||
{ templatePath: 'Dockerfile', destPath: 'Dockerfile' },
|
||||
{ templatePath: 'dockerignore', destPath: '.dockerignore' },
|
||||
]);
|
||||
changes.push(...dockerfileChanges);
|
||||
|
||||
const cliChanges = await this.analyzeTemplate('cli', [
|
||||
{ templatePath: 'cli.js', destPath: 'cli.js' },
|
||||
{ templatePath: 'cli.ts.js', destPath: 'cli.ts.js' },
|
||||
]);
|
||||
changes.push(...cliChanges);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update templates based on projectType
|
||||
if (projectType === 'website') {
|
||||
const websiteChanges = await this.analyzeTemplate('website_update', [
|
||||
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||
]);
|
||||
changes.push(...websiteChanges);
|
||||
} else if (projectType === 'service') {
|
||||
const serviceChanges = await this.analyzeTemplate('service_update', []);
|
||||
changes.push(...serviceChanges);
|
||||
} else if (projectType === 'wcc') {
|
||||
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
||||
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||
{ templatePath: 'html/index.ts', destPath: 'html/index.ts' },
|
||||
]);
|
||||
changes.push(...wccChanges);
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private async analyzeTemplate(
|
||||
templateName: string,
|
||||
files: Array<{ templatePath: string; destPath: string }>,
|
||||
): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||
|
||||
// Check if template exists
|
||||
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
||||
if (!templateExists) {
|
||||
logVerbose(`Template ${templateName} not found`);
|
||||
return changes;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const templateFilePath = plugins.path.join(templateDir, file.templatePath);
|
||||
const destFilePath = file.destPath;
|
||||
|
||||
// Check if template file exists
|
||||
const fileExists = await plugins.smartfs.file(templateFilePath).exists();
|
||||
if (!fileExists) {
|
||||
logVerbose(`Template file ${templateFilePath} not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read template content
|
||||
const templateContent = (await plugins.smartfs
|
||||
.file(templateFilePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Check if destination file exists
|
||||
const destExists = await plugins.smartfs.file(destFilePath).exists();
|
||||
let currentContent = '';
|
||||
if (destExists) {
|
||||
currentContent = (await plugins.smartfs
|
||||
.file(destFilePath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
}
|
||||
|
||||
// Only add change if content differs
|
||||
if (templateContent !== currentContent) {
|
||||
changes.push({
|
||||
type: destExists ? 'modify' : 'create',
|
||||
path: destFilePath,
|
||||
module: this.name,
|
||||
description: `Apply template ${templateName}/${file.templatePath}`,
|
||||
content: templateContent,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logVerbose(`Failed to read template ${templateFilePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (!change.content) return;
|
||||
|
||||
// Ensure destination directory exists
|
||||
const destDir = plugins.path.dirname(change.path);
|
||||
if (destDir && destDir !== '.') {
|
||||
await plugins.smartfs.directory(destDir).recursive().create();
|
||||
}
|
||||
|
||||
if (change.type === 'create') {
|
||||
await this.createFile(change.path, change.content);
|
||||
} else {
|
||||
await this.modifyFile(change.path, change.content);
|
||||
}
|
||||
logger.log('info', `Applied template to ${change.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,73 @@
|
||||
import { LegacyFormatter } from './legacy.formatter.js';
|
||||
import * as formatTsconfig from '../format.tsconfig.js';
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||
|
||||
export class TsconfigFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'tsconfig', formatTsconfig);
|
||||
export class TsconfigFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'tsconfig';
|
||||
}
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const tsconfigPath = 'tsconfig.json';
|
||||
|
||||
// Check if file exists
|
||||
const exists = await plugins.smartfs.file(tsconfigPath).exists();
|
||||
if (!exists) {
|
||||
logVerbose('tsconfig.json does not exist, skipping');
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Read current content
|
||||
const currentContent = (await plugins.smartfs
|
||||
.file(tsconfigPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
|
||||
// Parse and compute new content
|
||||
const tsconfigObject = JSON.parse(currentContent);
|
||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||
tsconfigObject.compilerOptions.baseUrl = '.';
|
||||
tsconfigObject.compilerOptions.paths = {};
|
||||
|
||||
// Get module paths from tspublish
|
||||
try {
|
||||
const tsPublishMod = await import('@git.zone/tspublish');
|
||||
const tsPublishInstance = new tsPublishMod.TsPublish();
|
||||
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
|
||||
|
||||
for (const publishModule of Object.keys(publishModules)) {
|
||||
const publishConfig = publishModules[publishModule];
|
||||
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
|
||||
`./${publishModule}/index.js`,
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
||||
}
|
||||
|
||||
const newContent = JSON.stringify(tsconfigObject, null, 2);
|
||||
|
||||
// Only add change if content differs
|
||||
if (newContent !== currentContent) {
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: tsconfigPath,
|
||||
module: this.name,
|
||||
description: 'Format tsconfig.json with path mappings',
|
||||
content: newContent,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (change.type !== 'modify' || !change.content) return;
|
||||
|
||||
await this.modifyFile(change.path, change.content);
|
||||
logger.log('info', 'Updated tsconfig.json');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export let run = async (
|
||||
interactive?: boolean;
|
||||
parallel?: boolean;
|
||||
verbose?: boolean;
|
||||
diff?: boolean; // Show file diffs
|
||||
} = {},
|
||||
): Promise<any> => {
|
||||
// Set verbose mode if requested
|
||||
@@ -132,6 +133,21 @@ export let run = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// Show diffs if requested (works in both dry-run and write modes)
|
||||
if (options.diff) {
|
||||
logger.log('info', 'Showing file diffs:');
|
||||
console.log('');
|
||||
|
||||
for (const formatter of activeFormatters) {
|
||||
const checkResult = await formatter.check();
|
||||
if (checkResult.hasDiff) {
|
||||
logger.log('info', `[${formatter.name}]`);
|
||||
formatter.displayAllDiffs(checkResult);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dry-run mode (default behavior)
|
||||
if (!shouldWrite) {
|
||||
logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
|
||||
|
||||
Reference in New Issue
Block a user