@entelligentsia/forgecli 0.8.4 → 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.
- package/CHANGELOG.md +53 -0
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +17 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/forge.js +7 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +16 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.js +5 -2
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/package.json +11 -3
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Shared JSON Schema validator for Forge.
|
|
4
|
-
//
|
|
5
|
-
// Minimal Draft-07 subset used by both store-cli.cjs (tool writes) and the
|
|
6
|
-
// write-boundary hook (direct agent writes). Keeps Forge dependency-free.
|
|
7
|
-
//
|
|
8
|
-
// Supported keywords:
|
|
9
|
-
// type (string|number|integer|boolean|array|object; union via array)
|
|
10
|
-
// required, properties, additionalProperties: false
|
|
11
|
-
// enum, minimum, maxLength, minLength, maxItems, items (type + maxLength)
|
|
12
|
-
// pattern (ECMA regex against string values)
|
|
13
|
-
// format: "date-time" (ISO 8601)
|
|
14
|
-
//
|
|
15
|
-
// Not supported (by design): $ref, allOf/anyOf/oneOf, propertyNames,
|
|
16
|
-
// dependencies, const. Schemas can express what Forge needs without them.
|
|
17
|
-
|
|
18
|
-
// Fields that may legitimately be null (nullable FKs / optional timing).
|
|
19
|
-
// Mirrors store-cli.cjs — keep in sync.
|
|
20
|
-
const NULLABLE_FIELDS = new Set([
|
|
21
|
-
'sprintId', 'taskId', 'endTimestamp', 'durationMinutes',
|
|
22
|
-
'feature_id', 'description', 'completedAt', 'resolvedAt'
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
function isDateTime(s) {
|
|
26
|
-
if (typeof s !== 'string') return false;
|
|
27
|
-
// Accept standard ISO 8601 / RFC 3339 date-time strings.
|
|
28
|
-
// Require: YYYY-MM-DDTHH:MM:SS(.sss)?(Z|±HH:MM)
|
|
29
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:?\d{2})$/.test(s);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function typeMatches(expected, val) {
|
|
33
|
-
if (expected === 'integer') return Number.isInteger(val);
|
|
34
|
-
if (expected === 'number') return typeof val === 'number' && !Number.isNaN(val);
|
|
35
|
-
if (expected === 'array') return Array.isArray(val);
|
|
36
|
-
if (expected === 'null') return val === null;
|
|
37
|
-
if (expected === 'object') return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
38
|
-
return typeof val === expected;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function validateRecord(record, schema, opts) {
|
|
42
|
-
opts = opts || {};
|
|
43
|
-
const errors = [];
|
|
44
|
-
|
|
45
|
-
if (record === null || typeof record !== 'object' || Array.isArray(record)) {
|
|
46
|
-
errors.push('record: expected object');
|
|
47
|
-
return errors;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const required = schema.required || [];
|
|
51
|
-
for (const field of required) {
|
|
52
|
-
const v = record[field];
|
|
53
|
-
if (v === undefined || v === '') {
|
|
54
|
-
errors.push(`${field}: missing required field`);
|
|
55
|
-
} else if (v === null && !NULLABLE_FIELDS.has(field)) {
|
|
56
|
-
errors.push(`${field}: missing required field`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const properties = schema.properties || {};
|
|
61
|
-
|
|
62
|
-
for (const [field, def] of Object.entries(properties)) {
|
|
63
|
-
const val = record[field];
|
|
64
|
-
if (val === undefined || val === null) continue;
|
|
65
|
-
|
|
66
|
-
if (def.type) {
|
|
67
|
-
const ok = Array.isArray(def.type)
|
|
68
|
-
? def.type.some(t => typeMatches(t, val))
|
|
69
|
-
: typeMatches(def.type, val);
|
|
70
|
-
if (!ok) {
|
|
71
|
-
errors.push(`${field}: expected ${def.type}, got ${Array.isArray(val) ? 'array' : typeof val}`);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (def.enum && !def.enum.includes(val)) {
|
|
77
|
-
errors.push(`${field}: value "${val}" not in [${def.enum.join(', ')}]`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (def.minimum !== undefined && typeof val === 'number' && val < def.minimum) {
|
|
81
|
-
errors.push(`${field}: value ${val} is below minimum ${def.minimum}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (typeof val === 'string') {
|
|
85
|
-
if (def.maxLength !== undefined && val.length > def.maxLength) {
|
|
86
|
-
errors.push(`${field}: value length ${val.length} exceeds maxLength ${def.maxLength}`);
|
|
87
|
-
}
|
|
88
|
-
if (def.minLength !== undefined && val.length < def.minLength) {
|
|
89
|
-
errors.push(`${field}: value length ${val.length} is below minLength ${def.minLength}`);
|
|
90
|
-
}
|
|
91
|
-
if (def.pattern) {
|
|
92
|
-
let re;
|
|
93
|
-
try { re = new RegExp(def.pattern); }
|
|
94
|
-
catch (_) { errors.push(`${field}: invalid schema pattern`); re = null; }
|
|
95
|
-
if (re && !re.test(val)) {
|
|
96
|
-
errors.push(`${field}: value "${val}" does not match pattern ${def.pattern}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (def.format === 'date-time' && !isDateTime(val)) {
|
|
100
|
-
errors.push(`${field}: value "${val}" is not a valid date-time`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (Array.isArray(val)) {
|
|
105
|
-
if (def.maxItems !== undefined && val.length > def.maxItems) {
|
|
106
|
-
errors.push(`${field}: array has ${val.length} items, exceeds maxItems ${def.maxItems}`);
|
|
107
|
-
}
|
|
108
|
-
if (def.items) {
|
|
109
|
-
val.forEach((item, idx) => {
|
|
110
|
-
if (def.items.type && !typeMatches(def.items.type, item)) {
|
|
111
|
-
errors.push(`${field}[${idx}]: expected ${def.items.type}, got ${typeof item}`);
|
|
112
|
-
}
|
|
113
|
-
if (def.items.maxLength !== undefined && typeof item === 'string' && item.length > def.items.maxLength) {
|
|
114
|
-
errors.push(`${field}[${idx}]: item length ${item.length} exceeds maxLength ${def.items.maxLength}`);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (schema.additionalProperties === false) {
|
|
122
|
-
const allowed = new Set(Object.keys(properties));
|
|
123
|
-
for (const key of Object.keys(record)) {
|
|
124
|
-
if (!allowed.has(key)) errors.push(`${key}: undeclared field`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Append a hint when validation fails so the LLM/user knows where to look
|
|
129
|
-
// for a canonical sample. The opts.entity hint is set by callers that know
|
|
130
|
-
// which entity they're validating (store-cli.cjs cmdWrite, cmdValidate).
|
|
131
|
-
if (errors.length > 0 && opts.entity) {
|
|
132
|
-
errors.push(
|
|
133
|
-
`(hint: run 'node store-cli.cjs template ${opts.entity}' for a canonical sample, ` +
|
|
134
|
-
`or 'node store-cli.cjs describe ${opts.entity}' for the raw JSON Schema)`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return errors;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
module.exports = { validateRecord, isDateTime, NULLABLE_FIELDS };
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Forge tool: manage-config
|
|
5
|
-
// Read and write .forge/config.json safely.
|
|
6
|
-
// Usage: manage-config get <key.path>
|
|
7
|
-
// manage-config list-pipelines
|
|
8
|
-
// manage-config pipeline add <name> --description <text> --phases <json>
|
|
9
|
-
// manage-config pipeline get <name>
|
|
10
|
-
// manage-config pipeline remove <name>
|
|
11
|
-
// manage-config resolve-forge-root
|
|
12
|
-
// manage-config set <key.path> <json-value>
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
const { findProjectRoot } = require('./lib/project-root.cjs');
|
|
18
|
-
|
|
19
|
-
const _projectRoot = findProjectRoot();
|
|
20
|
-
const CONFIG_PATH = _projectRoot
|
|
21
|
-
? path.join(_projectRoot, '.forge', 'config.json')
|
|
22
|
-
: path.join(process.cwd(), '.forge', 'config.json');
|
|
23
|
-
|
|
24
|
-
const VALID_ROLES = ['plan', 'review-plan', 'implement', 'review-code', 'validate', 'approve', 'commit'];
|
|
25
|
-
const VALID_NAME = /^[a-z0-9_-]+$/;
|
|
26
|
-
|
|
27
|
-
const ROLE_MODEL_DEFAULTS = {
|
|
28
|
-
'plan': 'sonnet',
|
|
29
|
-
'implement': 'sonnet',
|
|
30
|
-
'review-plan': 'opus',
|
|
31
|
-
'review-code': 'opus',
|
|
32
|
-
'validate': 'opus',
|
|
33
|
-
'approve': 'opus',
|
|
34
|
-
'commit': 'haiku'
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function readConfig() {
|
|
38
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
39
|
-
console.error('× .forge/config.json not found. Run /forge:init first.');
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
let raw;
|
|
43
|
-
try { raw = fs.readFileSync(CONFIG_PATH, 'utf8'); } catch (e) {
|
|
44
|
-
console.error(`× reading ${CONFIG_PATH}: ${e.message}`); process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
try { return { config: JSON.parse(raw), raw }; } catch (e) {
|
|
47
|
-
console.error(`× .forge/config.json is not valid JSON: ${e.message}`); process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function detectIndent(raw) {
|
|
52
|
-
const m = raw.match(/^([ \t]+)/m);
|
|
53
|
-
return m ? m[1] : ' ';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function writeConfig(config, indent) {
|
|
57
|
-
const json = JSON.stringify(config, null, indent) + '\n';
|
|
58
|
-
const tmp = CONFIG_PATH + '.tmp.' + process.pid;
|
|
59
|
-
try {
|
|
60
|
-
fs.writeFileSync(tmp, json, 'utf8');
|
|
61
|
-
fs.renameSync(tmp, CONFIG_PATH);
|
|
62
|
-
} catch (e) {
|
|
63
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
64
|
-
console.error(`× writing config: ${e.message}`); process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
69
|
-
|
|
70
|
-
function assertSafeKeys(dotPath) {
|
|
71
|
-
const keys = dotPath.split('.');
|
|
72
|
-
for (const key of keys) {
|
|
73
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
74
|
-
throw new Error(`Unsafe key path '${key}' in '${dotPath}' — prototype traversal blocked`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getByPath(obj, dotPath) {
|
|
80
|
-
assertSafeKeys(dotPath);
|
|
81
|
-
return dotPath.split('.').reduce((cur, key) => (cur != null && typeof cur === 'object' ? cur[key] : undefined), obj);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function setByPath(obj, dotPath, value) {
|
|
85
|
-
assertSafeKeys(dotPath);
|
|
86
|
-
const keys = dotPath.split('.');
|
|
87
|
-
let cur = obj;
|
|
88
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
89
|
-
if (cur[keys[i]] == null || typeof cur[keys[i]] !== 'object') cur[keys[i]] = {};
|
|
90
|
-
cur = cur[keys[i]];
|
|
91
|
-
}
|
|
92
|
-
cur[keys[keys.length - 1]] = value;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function validatePhases(phases) {
|
|
96
|
-
if (!Array.isArray(phases) || phases.length === 0) return 'At least one phase is required';
|
|
97
|
-
for (const [i, p] of phases.entries()) {
|
|
98
|
-
if (!p.command || typeof p.command !== 'string') return `Phase ${i + 1}: command must be a non-empty string`;
|
|
99
|
-
if (!VALID_ROLES.includes(p.role)) return `Phase ${i + 1}: role must be one of: ${VALID_ROLES.join(', ')}`;
|
|
100
|
-
if (p.maxIterations !== undefined && (!Number.isInteger(p.maxIterations) || p.maxIterations < 1))
|
|
101
|
-
return `Phase ${i + 1}: maxIterations must be a positive integer`;
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parseArgs(argv) {
|
|
107
|
-
const result = {};
|
|
108
|
-
for (let i = 0; i < argv.length; i++) {
|
|
109
|
-
if (argv[i].startsWith('--') && i + 1 < argv.length) {
|
|
110
|
-
result[argv[i].slice(2)] = argv[++i];
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = { getByPath, setByPath, assertSafeKeys, validatePhases, detectIndent, parseArgs, VALID_ROLES, VALID_NAME, ROLE_MODEL_DEFAULTS };
|
|
117
|
-
|
|
118
|
-
if (require.main === module) {
|
|
119
|
-
const [,, subcmd, ...args] = process.argv;
|
|
120
|
-
|
|
121
|
-
if (!subcmd) {
|
|
122
|
-
console.error([
|
|
123
|
-
'Usage: manage-config <subcommand> [options]',
|
|
124
|
-
'',
|
|
125
|
-
'Subcommands:',
|
|
126
|
-
' get <key.path> Print a config value',
|
|
127
|
-
' list-pipelines List all pipelines',
|
|
128
|
-
' pipeline add <name> --description <t> --phases <json>',
|
|
129
|
-
' pipeline get <name> Print a pipeline in full',
|
|
130
|
-
' pipeline remove <name>',
|
|
131
|
-
' pipeline backfill-models Backfill model fields from role defaults',
|
|
132
|
-
' resolve-forge-root Resolve Forge plugin root path',
|
|
133
|
-
' set <key.path> <json-value> Set an arbitrary value',
|
|
134
|
-
].join('\n'));
|
|
135
|
-
process.exit(2);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (subcmd === 'get') {
|
|
139
|
-
const keyPath = args[0];
|
|
140
|
-
if (!keyPath) { console.error('Usage: manage-config get <key.path>'); process.exit(2); }
|
|
141
|
-
const { config } = readConfig();
|
|
142
|
-
const value = getByPath(config, keyPath);
|
|
143
|
-
if (value === undefined) { console.error(`Key not found: ${keyPath}`); process.exit(1); }
|
|
144
|
-
console.log(value !== null && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
|
|
145
|
-
process.exit(0);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (subcmd === 'list-pipelines') {
|
|
149
|
-
const { config } = readConfig();
|
|
150
|
-
const pipelines = config.pipelines;
|
|
151
|
-
if (!pipelines || Object.keys(pipelines).length === 0) {
|
|
152
|
-
console.log('── No pipelines configured.');
|
|
153
|
-
process.exit(0);
|
|
154
|
-
}
|
|
155
|
-
console.log('| Name | Description | Phases |');
|
|
156
|
-
console.log('|------|-------------|--------|');
|
|
157
|
-
for (const [name, pl] of Object.entries(pipelines)) {
|
|
158
|
-
const desc = pl.description || '(none)';
|
|
159
|
-
const count = Array.isArray(pl.phases) ? pl.phases.length : 0;
|
|
160
|
-
console.log(`| ${name} | ${desc} | ${count} |`);
|
|
161
|
-
}
|
|
162
|
-
process.exit(0);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (subcmd === 'pipeline') {
|
|
166
|
-
const action = args[0];
|
|
167
|
-
|
|
168
|
-
if (action === 'get') {
|
|
169
|
-
const name = args[1];
|
|
170
|
-
if (!name) { console.error('Usage: manage-config pipeline get <name>'); process.exit(2); }
|
|
171
|
-
const { config } = readConfig();
|
|
172
|
-
if (!config.pipelines || !config.pipelines[name]) {
|
|
173
|
-
console.error(`× Pipeline '${name}' not found`); process.exit(1);
|
|
174
|
-
}
|
|
175
|
-
const pl = config.pipelines[name];
|
|
176
|
-
if (pl.description) console.log(`── ${pl.description}\n`);
|
|
177
|
-
console.log('| # | Role | Command | Workflow | Model | maxIter |');
|
|
178
|
-
console.log('|---|------|---------|----------|-------|---------|');
|
|
179
|
-
(pl.phases || []).forEach((p, i) => {
|
|
180
|
-
const wf = p.workflow || '(built-in)';
|
|
181
|
-
const model = p.model || '(default)';
|
|
182
|
-
const maxIter = p.maxIterations != null ? p.maxIterations : '—';
|
|
183
|
-
console.log(`| ${i + 1} | ${p.role} | \`${p.command}\` | \`${wf}\` | ${model} | ${maxIter} |`);
|
|
184
|
-
});
|
|
185
|
-
process.exit(0);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (action === 'add') {
|
|
189
|
-
const name = args[1];
|
|
190
|
-
if (!name) { console.error('Usage: manage-config pipeline add <name> --description <text> --phases <json>'); process.exit(2); }
|
|
191
|
-
if (!VALID_NAME.test(name)) { console.error(`× pipeline name must match [a-z0-9_-], got: ${name}`); process.exit(1); }
|
|
192
|
-
|
|
193
|
-
const flags = parseArgs(args.slice(2));
|
|
194
|
-
if (!flags.phases) { console.error('Error: --phases <json> is required'); process.exit(2); }
|
|
195
|
-
|
|
196
|
-
let phases;
|
|
197
|
-
try { phases = JSON.parse(flags.phases); } catch (e) {
|
|
198
|
-
console.error(`× --phases is not valid JSON: ${e.message}`); process.exit(2);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const err = validatePhases(phases);
|
|
202
|
-
if (err) { console.error(`× ${err}`); process.exit(1); }
|
|
203
|
-
|
|
204
|
-
const { config, raw } = readConfig();
|
|
205
|
-
if (!config.pipelines) config.pipelines = {};
|
|
206
|
-
config.pipelines[name] = flags.description
|
|
207
|
-
? { description: flags.description, phases }
|
|
208
|
-
: { phases };
|
|
209
|
-
writeConfig(config, detectIndent(raw));
|
|
210
|
-
console.log(`〇 Pipeline '${name}' saved.`);
|
|
211
|
-
process.exit(0);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (action === 'backfill-models') {
|
|
215
|
-
const { config, raw } = readConfig();
|
|
216
|
-
if (!config.pipelines || Object.keys(config.pipelines).length === 0) {
|
|
217
|
-
console.log('── No pipelines configured — nothing to backfill.');
|
|
218
|
-
process.exit(0);
|
|
219
|
-
}
|
|
220
|
-
let updated = 0;
|
|
221
|
-
for (const [name, pl] of Object.entries(config.pipelines)) {
|
|
222
|
-
if (!Array.isArray(pl.phases)) continue;
|
|
223
|
-
for (const phase of pl.phases) {
|
|
224
|
-
if (!phase.model && ROLE_MODEL_DEFAULTS[phase.role]) {
|
|
225
|
-
phase.model = ROLE_MODEL_DEFAULTS[phase.role];
|
|
226
|
-
updated++;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (updated === 0) {
|
|
231
|
-
console.log('〇 All pipeline phases already have model fields.');
|
|
232
|
-
process.exit(0);
|
|
233
|
-
}
|
|
234
|
-
writeConfig(config, detectIndent(raw));
|
|
235
|
-
console.log(`〇 Backfilled model fields on ${updated} phase(s) across ${Object.keys(config.pipelines).length} pipeline(s).`);
|
|
236
|
-
process.exit(0);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (action === 'remove') {
|
|
240
|
-
const name = args[1];
|
|
241
|
-
if (!name) { console.error('Usage: manage-config pipeline remove <name>'); process.exit(2); }
|
|
242
|
-
const { config, raw } = readConfig();
|
|
243
|
-
if (!config.pipelines || !config.pipelines[name]) {
|
|
244
|
-
console.error(`× Pipeline '${name}' not found`); process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
delete config.pipelines[name];
|
|
247
|
-
if (Object.keys(config.pipelines).length === 0) delete config.pipelines;
|
|
248
|
-
writeConfig(config, detectIndent(raw));
|
|
249
|
-
console.log(`〇 Pipeline '${name}' removed.`);
|
|
250
|
-
process.exit(0);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
console.error(`Unknown pipeline action: ${action}`);
|
|
254
|
-
process.exit(2);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (subcmd === 'set') {
|
|
258
|
-
const keyPath = args[0];
|
|
259
|
-
const valueStr = args[1];
|
|
260
|
-
if (!keyPath || valueStr === undefined) { console.error('Usage: manage-config set <key.path> <json-value>'); process.exit(2); }
|
|
261
|
-
// FR-005: If config.json does not exist, create a minimal {} config before reading.
|
|
262
|
-
// This allows `set` to work on fresh projects that haven't run /forge:init yet.
|
|
263
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
264
|
-
const configDir = path.dirname(CONFIG_PATH);
|
|
265
|
-
if (!fs.existsSync(configDir)) {
|
|
266
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
267
|
-
}
|
|
268
|
-
fs.writeFileSync(CONFIG_PATH, '{}\n', 'utf8');
|
|
269
|
-
}
|
|
270
|
-
let value;
|
|
271
|
-
try { value = JSON.parse(valueStr); } catch { value = valueStr; }
|
|
272
|
-
const { config, raw } = readConfig();
|
|
273
|
-
setByPath(config, keyPath, value);
|
|
274
|
-
writeConfig(config, detectIndent(raw));
|
|
275
|
-
console.log(`Set ${keyPath}.`);
|
|
276
|
-
process.exit(0);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// FR-010: resolve-forge-root — resolve the Forge plugin root path using
|
|
280
|
-
// three-tier priority: (1) CLAUDE_PLUGIN_ROOT env var, (2) cache/marketplace
|
|
281
|
-
// scan by forgeRef, (3) paths.forgeRoot fallback.
|
|
282
|
-
if (subcmd === 'resolve-forge-root') {
|
|
283
|
-
// Priority 1: CLAUDE_PLUGIN_ROOT env var (if set and directory exists)
|
|
284
|
-
const envRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
285
|
-
if (envRoot && envRoot.length > 0) {
|
|
286
|
-
try {
|
|
287
|
-
// Verify the directory exists and contains a valid plugin.json
|
|
288
|
-
const pluginJsonPath = path.join(envRoot, '.claude-plugin', 'plugin.json');
|
|
289
|
-
if (fs.existsSync(pluginJsonPath)) {
|
|
290
|
-
console.log(envRoot);
|
|
291
|
-
process.exit(0);
|
|
292
|
-
}
|
|
293
|
-
// Directory exists but no plugin.json — still use it if the directory itself exists
|
|
294
|
-
if (fs.existsSync(envRoot)) {
|
|
295
|
-
console.log(envRoot);
|
|
296
|
-
process.exit(0);
|
|
297
|
-
}
|
|
298
|
-
} catch { /* fall through to next priority */ }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const { config } = readConfig();
|
|
302
|
-
const forgeRef = getByPath(config, 'paths.forgeRef');
|
|
303
|
-
const forgeRoot = getByPath(config, 'paths.forgeRoot');
|
|
304
|
-
|
|
305
|
-
// Priority 2: Scan cache/marketplace directories by forgeRef
|
|
306
|
-
if (forgeRef && typeof forgeRef === 'string') {
|
|
307
|
-
const homeDir = os.homedir();
|
|
308
|
-
const candidates = [
|
|
309
|
-
path.join(homeDir, '.claude', 'plugins', 'cache', 'forge', 'forge', forgeRef),
|
|
310
|
-
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'skillforge', 'forge', 'forge', forgeRef),
|
|
311
|
-
];
|
|
312
|
-
for (const candidate of candidates) {
|
|
313
|
-
try {
|
|
314
|
-
const pluginJsonPath = path.join(candidate, '.claude-plugin', 'plugin.json');
|
|
315
|
-
if (fs.existsSync(pluginJsonPath)) {
|
|
316
|
-
// Validate that the plugin.json version matches forgeRef
|
|
317
|
-
const manifest = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
318
|
-
if (manifest.version === forgeRef) {
|
|
319
|
-
console.log(candidate);
|
|
320
|
-
process.exit(0);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
} catch { /* try next candidate */ }
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Priority 3: Fallback to paths.forgeRoot (deprecated but still read)
|
|
328
|
-
if (forgeRoot && typeof forgeRoot === 'string') {
|
|
329
|
-
console.log(forgeRoot);
|
|
330
|
-
process.exit(0);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// No resolution possible
|
|
334
|
-
console.error('× Cannot resolve Forge plugin root: no CLAUDE_PLUGIN_ROOT env var, no forgeRef cache match, and no forgeRoot in config.');
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
console.error(`Unknown subcommand: ${subcmd}`);
|
|
339
|
-
process.exit(2)
|
|
340
|
-
} // end require.main === module;
|