claude-dev-env 7.7.1 → 8.1.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/.agents/agents/clean-coder.md +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/audit-categories.json +0 -4
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/anti-corollary-tests.md +3 -1
- package/rules/archiving-agent-config.md +70 -0
- package/rules/asd-ste100-language.md +1 -1
- package/rules/ci-owns-the-gate.md +73 -0
- package/rules/cleanup-temp-files.md +1 -1
- package/rules/code-standards.md +3 -4
- package/rules/docstring-prose-matches-implementation.md +3 -3
- package/rules/explore-thoroughly.md +2 -2
- package/rules/failure-blast-radius.md +10 -9
- package/rules/git-workflow.md +27 -12
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/nas-ssh-invocation.md +1 -1
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/research-mode.md +3 -1
- package/rules/retired-hook-prose.md +10 -2
- package/rules/testing.md +3 -3
- package/rules/verify-before-asking.md +4 -1
- package/rules/verify-runtime-state.md +2 -2
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_codex_compat_materializer.py +3 -3
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -776
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const PSTACK_MARKETPLACE_REPOSITORY = 'michael-denyer/pstack-claude';
|
|
4
|
+
export const PSTACK_MARKETPLACE_NAME = 'pstack-claude';
|
|
5
|
+
export const PSTACK_PLUGIN_IDENTIFIER = `pstack@${PSTACK_MARKETPLACE_NAME}`;
|
|
6
|
+
export const PSTACK_PLUGIN_HOSTS = Object.freeze(['claude', 'codex']);
|
|
7
|
+
|
|
8
|
+
export const PSTACK_PLUGIN_OPT_OUT_FLAG = '--no-pstack';
|
|
9
|
+
export const PSTACK_PLUGIN_OPT_OUT_VARIABLE = 'CDE_INSTALL_PSTACK';
|
|
10
|
+
|
|
11
|
+
const HOST_PLANS = Object.freeze({
|
|
12
|
+
claude: Object.freeze({
|
|
13
|
+
executable: 'claude',
|
|
14
|
+
executableVariable: 'CDE_CLAUDE_EXECUTABLE',
|
|
15
|
+
homeVariable: 'CLAUDE_CONFIG_DIR',
|
|
16
|
+
commands: Object.freeze([
|
|
17
|
+
Object.freeze(['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]),
|
|
18
|
+
Object.freeze(['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]),
|
|
19
|
+
]),
|
|
20
|
+
}),
|
|
21
|
+
codex: Object.freeze({
|
|
22
|
+
executable: 'codex',
|
|
23
|
+
executableVariable: 'CDE_CODEX_EXECUTABLE',
|
|
24
|
+
homeVariable: 'CODEX_HOME',
|
|
25
|
+
commands: Object.freeze([
|
|
26
|
+
Object.freeze(['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]),
|
|
27
|
+
Object.freeze(['plugin', 'add', PSTACK_PLUGIN_IDENTIFIER]),
|
|
28
|
+
]),
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the marketplace and plugin commands one host installs pstack with.
|
|
34
|
+
*
|
|
35
|
+
* Claude Code and Codex publish the same marketplace under different plugin
|
|
36
|
+
* verbs, so the command list belongs to the host rather than to the caller.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} host Either `claude` or `codex`.
|
|
39
|
+
* @returns {{executable: string, executableVariable: string, homeVariable: string,
|
|
40
|
+
* commands: ReadonlyArray<ReadonlyArray<string>>}} The host's install plan.
|
|
41
|
+
*/
|
|
42
|
+
export function pstackPluginPlan(host) {
|
|
43
|
+
const plan = HOST_PLANS[host];
|
|
44
|
+
if (!plan) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Unsupported pstack plugin host ${host}: this installer knows ${PSTACK_PLUGIN_HOSTS.join(', ')}.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return plan;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decide whether a full install also installs the pstack plugin.
|
|
54
|
+
*
|
|
55
|
+
* The step reaches the network for the marketplace repository. `--no-pstack`
|
|
56
|
+
* on the command line, or `CDE_INSTALL_PSTACK=0` in the environment, turns it
|
|
57
|
+
* off for an air-gapped or offline run.
|
|
58
|
+
*
|
|
59
|
+
* @param {string[]} [argumentList] The command-line arguments after the script.
|
|
60
|
+
* @param {Record<string, string|undefined>} [environment] The process environment.
|
|
61
|
+
* @returns {boolean} True when this run installs the pstack plugin.
|
|
62
|
+
*/
|
|
63
|
+
export function shouldInstallPstackPlugin(
|
|
64
|
+
argumentList = process.argv.slice(2),
|
|
65
|
+
environment = process.env,
|
|
66
|
+
) {
|
|
67
|
+
if (argumentList.includes(PSTACK_PLUGIN_OPT_OUT_FLAG)) return false;
|
|
68
|
+
return environment[PSTACK_PLUGIN_OPT_OUT_VARIABLE] !== '0';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the process launch one platform needs to run a host command.
|
|
73
|
+
*
|
|
74
|
+
* Both hosts ship on Windows as a `.cmd` shim, and Node's synchronous spawn
|
|
75
|
+
* rejects a batch file with `EINVAL` unless a shell runs it, so Windows goes
|
|
76
|
+
* through `cmd.exe /d /s /c` with the whole command line as one verbatim
|
|
77
|
+
* argument. The executable carries its own quotes inside that line, which is
|
|
78
|
+
* why the line is passed verbatim rather than quoted again by Node. Every
|
|
79
|
+
* other platform launches the executable directly.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} executable The host command, a bare name or a path.
|
|
82
|
+
* @param {string[]} commandArguments The arguments after the executable.
|
|
83
|
+
* @param {string} [platform] The platform to build for.
|
|
84
|
+
* @param {Record<string, string|undefined>} [environment] Supplies `ComSpec`.
|
|
85
|
+
* @returns {{file: string, args: string[], windowsVerbatimArguments: boolean}} The launch.
|
|
86
|
+
*/
|
|
87
|
+
export function hostCommandInvocation(
|
|
88
|
+
executable,
|
|
89
|
+
commandArguments,
|
|
90
|
+
platform = process.platform,
|
|
91
|
+
environment = process.env,
|
|
92
|
+
) {
|
|
93
|
+
if (platform !== 'win32') {
|
|
94
|
+
return { file: executable, args: [...commandArguments], windowsVerbatimArguments: false };
|
|
95
|
+
}
|
|
96
|
+
const commandLine = [`"${executable}"`, ...commandArguments].join(' ');
|
|
97
|
+
return {
|
|
98
|
+
file: environment.ComSpec || 'cmd.exe',
|
|
99
|
+
args: ['/d', '/s', '/c', `"${commandLine}"`],
|
|
100
|
+
windowsVerbatimArguments: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runHostCommand(executable, commandArguments, options) {
|
|
105
|
+
const invocation = hostCommandInvocation(executable, commandArguments);
|
|
106
|
+
const spawned = spawnSync(invocation.file, invocation.args, {
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
env: { ...process.env, ...options.environment },
|
|
109
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
110
|
+
});
|
|
111
|
+
return { status: spawned.status, stderr: spawned.stderr ?? '', error: spawned.error };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const COMMAND_NOT_FOUND_EXIT_CODE = 9009;
|
|
115
|
+
const COMMAND_NOT_FOUND_PATTERN = /is not recognized as an internal or external command/;
|
|
116
|
+
|
|
117
|
+
function namesAnAbsentCommand(outcome) {
|
|
118
|
+
if (outcome.error?.code === 'ENOENT') return true;
|
|
119
|
+
return outcome.status === COMMAND_NOT_FOUND_EXIT_CODE
|
|
120
|
+
&& COMMAND_NOT_FOUND_PATTERN.test(outcome.stderr ?? '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function firstLine(text) {
|
|
124
|
+
const trimmed = (text ?? '').trim();
|
|
125
|
+
if (!trimmed) return '';
|
|
126
|
+
return trimmed.split(/\r?\n/).filter(Boolean).at(-1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function installForHost(host, homeDirectory, environment, runCommand) {
|
|
130
|
+
const plan = pstackPluginPlan(host);
|
|
131
|
+
const executable = environment[plan.executableVariable] || plan.executable;
|
|
132
|
+
const commandEnvironment = { [plan.homeVariable]: homeDirectory };
|
|
133
|
+
for (const commandArguments of plan.commands) {
|
|
134
|
+
const outcome = runCommand(executable, [...commandArguments], {
|
|
135
|
+
environment: commandEnvironment,
|
|
136
|
+
});
|
|
137
|
+
if (namesAnAbsentCommand(outcome)) {
|
|
138
|
+
return {
|
|
139
|
+
host,
|
|
140
|
+
executable,
|
|
141
|
+
status: 'skipped',
|
|
142
|
+
warning: `${executable} is not on PATH, so pstack was not installed for ${host}.`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (outcome.status !== 0 || outcome.error) {
|
|
146
|
+
const detail = firstLine(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
|
|
147
|
+
return {
|
|
148
|
+
host,
|
|
149
|
+
executable,
|
|
150
|
+
status: 'failed',
|
|
151
|
+
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { host, executable, status: 'installed', warning: null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Install the pstack plugin from its marketplace into each host's own home.
|
|
160
|
+
*
|
|
161
|
+
* Each host is one member of the batch. A host without its command-line tool
|
|
162
|
+
* is skipped and a host whose command fails is reported, so the rules, hooks,
|
|
163
|
+
* and skills this run already wrote still reach their durable places. An
|
|
164
|
+
* absent tool arrives as a spawn `ENOENT` on other platforms and as
|
|
165
|
+
* `cmd.exe`'s command-not-found exit code on Windows, and both read as
|
|
166
|
+
* skipped.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} [options] Install targets.
|
|
169
|
+
* @param {string} [options.claudeRoot] The managed Claude root to install into.
|
|
170
|
+
* @param {string} [options.codexHome] The Codex home to install into.
|
|
171
|
+
* @param {string[]} [options.hosts] The hosts to install. Defaults to both.
|
|
172
|
+
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
173
|
+
* @param {object} [dependencies] Seams for the command runner.
|
|
174
|
+
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome per host.
|
|
175
|
+
*/
|
|
176
|
+
export function installPstackPlugin(options = {}, dependencies = {}) {
|
|
177
|
+
const environment = options.environment ?? process.env;
|
|
178
|
+
const runCommand = dependencies.runCommand ?? runHostCommand;
|
|
179
|
+
const homeDirectories = { claude: options.claudeRoot, codex: options.codexHome };
|
|
180
|
+
const hosts = options.hosts ?? PSTACK_PLUGIN_HOSTS;
|
|
181
|
+
const hostOutcomes = hosts.map(host => installForHost(
|
|
182
|
+
host,
|
|
183
|
+
homeDirectories[host],
|
|
184
|
+
environment,
|
|
185
|
+
runCommand,
|
|
186
|
+
));
|
|
187
|
+
const failedCount = hostOutcomes.filter(outcome => outcome.status === 'failed').length;
|
|
188
|
+
const installedCount = hostOutcomes.filter(outcome => outcome.status === 'installed').length;
|
|
189
|
+
const status = failedCount > 0 ? 'failed' : (installedCount > 0 ? 'installed' : 'skipped');
|
|
190
|
+
const warnings = hostOutcomes.map(outcome => outcome.warning).filter(Boolean);
|
|
191
|
+
return { status, hosts: hostOutcomes, warning: warnings.join(' ') || null };
|
|
192
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
PSTACK_MARKETPLACE_REPOSITORY,
|
|
5
|
+
PSTACK_PLUGIN_HOSTS,
|
|
6
|
+
PSTACK_PLUGIN_IDENTIFIER,
|
|
7
|
+
hostCommandInvocation,
|
|
8
|
+
installPstackPlugin,
|
|
9
|
+
pstackPluginPlan,
|
|
10
|
+
shouldInstallPstackPlugin,
|
|
11
|
+
} from './install-pstack-plugin.mjs';
|
|
12
|
+
|
|
13
|
+
function recordingRunner(outcomeForCommand = () => ({ status: 0, stderr: '' })) {
|
|
14
|
+
const calls = [];
|
|
15
|
+
return {
|
|
16
|
+
calls,
|
|
17
|
+
runCommand(executable, commandArguments, options) {
|
|
18
|
+
calls.push({ executable, commandArguments, environment: options.environment });
|
|
19
|
+
return outcomeForCommand(executable, commandArguments);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ROOTS = Object.freeze({
|
|
25
|
+
claudeRoot: '/managed/.claude',
|
|
26
|
+
codexHome: '/managed/.codex',
|
|
27
|
+
environment: {},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const WINDOWS_ENVIRONMENT = Object.freeze({ ComSpec: 'C:\\Windows\\system32\\cmd.exe' });
|
|
31
|
+
|
|
32
|
+
test('the Claude plan carries the two documented plugin commands', () => {
|
|
33
|
+
const plan = pstackPluginPlan('claude');
|
|
34
|
+
assert.equal(plan.executable, 'claude');
|
|
35
|
+
assert.deepEqual(plan.commands, [
|
|
36
|
+
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
37
|
+
['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('the Codex plan adds the plugin with the Codex verb', () => {
|
|
42
|
+
const plan = pstackPluginPlan('codex');
|
|
43
|
+
assert.equal(plan.executable, 'codex');
|
|
44
|
+
assert.deepEqual(plan.commands, [
|
|
45
|
+
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
46
|
+
['plugin', 'add', PSTACK_PLUGIN_IDENTIFIER],
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an unsupported host names the hosts this installer knows', () => {
|
|
51
|
+
assert.throws(() => pstackPluginPlan('cursor'), /cursor/);
|
|
52
|
+
assert.deepEqual(PSTACK_PLUGIN_HOSTS, ['claude', 'codex']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('each host installs into the managed root this run wrote', () => {
|
|
56
|
+
const runner = recordingRunner();
|
|
57
|
+
const outcome = installPstackPlugin(ROOTS, runner);
|
|
58
|
+
assert.equal(outcome.status, 'installed');
|
|
59
|
+
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
60
|
+
['claude', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
61
|
+
['claude', 'plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
|
|
62
|
+
['codex', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
63
|
+
['codex', 'plugin', 'add', PSTACK_PLUGIN_IDENTIFIER],
|
|
64
|
+
]);
|
|
65
|
+
assert.equal(runner.calls[0].environment.CLAUDE_CONFIG_DIR, ROOTS.claudeRoot);
|
|
66
|
+
assert.equal(runner.calls[2].environment.CODEX_HOME, ROOTS.codexHome);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('an absent host command skips that host and leaves the other installed', () => {
|
|
70
|
+
const runner = recordingRunner((executable) => (executable === 'codex'
|
|
71
|
+
? { status: null, error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }) }
|
|
72
|
+
: { status: 0, stderr: '' }));
|
|
73
|
+
const outcome = installPstackPlugin(ROOTS, runner);
|
|
74
|
+
assert.equal(outcome.hosts.find(host => host.host === 'claude').status, 'installed');
|
|
75
|
+
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
76
|
+
assert.equal(codex.status, 'skipped');
|
|
77
|
+
assert.match(codex.warning, /codex/);
|
|
78
|
+
assert.equal(runner.calls.filter(call => call.executable === 'codex').length, 1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a failing command reports that host and still installs the next one', () => {
|
|
82
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
83
|
+
executable === 'claude' && commandArguments[1] === 'marketplace'
|
|
84
|
+
? { status: 1, stderr: 'marketplace unreachable' }
|
|
85
|
+
: { status: 0, stderr: '' }));
|
|
86
|
+
const outcome = installPstackPlugin(ROOTS, runner);
|
|
87
|
+
assert.equal(outcome.status, 'failed');
|
|
88
|
+
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
89
|
+
assert.equal(claude.status, 'failed');
|
|
90
|
+
assert.match(claude.warning, /marketplace unreachable/);
|
|
91
|
+
assert.equal(outcome.hosts.find(host => host.host === 'codex').status, 'installed');
|
|
92
|
+
assert.equal(runner.calls.filter(call => call.executable === 'claude').length, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('an executable override replaces the host command for that host only', () => {
|
|
96
|
+
const runner = recordingRunner();
|
|
97
|
+
installPstackPlugin(
|
|
98
|
+
{ ...ROOTS, environment: { CDE_CODEX_EXECUTABLE: '/opt/codex/bin/codex' } },
|
|
99
|
+
runner,
|
|
100
|
+
);
|
|
101
|
+
assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], [
|
|
102
|
+
'claude',
|
|
103
|
+
'/opt/codex/bin/codex',
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('the opt-out flag and variable both turn the step off', () => {
|
|
108
|
+
assert.equal(shouldInstallPstackPlugin([], {}), true);
|
|
109
|
+
assert.equal(shouldInstallPstackPlugin(['--no-pstack'], {}), false);
|
|
110
|
+
assert.equal(shouldInstallPstackPlugin([], { CDE_INSTALL_PSTACK: '0' }), false);
|
|
111
|
+
assert.equal(shouldInstallPstackPlugin([], { CDE_INSTALL_PSTACK: '1' }), true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a selected host list installs only that host', () => {
|
|
115
|
+
const runner = recordingRunner();
|
|
116
|
+
const outcome = installPstackPlugin({ ...ROOTS, hosts: ['codex'] }, runner);
|
|
117
|
+
assert.deepEqual(outcome.hosts.map(host => host.host), ['codex']);
|
|
118
|
+
assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], ['codex']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a non-Windows platform launches the host command directly', () => {
|
|
122
|
+
const invocation = hostCommandInvocation('claude', ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER], 'linux');
|
|
123
|
+
assert.deepEqual(invocation, {
|
|
124
|
+
file: 'claude',
|
|
125
|
+
args: ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
|
|
126
|
+
windowsVerbatimArguments: false,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('Windows launches the host command through cmd.exe, which alone can run a .cmd shim', () => {
|
|
131
|
+
const invocation = hostCommandInvocation(
|
|
132
|
+
'claude', ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER], 'win32', WINDOWS_ENVIRONMENT,
|
|
133
|
+
);
|
|
134
|
+
assert.equal(invocation.file, WINDOWS_ENVIRONMENT.ComSpec);
|
|
135
|
+
assert.deepEqual(invocation.args, [
|
|
136
|
+
'/d',
|
|
137
|
+
'/s',
|
|
138
|
+
'/c',
|
|
139
|
+
`""claude" plugin install ${PSTACK_PLUGIN_IDENTIFIER}"`,
|
|
140
|
+
]);
|
|
141
|
+
assert.equal(invocation.windowsVerbatimArguments, true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a Windows executable path holding a space stays one quoted token', () => {
|
|
145
|
+
const invocation = hostCommandInvocation(
|
|
146
|
+
'C:\\Program Files\\nodejs\\claude.cmd',
|
|
147
|
+
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
148
|
+
'win32',
|
|
149
|
+
WINDOWS_ENVIRONMENT,
|
|
150
|
+
);
|
|
151
|
+
assert.equal(
|
|
152
|
+
invocation.args.at(-1),
|
|
153
|
+
`""C:\\Program Files\\nodejs\\claude.cmd" plugin marketplace add ${PSTACK_MARKETPLACE_REPOSITORY}"`,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a Windows environment without ComSpec falls back to the bare shell name', () => {
|
|
158
|
+
const invocation = hostCommandInvocation('claude', ['plugin'], 'win32', {});
|
|
159
|
+
assert.equal(invocation.file, 'cmd.exe');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("cmd.exe's command-not-found exit code reads as an absent host, not a failure", () => {
|
|
163
|
+
const runner = recordingRunner((executable) => (executable === 'codex'
|
|
164
|
+
? {
|
|
165
|
+
status: 9009,
|
|
166
|
+
stderr: "'codex' is not recognized as an internal or external command,\noperable program or batch file.",
|
|
167
|
+
}
|
|
168
|
+
: { status: 0, stderr: '' }));
|
|
169
|
+
const outcome = installPstackPlugin(ROOTS, runner);
|
|
170
|
+
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
171
|
+
assert.equal(codex.status, 'skipped');
|
|
172
|
+
assert.equal(outcome.status, 'installed');
|
|
173
|
+
assert.equal(runner.calls.filter(call => call.executable === 'codex').length, 1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('a non-zero exit that is not command-not-found still reads as a failure', () => {
|
|
177
|
+
const runner = recordingRunner(() => ({ status: 9009, stderr: 'the marketplace rejected the catalog' }));
|
|
178
|
+
const outcome = installPstackPlugin({ ...ROOTS, hosts: ['claude'] }, runner);
|
|
179
|
+
assert.equal(outcome.hosts[0].status, 'failed');
|
|
180
|
+
assert.match(outcome.hosts[0].warning, /rejected the catalog/);
|
|
181
|
+
});
|
|
@@ -53,10 +53,6 @@ const CLEAN_CODER_POLICY_REFERENCES = [
|
|
|
53
53
|
'<managed-root>/rules/code-standards.md',
|
|
54
54
|
'packages/claude-dev-env/rules/code-standards.md',
|
|
55
55
|
],
|
|
56
|
-
[
|
|
57
|
-
'<managed-root>/rules/file-global-constants.md',
|
|
58
|
-
'packages/claude-dev-env/rules/file-global-constants.md',
|
|
59
|
-
],
|
|
60
56
|
[
|
|
61
57
|
'<managed-root>/rules/windows-filesystem-safe.md',
|
|
62
58
|
'packages/claude-dev-env/rules/windows-filesystem-safe.md',
|
|
@@ -118,6 +118,11 @@ test('resolveInstallRoot names ~/.codex/rules and allows files under it', () =>
|
|
|
118
118
|
});
|
|
119
119
|
const expectedDirectory = join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME, CODEX_RULES_DIRECTORY_NAME);
|
|
120
120
|
assert.equal(resolution.codexRulesInstallDirectory, expectedDirectory);
|
|
121
|
+
assert.equal(
|
|
122
|
+
resolution.codexHomeDirectory,
|
|
123
|
+
join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
|
|
124
|
+
'the pstack plugin step installs into this Codex home',
|
|
125
|
+
);
|
|
121
126
|
assert.equal(
|
|
122
127
|
isAllowedInstallDestination(join(expectedDirectory, CODEX_RULES_SHIPPED_FILE_NAME), resolution),
|
|
123
128
|
true,
|
|
@@ -140,6 +145,7 @@ test('CODEX_HOME relocates the Codex rules destination', () => {
|
|
|
140
145
|
resolution.codexRulesInstallDirectory,
|
|
141
146
|
join(relocatedHome, CODEX_RULES_DIRECTORY_NAME),
|
|
142
147
|
);
|
|
148
|
+
assert.equal(resolution.codexHomeDirectory, relocatedHome);
|
|
143
149
|
});
|
|
144
150
|
|
|
145
151
|
test('a full install copies shipped Codex rules and leaves a local default.rules in place', () => {
|