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.
- package/bin/create-prizmkit.js +7 -1
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +1 -10
- package/bundled/adapters/claude/command-adapter.js +5 -1
- package/bundled/adapters/claude/paths.js +1 -2
- package/bundled/adapters/shared/constants.js +13 -0
- package/bundled/adapters/shared/frontmatter.js +3 -0
- package/bundled/dev-pipeline/launch-daemon.sh +22 -23
- package/bundled/dev-pipeline/run.sh +16 -13
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +2 -15
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -17
- package/bundled/dev-pipeline/scripts/init-dev-team.py +11 -11
- package/bundled/dev-pipeline/scripts/update-bug-status.py +8 -67
- package/bundled/dev-pipeline/scripts/update-feature-status.py +8 -83
- package/bundled/dev-pipeline/scripts/utils.py +85 -0
- package/bundled/skills/_metadata.json +19 -3
- package/bundled/skills/feature-workflow/SKILL.md +317 -0
- package/bundled/skills/prizm-kit/SKILL.md +5 -3
- package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +1 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +21 -3
- package/bundled/skills/refactor-workflow/SKILL.md +340 -0
- package/bundled/templates/hooks/commit-intent-claude.json +16 -0
- package/bundled/templates/hooks/commit-intent-codebuddy.json +16 -0
- package/package.json +2 -3
- package/src/detect-platform.js +10 -2
- package/src/scaffold.js +38 -70
package/bin/create-prizmkit.js
CHANGED
|
@@ -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(
|
|
25
|
+
.version(pkg.version);
|
|
20
26
|
|
|
21
27
|
program
|
|
22
28
|
.command('install [directory]')
|
package/bundled/VERSION.json
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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':
|
|
198
|
-
'started_at':
|
|
199
|
-
'feature_list':
|
|
200
|
-
'env_overrides':
|
|
201
|
-
'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('
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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') ==
|
|
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
|
-
|
|
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') ==
|
|
431
|
-
|
|
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'{
|
|
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':
|
|
748
|
-
'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('
|
|
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
|
|
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
|
# ---------------------------------------------------------------------------
|