@entelligentsia/forgecli 0.8.4 → 0.9.1

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +165 -2
  3. package/dist/bin/argv.d.ts +2 -2
  4. package/dist/bin/argv.js +17 -0
  5. package/dist/bin/argv.js.map +1 -1
  6. package/dist/bin/config.d.ts +69 -0
  7. package/dist/bin/config.js +315 -0
  8. package/dist/bin/config.js.map +1 -0
  9. package/dist/bin/doctor.d.ts +1 -0
  10. package/dist/bin/doctor.js +12 -0
  11. package/dist/bin/doctor.js.map +1 -1
  12. package/dist/bin/forge.js +7 -0
  13. package/dist/bin/forge.js.map +1 -1
  14. package/dist/extensions/forgecli/config-command.d.ts +8 -0
  15. package/dist/extensions/forgecli/config-command.js +66 -0
  16. package/dist/extensions/forgecli/config-command.js.map +1 -0
  17. package/dist/extensions/forgecli/config-layer.d.ts +38 -0
  18. package/dist/extensions/forgecli/config-layer.js +68 -0
  19. package/dist/extensions/forgecli/config-layer.js.map +1 -0
  20. package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
  21. package/dist/extensions/forgecli/config-tui/component.js +236 -0
  22. package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
  23. package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
  24. package/dist/extensions/forgecli/config-tui/handler.js +240 -0
  25. package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
  26. package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
  27. package/dist/extensions/forgecli/config-tui/index.js +5 -0
  28. package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
  29. package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
  30. package/dist/extensions/forgecli/config-tui/keys.js +33 -0
  31. package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
  32. package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
  33. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
  34. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
  35. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
  36. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
  37. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
  38. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
  39. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
  40. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
  41. package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
  42. package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
  43. package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
  44. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
  45. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
  46. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
  47. package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
  48. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
  49. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
  50. package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
  51. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
  52. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
  53. package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
  54. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
  55. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
  56. package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
  57. package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
  58. package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
  59. package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
  60. package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
  61. package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
  62. package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
  63. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
  64. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
  65. package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
  66. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
  67. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
  68. package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
  69. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
  70. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
  71. package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
  72. package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
  73. package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
  74. package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
  75. package/dist/extensions/forgecli/config-tui/screens.js +78 -0
  76. package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
  77. package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
  78. package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
  79. package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
  80. package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
  81. package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
  82. package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
  83. package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
  84. package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
  85. package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
  86. package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
  87. package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
  88. package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
  89. package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
  90. package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
  91. package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
  92. package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
  93. package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
  94. package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
  95. package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
  96. package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
  97. package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
  98. package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
  99. package/dist/extensions/forgecli/config-tui/state.js +11 -0
  100. package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
  101. package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
  102. package/dist/extensions/forgecli/config-tui/theme.js +88 -0
  103. package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
  104. package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
  105. package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
  106. package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
  107. package/dist/extensions/forgecli/config-writer.d.ts +16 -0
  108. package/dist/extensions/forgecli/config-writer.js +63 -0
  109. package/dist/extensions/forgecli/config-writer.js.map +1 -0
  110. package/dist/extensions/forgecli/fix-bug.js +85 -1
  111. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  112. package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
  113. package/dist/extensions/forgecli/forge-commands.js +3 -8
  114. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  115. package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
  116. package/dist/extensions/forgecli/forge-subagent.js +19 -0
  117. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  118. package/dist/extensions/forgecli/index.js +16 -0
  119. package/dist/extensions/forgecli/index.js.map +1 -1
  120. package/dist/extensions/forgecli/input-router.d.ts +33 -0
  121. package/dist/extensions/forgecli/input-router.js +133 -0
  122. package/dist/extensions/forgecli/input-router.js.map +1 -0
  123. package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
  124. package/dist/extensions/forgecli/model-resolver.js +65 -0
  125. package/dist/extensions/forgecli/model-resolver.js.map +1 -0
  126. package/dist/extensions/forgecli/model-validator.d.ts +29 -0
  127. package/dist/extensions/forgecli/model-validator.js +107 -0
  128. package/dist/extensions/forgecli/model-validator.js.map +1 -0
  129. package/dist/extensions/forgecli/run-sprint.js +59 -0
  130. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  131. package/dist/extensions/forgecli/run-task.js +93 -1
  132. package/dist/extensions/forgecli/run-task.js.map +1 -1
  133. package/dist/extensions/forgecli/thread-switcher.js +5 -2
  134. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  135. package/dist/extensions/forgecli/whats-new-widget.js +5 -2
  136. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  137. package/package.json +11 -3
  138. package/dist/extensions/forgecli/review-command.d.ts +0 -2
  139. package/dist/extensions/forgecli/review-command.js +0 -184
  140. package/dist/extensions/forgecli/review-command.js.map +0 -1
  141. package/dist/forge-payload/.tools/banners.cjs +0 -435
  142. package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
  143. package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
  144. package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
  145. package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
  146. package/dist/forge-payload/.tools/collate.cjs +0 -1041
  147. package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
  148. package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
  149. package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
  150. package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
  151. package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
  152. package/dist/forge-payload/.tools/lib/result.js +0 -40
  153. package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
  154. package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
  155. package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
  156. package/dist/forge-payload/.tools/lib/validate.js +0 -141
  157. package/dist/forge-payload/.tools/manage-config.cjs +0 -340
  158. package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
  159. package/dist/forge-payload/.tools/package.json +0 -3
  160. package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
  161. package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
  162. package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
  163. package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
  164. package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
  165. package/dist/forge-payload/.tools/seed-store.cjs +0 -237
  166. package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
  167. package/dist/forge-payload/.tools/store-query.cjs +0 -319
  168. package/dist/forge-payload/.tools/store.cjs +0 -315
  169. package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
  170. 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;