@massu/core 1.10.7 → 1.12.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 +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `massu changelog <subcommand>` — CHANGELOG generation + verification CLI.
|
|
6
|
-
*
|
|
7
|
-
* Subcommands:
|
|
8
|
-
* generate Emit a draft CHANGELOG.md entry to stdout for commits since the
|
|
9
|
-
* last tag. Operator pipes/copies into CHANGELOG.md.
|
|
10
|
-
* verify Read current CHANGELOG.md latest entry; verify every plan-token
|
|
11
|
-
* in `git log <last-tag>..HEAD` subjects is referenced. Exit 0 if
|
|
12
|
-
* clean, exit 1 with one `gap: <token>` per missing.
|
|
13
|
-
*
|
|
14
|
-
* Plan ref: plan-1.9.0-plan-token-aware-changelog-batcher Phase C.
|
|
15
|
-
* Mirrors the `permissions <sub>` cluster precedent shipped in 1.8.0.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { execSync } from 'child_process';
|
|
19
|
-
import { existsSync, readFileSync } from 'fs';
|
|
20
|
-
import { resolve } from 'path';
|
|
21
|
-
import {
|
|
22
|
-
parseCommitsForPlanTokens,
|
|
23
|
-
loadPlanSummaries,
|
|
24
|
-
generateChangelogEntry,
|
|
25
|
-
findCoverageGaps,
|
|
26
|
-
MissingPlanFileError,
|
|
27
|
-
MissingChangelogSummaryError,
|
|
28
|
-
} from '../changelog-generator.ts';
|
|
29
|
-
|
|
30
|
-
function resolveRepoRoot(): string {
|
|
31
|
-
try {
|
|
32
|
-
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
33
|
-
} catch {
|
|
34
|
-
return process.cwd();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getLastTag(): string | null {
|
|
39
|
-
try {
|
|
40
|
-
return execSync('git describe --tags --abbrev=0', { encoding: 'utf-8' }).trim();
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getCommitSubjects(range: string): string[] {
|
|
47
|
-
try {
|
|
48
|
-
const out = execSync(`git log ${range} --pretty=format:%s`, { encoding: 'utf-8' });
|
|
49
|
-
return out.split('\n').filter((s) => s.length > 0);
|
|
50
|
-
} catch {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getCurrentVersion(repoRoot: string): string {
|
|
56
|
-
const pkgPath = resolve(repoRoot, 'packages/core/package.json');
|
|
57
|
-
if (!existsSync(pkgPath)) return '0.0.0';
|
|
58
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
59
|
-
return pkg.version || '0.0.0';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function todayDate(): string {
|
|
63
|
-
return new Date().toISOString().slice(0, 10);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function getLatestChangelogEntryBody(repoRoot: string): string {
|
|
67
|
-
const path = resolve(repoRoot, 'CHANGELOG.md');
|
|
68
|
-
if (!existsSync(path)) return '';
|
|
69
|
-
const content = readFileSync(path, 'utf-8');
|
|
70
|
-
// Find first `## [...]` heading and capture until next or EOF
|
|
71
|
-
const m = content.match(/^## \[[\d.]+\][^\n]*\n([\s\S]*?)(?=\n## \[|$)/m);
|
|
72
|
-
return m ? m[1] : '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function handleChangelogSubcommand(
|
|
76
|
-
args: string[],
|
|
77
|
-
): Promise<{ exitCode: number }> {
|
|
78
|
-
const sub = args[0];
|
|
79
|
-
const repoRoot = resolveRepoRoot();
|
|
80
|
-
const planDir = resolve(repoRoot, 'docs/plans');
|
|
81
|
-
|
|
82
|
-
switch (sub) {
|
|
83
|
-
case 'generate': {
|
|
84
|
-
const lastTag = getLastTag();
|
|
85
|
-
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
86
|
-
const subjects = getCommitSubjects(range);
|
|
87
|
-
const { tokens, maintenance } = parseCommitsForPlanTokens(subjects);
|
|
88
|
-
|
|
89
|
-
let planSummaries;
|
|
90
|
-
try {
|
|
91
|
-
planSummaries = loadPlanSummaries(tokens, planDir);
|
|
92
|
-
} catch (err) {
|
|
93
|
-
if (err instanceof MissingPlanFileError || err instanceof MissingChangelogSummaryError) {
|
|
94
|
-
process.stderr.write(`changelog generate: ${err.message}\n`);
|
|
95
|
-
return { exitCode: 2 };
|
|
96
|
-
}
|
|
97
|
-
throw err;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const entry = generateChangelogEntry({
|
|
101
|
-
version: getCurrentVersion(repoRoot),
|
|
102
|
-
date: todayDate(),
|
|
103
|
-
planSummaries,
|
|
104
|
-
maintenance,
|
|
105
|
-
});
|
|
106
|
-
process.stdout.write(entry);
|
|
107
|
-
return { exitCode: 0 };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'verify': {
|
|
111
|
-
const lastTag = getLastTag();
|
|
112
|
-
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
113
|
-
const subjects = getCommitSubjects(range);
|
|
114
|
-
const { tokens } = parseCommitsForPlanTokens(subjects);
|
|
115
|
-
|
|
116
|
-
const entryBody = getLatestChangelogEntryBody(repoRoot);
|
|
117
|
-
const gaps = findCoverageGaps(entryBody, tokens);
|
|
118
|
-
|
|
119
|
-
if (gaps.length === 0) {
|
|
120
|
-
process.stdout.write('All plan-tokens referenced.\n');
|
|
121
|
-
return { exitCode: 0 };
|
|
122
|
-
}
|
|
123
|
-
for (const t of gaps) {
|
|
124
|
-
process.stderr.write(`gap: ${t}\n`);
|
|
125
|
-
}
|
|
126
|
-
return { exitCode: 1 };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
case '--help':
|
|
130
|
-
case '-h':
|
|
131
|
-
case undefined: {
|
|
132
|
-
printChangelogHelp();
|
|
133
|
-
return { exitCode: 0 };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
default: {
|
|
137
|
-
process.stderr.write(`massu: unknown changelog subcommand: ${sub}\n`);
|
|
138
|
-
printChangelogHelp();
|
|
139
|
-
return { exitCode: 1 };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function printChangelogHelp(): void {
|
|
145
|
-
process.stdout.write(`
|
|
146
|
-
massu changelog <subcommand>
|
|
147
|
-
|
|
148
|
-
Subcommands:
|
|
149
|
-
generate Emit a draft CHANGELOG.md entry to stdout. Reads commit subjects
|
|
150
|
-
since the last git tag, groups by (plan-<token>) paren-notation,
|
|
151
|
-
looks up each plan file's ## Changelog Summary section, and emits
|
|
152
|
-
a Keep-a-Changelog 1.1.0 entry. Operator pipes/copies into
|
|
153
|
-
CHANGELOG.md (no forced overwrite).
|
|
154
|
-
|
|
155
|
-
verify Read-only check that the latest CHANGELOG.md entry references
|
|
156
|
-
every plan-token in commits since the last tag. Exit 0 if clean,
|
|
157
|
-
exit 1 with one 'gap: <token>' per missing.
|
|
158
|
-
|
|
159
|
-
Examples:
|
|
160
|
-
npx massu changelog generate > /tmp/draft-entry.md
|
|
161
|
-
npx massu changelog verify
|
|
162
|
-
|
|
163
|
-
Documentation: https://massu.ai/docs/reference/cli-reference#massu-changelog
|
|
164
|
-
`);
|
|
165
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `massu config check-drift` — CI-safe drift gate.
|
|
6
|
-
*
|
|
7
|
-
* Reads massu.config.yaml, runs detection, and compares the result with both
|
|
8
|
-
* the stored fingerprint (if present) and the structural detectDrift() check.
|
|
9
|
-
*
|
|
10
|
-
* Flags:
|
|
11
|
-
* --verbose Emit the full changes[] to stdout (field: before -> after).
|
|
12
|
-
*
|
|
13
|
-
* Exit codes:
|
|
14
|
-
* 0 no drift
|
|
15
|
-
* 1 drift detected
|
|
16
|
-
* 2 config missing or unparseable
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { existsSync, readFileSync } from 'fs';
|
|
20
|
-
import { resolve } from 'path';
|
|
21
|
-
import { parse as parseYaml } from 'yaml';
|
|
22
|
-
import { runDetection } from '../detect/index.ts';
|
|
23
|
-
import { computeFingerprint, detectDrift, type DriftChange } from '../detect/drift.ts';
|
|
24
|
-
import type { AnyConfig } from '../detect/migrate.ts';
|
|
25
|
-
|
|
26
|
-
export interface ConfigCheckDriftOptions {
|
|
27
|
-
verbose?: boolean;
|
|
28
|
-
cwd?: string;
|
|
29
|
-
silent?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ConfigCheckDriftResult {
|
|
33
|
-
exitCode: 0 | 1 | 2;
|
|
34
|
-
drifted: boolean;
|
|
35
|
-
changes: DriftChange[];
|
|
36
|
-
storedFingerprint: string | null;
|
|
37
|
-
currentFingerprint: string | null;
|
|
38
|
-
message?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function renderChanges(changes: DriftChange[]): string {
|
|
42
|
-
if (changes.length === 0) return '(none)\n';
|
|
43
|
-
return changes
|
|
44
|
-
.map((c) => ` ${c.field}: ${JSON.stringify(c.before)} -> ${JSON.stringify(c.after)}`)
|
|
45
|
-
.join('\n') + '\n';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function runConfigCheckDrift(
|
|
49
|
-
opts: ConfigCheckDriftOptions = {}
|
|
50
|
-
): Promise<ConfigCheckDriftResult> {
|
|
51
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
52
|
-
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
53
|
-
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
54
|
-
const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
|
|
55
|
-
|
|
56
|
-
if (!existsSync(configPath)) {
|
|
57
|
-
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
58
|
-
err(message + '\n');
|
|
59
|
-
return {
|
|
60
|
-
exitCode: 2,
|
|
61
|
-
drifted: false,
|
|
62
|
-
changes: [],
|
|
63
|
-
storedFingerprint: null,
|
|
64
|
-
currentFingerprint: null,
|
|
65
|
-
message,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let config: AnyConfig;
|
|
70
|
-
try {
|
|
71
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
72
|
-
// pattern-scanner-allow: yaml-parse — reason: v1.1.0 drift detector. Raw YAML compare needed at arbitrary cwd before getConfig() cache populates; the drift detector intentionally bypasses the cached parse to inspect on-disk state.
|
|
73
|
-
const parsed = parseYaml(content);
|
|
74
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
75
|
-
throw new Error('config is not a YAML object');
|
|
76
|
-
}
|
|
77
|
-
config = parsed as AnyConfig;
|
|
78
|
-
} catch (e) {
|
|
79
|
-
const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
|
|
80
|
-
err(message + '\n');
|
|
81
|
-
return {
|
|
82
|
-
exitCode: 2,
|
|
83
|
-
drifted: false,
|
|
84
|
-
changes: [],
|
|
85
|
-
storedFingerprint: null,
|
|
86
|
-
currentFingerprint: null,
|
|
87
|
-
message,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const detection = await runDetection(cwd);
|
|
92
|
-
const currentFp = computeFingerprint(detection);
|
|
93
|
-
const storedFp =
|
|
94
|
-
typeof (config.detection as Record<string, unknown> | undefined)?.fingerprint === 'string'
|
|
95
|
-
? ((config.detection as Record<string, unknown>).fingerprint as string)
|
|
96
|
-
: null;
|
|
97
|
-
|
|
98
|
-
const report = detectDrift(config, detection);
|
|
99
|
-
const fingerprintDrift = storedFp !== null && storedFp !== currentFp;
|
|
100
|
-
const drifted = report.drifted || fingerprintDrift;
|
|
101
|
-
|
|
102
|
-
if (!drifted) {
|
|
103
|
-
log('No drift detected.\n');
|
|
104
|
-
if (opts.verbose) {
|
|
105
|
-
log(`Fingerprint: ${currentFp}\n`);
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
exitCode: 0,
|
|
109
|
-
drifted: false,
|
|
110
|
-
changes: report.changes,
|
|
111
|
-
storedFingerprint: storedFp,
|
|
112
|
-
currentFingerprint: currentFp,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
err('Config drift detected; run `npx massu config refresh` to update.\n');
|
|
117
|
-
if (opts.verbose) {
|
|
118
|
-
if (storedFp !== null) {
|
|
119
|
-
log(`Fingerprint: ${storedFp.slice(0, 16)} -> ${currentFp.slice(0, 16)}\n`);
|
|
120
|
-
} else {
|
|
121
|
-
log(`Fingerprint (new): ${currentFp.slice(0, 16)}\n`);
|
|
122
|
-
}
|
|
123
|
-
log('Changes:\n');
|
|
124
|
-
log(renderChanges(report.changes));
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
exitCode: 1,
|
|
128
|
-
drifted: true,
|
|
129
|
-
changes: report.changes,
|
|
130
|
-
storedFingerprint: storedFp,
|
|
131
|
-
currentFingerprint: currentFp,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `massu config refresh` — re-run detection, diff against current config, apply
|
|
6
|
-
* or print-only (--dry-run).
|
|
7
|
-
*
|
|
8
|
-
* Merge semantics:
|
|
9
|
-
* - Detector-owned keys (framework, paths.source, verification, detection) are REFRESHED.
|
|
10
|
-
* - User-authored keys (rules, domains, canonical_paths, accessScopes,
|
|
11
|
-
* knownMismatches, dbAccessPattern, analytics, governance, security, team,
|
|
12
|
-
* regression, cloud, conventions, autoLearning, verification_types,
|
|
13
|
-
* python) are PRESERVED verbatim from the existing config.
|
|
14
|
-
*
|
|
15
|
-
* Flags:
|
|
16
|
-
* --dry-run Emit the diff to stdout, exit 0, never write.
|
|
17
|
-
* (none) Interactive: show diff, prompt for confirmation via @clack/prompts.
|
|
18
|
-
* When stdin is not a TTY, behaves as --dry-run with a note.
|
|
19
|
-
*
|
|
20
|
-
* Exit codes:
|
|
21
|
-
* 0 success (applied, or dry-run completed)
|
|
22
|
-
* 1 missing massu.config.yaml (run `massu init`)
|
|
23
|
-
* 2 unparseable massu.config.yaml
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
27
|
-
import { resolve } from 'path';
|
|
28
|
-
import { parse as parseYaml } from 'yaml';
|
|
29
|
-
import { runDetection } from '../detect/index.ts';
|
|
30
|
-
import { computeFingerprint } from '../detect/drift.ts';
|
|
31
|
-
import type { AnyConfig } from '../detect/migrate.ts';
|
|
32
|
-
import { copyUnknownKeys, preserveNestedSubkeys } from '../detect/passthrough.ts';
|
|
33
|
-
import { buildConfigFromDetection, renderConfigYaml, writeConfigAtomic } from './init.ts';
|
|
34
|
-
import { installAll } from './install-commands.ts';
|
|
35
|
-
import { resetConfig } from '../config.ts';
|
|
36
|
-
import { withInstallLock } from '../lib/installLock.ts';
|
|
37
|
-
|
|
38
|
-
const PRESERVED_FIELDS = [
|
|
39
|
-
'rules',
|
|
40
|
-
'domains',
|
|
41
|
-
'canonical_paths',
|
|
42
|
-
'verification_types',
|
|
43
|
-
'accessScopes',
|
|
44
|
-
'knownMismatches',
|
|
45
|
-
'dbAccessPattern',
|
|
46
|
-
'analytics',
|
|
47
|
-
'governance',
|
|
48
|
-
'security',
|
|
49
|
-
'team',
|
|
50
|
-
'regression',
|
|
51
|
-
'cloud',
|
|
52
|
-
'conventions',
|
|
53
|
-
'autoLearning',
|
|
54
|
-
'python',
|
|
55
|
-
'toolPrefix',
|
|
56
|
-
] as const;
|
|
57
|
-
|
|
58
|
-
export interface ConfigRefreshOptions {
|
|
59
|
-
dryRun?: boolean;
|
|
60
|
-
cwd?: string;
|
|
61
|
-
silent?: boolean;
|
|
62
|
-
/**
|
|
63
|
-
* Plan #2 P4-001: when true, skip the post-refresh `installAll` call so
|
|
64
|
-
* `.claude/commands/` is NOT re-templated. Used by tests to keep file I/O
|
|
65
|
-
* hermetic, and by users who want config-only refresh behavior.
|
|
66
|
-
*/
|
|
67
|
-
skipCommands?: boolean;
|
|
68
|
-
/**
|
|
69
|
-
* Plan 3a Phase 6: when true, bypass BOTH the non-TTY bail and the
|
|
70
|
-
* `@clack/prompts` confirm gate. The watcher daemon and `--yes` CLI flag
|
|
71
|
-
* use this to auto-apply detected changes. Combined with
|
|
72
|
-
* `skipCommands: true`, it lets the watcher delegate `installAll` to its
|
|
73
|
-
* own outer call (single install, single lock acquire — see iter-3 G3-A9).
|
|
74
|
-
*/
|
|
75
|
-
autoYes?: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface ConfigRefreshResult {
|
|
79
|
-
exitCode: 0 | 1 | 2;
|
|
80
|
-
applied: boolean;
|
|
81
|
-
dryRun: boolean;
|
|
82
|
-
diff: DiffLine[];
|
|
83
|
-
message?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface DiffLine {
|
|
87
|
-
kind: 'add' | 'remove' | 'change' | 'same';
|
|
88
|
-
path: string;
|
|
89
|
-
before?: unknown;
|
|
90
|
-
after?: unknown;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function flatten(obj: unknown, prefix = ''): Record<string, unknown> {
|
|
94
|
-
const out: Record<string, unknown> = {};
|
|
95
|
-
if (obj === null || obj === undefined) {
|
|
96
|
-
out[prefix || '<root>'] = obj;
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
|
100
|
-
out[prefix || '<root>'] = obj;
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
const rec = obj as Record<string, unknown>;
|
|
104
|
-
for (const [k, v] of Object.entries(rec)) {
|
|
105
|
-
const p = prefix ? `${prefix}.${k}` : k;
|
|
106
|
-
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
107
|
-
Object.assign(out, flatten(v, p));
|
|
108
|
-
} else {
|
|
109
|
-
out[p] = v;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function computeDiff(before: AnyConfig, after: AnyConfig): DiffLine[] {
|
|
116
|
-
const b = flatten(before);
|
|
117
|
-
const a = flatten(after);
|
|
118
|
-
const keys = new Set<string>([...Object.keys(b), ...Object.keys(a)]);
|
|
119
|
-
const sorted = [...keys].sort();
|
|
120
|
-
const lines: DiffLine[] = [];
|
|
121
|
-
for (const k of sorted) {
|
|
122
|
-
const bVal = b[k];
|
|
123
|
-
const aVal = a[k];
|
|
124
|
-
const bHas = k in b;
|
|
125
|
-
const aHas = k in a;
|
|
126
|
-
if (bHas && !aHas) {
|
|
127
|
-
lines.push({ kind: 'remove', path: k, before: bVal });
|
|
128
|
-
} else if (!bHas && aHas) {
|
|
129
|
-
lines.push({ kind: 'add', path: k, after: aVal });
|
|
130
|
-
} else if (JSON.stringify(bVal) !== JSON.stringify(aVal)) {
|
|
131
|
-
lines.push({ kind: 'change', path: k, before: bVal, after: aVal });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return lines;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function mergeRefresh(existing: AnyConfig, refreshed: AnyConfig): AnyConfig {
|
|
138
|
-
// P1-008: Start from detector output (fresh framework, paths.source, verification, detection).
|
|
139
|
-
const out: AnyConfig = { ...refreshed };
|
|
140
|
-
|
|
141
|
-
// Restore user-authored top-level sections verbatim.
|
|
142
|
-
for (const field of PRESERVED_FIELDS) {
|
|
143
|
-
if (existing[field] !== undefined) {
|
|
144
|
-
out[field] = existing[field];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Restore toolPrefix from existing (never let detector-defaulted 'massu' overwrite a custom prefix).
|
|
149
|
-
if (typeof existing.toolPrefix === 'string' && existing.toolPrefix !== '') {
|
|
150
|
-
out.toolPrefix = existing.toolPrefix;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// For detector-owned blocks (framework, paths, project), preserve any user subkey the detector didn't emit.
|
|
154
|
-
for (const block of ['framework', 'paths', 'project'] as const) {
|
|
155
|
-
const existingBlock = existing[block];
|
|
156
|
-
const outBlock = out[block];
|
|
157
|
-
if (
|
|
158
|
-
existingBlock && typeof existingBlock === 'object' && !Array.isArray(existingBlock) &&
|
|
159
|
-
outBlock && typeof outBlock === 'object' && !Array.isArray(outBlock)
|
|
160
|
-
) {
|
|
161
|
-
preserveNestedSubkeys(existingBlock, outBlock as Record<string, unknown>);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Restore user-set project.root (detector at init.ts:418 always writes 'auto'; user value wins).
|
|
166
|
-
// Separated from the block loop above for readability (A-004 architecture-review follow-up).
|
|
167
|
-
const existingProject = existing.project;
|
|
168
|
-
const outProject = out.project;
|
|
169
|
-
if (
|
|
170
|
-
existingProject && typeof existingProject === 'object' && !Array.isArray(existingProject) &&
|
|
171
|
-
outProject && typeof outProject === 'object' && !Array.isArray(outProject)
|
|
172
|
-
) {
|
|
173
|
-
const userRoot = (existingProject as Record<string, unknown>).root;
|
|
174
|
-
if (typeof userRoot === 'string' && userRoot !== '') {
|
|
175
|
-
(outProject as Record<string, unknown>).root = userRoot;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// paths.aliases is a 2-level-nested user block. Detector always writes
|
|
180
|
-
// { '@': <source-dir> }; user-authored alias map must survive. Spread user
|
|
181
|
-
// over detector so user keys win for any overlap AND user-only keys survive.
|
|
182
|
-
// (P5-002 discovery — a downstream consumer's paths.aliases['@'] was being overwritten.)
|
|
183
|
-
const existingPaths = existing.paths;
|
|
184
|
-
const outPaths = out.paths;
|
|
185
|
-
if (
|
|
186
|
-
existingPaths && typeof existingPaths === 'object' && !Array.isArray(existingPaths) &&
|
|
187
|
-
outPaths && typeof outPaths === 'object' && !Array.isArray(outPaths)
|
|
188
|
-
) {
|
|
189
|
-
const existingAliases = (existingPaths as Record<string, unknown>).aliases;
|
|
190
|
-
const outAliases = (outPaths as Record<string, unknown>).aliases;
|
|
191
|
-
if (
|
|
192
|
-
existingAliases && typeof existingAliases === 'object' && !Array.isArray(existingAliases) &&
|
|
193
|
-
outAliases && typeof outAliases === 'object' && !Array.isArray(outAliases)
|
|
194
|
-
) {
|
|
195
|
-
(outPaths as Record<string, unknown>).aliases = {
|
|
196
|
-
...(outAliases as Record<string, unknown>),
|
|
197
|
-
...(existingAliases as Record<string, unknown>),
|
|
198
|
-
};
|
|
199
|
-
} else if (existingAliases && typeof existingAliases === 'object' && !Array.isArray(existingAliases)) {
|
|
200
|
-
(outPaths as Record<string, unknown>).aliases = existingAliases;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// verification is the other 2-level-nested detector-owned block. Semantics
|
|
205
|
-
// mirror migrate.ts:132-138 buildVerificationBlock: user's custom language
|
|
206
|
-
// sections (e.g., a multi-runtime monorepo's `gateway`, `ios`, `runtime`, `web`) survive
|
|
207
|
-
// wholesale; user's command overrides on shared languages (e.g., `python`)
|
|
208
|
-
// win over detector defaults. (P5-002 discovery — a downstream consumer was losing 15
|
|
209
|
-
// verification command entries across 4 custom language sections plus
|
|
210
|
-
// having 4 python commands overwritten with detector defaults.)
|
|
211
|
-
const existingVer = existing.verification;
|
|
212
|
-
const outVer = out.verification;
|
|
213
|
-
if (
|
|
214
|
-
existingVer && typeof existingVer === 'object' && !Array.isArray(existingVer) &&
|
|
215
|
-
outVer && typeof outVer === 'object' && !Array.isArray(outVer)
|
|
216
|
-
) {
|
|
217
|
-
const eVer = existingVer as Record<string, unknown>;
|
|
218
|
-
const oVer = outVer as Record<string, unknown>;
|
|
219
|
-
for (const lang of Object.keys(eVer)) {
|
|
220
|
-
const userLang = eVer[lang];
|
|
221
|
-
if (userLang === undefined) continue;
|
|
222
|
-
if (!(lang in oVer)) {
|
|
223
|
-
// User-custom language (no detector counterpart) → preserve wholesale.
|
|
224
|
-
oVer[lang] = userLang;
|
|
225
|
-
} else if (
|
|
226
|
-
userLang && typeof userLang === 'object' && !Array.isArray(userLang) &&
|
|
227
|
-
oVer[lang] && typeof oVer[lang] === 'object' && !Array.isArray(oVer[lang])
|
|
228
|
-
) {
|
|
229
|
-
// Shared language → user commands win over detector defaults (spread).
|
|
230
|
-
oVer[lang] = {
|
|
231
|
-
...(oVer[lang] as Record<string, unknown>),
|
|
232
|
-
...(userLang as Record<string, unknown>),
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Preserve top-level user keys not handled above (mirrors P1-001 passthrough for upgrade).
|
|
239
|
-
const handledTopLevel = new Set<string>([
|
|
240
|
-
'schema_version', 'project', 'framework', 'paths', 'toolPrefix', 'verification', 'detection',
|
|
241
|
-
...PRESERVED_FIELDS,
|
|
242
|
-
]);
|
|
243
|
-
copyUnknownKeys(existing, out, handledTopLevel);
|
|
244
|
-
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function renderDiff(diff: DiffLine[]): string {
|
|
249
|
-
if (diff.length === 0) return '(no changes)\n';
|
|
250
|
-
const lines: string[] = [];
|
|
251
|
-
for (const d of diff) {
|
|
252
|
-
if (d.kind === 'add') lines.push(`+ ${d.path}: ${JSON.stringify(d.after)}`);
|
|
253
|
-
else if (d.kind === 'remove') lines.push(`- ${d.path}: ${JSON.stringify(d.before)}`);
|
|
254
|
-
else if (d.kind === 'change') {
|
|
255
|
-
lines.push(`~ ${d.path}: ${JSON.stringify(d.before)} -> ${JSON.stringify(d.after)}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return lines.join('\n') + '\n';
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export async function runConfigRefresh(opts: ConfigRefreshOptions = {}): Promise<ConfigRefreshResult> {
|
|
262
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
263
|
-
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
264
|
-
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
265
|
-
|
|
266
|
-
if (!existsSync(configPath)) {
|
|
267
|
-
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
268
|
-
if (!opts.silent) process.stderr.write(message + '\n');
|
|
269
|
-
return { exitCode: 1, applied: false, dryRun: !!opts.dryRun, diff: [], message };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let existing: AnyConfig;
|
|
273
|
-
try {
|
|
274
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
275
|
-
// pattern-scanner-allow: yaml-parse — reason: v1.1.0 config lifecycle command. getConfig() Zod-rejects pre-migration v1 configs (which is exactly what `massu config refresh` is meant to read + migrate). Raw parse is required to inspect a pre-upgrade YAML structure.
|
|
276
|
-
const parsed = parseYaml(content);
|
|
277
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
278
|
-
throw new Error('config is not a YAML object');
|
|
279
|
-
}
|
|
280
|
-
existing = parsed as AnyConfig;
|
|
281
|
-
} catch (err) {
|
|
282
|
-
const message = `Failed to parse massu.config.yaml: ${err instanceof Error ? err.message : String(err)}`;
|
|
283
|
-
if (!opts.silent) process.stderr.write(message + '\n');
|
|
284
|
-
return { exitCode: 2, applied: false, dryRun: !!opts.dryRun, diff: [], message };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const detection = await runDetection(cwd);
|
|
288
|
-
const refreshed = buildConfigFromDetection({
|
|
289
|
-
projectRoot: cwd,
|
|
290
|
-
detection,
|
|
291
|
-
projectName: typeof (existing.project as Record<string, unknown> | undefined)?.name === 'string'
|
|
292
|
-
? (existing.project as Record<string, unknown>).name as string
|
|
293
|
-
: undefined,
|
|
294
|
-
});
|
|
295
|
-
// buildConfigFromDetection already stamps detection.fingerprint. Double-check.
|
|
296
|
-
if (!(refreshed.detection as Record<string, unknown> | undefined)?.fingerprint) {
|
|
297
|
-
refreshed.detection = { fingerprint: computeFingerprint(detection) };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const merged = mergeRefresh(existing, refreshed);
|
|
301
|
-
const diff = computeDiff(existing, merged);
|
|
302
|
-
|
|
303
|
-
if (opts.dryRun) {
|
|
304
|
-
log('Config diff (dry-run; no changes written):\n');
|
|
305
|
-
log(renderDiff(diff));
|
|
306
|
-
return { exitCode: 0, applied: false, dryRun: true, diff };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (diff.length === 0) {
|
|
310
|
-
log('No changes needed — config is already up to date.\n');
|
|
311
|
-
return { exitCode: 0, applied: false, dryRun: false, diff };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Plan 3a Phase 6: when autoYes=true, skip BOTH the non-TTY bail and the
|
|
315
|
-
// confirm gate so the watcher (daemon stdin is detached) and the
|
|
316
|
-
// `--yes`/-y CLI flag actually apply changes.
|
|
317
|
-
if (!opts.autoYes) {
|
|
318
|
-
// Interactive prompt; fall back to dry-run semantics when not a TTY.
|
|
319
|
-
if (!process.stdin.isTTY) {
|
|
320
|
-
log('Config diff (non-interactive; pass --dry-run to suppress this note or run interactively to apply):\n');
|
|
321
|
-
log(renderDiff(diff));
|
|
322
|
-
return {
|
|
323
|
-
exitCode: 0,
|
|
324
|
-
applied: false,
|
|
325
|
-
dryRun: false,
|
|
326
|
-
diff,
|
|
327
|
-
message: 'non-interactive shell; no changes written',
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
log('Config diff:\n');
|
|
332
|
-
log(renderDiff(diff));
|
|
333
|
-
const { confirm } = await import('@clack/prompts');
|
|
334
|
-
const apply = await confirm({ message: 'Apply these changes to massu.config.yaml?' });
|
|
335
|
-
if (apply !== true) {
|
|
336
|
-
log('Aborted; no changes written.\n');
|
|
337
|
-
return { exitCode: 0, applied: false, dryRun: false, diff, message: 'aborted by user' };
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
log('Config diff (auto-applying via --yes / watcher):\n');
|
|
341
|
-
log(renderDiff(diff));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const yamlContent = renderConfigYaml(merged);
|
|
345
|
-
const writeRes = writeConfigAtomic(configPath, yamlContent);
|
|
346
|
-
if (!writeRes.validated) {
|
|
347
|
-
const message = `Failed to write config: ${writeRes.error}`;
|
|
348
|
-
if (!opts.silent) process.stderr.write(message + '\n');
|
|
349
|
-
return { exitCode: 2, applied: false, dryRun: false, diff, message };
|
|
350
|
-
}
|
|
351
|
-
log('Config refreshed.\n');
|
|
352
|
-
|
|
353
|
-
// Plan #2 P4-001: re-template `.claude/commands/` against the freshly
|
|
354
|
-
// written config so newly-detected stack changes get the right scaffolds.
|
|
355
|
-
// P4-003 (auto-delete half): if a stack is now declared and the empty-init
|
|
356
|
-
// placeholder still exists, remove it.
|
|
357
|
-
if (!opts.skipCommands) {
|
|
358
|
-
log('Will also re-template command files; pass --skip-commands to opt out.\n');
|
|
359
|
-
// Reset cached config so installAll reads the freshly-written YAML.
|
|
360
|
-
resetConfig();
|
|
361
|
-
try {
|
|
362
|
-
const installResult = withInstallLock(cwd, () => installAll(cwd));
|
|
363
|
-
const total =
|
|
364
|
-
installResult.totalInstalled +
|
|
365
|
-
installResult.totalUpdated +
|
|
366
|
-
installResult.totalSkipped +
|
|
367
|
-
installResult.totalKept;
|
|
368
|
-
log(`Re-templated ${total} command files (${installResult.totalInstalled} new, ${installResult.totalUpdated} updated).\n`);
|
|
369
|
-
|
|
370
|
-
// Auto-delete the empty-init placeholder if at least one stack-specific
|
|
371
|
-
// command was resolved this run (i.e., a non-zero install/update count).
|
|
372
|
-
const stackResolved =
|
|
373
|
-
installResult.totalInstalled > 0 || installResult.totalUpdated > 0;
|
|
374
|
-
if (stackResolved) {
|
|
375
|
-
const placeholderPath = resolve(installResult.claudeDir, 'commands', '_massu-needs-stack.md');
|
|
376
|
-
if (existsSync(placeholderPath)) {
|
|
377
|
-
try {
|
|
378
|
-
rmSync(placeholderPath, { force: true });
|
|
379
|
-
log('Removed _massu-needs-stack.md (stack now declared).\n');
|
|
380
|
-
} catch {
|
|
381
|
-
// Best-effort: never block refresh on placeholder cleanup.
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
} catch (err) {
|
|
386
|
-
// Don't fail the whole refresh if re-template breaks; the YAML was already written.
|
|
387
|
-
// InstallLockBusyError.message already follows plan §243 format —
|
|
388
|
-
// `installAll already running (PID=X) — try again in <N>s` — so we
|
|
389
|
-
// surface it verbatim rather than re-wrapping.
|
|
390
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
391
|
-
if (!opts.silent) process.stderr.write(`Warning: re-template failed: ${msg}\n`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return { exitCode: 0, applied: true, dryRun: false, diff };
|
|
396
|
-
}
|