@mmnto/cli 1.17.0 → 1.18.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/dist/commands/adr.d.ts.map +1 -1
- package/dist/commands/adr.js +3 -1
- package/dist/commands/adr.js.map +1 -1
- package/dist/commands/adr.test.js +2 -0
- package/dist/commands/adr.test.js.map +1 -1
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +63 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +62 -2
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/proposal.d.ts.map +1 -1
- package/dist/commands/proposal.js +3 -1
- package/dist/commands/proposal.js.map +1 -1
- package/dist/commands/proposal.test.js +1 -0
- package/dist/commands/proposal.test.js.map +1 -1
- package/dist/commands/retrospect.d.ts.map +1 -1
- package/dist/commands/retrospect.js +10 -21
- package/dist/commands/retrospect.js.map +1 -1
- package/dist/commands/retrospect.test.js +12 -5
- package/dist/commands/retrospect.test.js.map +1 -1
- package/dist/commands/shield-estimate.d.ts +8 -0
- package/dist/commands/shield-estimate.d.ts.map +1 -1
- package/dist/commands/shield-estimate.js +224 -0
- package/dist/commands/shield-estimate.js.map +1 -1
- package/dist/commands/shield-estimate.test.js +570 -0
- package/dist/commands/shield-estimate.test.js.map +1 -1
- package/dist/commands/shield.d.ts +8 -0
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/terminal-sanitize.d.ts +28 -0
- package/dist/terminal-sanitize.d.ts.map +1 -0
- package/dist/terminal-sanitize.js +36 -0
- package/dist/terminal-sanitize.js.map +1 -0
- package/dist/terminal-sanitize.test.d.ts +2 -0
- package/dist/terminal-sanitize.test.d.ts.map +1 -0
- package/dist/terminal-sanitize.test.js +46 -0
- package/dist/terminal-sanitize.test.js.map +1 -0
- package/dist/utils/governance.d.ts +38 -9
- package/dist/utils/governance.d.ts.map +1 -1
- package/dist/utils/governance.js +75 -23
- package/dist/utils/governance.js.map +1 -1
- package/dist/utils/governance.test.js +124 -1
- package/dist/utils/governance.test.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -0
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sanitizeForTerminal } from './terminal-sanitize.js';
|
|
3
|
+
describe('sanitizeForTerminal', () => {
|
|
4
|
+
it('strips CSI escape sequences (ESC [ … final byte)', () => {
|
|
5
|
+
expect(sanitizeForTerminal('\x1b[31mred\x1b[0m')).toBe('red');
|
|
6
|
+
expect(sanitizeForTerminal('a\x1b[1;33mb\x1b[mc')).toBe('abc');
|
|
7
|
+
});
|
|
8
|
+
it('replaces C0 control bytes with a space, preserving \\n and \\t', () => {
|
|
9
|
+
// \x07 BEL, \x08 BS, \x0b VT, \x0c FF, \x0e SO, \x1f US — all should become spaces.
|
|
10
|
+
expect(sanitizeForTerminal('a\x07b\x08c\x0bd\x0ce\x0ef\x1fg')).toBe('a b c d e f g');
|
|
11
|
+
// \n and \t survive (caller may collapse later).
|
|
12
|
+
expect(sanitizeForTerminal('line\n\twith\twhitespace')).toBe('line\n\twith\twhitespace');
|
|
13
|
+
// \x7f DEL also strips.
|
|
14
|
+
expect(sanitizeForTerminal('del\x7fhere')).toBe('del here');
|
|
15
|
+
});
|
|
16
|
+
it('strips \\r (CR) — a bare CR rewinds the terminal cursor', () => {
|
|
17
|
+
// CR mmnto-ai/totem#1739 R3 Critical: \r alone overwrites the current
|
|
18
|
+
// terminal line; preserving it would defeat the sanitizer's whole
|
|
19
|
+
// purpose. \n still survives.
|
|
20
|
+
expect(sanitizeForTerminal('safe\rrewound')).toBe('safe rewound');
|
|
21
|
+
expect(sanitizeForTerminal('safe\r\nwindows')).toBe('safe \nwindows');
|
|
22
|
+
// No CR survives anywhere in the output.
|
|
23
|
+
expect(sanitizeForTerminal('a\rb\rc\rd')).not.toContain('\r');
|
|
24
|
+
});
|
|
25
|
+
it('replaces C1 control bytes (\\x80-\\x9f) with a space — closes the original \\x7f gap', () => {
|
|
26
|
+
// \x9b is the 8-bit CSI introducer — directly equivalent to ESC [ on
|
|
27
|
+
// 8-bit-clean terminals. CR mmnto-ai/totem#1739 R2 caught the original
|
|
28
|
+
// regex stopped at \x7f leaving the C1 range open.
|
|
29
|
+
expect(sanitizeForTerminal('csi-8bit\x9bsequence')).toBe('csi-8bit sequence');
|
|
30
|
+
// Sweep the whole C1 range.
|
|
31
|
+
for (let cp = 0x80; cp <= 0x9f; cp += 1) {
|
|
32
|
+
const ctrl = String.fromCharCode(cp);
|
|
33
|
+
const out = sanitizeForTerminal(`a${ctrl}b`);
|
|
34
|
+
expect(out).toBe('a b');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
it('passes printable text through unchanged', () => {
|
|
38
|
+
expect(sanitizeForTerminal('plain text 123 !@#$%^&*()')).toBe('plain text 123 !@#$%^&*()');
|
|
39
|
+
// Unicode above C1 is preserved.
|
|
40
|
+
expect(sanitizeForTerminal('café — naïve')).toBe('café — naïve');
|
|
41
|
+
});
|
|
42
|
+
it('returns the empty string unchanged', () => {
|
|
43
|
+
expect(sanitizeForTerminal('')).toBe('');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=terminal-sanitize.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-sanitize.test.js","sourceRoot":"","sources":["../src/terminal-sanitize.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,mBAAmB,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9D,MAAM,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,oFAAoF;QACpF,MAAM,CAAC,mBAAmB,CAAC,iCAAiC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACrF,iDAAiD;QACjD,MAAM,CAAC,mBAAmB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACzF,wBAAwB;QACxB,MAAM,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,sEAAsE;QACtE,kEAAkE;QAClE,8BAA8B;QAC9B,MAAM,CAAC,mBAAmB,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAClE,MAAM,CAAC,mBAAmB,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACtE,yCAAyC;QACzC,MAAM,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,qEAAqE;QACrE,uEAAuE;QACvE,mDAAmD;QACnD,MAAM,CAAC,mBAAmB,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC9E,4BAA4B;QAC5B,KAAK,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YACrC,MAAM,GAAG,GAAG,mBAAmB,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,mBAAmB,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAC3F,iCAAiC;QACjC,MAAM,CAAC,mBAAmB,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
* its context via arguments so the tests can exercise both the submodule
|
|
7
7
|
* (`<totem>/.strategy/`) and the standalone (strategy-repo root) cases.
|
|
8
8
|
*/
|
|
9
|
+
import { type StrategyResolverConfig } from '@mmnto/totem';
|
|
9
10
|
export type GovernanceType = 'proposal' | 'adr';
|
|
10
11
|
export interface ScaffoldOptions {
|
|
11
12
|
type: GovernanceType;
|
|
12
13
|
title: string;
|
|
13
14
|
cwd: string;
|
|
15
|
+
/**
|
|
16
|
+
* Loaded `TotemConfig` (or any object with a `strategyRoot?: string` field).
|
|
17
|
+
* Forwarded to the strategy-root resolver so `TotemConfig.strategyRoot`
|
|
18
|
+
* wins precedence-2 over the sibling / submodule layers (mmnto-ai/totem#1710).
|
|
19
|
+
*/
|
|
20
|
+
config?: StrategyResolverConfig;
|
|
14
21
|
}
|
|
15
22
|
export interface GovernancePaths {
|
|
16
23
|
/** Directory that anchors the governance layout (strategy repo root or submodule root). */
|
|
@@ -22,20 +29,42 @@ export interface GovernancePaths {
|
|
|
22
29
|
/** Dashboard README refreshed by `docs:inject`. */
|
|
23
30
|
dashboardFile: string;
|
|
24
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort `TotemConfig` load for the governance commands. Returns
|
|
34
|
+
* `undefined` when the config is missing, unparseable, OR resolves to the
|
|
35
|
+
* global `~/.totem/` profile — both missing and unparseable are legitimate
|
|
36
|
+
* states for a freshly-cloned consumer repo, and a global-profile config
|
|
37
|
+
* is intentionally NOT used as a repo-local `strategyRoot` source (that
|
|
38
|
+
* would let one user's personal pointer leak across every repo on disk).
|
|
39
|
+
* The strategy-root resolver still has env / sibling / submodule layers
|
|
40
|
+
* to fall back on (mmnto-ai/totem#1710 R3 — CR R3 global-config-leak fix).
|
|
41
|
+
*
|
|
42
|
+
* Shared by `proposalNewCommand` and `adrNewCommand` so the load idiom +
|
|
43
|
+
* its `// totem-context: intentional best-effort` annotation live in one
|
|
44
|
+
* place (mmnto-ai/totem#1710 R2).
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadGovernanceConfig(cwd: string): Promise<StrategyResolverConfig | undefined>;
|
|
25
47
|
/**
|
|
26
48
|
* Resolve governance paths for the current invocation.
|
|
27
49
|
*
|
|
28
|
-
* Two supported
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
50
|
+
* Two layout shapes supported:
|
|
51
|
+
*
|
|
52
|
+
* 1. **Standalone (cwd IS the strategy repo)** — `<gitRoot>` itself contains
|
|
53
|
+
* `proposals/` or `adr/` at the top level. Used when the CLI runs from
|
|
54
|
+
* inside the strategy repo directly. Detected here in the helper before
|
|
55
|
+
* delegating to the resolver because the strategy-root resolver answers
|
|
56
|
+
* "where IS the strategy repo from somewhere else", not "are we INSIDE
|
|
57
|
+
* one already."
|
|
58
|
+
*
|
|
59
|
+
* 2. **Resolved (cwd is in a consuming repo)** — defer to
|
|
60
|
+
* `resolveStrategyRoot` (mmnto-ai/totem#1710). The resolver walks env →
|
|
61
|
+
* config → sibling → submodule precedence and returns an absolute path.
|
|
62
|
+
* Throws an actionable `TotemError` (ADR-088) when none of the layers
|
|
63
|
+
* resolve.
|
|
34
64
|
*
|
|
35
|
-
*
|
|
36
|
-
* layout can be detected.
|
|
65
|
+
* Also throws `TotemError` when cwd is not inside a git repo.
|
|
37
66
|
*/
|
|
38
|
-
export declare function resolveGovernancePaths(cwd: string, type: GovernanceType): GovernancePaths;
|
|
67
|
+
export declare function resolveGovernancePaths(cwd: string, type: GovernanceType, config?: StrategyResolverConfig): GovernancePaths;
|
|
39
68
|
/**
|
|
40
69
|
* Scan `targetDir` for `NNN-slug.md` files, parse the prefix to an int,
|
|
41
70
|
* and return `(max + 1)` zero-padded to three digits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"governance.d.ts","sourceRoot":"","sources":["../../src/utils/governance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"governance.d.ts","sourceRoot":"","sources":["../../src/utils/governance.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/governance.js
CHANGED
|
@@ -8,9 +8,37 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
|
-
import { resolveGitRoot, safeExec, TotemError } from '@mmnto/totem';
|
|
11
|
+
import { resolveGitRoot, resolveStrategyRoot, safeExec, TotemError, } from '@mmnto/totem';
|
|
12
|
+
import { sanitizeForTerminal } from '../terminal-sanitize.js';
|
|
12
13
|
import { log } from '../ui.js';
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Best-effort `TotemConfig` load for the governance commands. Returns
|
|
16
|
+
* `undefined` when the config is missing, unparseable, OR resolves to the
|
|
17
|
+
* global `~/.totem/` profile — both missing and unparseable are legitimate
|
|
18
|
+
* states for a freshly-cloned consumer repo, and a global-profile config
|
|
19
|
+
* is intentionally NOT used as a repo-local `strategyRoot` source (that
|
|
20
|
+
* would let one user's personal pointer leak across every repo on disk).
|
|
21
|
+
* The strategy-root resolver still has env / sibling / submodule layers
|
|
22
|
+
* to fall back on (mmnto-ai/totem#1710 R3 — CR R3 global-config-leak fix).
|
|
23
|
+
*
|
|
24
|
+
* Shared by `proposalNewCommand` and `adrNewCommand` so the load idiom +
|
|
25
|
+
* its `// totem-context: intentional best-effort` annotation live in one
|
|
26
|
+
* place (mmnto-ai/totem#1710 R2).
|
|
27
|
+
*/
|
|
28
|
+
export async function loadGovernanceConfig(cwd) {
|
|
29
|
+
try {
|
|
30
|
+
const { loadConfig, resolveConfigPath, isGlobalConfigPath } = await import('../utils.js');
|
|
31
|
+
const configPath = resolveConfigPath(cwd);
|
|
32
|
+
if (isGlobalConfigPath(configPath))
|
|
33
|
+
return undefined;
|
|
34
|
+
const config = (await loadConfig(configPath));
|
|
35
|
+
return config;
|
|
36
|
+
// totem-context: intentional best-effort load — missing/unparseable config is a legitimate state for a freshly-cloned consumer repo; resolver's other layers (env / sibling / submodule) still apply.
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
14
42
|
function targetSubpath(type) {
|
|
15
43
|
return type === 'proposal' ? path.join('proposals', 'active') : 'adr';
|
|
16
44
|
}
|
|
@@ -20,35 +48,59 @@ function templateFilename(type) {
|
|
|
20
48
|
/**
|
|
21
49
|
* Resolve governance paths for the current invocation.
|
|
22
50
|
*
|
|
23
|
-
* Two supported
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
51
|
+
* Two layout shapes supported:
|
|
52
|
+
*
|
|
53
|
+
* 1. **Standalone (cwd IS the strategy repo)** — `<gitRoot>` itself contains
|
|
54
|
+
* `proposals/` or `adr/` at the top level. Used when the CLI runs from
|
|
55
|
+
* inside the strategy repo directly. Detected here in the helper before
|
|
56
|
+
* delegating to the resolver because the strategy-root resolver answers
|
|
57
|
+
* "where IS the strategy repo from somewhere else", not "are we INSIDE
|
|
58
|
+
* one already."
|
|
59
|
+
*
|
|
60
|
+
* 2. **Resolved (cwd is in a consuming repo)** — defer to
|
|
61
|
+
* `resolveStrategyRoot` (mmnto-ai/totem#1710). The resolver walks env →
|
|
62
|
+
* config → sibling → submodule precedence and returns an absolute path.
|
|
63
|
+
* Throws an actionable `TotemError` (ADR-088) when none of the layers
|
|
64
|
+
* resolve.
|
|
29
65
|
*
|
|
30
|
-
*
|
|
31
|
-
* layout can be detected.
|
|
66
|
+
* Also throws `TotemError` when cwd is not inside a git repo.
|
|
32
67
|
*/
|
|
33
|
-
export function resolveGovernancePaths(cwd, type) {
|
|
68
|
+
export function resolveGovernancePaths(cwd, type, config) {
|
|
34
69
|
const gitRoot = resolveGitRoot(cwd);
|
|
35
70
|
if (gitRoot === null) {
|
|
36
|
-
|
|
71
|
+
// Sanitize `cwd` here too — the sister branch already does this for
|
|
72
|
+
// the resolved-but-no-layout error (mmnto-ai/totem#1710 R5). A working
|
|
73
|
+
// directory name with embedded ANSI/CR bytes would otherwise hit the
|
|
74
|
+
// CLI raw on the not-in-git-repo path (mmnto-ai/totem#1710 R6 / CR R6
|
|
75
|
+
// outside-diff Major).
|
|
76
|
+
throw new TotemError('CONFIG_MISSING', `Not inside a git repository: ${sanitizeForTerminal(cwd)}`, 'Run this command from inside a Totem or Totem-strategy repository checkout.');
|
|
37
77
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
// Standalone detection requires BOTH the type-specific target subdir AND
|
|
79
|
+
// a `templates/` folder at the repo root. The conjunction catches the
|
|
80
|
+
// canonical strategy-repo layout (proposals/active/ + adr/ + templates/)
|
|
81
|
+
// while avoiding false positives on consumer repos that happen to ship a
|
|
82
|
+
// top-level `adr/` docs folder without a strategy substrate.
|
|
83
|
+
const standaloneHasTarget = fs.existsSync(path.join(gitRoot, targetSubpath(type)));
|
|
84
|
+
const standaloneHasTemplates = fs.existsSync(path.join(gitRoot, 'templates'));
|
|
43
85
|
let rootDir;
|
|
44
|
-
if (
|
|
45
|
-
rootDir = submoduleRoot;
|
|
46
|
-
}
|
|
47
|
-
else if (standaloneHasProposals || standaloneHasAdr) {
|
|
86
|
+
if (standaloneHasTarget && standaloneHasTemplates) {
|
|
48
87
|
rootDir = gitRoot;
|
|
49
88
|
}
|
|
50
89
|
else {
|
|
51
|
-
|
|
90
|
+
const status = resolveStrategyRoot(cwd, { gitRoot, config });
|
|
91
|
+
if (!status.resolved) {
|
|
92
|
+
// Sanitize filesystem- and env-derived strings before they flow
|
|
93
|
+
// into the CLI-rendered TotemError. `cwd` and `gitRoot` are
|
|
94
|
+
// process inputs; `status.reason` carries env values
|
|
95
|
+
// (`TOTEM_STRATEGY_ROOT`, `STRATEGY_ROOT`) and config values
|
|
96
|
+
// verbatim. ANSI/CR bytes there would rewind the cursor when the
|
|
97
|
+
// error is rendered (mmnto-ai/totem#1710 R5 / CR R5 Major).
|
|
98
|
+
const safeCwd = sanitizeForTerminal(cwd);
|
|
99
|
+
const safeClonePath = sanitizeForTerminal(path.join(path.dirname(gitRoot), 'totem-strategy'));
|
|
100
|
+
const safeReason = sanitizeForTerminal(status.reason);
|
|
101
|
+
throw new TotemError('CONFIG_MISSING', `No Totem-strategy layout found from ${safeCwd}.`, `Clone the strategy repo as a sibling (e.g., \`git clone <strategy-url> ${safeClonePath}\`), set the TOTEM_STRATEGY_ROOT env var, or initialize the legacy \`.strategy/\` submodule. Resolver detail: ${safeReason}`);
|
|
102
|
+
}
|
|
103
|
+
rootDir = status.path;
|
|
52
104
|
}
|
|
53
105
|
const targetDir = path.join(rootDir, targetSubpath(type));
|
|
54
106
|
const templatePath = path.join(rootDir, 'templates', templateFilename(type));
|
|
@@ -269,7 +321,7 @@ function todayIso() {
|
|
|
269
321
|
* never strands a half-written artifact.
|
|
270
322
|
*/
|
|
271
323
|
export function scaffoldGovernanceArtifact(options, internals = {}) {
|
|
272
|
-
const paths = resolveGovernancePaths(options.cwd, options.type);
|
|
324
|
+
const paths = resolveGovernancePaths(options.cwd, options.type, options.config);
|
|
273
325
|
// Sanitize once at the orchestrator entry. Both the filename slug and the
|
|
274
326
|
// template body render against the cleaned form so a title carrying
|
|
275
327
|
// newlines, tabs, or control characters cannot inject markdown blocks
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"governance.js","sourceRoot":"","sources":["../../src/utils/governance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,
|
|
1
|
+
{"version":3,"file":"governance.js","sourceRoot":"","sources":["../../src/utils/governance.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,9 +45,12 @@ describe('resolveGovernancePaths', () => {
|
|
|
45
45
|
});
|
|
46
46
|
it('resolves standalone strategy path when submodule prefix is missing', async () => {
|
|
47
47
|
initGit(tmpDir);
|
|
48
|
-
// Standalone strategy repo — `
|
|
48
|
+
// Standalone strategy repo — `adr/` lives at the git root, no `.strategy/` prefix.
|
|
49
|
+
// `templates/` sentinel at the root distinguishes a real strategy repo from a
|
|
50
|
+
// consumer repo that happens to ship a top-level `adr/` docs folder.
|
|
49
51
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
50
52
|
fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true });
|
|
53
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
51
54
|
const { resolveGovernancePaths } = await import('./governance.js');
|
|
52
55
|
const paths = resolveGovernancePaths(tmpDir, 'adr');
|
|
53
56
|
expect(paths.rootDir).toBe(path.normalize(tmpDir));
|
|
@@ -55,6 +58,15 @@ describe('resolveGovernancePaths', () => {
|
|
|
55
58
|
expect(paths.dashboardFile).toBe(path.normalize(path.join(tmpDir, 'README.md')));
|
|
56
59
|
expect(paths.templatePath).toBe(path.normalize(path.join(tmpDir, 'templates', 'adr.md')));
|
|
57
60
|
});
|
|
61
|
+
it('rejects standalone false-positive when a consumer repo has only adr/ without templates/', async () => {
|
|
62
|
+
initGit(tmpDir);
|
|
63
|
+
// Consumer repo with a top-level `adr/` docs folder but NO `templates/`
|
|
64
|
+
// and NO sibling/submodule strategy. Pre-tightening, this would have
|
|
65
|
+
// false-positively scaffolded into the consumer's adr/ folder.
|
|
66
|
+
fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true });
|
|
67
|
+
const { resolveGovernancePaths } = await import('./governance.js');
|
|
68
|
+
expect(() => resolveGovernancePaths(tmpDir, 'adr')).toThrow(/strategy/i);
|
|
69
|
+
});
|
|
58
70
|
it('throws TotemError when cwd is not inside a git repository', async () => {
|
|
59
71
|
// tmpDir has no `.git/`; resolveGitRoot returns null.
|
|
60
72
|
const { resolveGovernancePaths } = await import('./governance.js');
|
|
@@ -66,6 +78,53 @@ describe('resolveGovernancePaths', () => {
|
|
|
66
78
|
const { resolveGovernancePaths } = await import('./governance.js');
|
|
67
79
|
expect(() => resolveGovernancePaths(tmpDir, 'proposal')).toThrow(/strategy/i);
|
|
68
80
|
});
|
|
81
|
+
it('error message names the canonical sibling clone path and env var (mmnto-ai/totem#1710)', async () => {
|
|
82
|
+
initGit(tmpDir);
|
|
83
|
+
const { resolveGovernancePaths } = await import('./governance.js');
|
|
84
|
+
let caught;
|
|
85
|
+
try {
|
|
86
|
+
resolveGovernancePaths(tmpDir, 'proposal');
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
caught = err;
|
|
90
|
+
}
|
|
91
|
+
const totemErr = caught;
|
|
92
|
+
const combined = `${totemErr.message ?? ''}\n${totemErr.recoveryHint ?? ''}`;
|
|
93
|
+
expect(combined).toMatch(/totem-strategy/i);
|
|
94
|
+
expect(combined).toMatch(/TOTEM_STRATEGY_ROOT/);
|
|
95
|
+
});
|
|
96
|
+
it('resolves sibling strategy path via the resolver when ../totem-strategy exists (mmnto-ai/totem#1710)', async () => {
|
|
97
|
+
// Layout:
|
|
98
|
+
// <parent>/repo/.git/ ← gitRoot (created via initGit)
|
|
99
|
+
// <parent>/totem-strategy/proposals/active/
|
|
100
|
+
const repo = path.join(tmpDir, 'repo');
|
|
101
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
102
|
+
initGit(repo);
|
|
103
|
+
const sibling = path.join(tmpDir, 'totem-strategy');
|
|
104
|
+
fs.mkdirSync(path.join(sibling, 'proposals', 'active'), { recursive: true });
|
|
105
|
+
const { resolveGovernancePaths } = await import('./governance.js');
|
|
106
|
+
const paths = resolveGovernancePaths(repo, 'proposal');
|
|
107
|
+
expect(paths.rootDir).toBe(path.normalize(sibling));
|
|
108
|
+
expect(paths.targetDir).toBe(path.normalize(path.join(sibling, 'proposals', 'active')));
|
|
109
|
+
});
|
|
110
|
+
it('resolves env-driven strategy path when TOTEM_STRATEGY_ROOT is set (mmnto-ai/totem#1710)', async () => {
|
|
111
|
+
initGit(tmpDir);
|
|
112
|
+
const elsewhere = path.join(tmpDir, 'elsewhere-strategy');
|
|
113
|
+
fs.mkdirSync(path.join(elsewhere, 'proposals', 'active'), { recursive: true });
|
|
114
|
+
const prev = process.env.TOTEM_STRATEGY_ROOT;
|
|
115
|
+
process.env.TOTEM_STRATEGY_ROOT = elsewhere;
|
|
116
|
+
try {
|
|
117
|
+
const { resolveGovernancePaths } = await import('./governance.js');
|
|
118
|
+
const paths = resolveGovernancePaths(tmpDir, 'proposal');
|
|
119
|
+
expect(paths.rootDir).toBe(path.normalize(elsewhere));
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (prev === undefined)
|
|
123
|
+
delete process.env.TOTEM_STRATEGY_ROOT;
|
|
124
|
+
else
|
|
125
|
+
process.env.TOTEM_STRATEGY_ROOT = prev;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
69
128
|
});
|
|
70
129
|
describe('getNextArtifactId', () => {
|
|
71
130
|
let tmpDir;
|
|
@@ -383,6 +442,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
383
442
|
initGit(tmpDir);
|
|
384
443
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
385
444
|
fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true });
|
|
445
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
386
446
|
const calls = [];
|
|
387
447
|
const exec = (cmd, args) => {
|
|
388
448
|
calls.push({ cmd, args });
|
|
@@ -409,6 +469,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
409
469
|
initGit(tmpDir);
|
|
410
470
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
411
471
|
fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true });
|
|
472
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
412
473
|
const exec = () => { };
|
|
413
474
|
const { scaffoldGovernanceArtifact } = await import('./governance.js');
|
|
414
475
|
const result = scaffoldGovernanceArtifact({ type: 'adr', title: 'Database Sharding', cwd: tmpDir }, { exec, date: '2026-04-21' });
|
|
@@ -425,6 +486,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
425
486
|
// template body see the cleaned form.
|
|
426
487
|
initGit(tmpDir);
|
|
427
488
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
489
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
428
490
|
const exec = () => { };
|
|
429
491
|
const { scaffoldGovernanceArtifact } = await import('./governance.js');
|
|
430
492
|
const result = scaffoldGovernanceArtifact({
|
|
@@ -445,6 +507,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
445
507
|
it('creates file on disk even when docs:inject is missing', async () => {
|
|
446
508
|
initGit(tmpDir);
|
|
447
509
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
510
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
448
511
|
const origWarn = console.error;
|
|
449
512
|
console.error = () => { };
|
|
450
513
|
try {
|
|
@@ -467,6 +530,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
467
530
|
initGit(tmpDir);
|
|
468
531
|
const target = path.join(tmpDir, 'proposals', 'active');
|
|
469
532
|
fs.mkdirSync(target, { recursive: true });
|
|
533
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
470
534
|
fs.writeFileSync(path.join(target, '001-alpha.md'), '# a\n', 'utf-8');
|
|
471
535
|
fs.writeFileSync(path.join(target, '003-beta.md'), '# b\n', 'utf-8');
|
|
472
536
|
const { scaffoldGovernanceArtifact } = await import('./governance.js');
|
|
@@ -478,6 +542,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
478
542
|
initGit(tmpDir);
|
|
479
543
|
const target = path.join(tmpDir, 'proposals', 'active');
|
|
480
544
|
fs.mkdirSync(target, { recursive: true });
|
|
545
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
481
546
|
// Seed with the exact filename the scaffolder would generate for this title:
|
|
482
547
|
// id=001 + slug='collide' ⇒ '001-collide.md'. We force this by ALSO seeding a
|
|
483
548
|
// higher-numbered file so getNextArtifactId returns 002, then we seed 002-collide.md
|
|
@@ -509,6 +574,7 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
509
574
|
it('throws TotemError BEFORE touching disk when title sanitizes to empty', async () => {
|
|
510
575
|
initGit(tmpDir);
|
|
511
576
|
fs.mkdirSync(path.join(tmpDir, 'proposals', 'active'), { recursive: true });
|
|
577
|
+
fs.mkdirSync(path.join(tmpDir, 'templates'), { recursive: true });
|
|
512
578
|
const execCalls = [];
|
|
513
579
|
const exec = (cmd) => {
|
|
514
580
|
execCalls.push(cmd);
|
|
@@ -522,4 +588,61 @@ describe('scaffoldGovernanceArtifact (orchestrator)', () => {
|
|
|
522
588
|
expect(entries).toEqual([]);
|
|
523
589
|
});
|
|
524
590
|
});
|
|
591
|
+
describe('loadGovernanceConfig (mmnto-ai/totem#1710 R3 — global config leak)', () => {
|
|
592
|
+
let tmpDir;
|
|
593
|
+
let homeDir;
|
|
594
|
+
let originalCwd;
|
|
595
|
+
let originalHomeEnv;
|
|
596
|
+
let originalUserprofile;
|
|
597
|
+
beforeEach(() => {
|
|
598
|
+
tmpDir = makeTmpDir();
|
|
599
|
+
homeDir = makeTmpDir('totem-home-');
|
|
600
|
+
originalCwd = process.cwd();
|
|
601
|
+
// os.homedir() reads HOME on POSIX and USERPROFILE on Windows; redirect
|
|
602
|
+
// both so the test runs cross-platform without leaking a real
|
|
603
|
+
// ~/.totem/ profile into the assertion.
|
|
604
|
+
originalHomeEnv = process.env['HOME'];
|
|
605
|
+
originalUserprofile = process.env['USERPROFILE'];
|
|
606
|
+
process.env['HOME'] = homeDir;
|
|
607
|
+
process.env['USERPROFILE'] = homeDir;
|
|
608
|
+
});
|
|
609
|
+
afterEach(() => {
|
|
610
|
+
process.chdir(originalCwd);
|
|
611
|
+
if (originalHomeEnv === undefined) {
|
|
612
|
+
delete process.env['HOME'];
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
process.env['HOME'] = originalHomeEnv;
|
|
616
|
+
}
|
|
617
|
+
if (originalUserprofile === undefined) {
|
|
618
|
+
delete process.env['USERPROFILE'];
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
process.env['USERPROFILE'] = originalUserprofile;
|
|
622
|
+
}
|
|
623
|
+
cleanTmpDir(tmpDir);
|
|
624
|
+
cleanTmpDir(homeDir);
|
|
625
|
+
});
|
|
626
|
+
it('returns undefined when the resolved config lives in the global ~/.totem/ profile', async () => {
|
|
627
|
+
// No local config in tmpDir — so resolveConfigPath walks up to the
|
|
628
|
+
// global ~/.totem/ profile. A user's personal global config
|
|
629
|
+
// typically encodes tier/embedder choices, NOT a strategy pointer
|
|
630
|
+
// for one specific repo. Loading it for governance scaffolding
|
|
631
|
+
// would let one user's strategyRoot leak across every repo on disk.
|
|
632
|
+
initGit(tmpDir);
|
|
633
|
+
const globalTotemDir = path.join(homeDir, '.totem');
|
|
634
|
+
fs.mkdirSync(globalTotemDir, { recursive: true });
|
|
635
|
+
fs.writeFileSync(path.join(globalTotemDir, 'totem.config.ts'), "export default { targets: [{ glob: '**/*.ts', type: 'code', strategy: 'typescript-ast' }], strategyRoot: '/some/personal/strategy' };\n", 'utf-8');
|
|
636
|
+
const { loadGovernanceConfig } = await import('./governance.js');
|
|
637
|
+
const config = await loadGovernanceConfig(tmpDir);
|
|
638
|
+
expect(config).toBeUndefined();
|
|
639
|
+
});
|
|
640
|
+
it('returns the local config when one exists in cwd', async () => {
|
|
641
|
+
initGit(tmpDir);
|
|
642
|
+
fs.writeFileSync(path.join(tmpDir, 'totem.config.ts'), "export default { targets: [{ glob: '**/*.ts', type: 'code', strategy: 'typescript-ast' }], strategyRoot: '../totem-strategy' };\n", 'utf-8');
|
|
643
|
+
const { loadGovernanceConfig } = await import('./governance.js');
|
|
644
|
+
const config = await loadGovernanceConfig(tmpDir);
|
|
645
|
+
expect(config?.strategyRoot).toBe('../totem-strategy');
|
|
646
|
+
});
|
|
647
|
+
});
|
|
525
648
|
//# sourceMappingURL=governance.test.js.map
|