peaks-cli 1.3.3 → 1.3.5
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/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/hook-handle.d.ts +2 -2
- package/dist/src/cli/commands/hook-handle.js +5 -10
- package/dist/src/cli/commands/hooks-commands.js +44 -29
- package/dist/src/cli/commands/project-commands.js +15 -5
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +1 -2
- package/dist/src/cli/program.js +3 -2
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
- package/dist/src/services/ide/adapters/claude-code-adapter.js +27 -2
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +19 -11
- package/dist/src/services/ide/adapters/trae-adapter.js +45 -19
- package/dist/src/services/ide/hook-protocol.d.ts +7 -4
- package/dist/src/services/ide/hook-protocol.js +7 -4
- package/dist/src/services/ide/ide-types.d.ts +61 -16
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +23 -103
- package/dist/src/services/progress/progress-service.js +24 -137
- package/dist/src/services/scan/file-size-scan.d.ts +4 -0
- package/dist/src/services/scan/file-size-scan.js +32 -3
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
- package/dist/src/services/skills/hooks-settings-service.js +153 -28
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/shared/incrementing-number.d.ts +0 -8
- package/dist/src/shared/incrementing-number.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +104 -62
- package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
- package/skills/peaks-rd/SKILL.md +88 -73
- package/skills/peaks-solo/SKILL.md +52 -22
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/runbook.md +21 -21
- package/skills/peaks-solo/references/sub-agent-dispatch.md +44 -1
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
- package/skills/peaks-ui/SKILL.md +18 -9
- package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
- package/dist/src/cli/commands/progress-close-kill.js +0 -152
- package/dist/src/cli/commands/progress-commands.d.ts +0 -3
- package/dist/src/cli/commands/progress-commands.js +0 -379
- package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
- package/dist/src/cli/commands/progress-start-spawn.js +0 -140
- package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
- package/dist/src/cli/commands/progress-watch-render.js +0 -308
|
@@ -10,36 +10,24 @@ import { getAdapter } from '../ide/ide-registry.js';
|
|
|
10
10
|
// values are computed lazily inside each public function call.
|
|
11
11
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
12
12
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
13
|
-
/** Sentinel substring identifying a peaks-managed sub-agent-progress hook entry. */
|
|
14
|
-
export const HOOK_PROGRESS_SENTINEL = 'peaks progress start';
|
|
15
13
|
/** Default (claude-code) hook command — kept as a stable export for tests. */
|
|
16
14
|
export const HOOK_ENFORCE_COMMAND = `peaks gate enforce --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
17
|
-
/** Default (claude-code) progress command — kept as a stable export for tests. */
|
|
18
|
-
export const HOOK_PROGRESS_COMMAND = `peaks progress start --project "\${CLAUDE_PROJECT_DIR}" --reason "auto-spawn for sub-agent Task" --quiet`;
|
|
19
15
|
function resolveHookSpec(ide) {
|
|
20
16
|
const adapter = getAdapter(ide);
|
|
21
17
|
if (ide === 'claude-code') {
|
|
22
18
|
return {
|
|
23
19
|
hookEnforceCommand: `peaks gate enforce --project "\${${adapter.envVar}}"`,
|
|
24
|
-
hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
|
|
25
20
|
hookEnforceSentinel: HOOK_ENFORCE_SENTINEL,
|
|
26
|
-
hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
|
|
27
21
|
hookEnforceMatcher: adapter.toolMatcher, // 'Bash'
|
|
28
|
-
|
|
29
|
-
hookEnforceEvent: adapter.hookEvent, // 'PreToolUse'
|
|
30
|
-
hookProgressEvent: adapter.hookEvent // 'PreToolUse' for Claude
|
|
22
|
+
hookEnforceEvent: adapter.hookEvent // 'PreToolUse'
|
|
31
23
|
};
|
|
32
24
|
}
|
|
33
25
|
if (ide === 'trae') {
|
|
34
26
|
return {
|
|
35
27
|
hookEnforceCommand: `peaks hook handle --project "\${${adapter.envVar}}"`,
|
|
36
|
-
hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
|
|
37
28
|
hookEnforceSentinel: 'peaks hook handle',
|
|
38
|
-
hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
|
|
39
29
|
hookEnforceMatcher: adapter.toolMatcher, // 'terminal'
|
|
40
|
-
|
|
41
|
-
hookEnforceEvent: adapter.hookEvent, // 'beforeToolCall'
|
|
42
|
-
hookProgressEvent: adapter.hookEvent // 'beforeToolCall' (no separate progress event yet for Trae)
|
|
30
|
+
hookEnforceEvent: adapter.hookEvent // 'beforeToolCall'
|
|
43
31
|
};
|
|
44
32
|
}
|
|
45
33
|
// Future adapters (codex, cursor, qoder, tongyi-lingma) — not yet registered.
|
|
@@ -51,6 +39,10 @@ function resolveHookSpec(ide) {
|
|
|
51
39
|
function resolveIde(options) {
|
|
52
40
|
return options?.ide ?? 'claude-code';
|
|
53
41
|
}
|
|
42
|
+
/** Slice #013: read the skipProgress opt-in flag. Slice #014: the underlying install no longer emits the progress-start entry, so the flag is effectively a no-op (kept for API stability). */
|
|
43
|
+
function resolveSkipProgress(options) {
|
|
44
|
+
return options?.skipProgress === true;
|
|
45
|
+
}
|
|
54
46
|
/** Resolve settings root dir for a scope. */
|
|
55
47
|
function resolveSettingsRoot(scope, projectRoot) {
|
|
56
48
|
if (scope === 'global')
|
|
@@ -101,24 +93,104 @@ function entryIsPeaksManaged(entry, sentinels) {
|
|
|
101
93
|
return sentinels.some((sentinel) => cmd.includes(sentinel));
|
|
102
94
|
});
|
|
103
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Slice #014: read the *actually-installed* peaks-managed hook entries
|
|
98
|
+
* from a settings object. Replaces the pre-#014 `listInstalledEntriesForIde`
|
|
99
|
+
* helper in `hooks-commands.ts`, which returned the IDE-EXPECTED list
|
|
100
|
+
* (a hardcoded 2-entry array per adapter) rather than what was on disk.
|
|
101
|
+
* That bug surfaced when slice #013's local cleanup installed
|
|
102
|
+
* `peaks hooks install --no-progress` (gate-enforce only), but the
|
|
103
|
+
* status command still reported `entries: [Bash, Task]` because the
|
|
104
|
+
* helper didn't read the file.
|
|
105
|
+
*
|
|
106
|
+
* The new helper:
|
|
107
|
+
* 1. reads each `hooks.<event>` array,
|
|
108
|
+
* 2. filters to entries that are peaks-managed for the given IDE
|
|
109
|
+
* (matches the legacy sentinel set: gate-enforce + the no-longer-
|
|
110
|
+
* installed progress-start),
|
|
111
|
+
* 3. returns one `{ matcher, sentinel }` row per entry, taking the
|
|
112
|
+
* FIRST matching sentinel per entry (entries have a single command
|
|
113
|
+
* handler in practice, but the loop tolerates multi-handler
|
|
114
|
+
* entries by taking the first match).
|
|
115
|
+
*
|
|
116
|
+
* Pre-#014 settings.json files that have a stale progress-start entry
|
|
117
|
+
* will see it surface in the result. This is intentional: the status
|
|
118
|
+
* command is the user's tool for "what is on disk right now", and
|
|
119
|
+
* surfacing a stale entry is the only way the user can know to run
|
|
120
|
+
* `peaks hooks install` (which now strips it) or `peaks hooks
|
|
121
|
+
* uninstall` (which removes it).
|
|
122
|
+
*/
|
|
123
|
+
export function readInstalledEntriesFromSettings(settings, ide) {
|
|
124
|
+
const sentinels = resolveLegacySentinels(ide);
|
|
125
|
+
// Walk every event key the settings file has, not just the
|
|
126
|
+
// adapter-declared one. A pre-#014 install could have left a
|
|
127
|
+
// progress-start entry on a different event than the gate-enforce
|
|
128
|
+
// entry (Trae: both on beforeToolCall, but a stale install on a
|
|
129
|
+
// future IDE could split them).
|
|
130
|
+
const hooksRoot = settings.hooks;
|
|
131
|
+
if (!hooksRoot || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot))
|
|
132
|
+
return [];
|
|
133
|
+
const result = [];
|
|
134
|
+
for (const eventKey of Object.keys(hooksRoot)) {
|
|
135
|
+
const entries = readHookEntriesFromHooks(hooksRoot, eventKey);
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (!entryIsPeaksManaged(entry, sentinels))
|
|
138
|
+
continue;
|
|
139
|
+
const matcher = typeof entry.matcher === 'string' ? entry.matcher : '';
|
|
140
|
+
// Find the first matching sentinel inside the entry's command
|
|
141
|
+
// handlers. For each handler, find the first sentinel substring
|
|
142
|
+
// it contains. We pick the first handler's first matching
|
|
143
|
+
// sentinel (entries have a single command in practice).
|
|
144
|
+
const firstHandler = Array.isArray(entry.hooks) ? entry.hooks[0] : undefined;
|
|
145
|
+
const cmd = typeof firstHandler?.command === 'string' ? firstHandler.command : '';
|
|
146
|
+
const sentinel = sentinels.find((s) => cmd.includes(s));
|
|
147
|
+
if (matcher === '' || sentinel === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
result.push({ matcher, sentinel });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
104
154
|
/**
|
|
105
155
|
* Compute the per-IDE peaks hook entries to merge into the settings file.
|
|
106
156
|
* Replaces the slice #1 hardcoded `PEAKS_HOOK_ENTRIES` constant; the constant
|
|
107
157
|
* remains exported (computed for claude-code) for backward compat.
|
|
158
|
+
*
|
|
159
|
+
* Slice #013 (`--no-progress` flag): when `skipProgress` is true, the
|
|
160
|
+
* progress-start entry is omitted from the returned list. Install with this
|
|
161
|
+
* flag will (a) NOT emit the progress hook entry, and (b) will idempotently
|
|
162
|
+
* remove any previously-installed progress entry (sentinel-based merge).
|
|
163
|
+
*
|
|
164
|
+
* Slice #014 (refactor — full removal): only the gate-enforce entry is
|
|
165
|
+
* ever emitted. The `skipProgress` parameter is kept for API stability
|
|
166
|
+
* but is a no-op — the returned list is always single-entry. The progress
|
|
167
|
+
* entry's sentinel is included in the legacy sentinel set so uninstall
|
|
168
|
+
* can find + remove any stale progress-start entry that an older
|
|
169
|
+
* `peaks hooks install` may have written before this slice.
|
|
108
170
|
*/
|
|
109
|
-
function resolveHookEntries(ide) {
|
|
171
|
+
function resolveHookEntries(ide, _skipProgress = false) {
|
|
110
172
|
const spec = resolveHookSpec(ide);
|
|
111
173
|
return [
|
|
112
|
-
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
113
|
-
{ sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
|
|
174
|
+
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
114
175
|
];
|
|
115
176
|
}
|
|
116
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Legacy sentinel set used by uninstall + status to find and remove stale
|
|
179
|
+
* progress-start entries written by pre-#014 installs. The progress-start
|
|
180
|
+
* sentinel is the literal substring that older installs emitted.
|
|
181
|
+
*/
|
|
182
|
+
const LEGACY_PROGRESS_START_SENTINEL = 'peaks progress start';
|
|
183
|
+
function resolveLegacySentinels(ide) {
|
|
184
|
+
if (ide === 'trae') {
|
|
185
|
+
return ['peaks hook handle', LEGACY_PROGRESS_START_SENTINEL];
|
|
186
|
+
}
|
|
187
|
+
return [HOOK_ENFORCE_SENTINEL, LEGACY_PROGRESS_START_SENTINEL];
|
|
188
|
+
}
|
|
189
|
+
/** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. Slice #014: only the gate-enforce entry. */
|
|
117
190
|
export const PEAKS_HOOK_ENTRIES = (() => {
|
|
118
191
|
const spec = resolveHookSpec('claude-code');
|
|
119
192
|
return [
|
|
120
|
-
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
121
|
-
{ sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
|
|
193
|
+
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
122
194
|
];
|
|
123
195
|
})();
|
|
124
196
|
function isInstalledForIde(settings, ide) {
|
|
@@ -133,8 +205,43 @@ function isInstalledForIde(settings, ide) {
|
|
|
133
205
|
}
|
|
134
206
|
return false;
|
|
135
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Slice #014: detect the "stale progress entry after pre-#014 install"
|
|
210
|
+
* case. The desired shape is gate-enforce-only. If the file currently
|
|
211
|
+
* has a peaks-managed progress-start entry (left behind by a pre-#014
|
|
212
|
+
* install), the install is NOT a no-op — it must strip the stale
|
|
213
|
+
* entry. This helper returns true exactly when the desired shape is
|
|
214
|
+
* fully reflected on disk: gate-enforce present AND no legacy
|
|
215
|
+
* progress-start present.
|
|
216
|
+
*/
|
|
217
|
+
function shapeMatchesDesired(settings, ide) {
|
|
218
|
+
const desiredEntries = resolveHookEntries(ide);
|
|
219
|
+
const desiredSentinels = new Set(desiredEntries.map((e) => e.sentinel));
|
|
220
|
+
const allPeaksSentinels = resolveLegacySentinels(ide);
|
|
221
|
+
const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
|
|
222
|
+
for (const eventKey of eventKeys) {
|
|
223
|
+
const present = readHookEventEntries(settings, eventKey);
|
|
224
|
+
const peaksPresent = present.filter((e) => entryIsPeaksManaged(e, allPeaksSentinels));
|
|
225
|
+
// (a) every peaks-managed entry currently on disk must match the
|
|
226
|
+
// desired sentinel set (no stale entries the caller wants removed).
|
|
227
|
+
for (const entry of peaksPresent) {
|
|
228
|
+
const entrySentinels = (entry.hooks ?? []).map((h) => allPeaksSentinels.find((s) => String(h.command ?? '').includes(s))).filter((s) => Boolean(s));
|
|
229
|
+
if (entrySentinels.some((s) => !desiredSentinels.has(s))) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// (b) every desired entry must be on disk.
|
|
234
|
+
for (const sentinel of desiredSentinels) {
|
|
235
|
+
const has = peaksPresent.some((entry) => (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(sentinel)));
|
|
236
|
+
if (!has)
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
136
242
|
export function planHookInstall(scope, projectRoot, options) {
|
|
137
243
|
const ide = resolveIde(options);
|
|
244
|
+
const _skipProgress = resolveSkipProgress(options);
|
|
138
245
|
const root = resolveSettingsRoot(scope, projectRoot);
|
|
139
246
|
const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
|
|
140
247
|
assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
|
|
@@ -152,24 +259,29 @@ export function planHookInstall(scope, projectRoot, options) {
|
|
|
152
259
|
};
|
|
153
260
|
}
|
|
154
261
|
/** Merge all peaks-managed hook entries into settings, preserving all other keys and hooks. */
|
|
155
|
-
function withHooksInstalledForIde(settings, ide) {
|
|
262
|
+
function withHooksInstalledForIde(settings, ide, _skipProgress = false) {
|
|
156
263
|
const existingHooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
|
|
157
264
|
? settings.hooks
|
|
158
265
|
: {};
|
|
159
|
-
// Per-IDE entries may map to different events (Trae:
|
|
160
|
-
// Claude:
|
|
161
|
-
// independently merged.
|
|
266
|
+
// Per-IDE entries may map to different events (Trae: gate-enforce on
|
|
267
|
+
// beforeToolCall; Claude: gate-enforce on PreToolUse). Group by event
|
|
268
|
+
// so each event array is independently merged.
|
|
162
269
|
const ourByEvent = new Map();
|
|
163
270
|
for (const spec of resolveHookEntries(ide)) {
|
|
164
271
|
const list = ourByEvent.get(spec.event) ?? [];
|
|
165
272
|
list.push(spec);
|
|
166
273
|
ourByEvent.set(spec.event, list);
|
|
167
274
|
}
|
|
168
|
-
|
|
275
|
+
// Slice #014: the legacy sentinel set includes the progress-start
|
|
276
|
+
// sentinel so a pre-#014 install's stale progress-start entry is
|
|
277
|
+
// stripped by the filter (the file converges on the new
|
|
278
|
+
// gate-enforce-only shape, idempotently). The desired set (passed
|
|
279
|
+
// in below) only contains the gate-enforce sentinel.
|
|
280
|
+
const allSentinels = resolveLegacySentinels(ide);
|
|
169
281
|
const nextHooks = { ...existingHooks };
|
|
170
282
|
for (const [eventKey, ourEntries] of ourByEvent) {
|
|
171
283
|
const existing = readHookEntriesFromHooks(nextHooks, eventKey);
|
|
172
|
-
const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry,
|
|
284
|
+
const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry, allSentinels));
|
|
173
285
|
const ourFormatted = ourEntries.map((spec) => ({
|
|
174
286
|
matcher: spec.matcher,
|
|
175
287
|
hooks: [{ type: 'command', command: spec.command }]
|
|
@@ -189,17 +301,25 @@ function withHooksInstalledForIde(settings, ide) {
|
|
|
189
301
|
}
|
|
190
302
|
export function applyHookInstall(scope, projectRoot, options) {
|
|
191
303
|
const ide = resolveIde(options);
|
|
304
|
+
const _skipProgress = resolveSkipProgress(options);
|
|
192
305
|
const root = resolveSettingsRoot(scope, projectRoot);
|
|
193
306
|
const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
|
|
194
307
|
assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
|
|
195
308
|
const exists = existsSync(settingsPath);
|
|
196
309
|
const settings = exists ? readJsonObjectFile(settingsPath) : {};
|
|
197
310
|
const spec = resolveHookSpec(ide);
|
|
311
|
+
// Slice #014: `alreadyInstalled` reflects the FULL desired shape
|
|
312
|
+
// (gate-enforce-only + no stale progress-start entry). Pre-#014
|
|
313
|
+
// installs that left a progress-start entry behind will be treated
|
|
314
|
+
// as not-yet-installed, so the merge strips the stale entry on the
|
|
315
|
+
// next install call. This is the only path that converges the file
|
|
316
|
+
// on the new shape; pure presence-checks are insufficient.
|
|
317
|
+
const alreadyInstalled = shapeMatchesDesired(settings, ide);
|
|
198
318
|
const baseResult = {
|
|
199
319
|
scope,
|
|
200
320
|
settingsPath,
|
|
201
321
|
exists,
|
|
202
|
-
alreadyInstalled
|
|
322
|
+
alreadyInstalled,
|
|
203
323
|
desiredCommand: spec.hookEnforceCommand,
|
|
204
324
|
sentinel: spec.hookEnforceSentinel,
|
|
205
325
|
matcher: spec.hookEnforceMatcher
|
|
@@ -220,7 +340,12 @@ export function removeHookInstall(scope, projectRoot, options) {
|
|
|
220
340
|
}
|
|
221
341
|
const settings = readJsonObjectFile(settingsPath);
|
|
222
342
|
const existingHooks = settings.hooks ?? {};
|
|
223
|
-
|
|
343
|
+
// Slice #014: uninstall must remove the gate-enforce entry AND any
|
|
344
|
+
// legacy progress-start entry that a pre-#014 install left behind.
|
|
345
|
+
// The legacy sentinel set covers both shapes so the uninstall
|
|
346
|
+
// converges the file on "no peaks-managed entries", regardless of
|
|
347
|
+
// what shape the file was in when the user ran uninstall.
|
|
348
|
+
const sentinels = resolveLegacySentinels(ide);
|
|
224
349
|
const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
|
|
225
350
|
let removedAny = false;
|
|
226
351
|
const nextHooks = { ...existingHooks };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE-aware wrapper for `peaks standards init` / `peaks standards update`.
|
|
3
|
+
*
|
|
4
|
+
* Slice #011-2026-06-07-ide-adapter-resource-profile: the original
|
|
5
|
+
* `executeProjectStandardsInit` / `executeProjectStandardsUpdate` always
|
|
6
|
+
* wrote to `CLAUDE.md` + `.claude/rules/**` regardless of which IDE
|
|
7
|
+
* the user was running. This wrapper dispatches on the IDE detected
|
|
8
|
+
* (or explicitly requested via `--ide`) and falls back to the legacy
|
|
9
|
+
* Claude Code path with a stderr warning when the detected IDE has
|
|
10
|
+
* no `standardsProfile` declared (Trae in slice 1.3.2).
|
|
11
|
+
*
|
|
12
|
+
* Two entry points:
|
|
13
|
+
*
|
|
14
|
+
* - `executeProjectStandardsInitIdeAware` — same signature as the
|
|
15
|
+
* underlying `executeProjectStandardsInit`, plus an optional
|
|
16
|
+
* `ideId` override that bypasses detection.
|
|
17
|
+
* - `executeProjectStandardsUpdateIdeAware` — same shape, for the
|
|
18
|
+
* `update` flow.
|
|
19
|
+
*
|
|
20
|
+
* Detection precedence:
|
|
21
|
+
* 1. Explicit `options.ideId` (CLI `--ide` flag)
|
|
22
|
+
* 2. `IdeRegistry.detect()` from cwd (or the `projectRoot` if given)
|
|
23
|
+
* 3. `null` (no IDE detected) → fall back to the legacy Claude Code path
|
|
24
|
+
*
|
|
25
|
+
* Fallback behavior: when the resolved IDE has no `standardsProfile`
|
|
26
|
+
* declared, the wrapper STILL calls the legacy Claude Code writer
|
|
27
|
+
* (so the user gets the files they would have gotten before slice #011)
|
|
28
|
+
* and emits a stderr warning with the IDE id and the fact that the
|
|
29
|
+
* adapter is UNVERIFIED for the standards profile. This keeps the
|
|
30
|
+
* "Trae is UNVERIFIED, ship a working file tree, surface the gap"
|
|
31
|
+
* contract intact.
|
|
32
|
+
*/
|
|
33
|
+
import type { IdeId } from '../ide/ide-types.js';
|
|
34
|
+
import { detectAllResourceTargets, getStandardsProfile } from '../ide/resource-profile.js';
|
|
35
|
+
import { type ProjectStandardsInitOptions, type ProjectStandardsInitResult, type ProjectStandardsUpdateResult } from './project-standards-service.js';
|
|
36
|
+
export type { ProjectStandardsInitResult, ProjectStandardsUpdateResult };
|
|
37
|
+
export type ProjectStandardsIdeAwareOptions = ProjectStandardsInitOptions & {
|
|
38
|
+
/**
|
|
39
|
+
* Explicit IDE override. When set, bypasses `IdeRegistry.detect()`
|
|
40
|
+
* (cwd + env heuristics) and uses the provided IDE id directly.
|
|
41
|
+
* Mirrors the `peaks hooks install --ide <id>` pattern.
|
|
42
|
+
*/
|
|
43
|
+
readonly ideId?: IdeId;
|
|
44
|
+
};
|
|
45
|
+
export type ProjectStandardsUpdateIdeAwareOptions = ProjectStandardsInitOptions & {
|
|
46
|
+
/** Explicit IDE override. See {@link ProjectStandardsIdeAwareOptions}. */
|
|
47
|
+
readonly ideId?: IdeId;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the active IDE id for a standards call. Order of precedence:
|
|
51
|
+
* 1. explicit `options.ideId`
|
|
52
|
+
* 2. `IdeRegistry.detect()` from `options.projectRoot`
|
|
53
|
+
* 3. `null` (no detected IDE — caller falls back to legacy)
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveStandardsIdeId(options: {
|
|
56
|
+
readonly projectRoot: string;
|
|
57
|
+
readonly ideId?: IdeId;
|
|
58
|
+
}): IdeId | null;
|
|
59
|
+
/**
|
|
60
|
+
* Run `peaks standards init` with IDE-aware dispatch.
|
|
61
|
+
*
|
|
62
|
+
* When the resolved IDE has a `standardsProfile`, the call still
|
|
63
|
+
* delegates to the existing `executeProjectStandardsInit` (the
|
|
64
|
+
* profile maps to the Claude Code path; future per-IDE writers
|
|
65
|
+
* plug in here). When the IDE is unregistered for the standards
|
|
66
|
+
* profile, the call delegates to the legacy path + emits a stderr
|
|
67
|
+
* warning.
|
|
68
|
+
*/
|
|
69
|
+
export declare function executeProjectStandardsInitIdeAware(options: ProjectStandardsIdeAwareOptions): ProjectStandardsInitResult;
|
|
70
|
+
/**
|
|
71
|
+
* Run `peaks standards update` with IDE-aware dispatch.
|
|
72
|
+
*
|
|
73
|
+
* Same dispatch rules as `executeProjectStandardsInitIdeAware`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function executeProjectStandardsUpdateIdeAware(options: ProjectStandardsUpdateIdeAwareOptions): ProjectStandardsUpdateResult;
|
|
76
|
+
/**
|
|
77
|
+
* Test seam + integration-test helper: returns the resolved IDE id
|
|
78
|
+
* for the call, plus the active standards profile. Exported for
|
|
79
|
+
* the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
|
|
80
|
+
* to assert the dispatch decision without running the full write.
|
|
81
|
+
*/
|
|
82
|
+
export declare function inspectStandardsDispatch(options: {
|
|
83
|
+
readonly projectRoot: string;
|
|
84
|
+
readonly ideId?: IdeId;
|
|
85
|
+
}): {
|
|
86
|
+
readonly ideId: IdeId | null;
|
|
87
|
+
readonly profile: ReturnType<typeof getStandardsProfile>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Test seam: the resource-profile accessor exposes
|
|
91
|
+
* `detectAllResourceTargets` for callers that need to enumerate
|
|
92
|
+
* across all registered IDEs. Re-exported here for convenience.
|
|
93
|
+
*/
|
|
94
|
+
export { detectAllResourceTargets };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { detectAllResourceTargets, getStandardsProfile, } from '../ide/resource-profile.js';
|
|
2
|
+
import { executeProjectStandardsInit, executeProjectStandardsUpdate, } from './project-standards-service.js';
|
|
3
|
+
import { detectInstalledIde } from '../ide/ide-detector.js';
|
|
4
|
+
function warnUnregisteredIde(ideId, projectRoot) {
|
|
5
|
+
process.stderr.write(`peaks standards: IDE '${ideId}' has no standardsProfile declared; ` +
|
|
6
|
+
`falling back to the legacy Claude Code path (CLAUDE.md + .claude/rules/**) ` +
|
|
7
|
+
`for project '${projectRoot}'. This is a slice #011 follow-up gap; ` +
|
|
8
|
+
`see .peaks/memory/ide-adapter-resource-profile-framework.md.\n`);
|
|
9
|
+
}
|
|
10
|
+
function warnNoIdeDetected(projectRoot) {
|
|
11
|
+
process.stderr.write(`peaks standards: no IDE detected in '${projectRoot}'; ` +
|
|
12
|
+
`writing to the legacy Claude Code path (CLAUDE.md + .claude/rules/**). ` +
|
|
13
|
+
`Pass --ide <id> to bypass detection.\n`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the active IDE id for a standards call. Order of precedence:
|
|
17
|
+
* 1. explicit `options.ideId`
|
|
18
|
+
* 2. `IdeRegistry.detect()` from `options.projectRoot`
|
|
19
|
+
* 3. `null` (no detected IDE — caller falls back to legacy)
|
|
20
|
+
*/
|
|
21
|
+
export function resolveStandardsIdeId(options) {
|
|
22
|
+
if (options.ideId !== undefined) {
|
|
23
|
+
return options.ideId;
|
|
24
|
+
}
|
|
25
|
+
return detectInstalledIde(options.projectRoot);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run `peaks standards init` with IDE-aware dispatch.
|
|
29
|
+
*
|
|
30
|
+
* When the resolved IDE has a `standardsProfile`, the call still
|
|
31
|
+
* delegates to the existing `executeProjectStandardsInit` (the
|
|
32
|
+
* profile maps to the Claude Code path; future per-IDE writers
|
|
33
|
+
* plug in here). When the IDE is unregistered for the standards
|
|
34
|
+
* profile, the call delegates to the legacy path + emits a stderr
|
|
35
|
+
* warning.
|
|
36
|
+
*/
|
|
37
|
+
export function executeProjectStandardsInitIdeAware(options) {
|
|
38
|
+
const ideId = resolveStandardsIdeId(options);
|
|
39
|
+
if (ideId === null) {
|
|
40
|
+
warnNoIdeDetected(options.projectRoot);
|
|
41
|
+
return executeProjectStandardsInit(options);
|
|
42
|
+
}
|
|
43
|
+
const profile = getStandardsProfile(ideId);
|
|
44
|
+
if (profile === null) {
|
|
45
|
+
warnUnregisteredIde(ideId, options.projectRoot);
|
|
46
|
+
return executeProjectStandardsInit(options);
|
|
47
|
+
}
|
|
48
|
+
// Claude Code path: profile matches the legacy writer. Future per-IDE
|
|
49
|
+
// writers (markdown+frontmatter, multiple rule roots, etc.) plug in
|
|
50
|
+
// here by branching on `profile.format` / `profile.rulesDir`.
|
|
51
|
+
return executeProjectStandardsInit(options);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run `peaks standards update` with IDE-aware dispatch.
|
|
55
|
+
*
|
|
56
|
+
* Same dispatch rules as `executeProjectStandardsInitIdeAware`.
|
|
57
|
+
*/
|
|
58
|
+
export function executeProjectStandardsUpdateIdeAware(options) {
|
|
59
|
+
const ideId = resolveStandardsIdeId(options);
|
|
60
|
+
if (ideId === null) {
|
|
61
|
+
warnNoIdeDetected(options.projectRoot);
|
|
62
|
+
return executeProjectStandardsUpdate(options);
|
|
63
|
+
}
|
|
64
|
+
const profile = getStandardsProfile(ideId);
|
|
65
|
+
if (profile === null) {
|
|
66
|
+
warnUnregisteredIde(ideId, options.projectRoot);
|
|
67
|
+
return executeProjectStandardsUpdate(options);
|
|
68
|
+
}
|
|
69
|
+
return executeProjectStandardsUpdate(options);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Test seam + integration-test helper: returns the resolved IDE id
|
|
73
|
+
* for the call, plus the active standards profile. Exported for
|
|
74
|
+
* the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
|
|
75
|
+
* to assert the dispatch decision without running the full write.
|
|
76
|
+
*/
|
|
77
|
+
export function inspectStandardsDispatch(options) {
|
|
78
|
+
const ideId = resolveStandardsIdeId(options);
|
|
79
|
+
if (ideId === null) {
|
|
80
|
+
return { ideId: null, profile: null };
|
|
81
|
+
}
|
|
82
|
+
return { ideId, profile: getStandardsProfile(ideId) };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Test seam: the resource-profile accessor exposes
|
|
86
|
+
* `detectAllResourceTargets` for callers that need to enumerate
|
|
87
|
+
* across all registered IDEs. Re-exported here for convenience.
|
|
88
|
+
*/
|
|
89
|
+
export { detectAllResourceTargets };
|
|
@@ -68,7 +68,7 @@ export type ProjectStandardsUpdateSummary = {
|
|
|
68
68
|
readonly reviewSuggestions: string[];
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
|
-
type ProjectStandardsInitOptions = {
|
|
71
|
+
export type ProjectStandardsInitOptions = {
|
|
72
72
|
readonly projectRoot: string;
|
|
73
73
|
readonly language?: string;
|
|
74
74
|
readonly apply?: boolean;
|
|
@@ -79,4 +79,3 @@ export declare function executeProjectStandardsInit(options: ProjectStandardsIni
|
|
|
79
79
|
export declare function executeProjectStandardsUpdate(options: ProjectStandardsInitOptions): ProjectStandardsUpdateResult;
|
|
80
80
|
export declare function summarizeProjectStandardsInitResult(result: ProjectStandardsInitResult): ProjectStandardsInitSummary;
|
|
81
81
|
export declare function summarizeProjectStandardsUpdateResult(result: ProjectStandardsUpdateResult): ProjectStandardsUpdateSummary;
|
|
82
|
-
export {};
|
|
@@ -11,14 +11,6 @@
|
|
|
11
11
|
* @returns Next available number (1, 2, 3, ...)
|
|
12
12
|
*/
|
|
13
13
|
export declare function getNextNumber(dirPath: string): number;
|
|
14
|
-
/**
|
|
15
|
-
* Build a numbered filename from a number and description.
|
|
16
|
-
* Format: 001-description-slug.md
|
|
17
|
-
*
|
|
18
|
-
* @param number - The file number (will be zero-padded to 3 digits)
|
|
19
|
-
* @param description - Human-readable description (converted to kebab-case slug)
|
|
20
|
-
* @returns Formatted filename like "001-feature-name.md"
|
|
21
|
-
*/
|
|
22
14
|
export declare function buildNumberedFilename(number: number, description: string): string;
|
|
23
15
|
/**
|
|
24
16
|
* Get the next numbered file path in a directory.
|
|
@@ -34,13 +34,23 @@ export function getNextNumber(dirPath) {
|
|
|
34
34
|
* @param description - Human-readable description (converted to kebab-case slug)
|
|
35
35
|
* @returns Formatted filename like "001-feature-name.md"
|
|
36
36
|
*/
|
|
37
|
+
// Windows supports up to 255 chars per filename component (and 260 for the
|
|
38
|
+
// full path). Pre-#015 the slug was silently truncated to 50 chars, which
|
|
39
|
+
// produced orphaned artefacts (the on-disk file no longer matched the
|
|
40
|
+
// request-id and the state machine could not find it). 255 is the OS-level
|
|
41
|
+
// ceiling; if a requestId exceeds that, `mkdir` / `writeFile` will surface a
|
|
42
|
+
// real ENAMETOOLONG error instead of a silent mismatch. We reserve 4 chars
|
|
43
|
+
// for the `<NNN>-` numeric prefix and 3 chars for the `.md` suffix, so
|
|
44
|
+
// the slug may be at most 248 chars (giving 4 + 248 + 3 = 255 total).
|
|
45
|
+
const MAX_FILENAME_LENGTH = 255;
|
|
46
|
+
const MAX_FILENAME_SLUG_LENGTH = MAX_FILENAME_LENGTH - 7;
|
|
37
47
|
export function buildNumberedFilename(number, description) {
|
|
38
48
|
const padded = String(number).padStart(3, '0');
|
|
39
49
|
const slug = description
|
|
40
50
|
.toLowerCase()
|
|
41
51
|
.replace(/[^a-z0-9]+/g, '-')
|
|
42
52
|
.replace(/^-|-$/g, '')
|
|
43
|
-
.slice(0,
|
|
53
|
+
.slice(0, MAX_FILENAME_SLUG_LENGTH);
|
|
44
54
|
return `${padded}-${slug}.md`;
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.3.
|
|
1
|
+
export declare const CLI_VERSION = "1.3.5";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.3.
|
|
1
|
+
export const CLI_VERSION = "1.3.5";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|