agentic-workflow-manager 9.17.0 → 9.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/src/commands/env/index.js +192 -0
- package/dist/src/commands/registry/doctor.js +428 -0
- package/dist/src/commands/registry/index.js +52 -0
- package/dist/src/commands/registry/scaffold.js +140 -0
- package/dist/src/commands/watch/index.js +3 -0
- package/dist/src/core/environments/resolve.js +122 -0
- package/dist/src/core/environments/store.js +323 -0
- package/dist/src/core/facts/catalog.js +138 -0
- package/dist/src/core/facts/needs.js +57 -0
- package/dist/src/core/facts/types.js +21 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/registry/doctor.test.js +283 -0
- package/dist/tests/commands/registry/scaffold.test.js +186 -0
- package/dist/tests/commands/watch/reopened-196.test.js +9 -2
- package/dist/tests/core/environments/resolve.test.js +347 -0
- package/dist/tests/core/environments/store.test.js +248 -0
- package/dist/tests/core/facts/catalog.test.js +179 -0
- package/dist/tests/core/facts/needs.test.js +88 -0
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_VOLATILE_TTL_SECONDS = void 0;
|
|
7
|
+
exports.parseFactsCatalog = parseFactsCatalog;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const registries_1 = require("../registries");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const VALUE_TYPES = ['string', 'path', 'enum'];
|
|
12
|
+
const RESOLUTIONS = ['prompt'];
|
|
13
|
+
const DURABILITIES = ['durable', 'volatile'];
|
|
14
|
+
const INJECTS = ['reference', 'context'];
|
|
15
|
+
/** Max volatile ttl (seconds) so updatedMs + ttl*1000 stays in a safe Date range. */
|
|
16
|
+
exports.MAX_VOLATILE_TTL_SECONDS = 365 * 24 * 3600;
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function parseFactEntry(id, raw) {
|
|
21
|
+
if (!types_1.FACT_ID_RE.test(id)) {
|
|
22
|
+
throw new types_1.FactsCatalogError(`Invalid fact id ${JSON.stringify(id)}: must match ${types_1.FACT_ID_RE}`);
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(raw)) {
|
|
25
|
+
throw new types_1.FactsCatalogError(`facts.${id} must be an object`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof raw.valueType !== 'string' || !VALUE_TYPES.includes(raw.valueType)) {
|
|
28
|
+
throw new types_1.FactsCatalogError(`facts.${id}.valueType must be one of ${VALUE_TYPES.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
const valueType = raw.valueType;
|
|
31
|
+
if (raw.resolution === 'command') {
|
|
32
|
+
throw new types_1.FactsCatalogError(`facts.${id}: resolution: command is not supported in v1 (use prompt / --value / editable store)`);
|
|
33
|
+
}
|
|
34
|
+
if (raw.resolution !== undefined) {
|
|
35
|
+
if (typeof raw.resolution !== 'string' || !RESOLUTIONS.includes(raw.resolution)) {
|
|
36
|
+
throw new types_1.FactsCatalogError(`facts.${id}.resolution must be "prompt" in v1`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (raw.durability !== undefined) {
|
|
40
|
+
if (typeof raw.durability !== 'string' || !DURABILITIES.includes(raw.durability)) {
|
|
41
|
+
throw new types_1.FactsCatalogError(`facts.${id}.durability must be durable|volatile`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (raw.inject !== undefined) {
|
|
45
|
+
if (typeof raw.inject !== 'string' || !INJECTS.includes(raw.inject)) {
|
|
46
|
+
throw new types_1.FactsCatalogError(`facts.${id}.inject must be reference|context`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const def = {
|
|
50
|
+
valueType,
|
|
51
|
+
resolution: raw.resolution ?? 'prompt',
|
|
52
|
+
durability: raw.durability ?? 'durable',
|
|
53
|
+
inject: raw.inject ?? 'reference',
|
|
54
|
+
};
|
|
55
|
+
if (raw.description !== undefined) {
|
|
56
|
+
if (typeof raw.description !== 'string') {
|
|
57
|
+
throw new types_1.FactsCatalogError(`facts.${id}.description must be a string`);
|
|
58
|
+
}
|
|
59
|
+
def.description = raw.description;
|
|
60
|
+
}
|
|
61
|
+
if (valueType === 'enum') {
|
|
62
|
+
if (!Array.isArray(raw.values) || raw.values.length === 0 || raw.values.some((v) => typeof v !== 'string')) {
|
|
63
|
+
throw new types_1.FactsCatalogError(`facts.${id}.values must be a non-empty string array when valueType is enum`);
|
|
64
|
+
}
|
|
65
|
+
def.values = raw.values;
|
|
66
|
+
}
|
|
67
|
+
else if (raw.values !== undefined) {
|
|
68
|
+
throw new types_1.FactsCatalogError(`facts.${id}.values is only allowed when valueType is enum`);
|
|
69
|
+
}
|
|
70
|
+
if (raw.ttl !== undefined) {
|
|
71
|
+
if (typeof raw.ttl !== 'number' || !Number.isFinite(raw.ttl) || raw.ttl < 0) {
|
|
72
|
+
throw new types_1.FactsCatalogError(`facts.${id}.ttl must be a non-negative number`);
|
|
73
|
+
}
|
|
74
|
+
def.ttl = raw.ttl;
|
|
75
|
+
}
|
|
76
|
+
if (def.durability === 'volatile') {
|
|
77
|
+
if (typeof def.ttl !== 'number' ||
|
|
78
|
+
!Number.isFinite(def.ttl) ||
|
|
79
|
+
def.ttl <= 0 ||
|
|
80
|
+
def.ttl > exports.MAX_VOLATILE_TTL_SECONDS) {
|
|
81
|
+
throw new types_1.FactsCatalogError(`facts.${id}: durability volatile requires finite ttl in (0, ${exports.MAX_VOLATILE_TTL_SECONDS}] seconds`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return def;
|
|
85
|
+
}
|
|
86
|
+
function parseObject(raw) {
|
|
87
|
+
if (!isRecord(raw)) {
|
|
88
|
+
throw new types_1.FactsCatalogError('facts catalog must be a JSON object');
|
|
89
|
+
}
|
|
90
|
+
if (raw.schemaVersion !== 1) {
|
|
91
|
+
throw new types_1.FactsCatalogError(`facts catalog schemaVersion must be 1, got ${JSON.stringify(raw.schemaVersion)}`);
|
|
92
|
+
}
|
|
93
|
+
if (!isRecord(raw.facts)) {
|
|
94
|
+
throw new types_1.FactsCatalogError('facts catalog must include a facts object');
|
|
95
|
+
}
|
|
96
|
+
const facts = {};
|
|
97
|
+
for (const [id, entry] of Object.entries(raw.facts)) {
|
|
98
|
+
if (id === '') {
|
|
99
|
+
throw new types_1.FactsCatalogError('fact id must be non-empty');
|
|
100
|
+
}
|
|
101
|
+
facts[id] = parseFactEntry(id, entry);
|
|
102
|
+
}
|
|
103
|
+
return { schemaVersion: 1, facts };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse a facts catalog from an in-memory object or from a file path string.
|
|
107
|
+
* Named errors: {@link FactsCatalogError}.
|
|
108
|
+
*/
|
|
109
|
+
function parseFactsCatalog(rawOrPath) {
|
|
110
|
+
if (typeof rawOrPath === 'string') {
|
|
111
|
+
try {
|
|
112
|
+
if (!(0, registries_1.assertRegularRegistryFile)(rawOrPath)) {
|
|
113
|
+
throw new types_1.FactsCatalogError(`facts catalog not found at ${rawOrPath}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e instanceof types_1.FactsCatalogError)
|
|
118
|
+
throw e;
|
|
119
|
+
throw new types_1.FactsCatalogError(e instanceof Error ? e.message : `Cannot inspect facts catalog at ${rawOrPath}: ${String(e)}`);
|
|
120
|
+
}
|
|
121
|
+
let text;
|
|
122
|
+
try {
|
|
123
|
+
text = fs_1.default.readFileSync(rawOrPath, 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
throw new types_1.FactsCatalogError(`Cannot read facts catalog at ${rawOrPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
127
|
+
}
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(text);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
throw new types_1.FactsCatalogError(`Invalid JSON in facts catalog at ${rawOrPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
134
|
+
}
|
|
135
|
+
return parseObject(parsed);
|
|
136
|
+
}
|
|
137
|
+
return parseObject(rawOrPath);
|
|
138
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readSkillNeeds = readSkillNeeds;
|
|
7
|
+
exports.findOrphanNeeds = findOrphanNeeds;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
const frontmatter_1 = require("../frontmatter");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
/**
|
|
13
|
+
* Read the `needs` list from a skill SKILL.md frontmatter.
|
|
14
|
+
* Absent → `[]`. Invalid shape → {@link SkillNeedsError}.
|
|
15
|
+
*/
|
|
16
|
+
function readSkillNeeds(skillMdPath) {
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs_1.default.lstatSync(skillMdPath);
|
|
20
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
21
|
+
throw new types_1.SkillNeedsError(`Skill path ${skillMdPath} must be a regular file`);
|
|
22
|
+
}
|
|
23
|
+
raw = fs_1.default.readFileSync(skillMdPath, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e instanceof types_1.SkillNeedsError)
|
|
27
|
+
throw e;
|
|
28
|
+
throw new types_1.SkillNeedsError(`Cannot read skill at ${skillMdPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
29
|
+
}
|
|
30
|
+
const block = (0, frontmatter_1.matchFrontmatterBlock)(raw);
|
|
31
|
+
if (block === null)
|
|
32
|
+
return [];
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = js_yaml_1.default.load(block);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw new types_1.SkillNeedsError(`Invalid skill frontmatter at ${skillMdPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed === null || parsed === undefined)
|
|
41
|
+
return [];
|
|
42
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
43
|
+
throw new types_1.SkillNeedsError(`Invalid skill frontmatter at ${skillMdPath}: expected a mapping`);
|
|
44
|
+
}
|
|
45
|
+
const needs = parsed.needs;
|
|
46
|
+
if (needs === undefined)
|
|
47
|
+
return [];
|
|
48
|
+
if (!Array.isArray(needs) || needs.some((n) => typeof n !== 'string' || n === '')) {
|
|
49
|
+
throw new types_1.SkillNeedsError(`Invalid needs at ${skillMdPath}: needs must be a YAML list of non-empty strings`);
|
|
50
|
+
}
|
|
51
|
+
return needs;
|
|
52
|
+
}
|
|
53
|
+
/** Ids in `needs` that are absent from the catalog (fail-closed; never invent). */
|
|
54
|
+
function findOrphanNeeds(needs, catalog) {
|
|
55
|
+
// Own keys only — `in` would treat Object.prototype (toString, …) as catalog facts.
|
|
56
|
+
return needs.filter((id) => !Object.hasOwn(catalog.facts, id));
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SkillNeedsError = exports.FactsCatalogError = exports.FACT_ID_RE = exports.FACTS_MIN_CLI_VERSION = void 0;
|
|
4
|
+
/** First CLI version that understands registry facts / needs. Bump only with intentional release. */
|
|
5
|
+
exports.FACTS_MIN_CLI_VERSION = '9.18.0';
|
|
6
|
+
/** Dotted fact ids: `repos.root`, `aws.profile.dev`. */
|
|
7
|
+
exports.FACT_ID_RE = /^[a-z][a-z0-9]*(\.[a-z0-9]+)+$/;
|
|
8
|
+
class FactsCatalogError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'FactsCatalogError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.FactsCatalogError = FactsCatalogError;
|
|
15
|
+
class SkillNeedsError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SkillNeedsError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.SkillNeedsError = SkillNeedsError;
|
package/dist/src/index.js
CHANGED
|
@@ -37,6 +37,7 @@ const doctor_1 = require("./commands/doctor");
|
|
|
37
37
|
const backup_1 = require("./commands/backup");
|
|
38
38
|
const init_1 = require("./commands/init");
|
|
39
39
|
const registry_1 = require("./commands/registry");
|
|
40
|
+
const env_1 = require("./commands/env");
|
|
40
41
|
const pin_1 = require("./commands/pin");
|
|
41
42
|
const export_1 = require("./commands/export");
|
|
42
43
|
const agent_1 = require("./commands/agent");
|
|
@@ -743,6 +744,7 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
743
744
|
(0, backup_1.registerBackupCommand)(exports.program);
|
|
744
745
|
(0, init_1.registerInitCommand)(exports.program);
|
|
745
746
|
(0, registry_1.registerRegistryCommand)(exports.program);
|
|
747
|
+
(0, env_1.registerEnvCommand)(exports.program);
|
|
746
748
|
(0, pin_1.registerPinCommands)(exports.program);
|
|
747
749
|
(0, export_1.registerExportCommand)(exports.program);
|
|
748
750
|
(0, agent_1.registerAgentCommand)(exports.program);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const commander_1 = require("commander");
|
|
10
|
+
const env_1 = require("../../../src/commands/env");
|
|
11
|
+
const store_1 = require("../../../src/core/environments/store");
|
|
12
|
+
const catalog = {
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
facts: {
|
|
15
|
+
'repos.root': {
|
|
16
|
+
valueType: 'path',
|
|
17
|
+
resolution: 'prompt',
|
|
18
|
+
durability: 'durable',
|
|
19
|
+
inject: 'reference',
|
|
20
|
+
},
|
|
21
|
+
'aws.profile': {
|
|
22
|
+
valueType: 'string',
|
|
23
|
+
resolution: 'prompt',
|
|
24
|
+
durability: 'durable',
|
|
25
|
+
inject: 'reference',
|
|
26
|
+
},
|
|
27
|
+
'env.kind': {
|
|
28
|
+
valueType: 'enum',
|
|
29
|
+
resolution: 'prompt',
|
|
30
|
+
durability: 'durable',
|
|
31
|
+
inject: 'reference',
|
|
32
|
+
values: ['dev', 'prod'],
|
|
33
|
+
},
|
|
34
|
+
'aws.token': {
|
|
35
|
+
valueType: 'string',
|
|
36
|
+
resolution: 'prompt',
|
|
37
|
+
durability: 'volatile',
|
|
38
|
+
inject: 'reference',
|
|
39
|
+
ttl: 1,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function isolateHome() {
|
|
44
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-env-cmd-'));
|
|
45
|
+
process.env.AWM_HOME = path_1.default.join(tmp, '.awm');
|
|
46
|
+
process.env.HOME = tmp;
|
|
47
|
+
return tmp;
|
|
48
|
+
}
|
|
49
|
+
async function run(argv, loadCatalog = () => catalog, extraDeps = {}) {
|
|
50
|
+
const prog = new commander_1.Command();
|
|
51
|
+
prog.exitOverride();
|
|
52
|
+
prog.configureOutput({ writeErr: () => undefined });
|
|
53
|
+
(0, env_1.registerEnvCommand)(prog, {
|
|
54
|
+
loadCatalog,
|
|
55
|
+
consultedRoots: () => ['/tmp/test-registry'],
|
|
56
|
+
stdinIsTTY: () => false,
|
|
57
|
+
...extraDeps,
|
|
58
|
+
});
|
|
59
|
+
const outSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
60
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
61
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
62
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
63
|
+
try {
|
|
64
|
+
await prog.parseAsync(['node', 'awm', 'env', ...argv]);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
// keep spies until we collect
|
|
68
|
+
}
|
|
69
|
+
const out = outSpy.mock.calls.map((c) => String(c[0])).join('') +
|
|
70
|
+
logSpy.mock.calls.map((c) => c.map(String).join(' ')).join('\n');
|
|
71
|
+
const err = errSpy.mock.calls.map((c) => String(c[0])).join('') +
|
|
72
|
+
errorSpy.mock.calls.map((c) => c.map(String).join(' ')).join('\n');
|
|
73
|
+
outSpy.mockRestore();
|
|
74
|
+
errSpy.mockRestore();
|
|
75
|
+
logSpy.mockRestore();
|
|
76
|
+
errorSpy.mockRestore();
|
|
77
|
+
return { out, err };
|
|
78
|
+
}
|
|
79
|
+
describe('awm env commands (RF-2, RF-3.3)', () => {
|
|
80
|
+
let tmp;
|
|
81
|
+
let prevHome;
|
|
82
|
+
let prevAwm;
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
prevHome = process.env.HOME;
|
|
85
|
+
prevAwm = process.env.AWM_HOME;
|
|
86
|
+
tmp = isolateHome();
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
90
|
+
if (prevHome === undefined)
|
|
91
|
+
delete process.env.HOME;
|
|
92
|
+
else
|
|
93
|
+
process.env.HOME = prevHome;
|
|
94
|
+
if (prevAwm === undefined)
|
|
95
|
+
delete process.env.AWM_HOME;
|
|
96
|
+
else
|
|
97
|
+
process.env.AWM_HOME = prevAwm;
|
|
98
|
+
});
|
|
99
|
+
it('set --value writes under environments/default/values.json without shelling out (RF-2.1, RF-3.3)', async () => {
|
|
100
|
+
const spawn = jest.spyOn(require('child_process'), 'spawnSync');
|
|
101
|
+
const execFile = jest.spyOn(require('child_process'), 'execFileSync');
|
|
102
|
+
await run(['set', 'repos.root', '--value', '/machine/repos']);
|
|
103
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/machine/repos');
|
|
104
|
+
expect(fs_1.default.existsSync((0, store_1.valuesPath)('default'))).toBe(true);
|
|
105
|
+
expect((0, store_1.valuesPath)('default')).toContain(path_1.default.join('.awm', 'environments', 'default', 'values.json'));
|
|
106
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
107
|
+
expect(execFile).not.toHaveBeenCalled();
|
|
108
|
+
spawn.mockRestore();
|
|
109
|
+
execFile.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
it('status reports missing for unset facts and fresh after set (RF-2.5)', async () => {
|
|
112
|
+
let { out } = await run(['status']);
|
|
113
|
+
expect(out).toMatch(/repos\.root:\s*missing/);
|
|
114
|
+
expect(out).toMatch(/aws\.profile:\s*missing/);
|
|
115
|
+
await run(['set', 'repos.root', '--value', '/r']);
|
|
116
|
+
({ out } = await run(['status']));
|
|
117
|
+
expect(out).toMatch(/repos\.root:\s*fresh/);
|
|
118
|
+
// Unset sibling stays missing — no silent default invent for other ids.
|
|
119
|
+
expect(out).toMatch(/aws\.profile:\s*missing/);
|
|
120
|
+
expect(out).not.toMatch(/aws\.profile:\s*fresh/);
|
|
121
|
+
});
|
|
122
|
+
it('status reports stale for expired volatile fact (RF-2.5)', async () => {
|
|
123
|
+
(0, store_1.writeFact)('default', 'aws.token', {
|
|
124
|
+
value: 'tok',
|
|
125
|
+
status: 'fresh',
|
|
126
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
127
|
+
}, { definition: catalog.facts['aws.token'] });
|
|
128
|
+
const { out } = await run(['status'], () => catalog, {
|
|
129
|
+
now: () => new Date('2026-10-12T00:00:02.000Z'),
|
|
130
|
+
});
|
|
131
|
+
expect(out).toMatch(/aws\.token:\s*stale/);
|
|
132
|
+
});
|
|
133
|
+
it('edit prints absolute store path', async () => {
|
|
134
|
+
const { out } = await run(['edit']);
|
|
135
|
+
expect(out).toContain((0, store_1.valuesPath)((0, store_1.getActiveEnvironmentName)()));
|
|
136
|
+
expect(path_1.default.isAbsolute(out.trim().split(/\s+/).find((t) => t.includes('values.json')) || (0, store_1.valuesPath)('default'))).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('refresh --value updates an existing fact without shell (RF-3.3)', async () => {
|
|
139
|
+
await run(['set', 'aws.profile', '--value', 'old']);
|
|
140
|
+
await run(['refresh', 'aws.profile', '--value', 'new-profile']);
|
|
141
|
+
expect((0, store_1.readValues)('default')['aws.profile'].value).toBe('new-profile');
|
|
142
|
+
});
|
|
143
|
+
it('refresh without --value in non-interactive mode errors', async () => {
|
|
144
|
+
await run(['set', 'aws.profile', '--value', 'old']);
|
|
145
|
+
await expect(run(['refresh', 'aws.profile'])).rejects.toThrow(/--value|value/i);
|
|
146
|
+
});
|
|
147
|
+
it('use switches active environment (RF-2.2)', async () => {
|
|
148
|
+
await run(['use', 'staging']);
|
|
149
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('staging');
|
|
150
|
+
await run(['set', 'repos.root', '--value', '/staging/repos']);
|
|
151
|
+
expect((0, store_1.valuesPath)('staging')).toContain(`${path_1.default.sep}staging${path_1.default.sep}values.json`);
|
|
152
|
+
expect((0, store_1.readValues)('staging')['repos.root'].value).toBe('/staging/repos');
|
|
153
|
+
});
|
|
154
|
+
it('set unknown id errors listing consulted roots', async () => {
|
|
155
|
+
await expect(run(['set', 'no.such.fact', '--value', 'x'])).rejects.toThrow(/no\.such\.fact/);
|
|
156
|
+
await expect(run(['set', 'no.such.fact', '--value', 'x'])).rejects.toThrow(/test-registry/);
|
|
157
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
158
|
+
});
|
|
159
|
+
it('set rejects empty path and leaves store unchanged', async () => {
|
|
160
|
+
await expect(run(['set', 'repos.root', '--value', ''])).rejects.toThrow(/path|empty|value/i);
|
|
161
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
162
|
+
});
|
|
163
|
+
it('set rejects invalid enum value', async () => {
|
|
164
|
+
await expect(run(['set', 'env.kind', '--value', 'qa'])).rejects.toThrow(/enum|dev|prod/i);
|
|
165
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
166
|
+
});
|
|
167
|
+
it('set never imports child_process for resolution (RF-3.3)', () => {
|
|
168
|
+
const src = fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../src/commands/env/index.ts'), 'utf-8');
|
|
169
|
+
expect(src).not.toMatch(/child_process/);
|
|
170
|
+
expect(src).not.toMatch(/\bexecSync\b|\bspawnSync\b|\bexecFileSync\b/);
|
|
171
|
+
});
|
|
172
|
+
it('set without --value on TTY uses promptFn (RF-3.3)', async () => {
|
|
173
|
+
const prompts = [];
|
|
174
|
+
await run(['set', 'repos.root'], () => catalog, {
|
|
175
|
+
stdinIsTTY: () => true,
|
|
176
|
+
promptValue: (q) => {
|
|
177
|
+
prompts.push(q);
|
|
178
|
+
return '/prompted/repos';
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
expect(prompts.length).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/prompted/repos');
|
|
183
|
+
});
|
|
184
|
+
it('set without --value when not TTY keeps requireValue error (RF-3.3)', async () => {
|
|
185
|
+
await expect(run(['set', 'repos.root'], () => catalog, { stdinIsTTY: () => false })).rejects.toThrow(/--value|value/i);
|
|
186
|
+
});
|
|
187
|
+
it('refresh --all on TTY prompts each store id via promptFn', async () => {
|
|
188
|
+
await run(['set', 'repos.root', '--value', '/old']);
|
|
189
|
+
await run(['set', 'aws.profile', '--value', 'old-profile']);
|
|
190
|
+
const seen = [];
|
|
191
|
+
const answers = {
|
|
192
|
+
'repos.root': '/new',
|
|
193
|
+
'aws.profile': 'new-profile',
|
|
194
|
+
};
|
|
195
|
+
await run(['refresh', '--all'], () => catalog, {
|
|
196
|
+
stdinIsTTY: () => true,
|
|
197
|
+
promptValue: (q) => {
|
|
198
|
+
const id = Object.keys(answers).find((k) => q.includes(k));
|
|
199
|
+
if (!id)
|
|
200
|
+
throw new Error(`unexpected prompt: ${q}`);
|
|
201
|
+
seen.push(id);
|
|
202
|
+
return answers[id];
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
expect(seen.sort()).toEqual(['aws.profile', 'repos.root']);
|
|
206
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/new');
|
|
207
|
+
expect((0, store_1.readValues)('default')['aws.profile'].value).toBe('new-profile');
|
|
208
|
+
});
|
|
209
|
+
it('refresh --all when not TTY keeps clear error', async () => {
|
|
210
|
+
await run(['set', 'repos.root', '--value', '/old']);
|
|
211
|
+
await expect(run(['refresh', '--all'], () => catalog, { stdinIsTTY: () => false })).rejects.toThrow(/--all|interactive|factId|--value|TTY/i);
|
|
212
|
+
});
|
|
213
|
+
it('inject prints buildInjection JSON (default reference)', async () => {
|
|
214
|
+
await run(['set', 'repos.root', '--value', '/r']);
|
|
215
|
+
const { out } = await run(['inject']);
|
|
216
|
+
const parsed = JSON.parse(out.trim());
|
|
217
|
+
expect(parsed.mode).toBe('reference');
|
|
218
|
+
expect(parsed.storePath).toContain('values.json');
|
|
219
|
+
expect(parsed.factIds).toEqual(expect.arrayContaining(['repos.root']));
|
|
220
|
+
expect(parsed.values).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
it('inject --mode context includes fresh context values when catalog allows', async () => {
|
|
223
|
+
const contextCatalog = {
|
|
224
|
+
schemaVersion: 1,
|
|
225
|
+
facts: {
|
|
226
|
+
'team.color': {
|
|
227
|
+
valueType: 'string',
|
|
228
|
+
resolution: 'prompt',
|
|
229
|
+
durability: 'durable',
|
|
230
|
+
inject: 'context',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
await run(['set', 'team.color', '--value', 'blue'], () => contextCatalog);
|
|
235
|
+
const { out } = await run(['inject', '--mode', 'context'], () => contextCatalog);
|
|
236
|
+
const parsed = JSON.parse(out.trim());
|
|
237
|
+
expect(parsed.mode).toBe('context');
|
|
238
|
+
expect(parsed.values).toEqual({ 'team.color': 'blue' });
|
|
239
|
+
});
|
|
240
|
+
it('mergeFactDefinitions refuses to weaken volatile → durable (secret revive)', () => {
|
|
241
|
+
const into = {
|
|
242
|
+
'aws.token': {
|
|
243
|
+
valueType: 'string',
|
|
244
|
+
resolution: 'prompt',
|
|
245
|
+
durability: 'volatile',
|
|
246
|
+
inject: 'context',
|
|
247
|
+
ttl: 60,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
(0, env_1.mergeFactDefinitions)(into, {
|
|
251
|
+
'aws.token': {
|
|
252
|
+
valueType: 'string',
|
|
253
|
+
resolution: 'prompt',
|
|
254
|
+
durability: 'durable',
|
|
255
|
+
inject: 'context',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
expect(into['aws.token'].durability).toBe('volatile');
|
|
259
|
+
expect(into['aws.token'].ttl).toBe(60);
|
|
260
|
+
});
|
|
261
|
+
});
|