backlogops-cli 0.1__py3-none-any.whl → 0.2__py3-none-any.whl

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.
@@ -12,21 +12,60 @@ underscore in the module name keeps it out of the command listing.
12
12
 
13
13
  import argparse
14
14
  import sys
15
+ from pathlib import Path
15
16
  from collections.abc import Callable
16
- from typing import Optional
17
+ from typing import Optional, TextIO
17
18
  import argcomplete
19
+ from backlogops_cli._migrate_warn import (
20
+ CliMigrateWarnHook, CliPresetMigrateWarnHook)
21
+ from backlogops_cli.bloc_version_reporter import BloCliVersionReporter
18
22
  from backlogops import (
19
- BacklogReleases, FormatRules, InputFormatConfig, OutputFormatConfig,
20
- ReleaseChanges, ReleaseDateChanges, format_content_changes,
21
- format_date_changes, read_available_teams, read_backlog_releases,
22
- resolve_input_config, resolve_output_config, write_backlog_releases,
23
- write_content_changes, write_date_changes)
23
+ BacklogOpsConfig, BacklogReleases, FileExistsCb, FormatRules, Levels,
24
+ ReleaseChanges, ReleaseDateChanges, allow_overwrite,
25
+ format_content_changes, format_date_changes, get_backlog_ops_config,
26
+ read_backlog_releases, resolve_input_config, resolve_output_config,
27
+ write_backlog_releases, write_content_changes, write_date_changes)
28
+
29
+
30
+ def overwrite_callback(force: bool, in_stream: Optional[TextIO] = None,
31
+ out_stream: Optional[TextIO] = None) -> FileExistsCb:
32
+ """Return a file-exists callback for writing CLI output files.
33
+
34
+ A writer calls the returned callback only when the target file
35
+ already exists. With ``force`` the overwrite is allowed silently.
36
+ Otherwise the user is asked on ``out_stream``/``in_stream`` and the
37
+ overwrite is allowed only on an explicit yes; any other answer, an
38
+ empty answer, or end of input refuses it with ``FileExistsError``.
39
+
40
+ Args:
41
+ force: Allow the overwrite without asking when True.
42
+ in_stream: Stream the answer is read from, or None for stdin.
43
+ out_stream: Stream the prompt is written to, or None for stdout.
44
+
45
+ Returns:
46
+ A callback suitable as ``file_exists_callback`` for the writers.
47
+ """
48
+ if force:
49
+ return allow_overwrite
50
+ reader = sys.stdin if in_stream is None else in_stream
51
+ writer = sys.stdout if out_stream is None else out_stream
52
+
53
+ def ask(file_name: str) -> None:
54
+ """Ask whether to overwrite ``file_name``; raise when refused."""
55
+ prompt = f'Output file {file_name} already exists. Overwrite? [y/N]: '
56
+ print(prompt, end='', file=writer)
57
+ writer.flush()
58
+ if reader.readline().strip().lower() in ('y', 'yes'):
59
+ return
60
+ raise FileExistsError(f'Did not overwrite existing file {file_name}.')
61
+ return ask
24
62
 
25
63
 
26
64
  def parsed_args(parser: argparse.ArgumentParser,
27
65
  args: Optional[list[str]]) -> argparse.Namespace:
28
66
  """Enable shell completion and parse the command line arguments."""
29
67
  argcomplete.autocomplete(parser)
68
+ BloCliVersionReporter().check_if_unsupported_python()
30
69
  return parser.parse_args(args)
31
70
 
32
71
 
@@ -38,85 +77,205 @@ def add_input_args(parser: argparse.ArgumentParser) -> None:
38
77
  help='Input format: a config file or a preset name.')
39
78
 
40
79
 
41
- def _input_presets(io_config: Optional[str]
42
- ) -> Optional[dict[str, InputFormatConfig]]:
43
- """Return the named input presets from a presets file, if given."""
44
- if io_config is None:
80
+ def add_config_arg(parser: argparse.ArgumentParser) -> None:
81
+ """Add the ``-c``/``--config`` backlog-ops configuration argument.
82
+
83
+ The configuration file holds the workforce, the named input and output
84
+ presets, the levels and the global status map. Without ``-c`` the file
85
+ is discovered the same way as the GUI.
86
+ """
87
+ parser.add_argument('-c', '--config', dest='config',
88
+ help='Backlog-ops configuration file (workforce, '
89
+ 'named presets, levels, status map). Without -c the '
90
+ 'file is found from $BACKLOGOPS_CFG, else '
91
+ 'backlogops.cfg in $BACKLOGOPS_DIR, else '
92
+ '$HOME/.backlogops.cfg.')
93
+
94
+
95
+ def _resolve_config(parsed: argparse.Namespace) -> BacklogOpsConfig:
96
+ """Return the backlog-ops configuration from ``-c`` or by discovery.
97
+
98
+ With ``-c`` the named file is read; an old file triggers a migration
99
+ warning. Without ``-c`` the file is discovered the same way as the GUI
100
+ (``$BACKLOGOPS_CFG``, then ``backlogops.cfg`` in ``$BACKLOGOPS_DIR``,
101
+ then ``$HOME/.backlogops.cfg``).
102
+
103
+ Raises:
104
+ ValueError: If ``-c`` names a file that does not exist.
105
+ RuntimeError: If no ``-c`` is given and no file is discovered.
106
+ """
107
+ config_file = parsed.config
108
+ if config_file is not None:
109
+ if not Path(config_file).is_file():
110
+ raise ValueError(f'Configuration file not found: {config_file}')
111
+ return get_backlog_ops_config(config_file, sys.stderr,
112
+ auto_ch_hook=CliMigrateWarnHook())
113
+ return get_backlog_ops_config(None, sys.stderr,
114
+ auto_ch_hook=CliMigrateWarnHook())
115
+
116
+
117
+ def required_config(parsed: argparse.Namespace) -> BacklogOpsConfig:
118
+ """Return the configuration, reporting a missing one as a ValueError.
119
+
120
+ Used by commands that cannot work without a configuration, such as the
121
+ estimate command, which needs the workforce.
122
+ """
123
+ try:
124
+ return _resolve_config(parsed)
125
+ except RuntimeError as error:
126
+ raise ValueError(str(error)) from error
127
+
128
+
129
+ def optional_config(parsed: argparse.Namespace) -> Optional[BacklogOpsConfig]:
130
+ """Return the configuration, or None with a note when none is found.
131
+
132
+ Used by commands that fall back to the built-in defaults (formats
133
+ inferred from the file name, no presets) when no configuration file is
134
+ available.
135
+ """
136
+ try:
137
+ return _resolve_config(parsed)
138
+ except RuntimeError:
139
+ print('No backlog-ops configuration file found; using built-in '
140
+ 'defaults.', file=sys.stderr)
45
141
  return None
46
- return read_available_teams(io_config, sys.stderr).input_configs
47
142
 
48
143
 
49
- def read_input(parsed: argparse.Namespace) -> BacklogReleases:
144
+ def io_levels(config: Optional[BacklogOpsConfig]) -> Optional[Levels]:
145
+ """Return the configured levels from ``config``, or None.
146
+
147
+ Args:
148
+ config: The resolved backlog-ops configuration, or None to use the
149
+ default levels.
150
+
151
+ Returns:
152
+ The levels configured in ``config``, or None when no configuration
153
+ is given.
154
+ """
155
+ return config.get_levels() if config is not None else None
156
+
157
+
158
+ def read_input(parsed: argparse.Namespace,
159
+ config: Optional[BacklogOpsConfig]) -> BacklogReleases:
50
160
  """Read and validate the backlog and releases from the input file.
51
161
 
52
162
  The input format is resolved from the ``--input-config`` value, which
53
163
  may be empty (inferred from the file name), a preset name looked up in
54
- the presets file given by ``--io-config``, or a config file path.
164
+ ``config``, or a config file path. When ``config`` is given its levels
165
+ and its library-wide status input map are honoured while reading the
166
+ items; the input configuration's own status map overrides the global
167
+ one per name.
55
168
 
56
169
  Args:
57
170
  parsed: Parsed command line arguments holding the input options
58
- added by :func:`add_input_args` and, optionally, the
59
- ``--io-config`` option added by :func:`add_output_args`.
171
+ added by :func:`add_input_args`.
172
+ config: The resolved backlog-ops configuration, or None to use the
173
+ built-in defaults.
60
174
 
61
175
  Returns:
62
176
  The validated backlog and releases read from the input file.
63
177
  """
64
- io_config = getattr(parsed, 'io_config', None)
65
- presets = _input_presets(io_config)
66
- config = resolve_input_config(parsed.input_config, data_file=parsed.input,
67
- presets=presets)
68
- data = read_backlog_releases(parsed.input, config)
178
+ presets = config.input_configs if config is not None else None
179
+ levels = config.get_levels() if config is not None else None
180
+ status_map = (config.get_status_input_map() if config is not None
181
+ else None)
182
+ in_config = resolve_input_config(parsed.input_config,
183
+ data_file=parsed.input, presets=presets,
184
+ auto_ch_hook=CliPresetMigrateWarnHook())
185
+ data = read_backlog_releases(parsed.input, in_config, levels, status_map)
69
186
  data.check_consistency(sys.stderr)
70
187
  return data
71
188
 
72
189
 
190
+ def add_force_arg(parser: argparse.ArgumentParser) -> None:
191
+ """Add the force flag that overwrites output files without asking."""
192
+ parser.add_argument('-f', '--force', dest='force', action='store_true',
193
+ help='Overwrite existing output files without '
194
+ 'asking.')
195
+
196
+
73
197
  def add_output_args(parser: argparse.ArgumentParser) -> None:
74
198
  """Add the output-file, output-config and ordering arguments."""
75
199
  parser.add_argument('-o', '--output', dest='output', required=True,
76
200
  help='Output data file to create.')
77
201
  parser.add_argument('-O', '--output-config', dest='output_config',
78
202
  help='Output format: a config file or a preset name.')
79
- parser.add_argument('--io-config', dest='io_config',
80
- help='Configuration file holding the named presets '
81
- '(by default the teams configuration file).')
82
203
  parser.add_argument('--releases-first', dest='releases_first',
83
204
  action='store_true',
84
205
  help='Write the releases before the backlog.')
206
+ add_force_arg(parser)
85
207
 
86
208
 
87
- def _output_presets(io_config: Optional[str]
88
- ) -> Optional[dict[str, OutputFormatConfig]]:
89
- """Return the named output presets from a presets file, if given."""
90
- if io_config is None:
91
- return None
92
- return read_available_teams(io_config, sys.stderr).output_configs
209
+ def build_io_parser(description: str, *, with_input: bool = True,
210
+ with_config: bool = True,
211
+ with_output: bool = True) -> argparse.ArgumentParser:
212
+ """Create a parser with the common input, config and output options.
213
+
214
+ Most data commands read a file, take the backlog-ops configuration,
215
+ and write a file, so this builds the parser with those option groups
216
+ already added. A command adds only its own extra options to the
217
+ returned parser. A group is left out when its flag is False, for a
218
+ command that does not read (or does not write) a backlog file.
219
+
220
+ Args:
221
+ description: The command description shown in the help text.
222
+ with_input: Add the input-file and input-config options.
223
+ with_config: Add the ``-c`` backlog-ops configuration option.
224
+ with_output: Add the output-file, output-config, ordering and
225
+ force options.
226
+
227
+ Returns:
228
+ The parser with the requested common options added.
229
+ """
230
+ parser = argparse.ArgumentParser(description=description)
231
+ if with_input:
232
+ add_input_args(parser)
233
+ if with_config:
234
+ add_config_arg(parser)
235
+ if with_output:
236
+ add_output_args(parser)
237
+ return parser
93
238
 
94
239
 
95
- def _write_output(parsed: argparse.Namespace, data: BacklogReleases) -> None:
240
+ def _write_output(parsed: argparse.Namespace,
241
+ config: Optional[BacklogOpsConfig],
242
+ data: BacklogReleases) -> None:
96
243
  """Write the backlog and releases to the configured output file."""
97
- presets = _output_presets(parsed.io_config)
98
- config = resolve_output_config(parsed.output_config,
99
- data_file=parsed.output, presets=presets)
244
+ presets = config.output_configs if config is not None else None
245
+ out_config = resolve_output_config(parsed.output_config,
246
+ data_file=parsed.output,
247
+ presets=presets,
248
+ auto_ch_hook=CliPresetMigrateWarnHook())
100
249
  rules = FormatRules(backlog_first=not parsed.releases_first)
101
- write_backlog_releases(data, parsed.output, config, rules)
250
+ write_backlog_releases(data, parsed.output, out_config, rules,
251
+ levels=io_levels(config),
252
+ file_exists_callback=overwrite_callback(
253
+ parsed.force))
102
254
 
103
255
 
104
256
  def run_write(parsed: argparse.Namespace,
105
- data_source: Callable[[], BacklogReleases]) -> int:
257
+ data_source: Callable[[Optional[BacklogOpsConfig]],
258
+ BacklogReleases]) -> int:
106
259
  """Build the data, write it to the output file, and report the result.
107
260
 
261
+ The configuration is resolved once from ``-c`` or by discovery, falling
262
+ back to the built-in defaults when none is found.
263
+
108
264
  Args:
109
265
  parsed: Parsed command line arguments holding the output options
110
- added by :func:`add_output_args`.
111
- data_source: Callable that returns the backlog and releases to
112
- write. It is called inside the error handling so that reading
113
- failures are reported like writing failures.
266
+ added by :func:`add_output_args` and the ``-c`` option added by
267
+ :func:`add_config_arg`.
268
+ data_source: Callable that receives the resolved configuration (or
269
+ None) and returns the backlog and releases to write. It is
270
+ called inside the error handling so that reading failures are
271
+ reported like writing failures.
114
272
 
115
273
  Returns:
116
274
  ``0`` on success, ``1`` when the data cannot be built or written.
117
275
  """
118
276
  try:
119
- _write_output(parsed, data_source())
277
+ config = optional_config(parsed)
278
+ _write_output(parsed, config, data_source(config))
120
279
  except (ValueError, TypeError, KeyError, OSError) as error:
121
280
  print(f'Could not write {parsed.output}: {error}', file=sys.stderr)
122
281
  return 1
@@ -146,55 +305,72 @@ def add_changes_arg(parser: argparse.ArgumentParser) -> None:
146
305
 
147
306
 
148
307
  def build_change_parser(description: str) -> argparse.ArgumentParser:
149
- """Build a parser with input, buffer, output and changes arguments."""
150
- parser = argparse.ArgumentParser(description=description)
151
- add_input_args(parser)
308
+ """Build a parser with input, config, output, buffer and changes."""
309
+ parser = build_io_parser(description)
152
310
  add_buffer_arg(parser)
153
- add_output_args(parser)
154
311
  add_changes_arg(parser)
155
312
  return parser
156
313
 
157
314
 
158
- def date_report(changes: ReleaseDateChanges
315
+ def date_report(changes: ReleaseDateChanges, file_exists_cb: FileExistsCb
159
316
  ) -> tuple[str, Optional[Callable[[str], None]]]:
160
- """Return the date change listing and a writer, None when empty."""
161
- writer = None if not changes else \
162
- (lambda path: write_date_changes(changes, path))
163
- return format_date_changes(changes), writer
317
+ """Return the date change listing and a writer, None when empty.
318
+
319
+ The writer overwrites an existing changes file as decided by
320
+ ``file_exists_cb``.
321
+ """
322
+ def write(path: str) -> None:
323
+ """Write the date changes, overwriting as the callback decides."""
324
+ write_date_changes(changes, path, file_exists_callback=file_exists_cb)
325
+ return format_date_changes(changes), (write if changes else None)
164
326
 
165
327
 
166
- def content_report(changes: ReleaseChanges
328
+ def content_report(changes: ReleaseChanges, file_exists_cb: FileExistsCb
167
329
  ) -> tuple[str, Optional[Callable[[str], None]]]:
168
- """Return the content change listing and a writer, None when empty."""
169
- writer = None if not changes else \
170
- (lambda path: write_content_changes(changes, path))
171
- return format_content_changes(changes), writer
172
-
330
+ """Return the content change listing and a writer, None when empty.
173
331
 
174
- def run_change_command(parsed: argparse.Namespace,
175
- produce: Callable[[BacklogReleases], tuple[
176
- str, Optional[Callable[[str], None]]]]) -> int:
332
+ The writer overwrites an existing changes file as decided by
333
+ ``file_exists_cb``.
334
+ """
335
+ def write(path: str) -> None:
336
+ """Write the content changes, overwriting as the callback decides."""
337
+ write_content_changes(changes, path,
338
+ file_exists_callback=file_exists_cb)
339
+ return format_content_changes(changes), (write if changes else None)
340
+
341
+
342
+ def run_change_command(
343
+ parsed: argparse.Namespace,
344
+ produce: Callable[[Optional[BacklogOpsConfig], BacklogReleases],
345
+ tuple[str, Optional[Callable[[str], None]]]],
346
+ require_config: bool = False) -> int:
177
347
  """Read, change, write the data, and emit the list of changes.
178
348
 
179
- The input is read and validated, ``produce`` changes it in place and
180
- returns the change listing as text together with a callback that
181
- writes the same changes to a file. The changed data is written to the
182
- output file, the listing is printed to stdout, and, when
183
- ``--changes-file`` is given, the changes are also written to that file.
349
+ The configuration is resolved once, the input is read and validated,
350
+ ``produce`` changes it in place and returns the change listing as text
351
+ together with a callback that writes the same changes to a file. The
352
+ changed data is written to the output file, the listing is printed to
353
+ stdout, and, when ``--changes-file`` is given, the changes are also
354
+ written to that file.
184
355
 
185
356
  Args:
186
- parsed: Parsed command line arguments holding the input, output
187
- and ``--changes-file`` options.
188
- produce: Callable that changes the data and returns the change
357
+ parsed: Parsed command line arguments holding the input, output,
358
+ ``-c`` and ``--changes-file`` options.
359
+ produce: Callable that receives the resolved configuration (or
360
+ None) and the data, changes the data, and returns the change
189
361
  listing text and a writer for the change file.
362
+ require_config: When True a missing configuration is reported as an
363
+ error instead of falling back to the built-in defaults.
190
364
 
191
365
  Returns:
192
366
  ``0`` on success, ``1`` when any step fails.
193
367
  """
194
368
  try:
195
- data = read_input(parsed)
196
- listing, write_changes = produce(data)
197
- _write_output(parsed, data)
369
+ config = (required_config(parsed) if require_config
370
+ else optional_config(parsed))
371
+ data = read_input(parsed, config)
372
+ listing, write_changes = produce(config, data)
373
+ _write_output(parsed, config, data)
198
374
  print(f'Wrote {parsed.output}')
199
375
  print(listing)
200
376
  _save_changes(parsed, write_changes)
@@ -0,0 +1,56 @@
1
+ #! /usr/local/bin/python3
2
+ """Backward-compatibility warning hooks for the command line interface.
3
+
4
+ When a command reads a file that needed backward-compatible normalization
5
+ (Reading an Old Configuration File), one of these hooks prints the
6
+ standard migration warning followed by command-specific instructions.
7
+ ``CliMigrateWarnHook`` is used when the old file is the backlog-ops
8
+ configuration file and shows the ``migrate_cfg`` command for the default
9
+ config kind. ``CliPresetMigrateWarnHook`` is used when the old file is a
10
+ stand-alone input or output preset file and shows the ``migrate_cfg``
11
+ command with the ``--kind`` option, because that option selects how a
12
+ preset file is migrated. The leading underscore in the module name keeps
13
+ it out of the command listing.
14
+ """
15
+ # PYTHON_ARGCOMPLETE_OK
16
+
17
+ # Copyright (c) 2026, Tom Björkholm
18
+ # MIT License
19
+
20
+ from config_as_json import MigrateCfgWarnHook
21
+
22
+
23
+ class CliMigrateWarnHook(MigrateCfgWarnHook):
24
+ """Tell the user to migrate an old config file with ``migrate_cfg``."""
25
+
26
+ @classmethod
27
+ def migrate_instructions(cls) -> str:
28
+ """Return the command line migration instructions.
29
+
30
+ Returns:
31
+ Text that points the user at the ``migrate_cfg`` command to
32
+ rewrite the configuration file in the current format.
33
+ """
34
+ txt = 'Run the migrate_cfg command to write the configuration file '
35
+ txt += 'in the\ncurrent format, for example:\n'
36
+ txt += ' python3 -m backlogops_cli.migrate_cfg -i OLD.cfg '
37
+ txt += '-o NEW.cfg\n\n'
38
+ return txt
39
+
40
+
41
+ class CliPresetMigrateWarnHook(MigrateCfgWarnHook):
42
+ """Tell the user to migrate an old preset file with ``migrate_cfg``."""
43
+
44
+ @classmethod
45
+ def migrate_instructions(cls) -> str:
46
+ """Return the command line preset migration instructions.
47
+
48
+ Returns:
49
+ Text that points the user at the ``migrate_cfg`` command with
50
+ the ``--kind`` option for input or output preset files.
51
+ """
52
+ txt = 'Run the migrate_cfg command with -k input or -k output to '
53
+ txt += 'write the\npreset file in the current format, for example:\n'
54
+ txt += ' python3 -m backlogops_cli.migrate_cfg -k input '
55
+ txt += '-i OLD.cfg -o NEW.cfg\n\n'
56
+ return txt
@@ -0,0 +1,92 @@
1
+ #! /usr/local/bin/python3
2
+ """Shared helpers for the configuration and preset wizard commands.
3
+
4
+ Both wizard commands write a JSON configuration file built interactively
5
+ through a ``WizardUiBridge``. They share the same command line shape (an
6
+ output file, a switch forcing the plain console interface, and a force
7
+ flag), the same overwrite check, and the same run-write-report flow. That
8
+ shared logic lives here; the leading underscore in the module name keeps
9
+ it out of the command listing.
10
+ """
11
+ # PYTHON_ARGCOMPLETE_OK
12
+
13
+ # Copyright (c) 2026, Tom Björkholm
14
+ # MIT License
15
+
16
+ import argparse
17
+ import sys
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+ from config_as_json import Config
21
+ from config_as_json.file_extension import fix_file_extension
22
+ from tableio_cfg_json import WizardUiBridge, WizardUiBridgeConsole, \
23
+ make_text_ui_bridge
24
+ from backlogops_cli._command_io import add_force_arg, overwrite_callback
25
+
26
+ CONFIG_EXTENSION = '.cfg'
27
+
28
+
29
+ def build_wizard_parser(description: str) -> argparse.ArgumentParser:
30
+ """Build a wizard parser with output, no-textual and force options."""
31
+ parser = argparse.ArgumentParser(description=description)
32
+ parser.add_argument('-o', '--output', dest='output', required=True,
33
+ help='Configuration file to write; the '
34
+ f'{CONFIG_EXTENSION} extension is added if missing.')
35
+ parser.add_argument('--no-textual', dest='no_textual', action='store_true',
36
+ help='Force the plain console interface instead of '
37
+ 'the Textual full-screen interface.')
38
+ add_force_arg(parser)
39
+ return parser
40
+
41
+
42
+ def _check_overwrite(output: str, force: bool) -> None:
43
+ """Ask before overwriting an existing configuration file.
44
+
45
+ The check runs before the wizard, so the user is not asked to confirm
46
+ an overwrite only after entering the whole configuration.
47
+ """
48
+ if Path(output).exists():
49
+ overwrite_callback(force)(output)
50
+
51
+
52
+ def _make_bridge(no_textual: bool) -> WizardUiBridge:
53
+ """Return the console bridge when forced, else the best text bridge.
54
+
55
+ Without ``--no-textual`` the factory returns a Textual full-screen
56
+ bridge in a real terminal and a console bridge otherwise, such as when
57
+ input is redirected or under tests.
58
+ """
59
+ if no_textual:
60
+ return WizardUiBridgeConsole(sys.stdout, sys.stdin, sys.stderr)
61
+ return make_text_ui_bridge(sys.stdout, sys.stdin, sys.stderr)
62
+
63
+
64
+ def run_wizard_to_file(parsed: argparse.Namespace,
65
+ wizard: Callable[[WizardUiBridge], Config],
66
+ label: str) -> int:
67
+ """Run a wizard, write its configuration to the output file, report.
68
+
69
+ The output filename receives the ``.cfg`` extension when it is not
70
+ already present.
71
+
72
+ Args:
73
+ parsed: Parsed arguments holding ``output``, ``force`` and
74
+ ``no_textual``.
75
+ wizard: Wizard called with the chosen UI bridge; it returns a
76
+ configuration object that knows how to write itself.
77
+ label: Human-readable name of what was written, for the message.
78
+
79
+ Returns:
80
+ ``0`` on success, ``1`` when the wizard is abandoned or the
81
+ configuration is rejected or cannot be written.
82
+ """
83
+ output = fix_file_extension(parsed.output, CONFIG_EXTENSION)
84
+ try:
85
+ _check_overwrite(output, parsed.force)
86
+ config = wizard(_make_bridge(parsed.no_textual))
87
+ config.write(to_json_filename=output, stderr_file=sys.stderr)
88
+ except (ValueError, TypeError, KeyError, EOFError, OSError) as error:
89
+ print(f'Could not create the configuration: {error}', file=sys.stderr)
90
+ return 1
91
+ print(f'{label} written to {output}')
92
+ return 0
@@ -20,7 +20,8 @@ from datetime import timedelta
20
20
  from typing import Callable, Optional
21
21
  from backlogops import BacklogReleases
22
22
  from backlogops_cli._command_io import (
23
- build_change_parser, content_report, parsed_args, run_change_command)
23
+ build_change_parser, content_report, overwrite_callback, parsed_args,
24
+ run_change_command)
24
25
 
25
26
  DESCRIPTION = 'Adjust release content to fit the planned release dates'
26
27
 
@@ -34,7 +35,7 @@ def _adjust(parsed: argparse.Namespace, data: BacklogReleases
34
35
  ) -> tuple[str, Optional[Callable[[str], None]]]:
35
36
  """Adjust the release content and return the change report."""
36
37
  changes = data.adjust_release_content(timedelta(days=parsed.buffer_days))
37
- return content_report(changes)
38
+ return content_report(changes, overwrite_callback(parsed.force))
38
39
 
39
40
 
40
41
  def main(args: Optional[list[str]] = None) -> int:
@@ -48,7 +49,7 @@ def main(args: Optional[list[str]] = None) -> int:
48
49
  written.
49
50
  """
50
51
  parsed = parsed_args(build_parser(), args)
51
- return run_change_command(parsed, lambda data: _adjust(parsed, data))
52
+ return run_change_command(parsed, lambda _, data: _adjust(parsed, data))
52
53
 
53
54
 
54
55
  if __name__ == '__main__': # pragma: no cover
@@ -0,0 +1,25 @@
1
+ #! /usr/local/bin/python3
2
+ """Version reporter for the backlogops_cli package."""
3
+
4
+ # Copyright (c) 2026 Tom Björkholm
5
+ # MIT License
6
+
7
+ from typing import override
8
+ from backlogops.blo_version_reporter import BloVersionReporter
9
+
10
+
11
+ class BloCliVersionReporter(BloVersionReporter):
12
+ """Version reporter for the backlogops_cli package."""
13
+
14
+ @override
15
+ def package_names(self) -> list[str]:
16
+ """Return the package names that this package reports."""
17
+ ret = ['backlogops-cli']
18
+ ret += super().package_names()
19
+ return ret
20
+
21
+ @override
22
+ @classmethod
23
+ def get_main_package_name(cls) -> str:
24
+ """Return the name of the main package."""
25
+ return 'backlogops-cli'
@@ -0,0 +1,42 @@
1
+ #! /usr/local/bin/python3
2
+ """Run the backlog-ops configuration wizard and store the result."""
3
+
4
+ # PYTHON_ARGCOMPLETE_OK
5
+ # Copyright (c) 2026, Tom Björkholm
6
+ # MIT License
7
+
8
+ import argparse
9
+ import sys
10
+ from typing import Optional
11
+ from backlogops import backlog_ops_wizard
12
+ from backlogops_cli._command_io import parsed_args
13
+ from backlogops_cli._wizard_io import build_wizard_parser, run_wizard_to_file
14
+
15
+ DESCRIPTION = 'Create a backlog-ops configuration file via a wizard'
16
+
17
+
18
+ def build_parser() -> argparse.ArgumentParser:
19
+ """Build the command line parser for the config wizard command."""
20
+ return build_wizard_parser(DESCRIPTION)
21
+
22
+
23
+ def main(args: Optional[list[str]] = None) -> int:
24
+ """Run the interactive wizard and write the backlog-ops configuration.
25
+
26
+ The output filename receives the ``.cfg`` extension when it is not
27
+ already present.
28
+
29
+ Args:
30
+ args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
31
+
32
+ Returns:
33
+ ``0`` on success, ``1`` when the entered configuration is rejected
34
+ or cannot be written.
35
+ """
36
+ parsed = parsed_args(build_parser(), args)
37
+ return run_wizard_to_file(parsed, backlog_ops_wizard,
38
+ 'Backlog-ops configuration')
39
+
40
+
41
+ if __name__ == '__main__': # pragma: no cover
42
+ sys.exit(main())