factorix 0.8.1 → 0.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/completion/_factorix.bash +38 -1
- data/completion/_factorix.fish +56 -0
- data/completion/_factorix.zsh +59 -0
- data/lib/factorix/changelog.rb +192 -0
- data/lib/factorix/cli/commands/mod/changelog/add.rb +36 -0
- data/lib/factorix/cli/commands/mod/changelog/check.rb +99 -0
- data/lib/factorix/cli/commands/mod/changelog/release.rb +45 -0
- data/lib/factorix/cli.rb +3 -0
- data/lib/factorix/errors.rb +2 -0
- data/lib/factorix/version.rb +1 -1
- data/sig/factorix/changelog.rbs +34 -0
- data/sig/factorix/cli/commands/mod/changelog/add.rbs +17 -0
- data/sig/factorix/cli/commands/mod/changelog/check.rbs +17 -0
- data/sig/factorix/cli/commands/mod/changelog/release.rbs +17 -0
- data/sig/factorix/errors.rbs +3 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 970e619a2c58b6ee5c7502adc6803469accf6ee618e41e580df5f950a8b2ed15
|
|
4
|
+
data.tar.gz: 6651e0dea495831ede26d577410722d5ea08542c6ee2516772c413ddec1b7b55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d10a143153a82c1d2b969d928ce85dd22c8c9bf95c793fe27e3287cc5f7408a00fcec47d839295d40390655667a3b1b6e1ab8e76947c9ce98eec8455f87438f
|
|
7
|
+
data.tar.gz: 2feeb57d5fc6835ee674f3c024b7ef5a4710e3b4f496dc7ada5ecc76add61e1b0fc3dcc3c7e95aea2ae57d131b28d124da04c83bb7ca1c04869067cac251e4c7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-02-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `mod changelog add` command to add entries to MOD changelog
|
|
8
|
+
- Add `mod changelog check` command to validate MOD changelog structure
|
|
9
|
+
- Add `mod changelog release` command to convert Unreleased section to a versioned section
|
|
10
|
+
|
|
3
11
|
## [0.8.1] - 2026-02-17
|
|
4
12
|
|
|
5
13
|
### Added
|
data/completion/_factorix.bash
CHANGED
|
@@ -20,7 +20,7 @@ _factorix() {
|
|
|
20
20
|
local commands="version man launch path download mod cache completion"
|
|
21
21
|
|
|
22
22
|
# mod subcommands
|
|
23
|
-
local mod_commands="check list show enable disable install uninstall update download upload edit search sync image settings"
|
|
23
|
+
local mod_commands="changelog check list show enable disable install uninstall update download upload edit search sync image settings"
|
|
24
24
|
|
|
25
25
|
# cache subcommands
|
|
26
26
|
local cache_commands="stat evict"
|
|
@@ -170,6 +170,43 @@ _factorix() {
|
|
|
170
170
|
COMPREPLY=($(compgen -f -X '!*.zip' -- "$cur"))
|
|
171
171
|
fi
|
|
172
172
|
;;
|
|
173
|
+
changelog)
|
|
174
|
+
if [[ $cword -eq 3 ]]; then
|
|
175
|
+
COMPREPLY=($(compgen -W "add check release" -- "$cur"))
|
|
176
|
+
else
|
|
177
|
+
case "${words[3]}" in
|
|
178
|
+
add)
|
|
179
|
+
if [[ "$prev" == "--version" ]]; then
|
|
180
|
+
COMPREPLY=($(compgen -W "Unreleased" -- "$cur"))
|
|
181
|
+
elif [[ "$prev" == "--category" ]]; then
|
|
182
|
+
local categories="'Major Features' Features 'Minor Features' Graphics Sounds Optimizations Balancing 'Combat Balancing' 'Circuit Network' Changes Bugfixes Modding Scripting Gui Control Translation Debug 'Ease of use' Info Locale Compatibility"
|
|
183
|
+
COMPREPLY=($(compgen -W "$categories" -- "$cur"))
|
|
184
|
+
elif [[ "$prev" == "--changelog" ]]; then
|
|
185
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
186
|
+
elif [[ "$cur" == -* ]]; then
|
|
187
|
+
COMPREPLY=($(compgen -W "$global_opts --version --category --changelog" -- "$cur"))
|
|
188
|
+
fi
|
|
189
|
+
;;
|
|
190
|
+
check)
|
|
191
|
+
if [[ "$prev" == "--changelog" ]] || [[ "$prev" == "--info-json" ]]; then
|
|
192
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
193
|
+
elif [[ "$cur" == -* ]]; then
|
|
194
|
+
COMPREPLY=($(compgen -W "$global_opts --release --changelog --info-json" -- "$cur"))
|
|
195
|
+
fi
|
|
196
|
+
;;
|
|
197
|
+
release)
|
|
198
|
+
if [[ "$prev" == "--changelog" ]] || [[ "$prev" == "--info-json" ]]; then
|
|
199
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
200
|
+
elif [[ "$cur" == -* ]]; then
|
|
201
|
+
COMPREPLY=($(compgen -W "$global_opts --version --date --changelog --info-json" -- "$cur"))
|
|
202
|
+
fi
|
|
203
|
+
;;
|
|
204
|
+
*)
|
|
205
|
+
COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
fi
|
|
209
|
+
;;
|
|
173
210
|
image)
|
|
174
211
|
if [[ $cword -eq 3 ]]; then
|
|
175
212
|
COMPREPLY=($(compgen -W "$image_commands" -- "$cur"))
|
data/completion/_factorix.fish
CHANGED
|
@@ -64,6 +64,40 @@ function __factorix_using_subcommand
|
|
|
64
64
|
return 1
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Helper function to check 3-level nested subcommand (e.g., mod changelog add)
|
|
68
|
+
function __factorix_using_sub_subcommand
|
|
69
|
+
set -l cmd (commandline -opc)
|
|
70
|
+
set -l argc (count $cmd)
|
|
71
|
+
|
|
72
|
+
if test $argc -lt 4
|
|
73
|
+
return 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
set -l parent $argv[1]
|
|
77
|
+
set -l sub $argv[2]
|
|
78
|
+
set -l subsub $argv[3]
|
|
79
|
+
|
|
80
|
+
set -l level 0
|
|
81
|
+
for i in (seq 2 $argc)
|
|
82
|
+
switch $cmd[$i]
|
|
83
|
+
case '-*'
|
|
84
|
+
continue
|
|
85
|
+
case '*'
|
|
86
|
+
if test $level -eq 0
|
|
87
|
+
test "$cmd[$i]" = "$parent"; or return 1
|
|
88
|
+
set level 1
|
|
89
|
+
else if test $level -eq 1
|
|
90
|
+
test "$cmd[$i]" = "$sub"; or return 1
|
|
91
|
+
set level 2
|
|
92
|
+
else
|
|
93
|
+
test "$cmd[$i]" = "$subsub"; and return 0
|
|
94
|
+
return 1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
return 1
|
|
99
|
+
end
|
|
100
|
+
|
|
67
101
|
# Disable file completion by default
|
|
68
102
|
complete -c factorix -f
|
|
69
103
|
|
|
@@ -125,6 +159,7 @@ complete -c factorix -n "__factorix_using_command mod" -a upload -d 'Upload MOD
|
|
|
125
159
|
complete -c factorix -n "__factorix_using_command mod" -a edit -d 'Edit MOD metadata on Factorio MOD Portal'
|
|
126
160
|
complete -c factorix -n "__factorix_using_command mod" -a search -d 'Search MODs on Factorio MOD Portal'
|
|
127
161
|
complete -c factorix -n "__factorix_using_command mod" -a sync -d 'Sync MOD states from a save file'
|
|
162
|
+
complete -c factorix -n "__factorix_using_command mod" -a changelog -d 'MOD changelog management'
|
|
128
163
|
complete -c factorix -n "__factorix_using_command mod" -a image -d 'MOD image management'
|
|
129
164
|
complete -c factorix -n "__factorix_using_command mod" -a settings -d 'MOD settings management'
|
|
130
165
|
|
|
@@ -195,6 +230,27 @@ complete -c factorix -n "__factorix_using_subcommand mod sync" -s y -l yes -d 'S
|
|
|
195
230
|
complete -c factorix -n "__factorix_using_subcommand mod sync" -s j -l jobs -d 'Number of parallel downloads' -r
|
|
196
231
|
complete -c factorix -n "__factorix_using_subcommand mod sync" -ra '(__fish_complete_suffix .zip)'
|
|
197
232
|
|
|
233
|
+
# mod changelog subcommands
|
|
234
|
+
complete -c factorix -n "__factorix_using_subcommand mod changelog" -a add -d 'Add an entry to MOD changelog'
|
|
235
|
+
complete -c factorix -n "__factorix_using_subcommand mod changelog" -a check -d 'Validate MOD changelog structure'
|
|
236
|
+
complete -c factorix -n "__factorix_using_subcommand mod changelog" -a release -d 'Release Unreleased changelog section with a version'
|
|
237
|
+
|
|
238
|
+
# mod changelog add options
|
|
239
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog add" -l version -d 'Version (X.Y.Z or Unreleased)' -ra 'Unreleased'
|
|
240
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog add" -l category -d 'Category name' -xa "'Major Features' Features 'Minor Features' Graphics Sounds Optimizations Balancing 'Combat Balancing' 'Circuit Network' Changes Bugfixes Modding Scripting Gui Control Translation Debug 'Ease of use' Info Locale Compatibility"
|
|
241
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog add" -l changelog -d 'Path to changelog file' -rF
|
|
242
|
+
|
|
243
|
+
# mod changelog check options
|
|
244
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog check" -l release -d 'Disallow Unreleased section'
|
|
245
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog check" -l changelog -d 'Path to changelog file' -rF
|
|
246
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog check" -l info-json -d 'Path to info.json file' -rF
|
|
247
|
+
|
|
248
|
+
# mod changelog release options
|
|
249
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog release" -l version -d 'Version (X.Y.Z)' -r
|
|
250
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog release" -l date -d 'Release date (YYYY-MM-DD)' -r
|
|
251
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog release" -l changelog -d 'Path to changelog file' -rF
|
|
252
|
+
complete -c factorix -n "__factorix_using_sub_subcommand mod changelog release" -l info-json -d 'Path to info.json file' -rF
|
|
253
|
+
|
|
198
254
|
# mod image subcommands
|
|
199
255
|
complete -c factorix -n "__factorix_using_subcommand mod image" -a list -d 'List MOD images'
|
|
200
256
|
complete -c factorix -n "__factorix_using_subcommand mod image" -a add -d 'Add an image to a MOD'
|
data/completion/_factorix.zsh
CHANGED
|
@@ -147,6 +147,7 @@ _factorix_mod() {
|
|
|
147
147
|
'edit:Edit MOD metadata on Factorio MOD Portal'
|
|
148
148
|
'search:Search MODs on Factorio MOD Portal'
|
|
149
149
|
'sync:Sync MOD states from a save file'
|
|
150
|
+
'changelog:MOD changelog management'
|
|
150
151
|
'image:MOD image management'
|
|
151
152
|
'settings:MOD settings management'
|
|
152
153
|
)
|
|
@@ -257,6 +258,9 @@ _factorix_mod() {
|
|
|
257
258
|
'(-j --jobs)'{-j,--jobs}'[Number of parallel downloads]:jobs:' \
|
|
258
259
|
'1:save file:_files -g "*.zip"'
|
|
259
260
|
;;
|
|
261
|
+
changelog)
|
|
262
|
+
_factorix_mod_changelog
|
|
263
|
+
;;
|
|
260
264
|
image)
|
|
261
265
|
_factorix_mod_image
|
|
262
266
|
;;
|
|
@@ -268,6 +272,61 @@ _factorix_mod() {
|
|
|
268
272
|
esac
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
_factorix_mod_changelog() {
|
|
276
|
+
local context state state_descr line
|
|
277
|
+
typeset -A opt_args
|
|
278
|
+
|
|
279
|
+
local -a global_opts
|
|
280
|
+
global_opts=(
|
|
281
|
+
'(-c --config-path)'{-c,--config-path}'[Path to configuration file]:config file:_files'
|
|
282
|
+
'--log-level[Set log level]:level:(debug info warn error fatal)'
|
|
283
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
_arguments -C \
|
|
287
|
+
'1:subcommand:->subcommand' \
|
|
288
|
+
'*::arg:->args'
|
|
289
|
+
|
|
290
|
+
case $state in
|
|
291
|
+
subcommand)
|
|
292
|
+
local -a subcommands
|
|
293
|
+
subcommands=(
|
|
294
|
+
'add:Add an entry to MOD changelog'
|
|
295
|
+
'check:Validate MOD changelog structure'
|
|
296
|
+
'release:Release Unreleased changelog section with a version'
|
|
297
|
+
)
|
|
298
|
+
_describe -t subcommands 'changelog subcommand' subcommands
|
|
299
|
+
;;
|
|
300
|
+
args)
|
|
301
|
+
case $line[1] in
|
|
302
|
+
add)
|
|
303
|
+
_arguments \
|
|
304
|
+
$global_opts \
|
|
305
|
+
'--version[Version (X.Y.Z or Unreleased)]:version:(Unreleased)' \
|
|
306
|
+
'--category[Category name]:category:(Major\ Features Features Minor\ Features Graphics Sounds Optimizations Balancing Combat\ Balancing Circuit\ Network Changes Bugfixes Modding Scripting Gui Control Translation Debug Ease\ of\ use Info Locale Compatibility)' \
|
|
307
|
+
'--changelog[Path to changelog file]:changelog file:_files' \
|
|
308
|
+
'*:entry text:'
|
|
309
|
+
;;
|
|
310
|
+
check)
|
|
311
|
+
_arguments \
|
|
312
|
+
$global_opts \
|
|
313
|
+
'--release[Disallow Unreleased section]' \
|
|
314
|
+
'--changelog[Path to changelog file]:changelog file:_files' \
|
|
315
|
+
'--info-json[Path to info.json file]:info.json file:_files'
|
|
316
|
+
;;
|
|
317
|
+
release)
|
|
318
|
+
_arguments \
|
|
319
|
+
$global_opts \
|
|
320
|
+
'--version[Version (X.Y.Z)]:version:' \
|
|
321
|
+
'--date[Release date (YYYY-MM-DD)]:date:' \
|
|
322
|
+
'--changelog[Path to changelog file]:changelog file:_files' \
|
|
323
|
+
'--info-json[Path to info.json file]:info.json file:_files'
|
|
324
|
+
;;
|
|
325
|
+
esac
|
|
326
|
+
;;
|
|
327
|
+
esac
|
|
328
|
+
}
|
|
329
|
+
|
|
271
330
|
_factorix_mod_image() {
|
|
272
331
|
local context state state_descr line
|
|
273
332
|
typeset -A opt_args
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
# Parser and writer for Factorio MOD changelog.txt files
|
|
7
|
+
#
|
|
8
|
+
# @see https://wiki.factorio.com/Tutorial:Mod_changelog_format
|
|
9
|
+
class Changelog
|
|
10
|
+
SEPARATOR = ("-" * 99).freeze
|
|
11
|
+
public_constant :SEPARATOR
|
|
12
|
+
|
|
13
|
+
UNRELEASED = "Unreleased"
|
|
14
|
+
public_constant :UNRELEASED
|
|
15
|
+
|
|
16
|
+
Section = Data.define(:version, :date, :categories)
|
|
17
|
+
private_class_method :new
|
|
18
|
+
|
|
19
|
+
# Load a changelog from a file
|
|
20
|
+
#
|
|
21
|
+
# @param path [Pathname] path to changelog.txt
|
|
22
|
+
# @return [Changelog]
|
|
23
|
+
# @raise [ChangelogParseError] if the file content is malformed
|
|
24
|
+
def self.load(path)
|
|
25
|
+
return new([]) unless path.exist?
|
|
26
|
+
|
|
27
|
+
parse(path.read)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Parse changelog text content
|
|
31
|
+
#
|
|
32
|
+
# @param text [String] changelog content
|
|
33
|
+
# @return [Changelog]
|
|
34
|
+
# @raise [ChangelogParseError] if the content is malformed
|
|
35
|
+
def self.parse(text)
|
|
36
|
+
tree = Grammar.new.parse(text)
|
|
37
|
+
sections = Transform.new.apply(tree)
|
|
38
|
+
new(Array(sections))
|
|
39
|
+
rescue Parslet::ParseFailed => e
|
|
40
|
+
raise ChangelogParseError, e.message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param sections [Array<Section>] changelog sections
|
|
44
|
+
def initialize(sections)
|
|
45
|
+
@sections = sections
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Array<Section>]
|
|
49
|
+
attr_reader :sections
|
|
50
|
+
|
|
51
|
+
# Save the changelog to a file
|
|
52
|
+
#
|
|
53
|
+
# @param path [Pathname] path to write
|
|
54
|
+
# @return [void]
|
|
55
|
+
def save(path)
|
|
56
|
+
path.write(to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Add an entry to the changelog
|
|
60
|
+
#
|
|
61
|
+
# @param version [MODVersion, String] target version (or Changelog::UNRELEASED)
|
|
62
|
+
# @param category [String] category name
|
|
63
|
+
# @param entry [String] entry text
|
|
64
|
+
# @return [void]
|
|
65
|
+
# @raise [InvalidArgumentError] if the entry already exists
|
|
66
|
+
def add_entry(version, category, entry)
|
|
67
|
+
raise InvalidArgumentError, "entry must not be blank" if entry.match?(/\A[[:space:]]*\z/)
|
|
68
|
+
|
|
69
|
+
section = find_or_create_section(version)
|
|
70
|
+
entries = (section.categories[category] ||= [])
|
|
71
|
+
raise InvalidArgumentError, "duplicate entry: #{entry}" if entries.include?(entry)
|
|
72
|
+
|
|
73
|
+
entries << entry
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Replace the first section (Unreleased) with a versioned section
|
|
77
|
+
# @param version [MODVersion] target version
|
|
78
|
+
# @param date [String] release date (YYYY-MM-DD)
|
|
79
|
+
# @return [void]
|
|
80
|
+
def release_section(version, date:)
|
|
81
|
+
raise InvalidOperationError, "First section is not Unreleased" unless @sections.first&.version == UNRELEASED
|
|
82
|
+
raise InvalidOperationError, "Version #{version} already exists" if @sections.any? {|s| s.version == version }
|
|
83
|
+
|
|
84
|
+
unreleased = @sections.first
|
|
85
|
+
@sections[0] = Section[version:, date:, categories: unreleased.categories]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Render the changelog as a string
|
|
89
|
+
#
|
|
90
|
+
# @return [String]
|
|
91
|
+
def to_s
|
|
92
|
+
@sections.map {|section| format_section(section) }.join("\n") + "\n"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def find_or_create_section(version)
|
|
96
|
+
@sections.find {|s| s.version == version } || create_section(version)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private def create_section(version)
|
|
100
|
+
section = Section[version:, date: nil, categories: {}]
|
|
101
|
+
@sections.unshift(section)
|
|
102
|
+
section
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private def format_section(section)
|
|
106
|
+
lines = [SEPARATOR]
|
|
107
|
+
lines << "Version: #{section.version}"
|
|
108
|
+
lines << "Date: #{section.date}" if section.date
|
|
109
|
+
section.categories.each do |cat, entries|
|
|
110
|
+
lines << " #{cat}:"
|
|
111
|
+
entries.each do |entry|
|
|
112
|
+
first, *rest = entry.split("\n")
|
|
113
|
+
lines << " - #{first}"
|
|
114
|
+
rest.each {|line| lines << " #{line}" }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
lines.join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Parslet grammar for Factorio changelog.txt
|
|
121
|
+
class Grammar < Parslet::Parser
|
|
122
|
+
rule(:newline) { str("\r\n") | str("\n") }
|
|
123
|
+
rule(:rest_of_line) { (newline.absent? >> any).repeat(1) }
|
|
124
|
+
rule(:blank_line) { match[' \t'].repeat >> newline }
|
|
125
|
+
|
|
126
|
+
rule(:separator) { str("-").repeat(99, 99) >> newline }
|
|
127
|
+
|
|
128
|
+
rule(:version_line) { str("Version: ") >> rest_of_line.as(:version) >> newline }
|
|
129
|
+
rule(:date_line) { str("Date: ") >> rest_of_line.as(:date) >> newline }
|
|
130
|
+
|
|
131
|
+
# Category name: everything between " " and ":\n", captured via negative lookahead
|
|
132
|
+
rule(:category_line) { str(" ") >> ((str(":\n") | str(":\r\n")).absent? >> any).repeat(1).as(:category) >> str(":") >> newline }
|
|
133
|
+
|
|
134
|
+
rule(:entry_first_line) { str(" - ") >> rest_of_line.as(:first) >> newline }
|
|
135
|
+
rule(:continuation_line) { str(" ") >> rest_of_line >> newline }
|
|
136
|
+
rule(:entry) { entry_first_line >> continuation_line.repeat.as(:rest) }
|
|
137
|
+
|
|
138
|
+
rule(:category_block) { category_line >> entry.repeat(1).as(:entries) }
|
|
139
|
+
|
|
140
|
+
rule(:section) do
|
|
141
|
+
separator >>
|
|
142
|
+
version_line >>
|
|
143
|
+
date_line.maybe.as(:date_line) >>
|
|
144
|
+
blank_line.repeat >>
|
|
145
|
+
category_block.repeat.as(:categories) >>
|
|
146
|
+
blank_line.repeat
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
rule(:changelog) { section.repeat(1).as(:sections) }
|
|
150
|
+
|
|
151
|
+
root(:changelog)
|
|
152
|
+
end
|
|
153
|
+
private_constant :Grammar
|
|
154
|
+
|
|
155
|
+
# Transform parsed tree into Section objects
|
|
156
|
+
class Transform < Parslet::Transform
|
|
157
|
+
rule(first: simple(:first), rest: subtree(:rest)) do
|
|
158
|
+
continuations = rest.is_a?(Array) ? rest : [rest]
|
|
159
|
+
parts = [first.to_s]
|
|
160
|
+
continuations.each {|c| parts << c.to_s.delete_prefix(" ") if c.is_a?(Parslet::Slice) }
|
|
161
|
+
parts.join("\n")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
rule(category: simple(:cat), entries: subtree(:entries)) do
|
|
165
|
+
entry_list = entries.is_a?(Array) ? entries : [entries]
|
|
166
|
+
{cat.to_s => entry_list}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
rule(version: simple(:ver), date_line: subtree(:date_data), categories: subtree(:cats)) do
|
|
170
|
+
ver_str = ver.to_s.strip
|
|
171
|
+
version = ver_str.casecmp("unreleased").zero? ? UNRELEASED : MODVersion.from_string(ver_str)
|
|
172
|
+
|
|
173
|
+
date = date_data.is_a?(Hash) ? date_data[:date].to_s.strip : nil
|
|
174
|
+
|
|
175
|
+
categories = {}
|
|
176
|
+
cat_list = cats.is_a?(Array) ? cats : [cats]
|
|
177
|
+
cat_list.each do |cat_hash|
|
|
178
|
+
next unless cat_hash.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
categories.merge!(cat_hash)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Section[version:, date:, categories:]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
rule(sections: subtree(:secs)) do
|
|
187
|
+
secs.is_a?(Array) ? secs : [secs]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
private_constant :Transform
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
module Changelog
|
|
8
|
+
# Add an entry to a MOD's changelog.txt
|
|
9
|
+
class Add < Base
|
|
10
|
+
desc "Add an entry to MOD changelog"
|
|
11
|
+
|
|
12
|
+
option :version, default: "Unreleased", desc: "Version (X.Y.Z or Unreleased)"
|
|
13
|
+
option :category, required: true, desc: "Category (e.g., Features, Bugfixes)"
|
|
14
|
+
option :changelog, default: "changelog.txt", desc: "Path to changelog file"
|
|
15
|
+
|
|
16
|
+
argument :entry, type: :array, required: true, desc: "Entry text"
|
|
17
|
+
|
|
18
|
+
# @param version [String] version string (X.Y.Z or Unreleased)
|
|
19
|
+
# @param category [String] category name
|
|
20
|
+
# @param entry [Array<String>] entry text words
|
|
21
|
+
# @param changelog [String] path to changelog file
|
|
22
|
+
# @return [void]
|
|
23
|
+
def call(version:, category:, entry:, changelog: "changelog.txt", **)
|
|
24
|
+
target_version = version.casecmp("unreleased").zero? ? Factorix::Changelog::UNRELEASED : MODVersion.from_string(version)
|
|
25
|
+
path = Pathname(changelog)
|
|
26
|
+
log = Factorix::Changelog.load(path)
|
|
27
|
+
log.add_entry(target_version, category, entry.join(" "))
|
|
28
|
+
log.save(path)
|
|
29
|
+
say "Added entry to #{target_version} [#{category}]", prefix: :success
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
module Changelog
|
|
8
|
+
# Validate the structure of a MOD changelog file
|
|
9
|
+
class Check < Base
|
|
10
|
+
desc "Validate MOD changelog structure"
|
|
11
|
+
|
|
12
|
+
option :release, type: :flag, default: false, desc: "Disallow Unreleased section"
|
|
13
|
+
option :changelog, default: "changelog.txt", desc: "Path to changelog file"
|
|
14
|
+
option :info_json, default: "info.json", desc: "Path to info.json file"
|
|
15
|
+
|
|
16
|
+
# @param release [Boolean] disallow Unreleased section
|
|
17
|
+
# @param changelog [String] path to changelog file
|
|
18
|
+
# @param info_json [String] path to info.json file
|
|
19
|
+
# @return [void]
|
|
20
|
+
def call(release: false, changelog: "changelog.txt", info_json: "info.json", **)
|
|
21
|
+
errors = []
|
|
22
|
+
|
|
23
|
+
log = parse_changelog(Pathname(changelog), errors)
|
|
24
|
+
return report(errors) unless log
|
|
25
|
+
|
|
26
|
+
validate_unreleased_position(log, errors)
|
|
27
|
+
validate_version_order(log, errors)
|
|
28
|
+
|
|
29
|
+
if release
|
|
30
|
+
validate_release_mode(log, Pathname(info_json), errors)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
report(errors)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def parse_changelog(path, errors)
|
|
37
|
+
Factorix::Changelog.load(path)
|
|
38
|
+
rescue ChangelogParseError => e
|
|
39
|
+
errors << "Failed to parse changelog: #{e.message}"
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def validate_unreleased_position(log, errors)
|
|
44
|
+
log.sections.each_with_index do |section, index|
|
|
45
|
+
next unless section.version == Factorix::Changelog::UNRELEASED
|
|
46
|
+
next if index.zero?
|
|
47
|
+
|
|
48
|
+
errors << "Unreleased section must be the first section"
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def validate_version_order(log, errors)
|
|
54
|
+
versioned = log.sections.select {|s| s.version.is_a?(MODVersion) }
|
|
55
|
+
versioned.each_cons(2) do |a, b|
|
|
56
|
+
next if a.version > b.version
|
|
57
|
+
|
|
58
|
+
errors << "Versions are not in descending order: #{a.version} should be greater than #{b.version}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def validate_release_mode(log, info_json_path, errors)
|
|
63
|
+
if log.sections.any? {|s| s.version == Factorix::Changelog::UNRELEASED }
|
|
64
|
+
errors << "Unreleased section is not allowed in release mode"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
validate_info_json_version(log, info_json_path, errors)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def validate_info_json_version(log, info_json_path, errors)
|
|
71
|
+
unless info_json_path.exist?
|
|
72
|
+
errors << "info.json not found: #{info_json_path}"
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
info = InfoJSON.from_json(info_json_path.read)
|
|
77
|
+
first_versioned = log.sections.find {|s| s.version.is_a?(MODVersion) }
|
|
78
|
+
return unless first_versioned
|
|
79
|
+
|
|
80
|
+
return if info.version == first_versioned.version
|
|
81
|
+
|
|
82
|
+
errors << "info.json version (#{info.version}) does not match first changelog version (#{first_versioned.version})"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def report(errors)
|
|
86
|
+
if errors.empty?
|
|
87
|
+
say "Changelog is valid", prefix: :success
|
|
88
|
+
else
|
|
89
|
+
say "Changelog validation failed:", prefix: :error
|
|
90
|
+
errors.each {|msg| say " - #{msg}" }
|
|
91
|
+
raise ValidationError, "Changelog validation failed"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module MOD
|
|
7
|
+
module Changelog
|
|
8
|
+
# Convert the Unreleased section to a versioned release
|
|
9
|
+
class Release < Base
|
|
10
|
+
desc "Convert Unreleased changelog section to a versioned section"
|
|
11
|
+
|
|
12
|
+
option :version, desc: "Version (X.Y.Z, default: from info.json)"
|
|
13
|
+
option :date, desc: "Release date (YYYY-MM-DD, default: today UTC)"
|
|
14
|
+
option :changelog, default: "changelog.txt", desc: "Path to changelog file"
|
|
15
|
+
option :info_json, default: "info.json", desc: "Path to info.json file"
|
|
16
|
+
|
|
17
|
+
# @param version [String, nil] version string (X.Y.Z)
|
|
18
|
+
# @param date [String, nil] release date (YYYY-MM-DD)
|
|
19
|
+
# @param changelog [String] path to changelog file
|
|
20
|
+
# @param info_json [String] path to info.json file
|
|
21
|
+
# @return [void]
|
|
22
|
+
def call(version: nil, date: nil, changelog: "changelog.txt", info_json: "info.json", **)
|
|
23
|
+
parsed_version = resolve_version(version, Pathname(info_json))
|
|
24
|
+
release_date = date || Time.now.utc.strftime("%Y-%m-%d")
|
|
25
|
+
path = Pathname(changelog)
|
|
26
|
+
log = Factorix::Changelog.load(path)
|
|
27
|
+
log.release_section(parsed_version, date: release_date)
|
|
28
|
+
log.save(path)
|
|
29
|
+
say "Converted Unreleased to #{parsed_version} (#{release_date})", prefix: :success
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def resolve_version(version, info_json_path)
|
|
33
|
+
if version
|
|
34
|
+
MODVersion.from_string(version)
|
|
35
|
+
else
|
|
36
|
+
info = InfoJSON.from_json(info_json_path.read)
|
|
37
|
+
info.version
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/factorix/cli.rb
CHANGED
|
@@ -19,6 +19,9 @@ module Factorix
|
|
|
19
19
|
register "path", Commands::Path
|
|
20
20
|
register "download", Commands::Download
|
|
21
21
|
register "completion", Commands::Completion
|
|
22
|
+
register "mod changelog add", Commands::MOD::Changelog::Add
|
|
23
|
+
register "mod changelog check", Commands::MOD::Changelog::Check
|
|
24
|
+
register "mod changelog release", Commands::MOD::Changelog::Release
|
|
22
25
|
register "mod check", Commands::MOD::Check
|
|
23
26
|
register "mod list", Commands::MOD::List
|
|
24
27
|
register "mod show", Commands::MOD::Show
|
data/lib/factorix/errors.rb
CHANGED
|
@@ -69,6 +69,8 @@ module Factorix
|
|
|
69
69
|
# MOD settings file errors
|
|
70
70
|
class MODSectionNotFoundError < FileFormatError; end
|
|
71
71
|
|
|
72
|
+
class ChangelogParseError < FileFormatError; end
|
|
73
|
+
|
|
72
74
|
# =====================================
|
|
73
75
|
# Domain layer errors
|
|
74
76
|
# =====================================
|
data/lib/factorix/version.rb
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class Changelog
|
|
7
|
+
class Section < Data
|
|
8
|
+
attr_reader version: MODVersion | String
|
|
9
|
+
attr_reader date: String?
|
|
10
|
+
attr_reader categories: Hash[String, Array[String]]
|
|
11
|
+
|
|
12
|
+
def self.[]: (version: MODVersion | String, date: String?, categories: Hash[String, Array[String]]) -> Section
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
SEPARATOR: String
|
|
16
|
+
UNRELEASED: String
|
|
17
|
+
|
|
18
|
+
def self.load: (Pathname path) -> Changelog
|
|
19
|
+
|
|
20
|
+
def self.parse: (String text) -> Changelog
|
|
21
|
+
|
|
22
|
+
private def self.new: (Array[Section] sections) -> instance
|
|
23
|
+
|
|
24
|
+
def save: (Pathname path) -> void
|
|
25
|
+
|
|
26
|
+
def add_entry: (MODVersion | String version, String category, String entry) -> void
|
|
27
|
+
|
|
28
|
+
def release_section: (MODVersion version, date: String) -> void
|
|
29
|
+
|
|
30
|
+
attr_reader sections: Array[Section]
|
|
31
|
+
|
|
32
|
+
def to_s: () -> String
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
module Changelog
|
|
10
|
+
class Add < Base
|
|
11
|
+
def call: (?version: String, category: String, entry: Array[String], ?changelog: String, **untyped) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
module Changelog
|
|
10
|
+
class Check < Base
|
|
11
|
+
def call: (?release: bool, ?changelog: String, ?info_json: String, **untyped) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
module Changelog
|
|
10
|
+
class Release < Base
|
|
11
|
+
def call: (?version: String?, ?date: String?, ?changelog: String, ?info_json: String, **untyped) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/sig/factorix/errors.rbs
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: factorix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OZAWA Sakuro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -255,6 +255,7 @@ files:
|
|
|
255
255
|
- lib/factorix/cache/file_system.rb
|
|
256
256
|
- lib/factorix/cache/redis.rb
|
|
257
257
|
- lib/factorix/cache/s3.rb
|
|
258
|
+
- lib/factorix/changelog.rb
|
|
258
259
|
- lib/factorix/cli.rb
|
|
259
260
|
- lib/factorix/cli/commands/backup_support.rb
|
|
260
261
|
- lib/factorix/cli/commands/base.rb
|
|
@@ -267,6 +268,9 @@ files:
|
|
|
267
268
|
- lib/factorix/cli/commands/download_support.rb
|
|
268
269
|
- lib/factorix/cli/commands/launch.rb
|
|
269
270
|
- lib/factorix/cli/commands/man.rb
|
|
271
|
+
- lib/factorix/cli/commands/mod/changelog/add.rb
|
|
272
|
+
- lib/factorix/cli/commands/mod/changelog/check.rb
|
|
273
|
+
- lib/factorix/cli/commands/mod/changelog/release.rb
|
|
270
274
|
- lib/factorix/cli/commands/mod/check.rb
|
|
271
275
|
- lib/factorix/cli/commands/mod/disable.rb
|
|
272
276
|
- lib/factorix/cli/commands/mod/download.rb
|
|
@@ -362,6 +366,7 @@ files:
|
|
|
362
366
|
- sig/factorix/cache/file_system.rbs
|
|
363
367
|
- sig/factorix/cache/redis.rbs
|
|
364
368
|
- sig/factorix/cache/s3.rbs
|
|
369
|
+
- sig/factorix/changelog.rbs
|
|
365
370
|
- sig/factorix/cli.rbs
|
|
366
371
|
- sig/factorix/cli/commands/base.rbs
|
|
367
372
|
- sig/factorix/cli/commands/cache/evict.rbs
|
|
@@ -371,6 +376,9 @@ files:
|
|
|
371
376
|
- sig/factorix/cli/commands/confirmable.rbs
|
|
372
377
|
- sig/factorix/cli/commands/download_support.rbs
|
|
373
378
|
- sig/factorix/cli/commands/launch.rbs
|
|
379
|
+
- sig/factorix/cli/commands/mod/changelog/add.rbs
|
|
380
|
+
- sig/factorix/cli/commands/mod/changelog/check.rbs
|
|
381
|
+
- sig/factorix/cli/commands/mod/changelog/release.rbs
|
|
374
382
|
- sig/factorix/cli/commands/mod/check.rbs
|
|
375
383
|
- sig/factorix/cli/commands/mod/disable.rbs
|
|
376
384
|
- sig/factorix/cli/commands/mod/download.rbs
|