prizmkit 1.0.3 → 1.0.4

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.
@@ -9,14 +9,20 @@
9
9
  * npx prizmkit uninstall . --dry-run
10
10
  */
11
11
 
12
+ import { readFileSync } from 'fs';
13
+ import { dirname, join } from 'path';
14
+ import { fileURLToPath } from 'url';
12
15
  import { program } from 'commander';
13
16
  import { runScaffold } from '../src/index.js';
14
17
  import { runClean } from '../src/clean.js';
15
18
 
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
21
+
16
22
  program
17
23
  .name('prizmkit')
18
24
  .description('PrizmKit project installer/uninstaller')
19
- .version('1.0.0');
25
+ .version(pkg.version);
20
26
 
21
27
  program
22
28
  .command('install [directory]')
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.0.0",
3
- "bundledAt": "2026-03-10T01:40:43.750Z",
4
- "bundledFrom": "/Users/loneyu/SelfProjects/PrizmKitEvolvingProgramming"
2
+ "frameworkVersion": "1.0.4",
3
+ "bundledAt": "2026-03-11T11:34:52.898Z",
4
+ "bundledFrom": "85fad5e"
5
5
  }
@@ -11,20 +11,11 @@
11
11
  */
12
12
 
13
13
  import { parseFrontmatter, buildMarkdown } from '../shared/frontmatter.js';
14
+ import { TOOL_MAPPING } from '../shared/constants.js';
14
15
  import { mkdirSync } from 'node:fs';
15
16
  import { readFile, writeFile } from 'node:fs/promises';
16
17
  import path from 'path';
17
18
 
18
- // Tool name mapping from CodeBuddy to Claude Code
19
- // Claude Code 原生 Agent Teams 支持 SendMessage 工具进行 teammate 间通信
20
- const TOOL_MAPPING = {
21
- 'TaskCreate': 'Task',
22
- 'TaskGet': 'Task',
23
- 'TaskUpdate': 'Task',
24
- 'TaskList': 'Task',
25
- 'SendMessage': 'SendMessage',
26
- };
27
-
28
19
  // Default model for Claude Code agents
29
20
  const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
30
21
 
@@ -25,8 +25,12 @@ export function convertSkillToCommand(skillContent, skillName) {
25
25
  const { frontmatter, body } = parseFrontmatter(skillContent);
26
26
 
27
27
  // Claude Code command frontmatter only uses description
28
+ // Also convert prizmkit.xxx references in the description field
29
+ let desc = frontmatter.description || `PrizmKit ${skillName} command`;
30
+ desc = desc.replace(/prizmkit\.(\w+)/g, (_m, sub) => `/prizmkit-${sub.replace(/_/g, '-')}`);
31
+
28
32
  const claudeFrontmatter = {
29
- description: frontmatter.description || `PrizmKit ${skillName} command`,
33
+ description: desc,
30
34
  };
31
35
 
32
36
  // Rewrite ${SKILL_DIR} references to use relative path from .claude/commands/<name>/
@@ -20,8 +20,7 @@ export const GLOBAL_AGENTS_DIR = '.claude/agents';
20
20
  // Command file conventions (equivalent to skills)
21
21
  // Note: Actual command files are named <skill-name>.md (e.g. prizmkit-specify.md)
22
22
  // Directory-based commands with assets also use <skill-name>.md inside the directory.
23
- export const COMMAND_DEFINITION_FILE = '<skill-name>.md';
24
- export const COMMAND_DIR_VAR = '${COMMAND_DIR}'; // May not be natively supported
23
+ // Claude Code does not have a native ${COMMAND_DIR} variable equivalent.
25
24
 
26
25
  // Agent definition format
27
26
  export const AGENT_FILE_EXT = '.md';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared constants for PrizmKit adapters.
3
+ */
4
+
5
+ // Tool name mapping from CodeBuddy to Claude Code
6
+ // Claude Code 原生 Agent Teams 支持 SendMessage 工具进行 teammate 间通信
7
+ export const TOOL_MAPPING = {
8
+ 'TaskCreate': 'Task',
9
+ 'TaskGet': 'Task',
10
+ 'TaskUpdate': 'Task',
11
+ 'TaskList': 'Task',
12
+ 'SendMessage': 'SendMessage',
13
+ };
@@ -5,6 +5,9 @@
5
5
 
6
6
  /**
7
7
  * Parse YAML frontmatter from a markdown file content string.
8
+ * LIMITATION: Only supports flat key: value pairs. Does not support
9
+ * YAML arrays, nested objects, or multi-line values. For complex YAML
10
+ * needs, consider using a full YAML parser library.
8
11
  * @param {string} content - Full file content with optional frontmatter
9
12
  * @returns {{ frontmatter: Object, body: string }}
10
13
  */
@@ -191,18 +191,18 @@ cmd_start() {
191
191
 
192
192
  # Write start metadata
193
193
  python3 -c "
194
- import json
195
- from datetime import datetime
194
+ import json, sys, os
195
+ pid, started_at, feature_list, env_overrides, log_file, state_dir = int(sys.argv[1]), sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6]
196
196
  data = {
197
- 'pid': $pipeline_pid,
198
- 'started_at': '$start_time',
199
- 'feature_list': '$feature_list',
200
- 'env_overrides': '$env_overrides',
201
- 'log_file': '$LOG_FILE'
197
+ 'pid': pid,
198
+ 'started_at': started_at,
199
+ 'feature_list': feature_list,
200
+ 'env_overrides': env_overrides,
201
+ 'log_file': log_file
202
202
  }
203
- with open('$STATE_DIR/.pipeline-meta.json', 'w') as f:
203
+ with open(os.path.join(state_dir, '.pipeline-meta.json'), 'w') as f:
204
204
  json.dump(data, f, indent=2)
205
- " 2>/dev/null || true
205
+ " "$pipeline_pid" "$start_time" "$feature_list" "$env_overrides" "$LOG_FILE" "$STATE_DIR" 2>/dev/null || true
206
206
 
207
207
  # Wait briefly and verify
208
208
  sleep 2
@@ -304,10 +304,10 @@ cmd_status() {
304
304
  if [[ -f "$STATE_DIR/.pipeline-meta.json" ]]; then
305
305
  local last_feature_list
306
306
  last_feature_list=$(python3 -c "
307
- import json
308
- with open('$STATE_DIR/.pipeline-meta.json') as f:
307
+ import json, sys
308
+ with open(sys.argv[1]) as f:
309
309
  print(json.load(f).get('feature_list', ''))
310
- " 2>/dev/null || echo "")
310
+ " "$STATE_DIR/.pipeline-meta.json" 2>/dev/null || echo "")
311
311
 
312
312
  if [[ -n "$last_feature_list" && -f "$last_feature_list" ]]; then
313
313
  echo "" >&2
@@ -339,15 +339,15 @@ with open('$STATE_DIR/.pipeline-meta.json') as f:
339
339
  local feature_list_path=""
340
340
  if [[ -f "$STATE_DIR/.pipeline-meta.json" ]]; then
341
341
  started_at=$(python3 -c "
342
- import json
343
- with open('$STATE_DIR/.pipeline-meta.json') as f:
342
+ import json, sys
343
+ with open(sys.argv[1]) as f:
344
344
  print(json.load(f).get('started_at', ''))
345
- " 2>/dev/null || echo "")
345
+ " "$STATE_DIR/.pipeline-meta.json" 2>/dev/null || echo "")
346
346
  feature_list_path=$(python3 -c "
347
- import json
348
- with open('$STATE_DIR/.pipeline-meta.json') as f:
347
+ import json, sys
348
+ with open(sys.argv[1]) as f:
349
349
  print(json.load(f).get('feature_list', ''))
350
- " 2>/dev/null || echo "")
350
+ " "$STATE_DIR/.pipeline-meta.json" 2>/dev/null || echo "")
351
351
  fi
352
352
 
353
353
  log_success "Pipeline is running (PID: $pid)"
@@ -381,20 +381,19 @@ with open('$STATE_DIR/.pipeline-meta.json') as f:
381
381
  if [[ -n "$feature_list_path" && -f "$feature_list_path" ]]; then
382
382
  progress_json=$(python3 -c "
383
383
  import json, sys, os
384
- sys.path.insert(0, '$SCRIPT_DIR/scripts')
385
- from datetime import datetime
386
384
 
387
385
  def load_json(p):
388
386
  with open(p, 'r') as f:
389
387
  return json.load(f)
390
388
 
391
- fl = load_json('$feature_list_path')
389
+ feature_list_path, state_dir = sys.argv[1], sys.argv[2]
390
+ fl = load_json(feature_list_path)
392
391
  features = fl.get('features', [])
393
392
  total = len(features)
394
393
  counts = {'completed': 0, 'in_progress': 0, 'failed': 0, 'pending': 0, 'skipped': 0}
395
394
  for feat in features:
396
395
  fid = feat.get('id', '')
397
- sp = os.path.join('$STATE_DIR', 'features', fid, 'status.json')
396
+ sp = os.path.join(state_dir, 'features', fid, 'status.json')
398
397
  if os.path.isfile(sp):
399
398
  fs = load_json(sp)
400
399
  st = fs.get('status', 'pending')
@@ -414,7 +413,7 @@ print(json.dumps({
414
413
  'pending': counts['pending'],
415
414
  'percent': pct
416
415
  }))
417
- " 2>/dev/null || echo "")
416
+ " "$feature_list_path" "$STATE_DIR" 2>/dev/null || echo "")
418
417
  fi
419
418
 
420
419
  if [[ -n "$progress_json" ]]; then
@@ -402,14 +402,15 @@ run_one() {
402
402
  local feature_title
403
403
  feature_title=$(python3 -c "
404
404
  import json, sys
405
- with open('$feature_list') as f:
405
+ feature_list_path, fid = sys.argv[1], sys.argv[2]
406
+ with open(feature_list_path) as f:
406
407
  data = json.load(f)
407
408
  for feat in data.get('features', []):
408
- if feat.get('id') == '$feature_id':
409
+ if feat.get('id') == fid:
409
410
  print(feat.get('title', ''))
410
411
  sys.exit(0)
411
412
  sys.exit(1)
412
- " 2>/dev/null) || {
413
+ " "$feature_list" "$feature_id" 2>/dev/null) || {
413
414
  log_error "Feature $feature_id not found in $feature_list"
414
415
  exit 1
415
416
  }
@@ -424,19 +425,20 @@ sys.exit(1)
424
425
  local feature_slug
425
426
  feature_slug=$(python3 -c "
426
427
  import json, re, sys
427
- with open('$feature_list') as f:
428
+ feature_list_path, fid = sys.argv[1], sys.argv[2]
429
+ with open(feature_list_path) as f:
428
430
  data = json.load(f)
429
431
  for feat in data.get('features', []):
430
- if feat.get('id') == '$feature_id':
431
- fid = feat['id'].replace('F-', '').replace('f-', '').zfill(3)
432
+ if feat.get('id') == fid:
433
+ fnum = feat['id'].replace('F-', '').replace('f-', '').zfill(3)
432
434
  title = feat.get('title', '').lower()
433
435
  title = re.sub(r'[^a-z0-9\s-]', '', title)
434
436
  title = re.sub(r'[\s]+', '-', title.strip())
435
437
  title = re.sub(r'-+', '-', title).strip('-')
436
- print(f'{fid}-{title}')
438
+ print(f'{fnum}-{title}')
437
439
  sys.exit(0)
438
440
  sys.exit(1)
439
- " 2>/dev/null) || {
441
+ " "$feature_list" "$feature_id" 2>/dev/null) || {
440
442
  log_warn "Could not determine feature slug for cleanup"
441
443
  feature_slug=""
442
444
  }
@@ -741,16 +743,17 @@ for f in data.get('stuck_features', []):
741
743
 
742
744
  # Update current session tracking
743
745
  python3 -c "
744
- import json, sys
746
+ import json, sys, os
745
747
  from datetime import datetime
748
+ feature_id, session_id, state_dir = sys.argv[1], sys.argv[2], sys.argv[3]
746
749
  data = {
747
- 'feature_id': '$feature_id',
748
- 'session_id': '$session_id',
750
+ 'feature_id': feature_id,
751
+ 'session_id': session_id,
749
752
  'started_at': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
750
753
  }
751
- with open('$STATE_DIR/current-session.json', 'w') as f:
754
+ with open(os.path.join(state_dir, 'current-session.json'), 'w') as f:
752
755
  json.dump(data, f, indent=2)
753
- "
756
+ " "$feature_id" "$session_id" "$STATE_DIR"
754
757
 
755
758
  # Spawn session and wait
756
759
  log_info "Spawning AI CLI session: $session_id"
@@ -19,6 +19,8 @@ import os
19
19
  import re
20
20
  import sys
21
21
 
22
+ from utils import load_json_file
23
+
22
24
 
23
25
  DEFAULT_MAX_RETRIES = 3
24
26
 
@@ -87,21 +89,6 @@ def parse_args():
87
89
  return parser.parse_args()
88
90
 
89
91
 
90
- def load_json_file(path):
91
- """Load and return parsed JSON from a file."""
92
- abs_path = os.path.abspath(path)
93
- if not os.path.isfile(abs_path):
94
- return None, "File not found: {}".format(abs_path)
95
- try:
96
- with open(abs_path, "r", encoding="utf-8") as f:
97
- data = json.load(f)
98
- except json.JSONDecodeError as e:
99
- return None, "Invalid JSON: {}".format(str(e))
100
- except IOError as e:
101
- return None, "Cannot read file: {}".format(str(e))
102
- return data, None
103
-
104
-
105
92
  def read_text_file(path):
106
93
  """Read and return the text content of a file."""
107
94
  abs_path = os.path.abspath(path)
@@ -19,6 +19,8 @@ import os
19
19
  import re
20
20
  import sys
21
21
 
22
+ from utils import load_json_file
23
+
22
24
 
23
25
  DEFAULT_MAX_RETRIES = 3
24
26
 
@@ -42,21 +44,6 @@ def parse_args():
42
44
  return parser.parse_args()
43
45
 
44
46
 
45
- def load_json_file(path):
46
- """Load and return parsed JSON from a file."""
47
- abs_path = os.path.abspath(path)
48
- if not os.path.isfile(abs_path):
49
- return None, "File not found: {}".format(abs_path)
50
- try:
51
- with open(abs_path, "r", encoding="utf-8") as f:
52
- data = json.load(f)
53
- except json.JSONDecodeError as e:
54
- return None, "Invalid JSON: {}".format(str(e))
55
- except IOError as e:
56
- return None, "Cannot read file: {}".format(str(e))
57
- return data, None
58
-
59
-
60
47
  def read_text_file(path):
61
48
  """Read and return the text content of a file."""
62
49
  abs_path = os.path.abspath(path)
@@ -238,9 +225,8 @@ def build_replacements(args, bug, global_context, script_dir):
238
225
  else:
239
226
  fix_scope = bug.get("title", "unknown").split()[0].lower() if bug.get("title") else "unknown"
240
227
 
241
- # Determine if manual/hybrid verification
228
+ # Determine verification type
242
229
  vtype = bug.get("verification_type", "automated")
243
- is_manual_or_hybrid = vtype in ("manual", "hybrid")
244
230
 
245
231
  replacements = {
246
232
  "{{RUN_ID}}": args.run_id,
@@ -51,21 +51,21 @@ def create_directories(project_root, feature_slug=None):
51
51
  dirs_to_create.extend([
52
52
  ".prizmkit/specs",
53
53
  ])
54
-
54
+
55
55
  created = []
56
56
  for dir_path in dirs_to_create:
57
57
  full_path = os.path.join(project_root, dir_path)
58
58
  if not os.path.exists(full_path):
59
59
  os.makedirs(full_path, exist_ok=True)
60
60
  created.append(dir_path)
61
-
61
+
62
62
  return created
63
63
 
64
64
 
65
65
  def init_prizmkit_config(project_root, feature_id):
66
66
  """Initialize or update .prizmkit/config.json."""
67
67
  config_path = os.path.join(project_root, ".prizmkit", "config.json")
68
-
68
+
69
69
  if os.path.exists(config_path):
70
70
  # Update existing config
71
71
  with open(config_path, "r", encoding="utf-8") as f:
@@ -84,7 +84,7 @@ def init_prizmkit_config(project_root, feature_id):
84
84
  project_name = pkg.get("name", project_name)
85
85
  except (json.JSONDecodeError, IOError):
86
86
  pass
87
-
87
+
88
88
  config = {
89
89
  "adoption_mode": "active",
90
90
  "speckit_hooks_enabled": True,
@@ -93,17 +93,17 @@ def init_prizmkit_config(project_root, feature_id):
93
93
  "feature_prefix": "F-",
94
94
  "current_feature": feature_id,
95
95
  }
96
-
96
+
97
97
  with open(config_path, "w", encoding="utf-8") as f:
98
98
  json.dump(config, f, indent=2)
99
-
99
+
100
100
  return config_path
101
101
 
102
102
 
103
103
  def main():
104
104
  args = parse_args()
105
105
  project_root = os.path.abspath(args.project_root)
106
-
106
+
107
107
  if not os.path.isdir(project_root):
108
108
  result = {
109
109
  "success": False,
@@ -111,13 +111,13 @@ def main():
111
111
  }
112
112
  print(json.dumps(result, indent=2))
113
113
  sys.exit(1)
114
-
114
+
115
115
  # Create directories
116
116
  created_dirs = create_directories(project_root, args.feature_slug)
117
-
117
+
118
118
  # Initialize config
119
119
  config_path = init_prizmkit_config(project_root, args.feature_id)
120
-
120
+
121
121
  result = {
122
122
  "success": True,
123
123
  "project_root": project_root,
@@ -125,7 +125,7 @@ def main():
125
125
  "directories_created": created_dirs,
126
126
  "config_path": config_path,
127
127
  }
128
-
128
+
129
129
  print(json.dumps(result, indent=2))
130
130
  sys.exit(0)
131
131
 
@@ -21,9 +21,16 @@ import argparse
21
21
  import json
22
22
  import os
23
23
  import shutil
24
- import sys
25
24
  from datetime import datetime, timezone
26
25
 
26
+ from utils import (
27
+ load_json_file,
28
+ write_json_file,
29
+ error_out,
30
+ pad_right,
31
+ _build_progress_bar,
32
+ )
33
+
27
34
 
28
35
  SESSION_STATUS_VALUES = [
29
36
  "success",
@@ -71,37 +78,6 @@ def now_iso():
71
78
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
72
79
 
73
80
 
74
- def load_json_file(path):
75
- abs_path = os.path.abspath(path)
76
- if not os.path.isfile(abs_path):
77
- return None, "File not found: {}".format(abs_path)
78
- try:
79
- with open(abs_path, "r", encoding="utf-8") as f:
80
- data = json.load(f)
81
- except json.JSONDecodeError as e:
82
- return None, "Invalid JSON: {}".format(str(e))
83
- except IOError as e:
84
- return None, "Cannot read file: {}".format(str(e))
85
- return data, None
86
-
87
-
88
- def write_json_file(path, data):
89
- abs_path = os.path.abspath(path)
90
- parent = os.path.dirname(abs_path)
91
- if parent and not os.path.isdir(parent):
92
- try:
93
- os.makedirs(parent, exist_ok=True)
94
- except OSError as e:
95
- return "Cannot create directory: {}".format(str(e))
96
- try:
97
- with open(abs_path, "w", encoding="utf-8") as f:
98
- json.dump(data, f, indent=2, ensure_ascii=False)
99
- f.write("\n")
100
- except IOError as e:
101
- return "Cannot write file: {}".format(str(e))
102
- return None
103
-
104
-
105
81
  def load_bug_status(state_dir, bug_id):
106
82
  status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
107
83
  if not os.path.isfile(status_path):
@@ -403,31 +379,6 @@ COLOR_RESET = "\033[0m"
403
379
  BOX_WIDTH = 68
404
380
 
405
381
 
406
- def pad_right(text, width):
407
- visible = text
408
- i = 0
409
- visible_len = 0
410
- while i < len(text):
411
- if text[i] == "\033":
412
- while i < len(text) and text[i] != "m":
413
- i += 1
414
- i += 1
415
- else:
416
- visible_len += 1
417
- i += 1
418
- padding = width - visible_len
419
- if padding > 0:
420
- return text + " " * padding
421
- return text
422
-
423
-
424
- def _build_progress_bar(percent, width=20):
425
- filled = int(width * percent / 100)
426
- empty = width - filled
427
- bar = "█" * filled + "░" * empty
428
- return "{} {:>3}%".format(bar, int(percent))
429
-
430
-
431
382
  SEVERITY_ICONS = {
432
383
  "critical": COLOR_RED + "🔴" + COLOR_RESET,
433
384
  "high": COLOR_MAGENTA + "🟠" + COLOR_RESET,
@@ -697,16 +648,6 @@ def action_pause(state_dir):
697
648
  print(json.dumps(result, indent=2, ensure_ascii=False))
698
649
 
699
650
 
700
- # ---------------------------------------------------------------------------
701
- # Helpers
702
- # ---------------------------------------------------------------------------
703
-
704
- def error_out(message):
705
- output = {"error": message}
706
- print(json.dumps(output, indent=2, ensure_ascii=False))
707
- sys.exit(1)
708
-
709
-
710
651
  # ---------------------------------------------------------------------------
711
652
  # Main
712
653
  # ---------------------------------------------------------------------------
@@ -23,9 +23,16 @@ import json
23
23
  import os
24
24
  import re
25
25
  import shutil
26
- import sys
27
26
  from datetime import datetime, timezone
28
27
 
28
+ from utils import (
29
+ load_json_file,
30
+ write_json_file,
31
+ error_out,
32
+ pad_right,
33
+ _build_progress_bar,
34
+ )
35
+
29
36
 
30
37
  SESSION_STATUS_VALUES = [
31
38
  "success",
@@ -99,45 +106,6 @@ def now_iso():
99
106
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
100
107
 
101
108
 
102
- def load_json_file(path):
103
- """Load and return parsed JSON from a file.
104
-
105
- Returns (data, error_string). On success error_string is None.
106
- """
107
- abs_path = os.path.abspath(path)
108
- if not os.path.isfile(abs_path):
109
- return None, "File not found: {}".format(abs_path)
110
- try:
111
- with open(abs_path, "r", encoding="utf-8") as f:
112
- data = json.load(f)
113
- except json.JSONDecodeError as e:
114
- return None, "Invalid JSON: {}".format(str(e))
115
- except IOError as e:
116
- return None, "Cannot read file: {}".format(str(e))
117
- return data, None
118
-
119
-
120
- def write_json_file(path, data):
121
- """Write data as JSON to a file. Creates parent directories if needed.
122
-
123
- Returns an error string on failure, None on success.
124
- """
125
- abs_path = os.path.abspath(path)
126
- parent = os.path.dirname(abs_path)
127
- if parent and not os.path.isdir(parent):
128
- try:
129
- os.makedirs(parent, exist_ok=True)
130
- except OSError as e:
131
- return "Cannot create directory: {}".format(str(e))
132
- try:
133
- with open(abs_path, "w", encoding="utf-8") as f:
134
- json.dump(data, f, indent=2, ensure_ascii=False)
135
- f.write("\n")
136
- except IOError as e:
137
- return "Cannot write file: {}".format(str(e))
138
- return None
139
-
140
-
141
109
  def load_feature_status(state_dir, feature_id):
142
110
  """Load the status.json for a feature.
143
111
 
@@ -509,27 +477,6 @@ COLOR_RESET = "\033[0m"
509
477
  BOX_WIDTH = 68
510
478
 
511
479
 
512
- def pad_right(text, width):
513
- """Pad text with spaces to fill width, accounting for ANSI escape codes."""
514
- # Strip ANSI codes to calculate visible length
515
- visible = text
516
- i = 0
517
- visible_len = 0
518
- while i < len(text):
519
- if text[i] == "\033":
520
- # Skip until 'm'
521
- while i < len(text) and text[i] != "m":
522
- i += 1
523
- i += 1 # skip the 'm'
524
- else:
525
- visible_len += 1
526
- i += 1
527
- padding = width - visible_len
528
- if padding > 0:
529
- return text + " " * padding
530
- return text
531
-
532
-
533
480
  def _calc_feature_duration(state_dir, feature_id):
534
481
  """计算已完成 Feature 的耗时(秒)。
535
482
 
@@ -579,17 +526,6 @@ def _format_duration(seconds):
579
526
  return "{}h{}m".format(h, m)
580
527
 
581
528
 
582
- def _build_progress_bar(percent, width=20):
583
- """生成文本进度条。
584
-
585
- 例如: ████████░░░░░░░░░░░░ 40%
586
- """
587
- filled = int(width * percent / 100)
588
- empty = width - filled
589
- bar = "█" * filled + "░" * empty
590
- return "{} {:>3}%".format(bar, int(percent))
591
-
592
-
593
529
  def _estimate_remaining_time(features, state_dir, counts):
594
530
  """基于已完成 Feature 的历史耗时,按 complexity 加权预估剩余时间。
595
531
 
@@ -1015,17 +951,6 @@ def action_pause(state_dir):
1015
951
  print(json.dumps(result, indent=2, ensure_ascii=False))
1016
952
 
1017
953
 
1018
- # ---------------------------------------------------------------------------
1019
- # Helpers
1020
- # ---------------------------------------------------------------------------
1021
-
1022
- def error_out(message):
1023
- """Print an error JSON and exit with code 1."""
1024
- output = {"error": message}
1025
- print(json.dumps(output, indent=2, ensure_ascii=False))
1026
- sys.exit(1)
1027
-
1028
-
1029
954
  # ---------------------------------------------------------------------------
1030
955
  # Main
1031
956
  # ---------------------------------------------------------------------------