agentic-workflow-manager 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +179 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +97 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +3 -2
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +177 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- package/package.json +1 -1
|
@@ -3,61 +3,165 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.UnsupportedRendererError = exports.AGENT_TARGETS = void 0;
|
|
7
|
+
exports.isAgentTarget = isAgentTarget;
|
|
8
|
+
exports.requireAgentTarget = requireAgentTarget;
|
|
9
|
+
exports.providers = providers;
|
|
10
|
+
exports.providerFor = providerFor;
|
|
7
11
|
exports.getTargetPath = getTargetPath;
|
|
8
12
|
exports.getHookConfig = getHookConfig;
|
|
13
|
+
exports.getSettingsMergeHookConfig = getSettingsMergeHookConfig;
|
|
14
|
+
exports.assertLinkRenderer = assertLinkRenderer;
|
|
9
15
|
exports.getInjection = getInjection;
|
|
10
16
|
// src/providers/index.ts
|
|
11
17
|
const path_1 = __importDefault(require("path"));
|
|
12
18
|
const paths_1 = require("../core/paths");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
exports.AGENT_TARGETS = ['antigravity', 'opencode', 'claude-code', 'codex'];
|
|
20
|
+
function isAgentTarget(value) {
|
|
21
|
+
return typeof value === 'string' &&
|
|
22
|
+
exports.AGENT_TARGETS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
/** Assertion form of `isAgentTarget` — narrows or throws a clear, user-facing error. */
|
|
25
|
+
function requireAgentTarget(value) {
|
|
26
|
+
if (!isAgentTarget(value)) {
|
|
27
|
+
throw new Error(`Invalid agent target: ${String(value)}. Use: ${exports.AGENT_TARGETS.join(', ')}.`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
class UnsupportedRendererError extends Error {
|
|
32
|
+
}
|
|
33
|
+
exports.UnsupportedRendererError = UnsupportedRendererError;
|
|
34
|
+
function providers() {
|
|
35
|
+
const home = (0, paths_1.homeDir)();
|
|
36
|
+
const awm = (0, paths_1.awmHome)();
|
|
37
|
+
return {
|
|
38
|
+
antigravity: {
|
|
39
|
+
label: 'Antigravity',
|
|
40
|
+
skill: {
|
|
41
|
+
global: path_1.default.join(home, '.gemini/antigravity/skills'),
|
|
42
|
+
local: '.agent/skills',
|
|
43
|
+
renderer: 'link',
|
|
44
|
+
},
|
|
45
|
+
workflow: {
|
|
46
|
+
global: path_1.default.join(home, '.gemini/antigravity/global_workflows'),
|
|
47
|
+
local: '.agent/workflows',
|
|
48
|
+
renderer: 'link',
|
|
49
|
+
},
|
|
50
|
+
agent: null,
|
|
51
|
+
},
|
|
52
|
+
opencode: {
|
|
53
|
+
label: 'OpenCode',
|
|
54
|
+
skill: {
|
|
55
|
+
global: path_1.default.join(home, '.agents/skills'),
|
|
56
|
+
local: '.agents/skills',
|
|
57
|
+
renderer: 'link',
|
|
58
|
+
},
|
|
59
|
+
workflow: null,
|
|
60
|
+
agent: {
|
|
61
|
+
global: path_1.default.join(home, '.config/opencode/agents'),
|
|
62
|
+
local: '.agents/profiles',
|
|
63
|
+
renderer: 'link',
|
|
64
|
+
},
|
|
65
|
+
injection: {
|
|
66
|
+
type: 'config-instructions',
|
|
67
|
+
configPath: path_1.default.join(home, '.config/opencode/opencode.json'),
|
|
68
|
+
field: 'instructions',
|
|
69
|
+
},
|
|
31
70
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
'claude-code': {
|
|
72
|
+
label: 'Claude Code',
|
|
73
|
+
skill: {
|
|
74
|
+
global: path_1.default.join(home, '.claude/skills'),
|
|
75
|
+
local: '.claude/skills',
|
|
76
|
+
renderer: 'link',
|
|
77
|
+
},
|
|
78
|
+
workflow: null,
|
|
79
|
+
agent: {
|
|
80
|
+
global: path_1.default.join(home, '.claude/agents'),
|
|
81
|
+
local: '.claude/agents',
|
|
82
|
+
renderer: 'link',
|
|
83
|
+
},
|
|
84
|
+
hooks: {
|
|
85
|
+
type: 'cc-settings-merge',
|
|
86
|
+
settingsPath: path_1.default.join(home, '.claude/settings.json'),
|
|
87
|
+
scriptsDir: path_1.default.join(awm, 'hooks'),
|
|
88
|
+
matcher: 'startup|clear|compact',
|
|
89
|
+
eventName: 'SessionStart',
|
|
90
|
+
},
|
|
91
|
+
injection: { type: 'cc-settings-merge' },
|
|
44
92
|
},
|
|
45
|
-
|
|
93
|
+
codex: {
|
|
94
|
+
label: 'Codex',
|
|
95
|
+
minimumVersion: '0.145.0',
|
|
96
|
+
versionCommand: { command: 'codex', args: ['--version'] },
|
|
97
|
+
skill: {
|
|
98
|
+
global: path_1.default.join(home, '.agents/skills'),
|
|
99
|
+
local: '.agents/skills',
|
|
100
|
+
renderer: 'link',
|
|
101
|
+
},
|
|
102
|
+
workflow: null,
|
|
103
|
+
agent: {
|
|
104
|
+
global: path_1.default.join(home, '.codex/agents'),
|
|
105
|
+
local: '.codex/agents',
|
|
106
|
+
renderer: 'codex-agent-toml',
|
|
107
|
+
},
|
|
108
|
+
hooks: {
|
|
109
|
+
type: 'codex-hooks-json',
|
|
110
|
+
settingsPath: path_1.default.join(home, '.codex/hooks.json'),
|
|
111
|
+
scriptsDir: path_1.default.join(awm, 'hooks/codex'),
|
|
112
|
+
matcher: 'startup|resume|clear|compact',
|
|
113
|
+
eventName: 'SessionStart',
|
|
114
|
+
},
|
|
115
|
+
injection: {
|
|
116
|
+
type: 'managed-agents-md',
|
|
117
|
+
globalPath: path_1.default.join(home, '.codex/AGENTS.md'),
|
|
118
|
+
localFile: 'AGENTS.md',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function providerFor(agent) {
|
|
124
|
+
if (!isAgentTarget(agent)) {
|
|
125
|
+
throw new Error(`Unknown agent target: ${String(agent)}`);
|
|
46
126
|
}
|
|
47
|
-
|
|
127
|
+
return providers()[agent];
|
|
128
|
+
}
|
|
48
129
|
function getTargetPath(type, agent, scope) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
130
|
+
if (!['skill', 'workflow', 'agent'].includes(type)) {
|
|
131
|
+
throw new Error(`Unknown artifact type: ${String(type)}`);
|
|
132
|
+
}
|
|
133
|
+
if (!['global', 'local'].includes(scope)) {
|
|
134
|
+
throw new Error(`Unknown scope: ${String(scope)}`);
|
|
135
|
+
}
|
|
136
|
+
const provider = providerFor(agent);
|
|
52
137
|
const config = provider[type];
|
|
53
138
|
if (!config)
|
|
54
139
|
throw new Error(`${type}s are not supported by ${provider.label}.`);
|
|
55
140
|
return config[scope];
|
|
56
141
|
}
|
|
57
142
|
function getHookConfig(agent) {
|
|
58
|
-
|
|
59
|
-
|
|
143
|
+
return providerFor(agent).hooks;
|
|
144
|
+
}
|
|
145
|
+
function getSettingsMergeHookConfig(agent) {
|
|
146
|
+
const config = getHookConfig(agent);
|
|
147
|
+
if (!config) {
|
|
148
|
+
throw new Error(`hooks not supported for agent target: ${agent}`);
|
|
149
|
+
}
|
|
150
|
+
if (config.type !== 'cc-settings-merge') {
|
|
151
|
+
throw new Error(`${providerFor(agent).label} hook strategy is not implemented yet`);
|
|
152
|
+
}
|
|
153
|
+
return config;
|
|
154
|
+
}
|
|
155
|
+
function assertLinkRenderer(type, agent) {
|
|
156
|
+
if (!['skill', 'workflow', 'agent'].includes(type)) {
|
|
157
|
+
throw new Error(`Unknown artifact type: ${String(type)}`);
|
|
158
|
+
}
|
|
159
|
+
const config = providerFor(agent)[type];
|
|
160
|
+
if (config && config.renderer !== 'link') {
|
|
161
|
+
throw new UnsupportedRendererError(`Renderer '${config.renderer}' for ${agent} ${type} artifacts is not implemented yet`);
|
|
162
|
+
}
|
|
163
|
+
return config;
|
|
60
164
|
}
|
|
61
165
|
function getInjection(agent) {
|
|
62
|
-
return
|
|
166
|
+
return providerFor(agent).injection;
|
|
63
167
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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.preflightLinkArtifactsForCli = preflightLinkArtifactsForCli;
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const provider_artifacts_1 = require("../core/provider-artifacts");
|
|
9
|
+
function defaultIO() {
|
|
10
|
+
return {
|
|
11
|
+
error: (message) => { console.error(message); },
|
|
12
|
+
exit: (code) => { process.exit(code); },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function preflightLinkArtifactsForCli(pairs, io = defaultIO()) {
|
|
16
|
+
try {
|
|
17
|
+
(0, provider_artifacts_1.preflightLinkArtifactPairs)(pairs);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
io.error(picocolors_1.default.red(message));
|
|
23
|
+
io.exit(1);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/src/utils/config.js
CHANGED
|
@@ -5,16 +5,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.preferencesExist = preferencesExist;
|
|
7
7
|
exports.getPreferences = getPreferences;
|
|
8
|
+
exports.loadPreferences = loadPreferences;
|
|
8
9
|
exports.savePreferences = savePreferences;
|
|
10
|
+
exports.enableAgent = enableAgent;
|
|
9
11
|
// src/utils/config.ts
|
|
10
12
|
const fs_1 = __importDefault(require("fs"));
|
|
11
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const providers_1 = require("../providers");
|
|
12
15
|
const paths_1 = require("../core/paths");
|
|
13
16
|
const DEFAULT_PREFS = {
|
|
14
17
|
// claude-code matches `awm init`'s own documented default (see init.ts / `awm init --help`).
|
|
15
18
|
// Previously 'antigravity', which silently mis-installed bundles in claude-code
|
|
16
19
|
// environments when `awm add` (the first getPreferences caller) stamped it to disk (#7).
|
|
17
20
|
defaultAgent: 'claude-code',
|
|
21
|
+
enabledAgents: ['claude-code'],
|
|
18
22
|
installMethod: 'symlink',
|
|
19
23
|
defaultScope: 'local'
|
|
20
24
|
};
|
|
@@ -28,19 +32,107 @@ function prefsFile() {
|
|
|
28
32
|
function preferencesExist() {
|
|
29
33
|
return fs_1.default.existsSync(prefsFile());
|
|
30
34
|
}
|
|
35
|
+
function isNonEmptyString(value) {
|
|
36
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
function isPlainObject(value) {
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
40
|
+
return false;
|
|
41
|
+
const prototype = Object.getPrototypeOf(value);
|
|
42
|
+
return prototype === Object.prototype || prototype === null;
|
|
43
|
+
}
|
|
44
|
+
function normalizePreferences(value, allowEnabledAgentsMigration = true) {
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
46
|
+
throw new Error('preferences.json must contain a JSON object');
|
|
47
|
+
}
|
|
48
|
+
const raw = value;
|
|
49
|
+
if (!(0, providers_1.isAgentTarget)(raw.defaultAgent)) {
|
|
50
|
+
throw new Error('preferences.json has an invalid defaultAgent');
|
|
51
|
+
}
|
|
52
|
+
if (raw.installMethod !== 'symlink' && raw.installMethod !== 'copy') {
|
|
53
|
+
throw new Error('preferences.json has an invalid installMethod');
|
|
54
|
+
}
|
|
55
|
+
if (raw.defaultScope !== 'global' && raw.defaultScope !== 'local') {
|
|
56
|
+
throw new Error('preferences.json has an invalid defaultScope');
|
|
57
|
+
}
|
|
58
|
+
if (raw.baseRemote !== undefined && !isNonEmptyString(raw.baseRemote)) {
|
|
59
|
+
throw new Error('preferences.json has an invalid baseRemote');
|
|
60
|
+
}
|
|
61
|
+
if (raw.channel !== undefined && raw.channel !== 'stable' && raw.channel !== 'dev') {
|
|
62
|
+
throw new Error('preferences.json has an invalid channel');
|
|
63
|
+
}
|
|
64
|
+
if (raw.pins !== undefined && (!isPlainObject(raw.pins) ||
|
|
65
|
+
!Object.entries(raw.pins).every(([name, version]) => isNonEmptyString(name) && isNonEmptyString(version)))) {
|
|
66
|
+
throw new Error('preferences.json has invalid pins');
|
|
67
|
+
}
|
|
68
|
+
if (raw.enabledAgents === undefined && !allowEnabledAgentsMigration) {
|
|
69
|
+
throw new Error('preferences.json has an invalid enabledAgents');
|
|
70
|
+
}
|
|
71
|
+
const source = raw.enabledAgents === undefined ? [raw.defaultAgent] : raw.enabledAgents;
|
|
72
|
+
if (!Array.isArray(source) || !source.every(providers_1.isAgentTarget)) {
|
|
73
|
+
throw new Error('preferences.json has an invalid enabledAgents');
|
|
74
|
+
}
|
|
75
|
+
const enabledAgents = Array.from(new Set(source));
|
|
76
|
+
if (!enabledAgents.includes(raw.defaultAgent)) {
|
|
77
|
+
throw new Error('preferences.json defaultAgent must be included in enabledAgents');
|
|
78
|
+
}
|
|
79
|
+
const prefs = { ...raw, enabledAgents };
|
|
80
|
+
return {
|
|
81
|
+
prefs,
|
|
82
|
+
changed: raw.enabledAgents === undefined ||
|
|
83
|
+
JSON.stringify(source) !== JSON.stringify(enabledAgents),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
31
86
|
function getPreferences() {
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
87
|
+
const loaded = loadPreferences();
|
|
88
|
+
if (!loaded.exists) {
|
|
34
89
|
savePreferences(DEFAULT_PREFS);
|
|
35
|
-
return DEFAULT_PREFS;
|
|
90
|
+
return { ...DEFAULT_PREFS, enabledAgents: [...DEFAULT_PREFS.enabledAgents] };
|
|
91
|
+
}
|
|
92
|
+
if (loaded.migrationRequired)
|
|
93
|
+
savePreferences(loaded.prefs);
|
|
94
|
+
return loaded.prefs;
|
|
95
|
+
}
|
|
96
|
+
function loadPreferences(initialAgent = 'claude-code') {
|
|
97
|
+
if (!(0, providers_1.isAgentTarget)(initialAgent)) {
|
|
98
|
+
throw new Error('initialAgent must be a valid agent target');
|
|
36
99
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
100
|
+
const file = prefsFile();
|
|
101
|
+
if (!fs_1.default.existsSync(file)) {
|
|
102
|
+
const prefs = {
|
|
103
|
+
...DEFAULT_PREFS,
|
|
104
|
+
defaultAgent: initialAgent,
|
|
105
|
+
enabledAgents: [initialAgent],
|
|
106
|
+
};
|
|
107
|
+
return { prefs, exists: false, migrationRequired: true };
|
|
108
|
+
}
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`${file} is not valid JSON. Fix it manually, then re-run.`);
|
|
115
|
+
}
|
|
116
|
+
const result = normalizePreferences(parsed);
|
|
117
|
+
return { prefs: result.prefs, exists: true, migrationRequired: result.changed };
|
|
39
118
|
}
|
|
40
119
|
function savePreferences(prefs) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
120
|
+
const normalized = normalizePreferences(prefs, false).prefs;
|
|
121
|
+
const file = prefsFile();
|
|
122
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
123
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
124
|
+
fs_1.default.writeFileSync(temp, JSON.stringify(normalized, null, 2) + '\n', {
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
mode: 0o600,
|
|
127
|
+
});
|
|
128
|
+
fs_1.default.renameSync(temp, file);
|
|
129
|
+
}
|
|
130
|
+
function enableAgent(prefs, agent) {
|
|
131
|
+
normalizePreferences(prefs);
|
|
132
|
+
if (!(0, providers_1.isAgentTarget)(agent)) {
|
|
133
|
+
throw new Error('agent must be a valid agent target');
|
|
44
134
|
}
|
|
45
|
-
|
|
135
|
+
return prefs.enabledAgents.includes(agent)
|
|
136
|
+
? prefs
|
|
137
|
+
: { ...prefs, enabledAgents: [...prefs.enabledAgents, agent] };
|
|
46
138
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
describe('agent command — listAgents / disableAgent', () => {
|
|
10
|
+
let tmpHome;
|
|
11
|
+
let originalHome;
|
|
12
|
+
let originalAwmHome;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-agentcmd-'));
|
|
15
|
+
originalHome = process.env.HOME;
|
|
16
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
17
|
+
process.env.HOME = tmpHome;
|
|
18
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
19
|
+
jest.resetModules();
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
23
|
+
if (originalHome === undefined)
|
|
24
|
+
delete process.env.HOME;
|
|
25
|
+
else
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
if (originalAwmHome === undefined)
|
|
28
|
+
delete process.env.AWM_HOME;
|
|
29
|
+
else
|
|
30
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
31
|
+
});
|
|
32
|
+
function prefsWith(enabledAgents, defaultAgent) {
|
|
33
|
+
const { savePreferences } = require('../../src/utils/config');
|
|
34
|
+
savePreferences({
|
|
35
|
+
defaultAgent,
|
|
36
|
+
enabledAgents,
|
|
37
|
+
installMethod: 'symlink',
|
|
38
|
+
defaultScope: 'local',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function writePrefs(enabledAgents, defaultAgent) {
|
|
42
|
+
prefsWith(enabledAgents, defaultAgent);
|
|
43
|
+
}
|
|
44
|
+
function readPrefs() {
|
|
45
|
+
const file = path_1.default.join(process.env.AWM_HOME, 'preferences.json');
|
|
46
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
it('lists supported, enabled and default state', () => {
|
|
49
|
+
writePrefs(['claude-code', 'codex'], 'claude-code');
|
|
50
|
+
const { listAgents } = require('../../src/commands/agent');
|
|
51
|
+
expect(listAgents()).toEqual(expect.arrayContaining([
|
|
52
|
+
{ id: 'claude-code', label: 'Claude Code', enabled: true, default: true },
|
|
53
|
+
{ id: 'codex', label: 'Codex', enabled: true, default: false },
|
|
54
|
+
{ id: 'opencode', label: 'OpenCode', enabled: false, default: false },
|
|
55
|
+
]));
|
|
56
|
+
});
|
|
57
|
+
it('refuses to disable the default without a replacement', () => {
|
|
58
|
+
writePrefs(['claude-code', 'codex'], 'claude-code');
|
|
59
|
+
const { disableAgent } = require('../../src/commands/agent');
|
|
60
|
+
expect(() => disableAgent('claude-code')).toThrow('--default <agent>');
|
|
61
|
+
});
|
|
62
|
+
it('disables management state without deleting provider files', () => {
|
|
63
|
+
const marker = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
64
|
+
fs_1.default.mkdirSync(path_1.default.dirname(marker), { recursive: true });
|
|
65
|
+
fs_1.default.writeFileSync(marker, 'keep');
|
|
66
|
+
writePrefs(['claude-code', 'codex'], 'claude-code');
|
|
67
|
+
const { disableAgent } = require('../../src/commands/agent');
|
|
68
|
+
disableAgent('codex');
|
|
69
|
+
expect(readPrefs().enabledAgents).toEqual(['claude-code']);
|
|
70
|
+
expect(fs_1.default.readFileSync(marker, 'utf8')).toBe('keep');
|
|
71
|
+
});
|
|
72
|
+
it('disabling the default WITH a valid replacement succeeds and switches the default', () => {
|
|
73
|
+
writePrefs(['claude-code', 'codex'], 'claude-code');
|
|
74
|
+
const { disableAgent } = require('../../src/commands/agent');
|
|
75
|
+
disableAgent('claude-code', 'codex');
|
|
76
|
+
expect(readPrefs()).toEqual({
|
|
77
|
+
defaultAgent: 'codex',
|
|
78
|
+
enabledAgents: ['codex'],
|
|
79
|
+
installMethod: 'symlink',
|
|
80
|
+
defaultScope: 'local',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it('rejects a replacement default that is not itself enabled', () => {
|
|
84
|
+
writePrefs(['claude-code', 'codex'], 'claude-code');
|
|
85
|
+
const { disableAgent } = require('../../src/commands/agent');
|
|
86
|
+
expect(() => disableAgent('claude-code', 'opencode')).toThrow('must remain enabled');
|
|
87
|
+
});
|
|
88
|
+
it('rejects disabling an agent that is not enabled', () => {
|
|
89
|
+
writePrefs(['claude-code'], 'claude-code');
|
|
90
|
+
const { disableAgent } = require('../../src/commands/agent');
|
|
91
|
+
expect(() => disableAgent('codex')).toThrow('is not enabled');
|
|
92
|
+
});
|
|
93
|
+
it('registers `agent list` and `agent disable` subcommands', () => {
|
|
94
|
+
const { Command } = require('commander');
|
|
95
|
+
const { registerAgentCommand } = require('../../src/commands/agent');
|
|
96
|
+
const program = new Command();
|
|
97
|
+
registerAgentCommand(program);
|
|
98
|
+
const agent = program.commands.find((c) => c.name() === 'agent');
|
|
99
|
+
expect(agent).toBeDefined();
|
|
100
|
+
const subNames = agent.commands.map((c) => c.name());
|
|
101
|
+
expect(subNames).toEqual(expect.arrayContaining(['list', 'disable']));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
// tests/commands/backup.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const install_transaction_1 = require("../../src/core/install-transaction");
|
|
12
|
+
const backup_1 = require("../../src/commands/backup");
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let tmpWork;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalAwmHome;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-backupcmd-home-'));
|
|
19
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-backupcmd-work-'));
|
|
20
|
+
originalHome = process.env.HOME;
|
|
21
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
22
|
+
process.env.HOME = tmpHome;
|
|
23
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
27
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
28
|
+
if (originalHome === undefined)
|
|
29
|
+
delete process.env.HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.HOME = originalHome;
|
|
32
|
+
if (originalAwmHome === undefined)
|
|
33
|
+
delete process.env.AWM_HOME;
|
|
34
|
+
else
|
|
35
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
36
|
+
});
|
|
37
|
+
describe('restoreBackup', () => {
|
|
38
|
+
it('restores only manifest targets and rejects path traversal ids', () => {
|
|
39
|
+
const codexAgentsFile = path_1.default.join(tmpWork, 'AGENTS.md');
|
|
40
|
+
const preferencesFile = path_1.default.join(tmpWork, 'preferences.json');
|
|
41
|
+
const unrelatedFile = path_1.default.join(tmpWork, 'unrelated.txt');
|
|
42
|
+
fs_1.default.writeFileSync(codexAgentsFile, 'codex-before');
|
|
43
|
+
fs_1.default.writeFileSync(preferencesFile, 'prefs-before');
|
|
44
|
+
fs_1.default.writeFileSync(unrelatedFile, 'keep');
|
|
45
|
+
const session = (0, install_transaction_1.beginBackupSession)([codexAgentsFile, preferencesFile]);
|
|
46
|
+
fs_1.default.writeFileSync(codexAgentsFile, 'codex-after');
|
|
47
|
+
fs_1.default.writeFileSync(preferencesFile, 'prefs-after');
|
|
48
|
+
expect(() => (0, install_transaction_1.restoreBackup)('../outside')).toThrow('invalid backup transaction id');
|
|
49
|
+
const validTransactionId = session.transactionId;
|
|
50
|
+
const result = (0, install_transaction_1.restoreBackup)(validTransactionId);
|
|
51
|
+
expect(result.restored).toEqual([codexAgentsFile, preferencesFile]);
|
|
52
|
+
expect(fs_1.default.readFileSync(unrelatedFile, 'utf8')).toBe('keep'); // verifies R25
|
|
53
|
+
expect(fs_1.default.readFileSync(codexAgentsFile, 'utf8')).toBe('codex-before');
|
|
54
|
+
expect(fs_1.default.readFileSync(preferencesFile, 'utf8')).toBe('prefs-before');
|
|
55
|
+
});
|
|
56
|
+
it('rejects ids that embed a path separator even when digit-prefixed', () => {
|
|
57
|
+
expect(() => (0, install_transaction_1.restoreBackup)('2024-01-01T00-00-00-000Z/../../etc')).toThrow('invalid backup transaction id');
|
|
58
|
+
});
|
|
59
|
+
it('rejects a well-formed but nonexistent transaction id', () => {
|
|
60
|
+
expect(() => (0, install_transaction_1.restoreBackup)('2020-01-01T00-00-00-000Z')).toThrow();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('listBackups', () => {
|
|
64
|
+
it('lists recorded transactions newest first, without leaking file contents', () => {
|
|
65
|
+
const fileA = path_1.default.join(tmpWork, 'a.txt');
|
|
66
|
+
fs_1.default.writeFileSync(fileA, 'secret-value');
|
|
67
|
+
const session = (0, install_transaction_1.beginBackupSession)([fileA]);
|
|
68
|
+
const backups = (0, install_transaction_1.listBackups)();
|
|
69
|
+
expect(backups.length).toBe(1);
|
|
70
|
+
expect(backups[0].id).toBe(session.transactionId);
|
|
71
|
+
expect(backups[0].targets).toEqual([fileA]);
|
|
72
|
+
expect(backups[0].committed).toBe(false);
|
|
73
|
+
expect(JSON.stringify(backups)).not.toContain('secret-value');
|
|
74
|
+
});
|
|
75
|
+
it('returns an empty list when no backups exist yet', () => {
|
|
76
|
+
expect((0, install_transaction_1.listBackups)()).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('registerBackupCommand', () => {
|
|
80
|
+
it('registers `backup list` and `backup restore` subcommands', () => {
|
|
81
|
+
const program = new commander_1.Command();
|
|
82
|
+
(0, backup_1.registerBackupCommand)(program);
|
|
83
|
+
const backup = program.commands.find((c) => c.name() === 'backup');
|
|
84
|
+
expect(backup).toBeDefined();
|
|
85
|
+
const subNames = backup.commands.map((c) => c.name());
|
|
86
|
+
expect(subNames).toEqual(expect.arrayContaining(['list', 'restore']));
|
|
87
|
+
});
|
|
88
|
+
it('`backup list --json` prints the same data as listBackups()', () => {
|
|
89
|
+
const fileA = path_1.default.join(tmpWork, 'b.txt');
|
|
90
|
+
fs_1.default.writeFileSync(fileA, 'v');
|
|
91
|
+
(0, install_transaction_1.beginBackupSession)([fileA]);
|
|
92
|
+
const program = new commander_1.Command();
|
|
93
|
+
program.exitOverride();
|
|
94
|
+
(0, backup_1.registerBackupCommand)(program);
|
|
95
|
+
let output = '';
|
|
96
|
+
const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
97
|
+
output += chunk.toString();
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
program.parse(['node', 'awm', 'backup', 'list', '--json']);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
write.mockRestore();
|
|
105
|
+
}
|
|
106
|
+
const parsed = JSON.parse(output);
|
|
107
|
+
expect(parsed).toEqual((0, install_transaction_1.listBackups)());
|
|
108
|
+
});
|
|
109
|
+
});
|