claude-dev-env 8.33.1 → 8.34.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/bin/install-pstack-plugin.mjs +65 -33
- package/bin/install-pstack-plugin.test.mjs +93 -17
- package/bin/install.agents-home.test.mjs +1 -0
- package/bin/install.codex-rules.test.mjs +2 -0
- package/bin/install.cursor-rules.test.mjs +1 -0
- package/bin/install.mjs +23 -9
- package/bin/install.prune.test.mjs +1 -0
- package/bin/install.question-presentation.test.mjs +4 -0
- package/bin/install.settings-defaults.test.mjs +1 -0
- package/bin/install.test.mjs +76 -0
- package/bin/install.transaction.test.mjs +8 -0
- package/bin/install.uninstall-transaction.test.mjs +2 -0
- package/hooks/advisory/pr_done_reminder.py +3 -1
- package/hooks/advisory/test_pr_done_reminder.py +52 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +4 -0
- package/package.json +1 -1
|
@@ -8,64 +8,94 @@ export const PSTACK_PLUGIN_HOSTS = Object.freeze(['claude', 'codex']);
|
|
|
8
8
|
export const PSTACK_PLUGIN_OPT_OUT_FLAG = '--no-pstack';
|
|
9
9
|
export const PSTACK_PLUGIN_OPT_OUT_VARIABLE = 'CDE_INSTALL_PSTACK';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
export const PSTACK_PLUGIN_SPEC = Object.freeze({
|
|
12
|
+
name: 'pstack',
|
|
13
|
+
label: 'Pstack',
|
|
14
|
+
marketplaceRepository: PSTACK_MARKETPLACE_REPOSITORY,
|
|
15
|
+
marketplaceAddArguments: Object.freeze([]),
|
|
16
|
+
pluginIdentifier: PSTACK_PLUGIN_IDENTIFIER,
|
|
17
|
+
hosts: PSTACK_PLUGIN_HOSTS,
|
|
18
|
+
optOutFlag: PSTACK_PLUGIN_OPT_OUT_FLAG,
|
|
19
|
+
optOutVariable: PSTACK_PLUGIN_OPT_OUT_VARIABLE,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const USAGE_WRAPUP_PLUGIN_SPEC = Object.freeze({
|
|
23
|
+
name: 'usage-wrapup',
|
|
24
|
+
label: 'Usage-wrapup',
|
|
25
|
+
marketplaceRepository: 'jl-cmd/claude-dev-env',
|
|
26
|
+
marketplaceAddArguments: Object.freeze(['--sparse', '.claude-plugin']),
|
|
27
|
+
pluginIdentifier: 'usage-wrapup@claude-dev-env',
|
|
28
|
+
hosts: Object.freeze(['claude']),
|
|
29
|
+
optOutFlag: '--no-usage-wrapup',
|
|
30
|
+
optOutVariable: 'CDE_INSTALL_USAGE_WRAPUP',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const HOST_DEFINITIONS = Object.freeze({
|
|
12
34
|
claude: Object.freeze({
|
|
13
35
|
executable: 'claude',
|
|
14
36
|
executableVariable: 'CDE_CLAUDE_EXECUTABLE',
|
|
15
37
|
homeVariable: 'CLAUDE_CONFIG_DIR',
|
|
16
|
-
|
|
17
|
-
Object.freeze(['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]),
|
|
18
|
-
Object.freeze(['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]),
|
|
19
|
-
]),
|
|
38
|
+
installVerb: 'install',
|
|
20
39
|
}),
|
|
21
40
|
codex: Object.freeze({
|
|
22
41
|
executable: 'codex',
|
|
23
42
|
executableVariable: 'CDE_CODEX_EXECUTABLE',
|
|
24
43
|
homeVariable: 'CODEX_HOME',
|
|
25
|
-
|
|
26
|
-
Object.freeze(['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]),
|
|
27
|
-
Object.freeze(['plugin', 'add', PSTACK_PLUGIN_IDENTIFIER]),
|
|
28
|
-
]),
|
|
44
|
+
installVerb: 'add',
|
|
29
45
|
}),
|
|
30
46
|
});
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
|
-
* Read the marketplace and plugin commands one host installs
|
|
49
|
+
* Read the marketplace and plugin commands one host installs a plugin with.
|
|
34
50
|
*
|
|
35
|
-
* Claude Code and Codex publish
|
|
36
|
-
*
|
|
51
|
+
* Claude Code and Codex publish a marketplace under different plugin verbs, so
|
|
52
|
+
* the install verb belongs to the host and the marketplace and identifier
|
|
53
|
+
* belong to the plugin spec. A spec whose marketplace is a large repository
|
|
54
|
+
* passes `--sparse .claude-plugin` among its marketplace add arguments, so the
|
|
55
|
+
* clone holds only the catalog and stays clear of Windows path-length limits.
|
|
37
56
|
*
|
|
38
|
-
* @param {
|
|
57
|
+
* @param {typeof PSTACK_PLUGIN_SPEC} spec The plugin to install.
|
|
58
|
+
* @param {string} host A host the spec names, `claude` or `codex`.
|
|
39
59
|
* @returns {{executable: string, executableVariable: string, homeVariable: string,
|
|
40
60
|
* commands: ReadonlyArray<ReadonlyArray<string>>}} The host's install plan.
|
|
41
61
|
*/
|
|
42
|
-
export function
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
62
|
+
export function marketplacePluginPlan(spec, host) {
|
|
63
|
+
const hostDefinition = HOST_DEFINITIONS[host];
|
|
64
|
+
if (!hostDefinition || !spec.hosts.includes(host)) {
|
|
45
65
|
throw new Error(
|
|
46
|
-
`Unsupported
|
|
66
|
+
`Unsupported ${spec.name} plugin host ${host}: this installer knows ${spec.hosts.join(', ')}.`,
|
|
47
67
|
);
|
|
48
68
|
}
|
|
49
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
executable: hostDefinition.executable,
|
|
71
|
+
executableVariable: hostDefinition.executableVariable,
|
|
72
|
+
homeVariable: hostDefinition.homeVariable,
|
|
73
|
+
commands: [
|
|
74
|
+
['plugin', 'marketplace', 'add', spec.marketplaceRepository, ...spec.marketplaceAddArguments],
|
|
75
|
+
['plugin', hostDefinition.installVerb, spec.pluginIdentifier],
|
|
76
|
+
],
|
|
77
|
+
};
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
/**
|
|
53
|
-
* Decide whether a full install also installs
|
|
81
|
+
* Decide whether a full install also installs one marketplace plugin.
|
|
54
82
|
*
|
|
55
|
-
* The step reaches the network for the marketplace repository.
|
|
56
|
-
* on the command line, or `
|
|
57
|
-
* off for an air-gapped or offline run.
|
|
83
|
+
* The step reaches the network for the marketplace repository. The spec's
|
|
84
|
+
* opt-out flag on the command line, or its opt-out variable set to `0` in the
|
|
85
|
+
* environment, turns it off for an air-gapped or offline run.
|
|
58
86
|
*
|
|
87
|
+
* @param {typeof PSTACK_PLUGIN_SPEC} spec The plugin to install.
|
|
59
88
|
* @param {string[]} [argumentList] The command-line arguments after the script.
|
|
60
89
|
* @param {Record<string, string|undefined>} [environment] The process environment.
|
|
61
|
-
* @returns {boolean} True when this run installs the
|
|
90
|
+
* @returns {boolean} True when this run installs the plugin.
|
|
62
91
|
*/
|
|
63
|
-
export function
|
|
92
|
+
export function shouldInstallMarketplacePlugin(
|
|
93
|
+
spec,
|
|
64
94
|
argumentList = process.argv.slice(2),
|
|
65
95
|
environment = process.env,
|
|
66
96
|
) {
|
|
67
|
-
if (argumentList.includes(
|
|
68
|
-
return environment[
|
|
97
|
+
if (argumentList.includes(spec.optOutFlag)) return false;
|
|
98
|
+
return environment[spec.optOutVariable] !== '0';
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
/**
|
|
@@ -126,8 +156,8 @@ function firstLine(text) {
|
|
|
126
156
|
return trimmed.split(/\r?\n/).filter(Boolean).at(-1);
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
function installForHost(host, homeDirectory, environment, runCommand) {
|
|
130
|
-
const plan =
|
|
159
|
+
function installForHost(spec, host, homeDirectory, environment, runCommand) {
|
|
160
|
+
const plan = marketplacePluginPlan(spec, host);
|
|
131
161
|
const executable = environment[plan.executableVariable] || plan.executable;
|
|
132
162
|
const commandEnvironment = { [plan.homeVariable]: homeDirectory };
|
|
133
163
|
for (const commandArguments of plan.commands) {
|
|
@@ -139,7 +169,7 @@ function installForHost(host, homeDirectory, environment, runCommand) {
|
|
|
139
169
|
host,
|
|
140
170
|
executable,
|
|
141
171
|
status: 'skipped',
|
|
142
|
-
warning: `${executable} is not on PATH, so
|
|
172
|
+
warning: `${executable} is not on PATH, so ${spec.name} was not installed for ${host}.`,
|
|
143
173
|
};
|
|
144
174
|
}
|
|
145
175
|
if (outcome.status !== 0 || outcome.error) {
|
|
@@ -156,7 +186,7 @@ function installForHost(host, homeDirectory, environment, runCommand) {
|
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
/**
|
|
159
|
-
* Install
|
|
189
|
+
* Install one plugin from its marketplace into each host's own home.
|
|
160
190
|
*
|
|
161
191
|
* Each host is one member of the batch. A host without its command-line tool
|
|
162
192
|
* is skipped and a host whose command fails is reported, so the rules, hooks,
|
|
@@ -165,20 +195,22 @@ function installForHost(host, homeDirectory, environment, runCommand) {
|
|
|
165
195
|
* `cmd.exe`'s command-not-found exit code on Windows, and both read as
|
|
166
196
|
* skipped.
|
|
167
197
|
*
|
|
198
|
+
* @param {typeof PSTACK_PLUGIN_SPEC} spec The plugin to install.
|
|
168
199
|
* @param {object} [options] Install targets.
|
|
169
200
|
* @param {string} [options.claudeRoot] The managed Claude root to install into.
|
|
170
201
|
* @param {string} [options.codexHome] The Codex home to install into.
|
|
171
|
-
* @param {string[]} [options.hosts] The hosts to install. Defaults to
|
|
202
|
+
* @param {string[]} [options.hosts] The hosts to install. Defaults to the spec's hosts.
|
|
172
203
|
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
173
204
|
* @param {object} [dependencies] Seams for the command runner.
|
|
174
205
|
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome per host.
|
|
175
206
|
*/
|
|
176
|
-
export function
|
|
207
|
+
export function installMarketplacePlugin(spec, options = {}, dependencies = {}) {
|
|
177
208
|
const environment = options.environment ?? process.env;
|
|
178
209
|
const runCommand = dependencies.runCommand ?? runHostCommand;
|
|
179
210
|
const homeDirectories = { claude: options.claudeRoot, codex: options.codexHome };
|
|
180
|
-
const hosts = options.hosts ??
|
|
211
|
+
const hosts = options.hosts ?? spec.hosts;
|
|
181
212
|
const hostOutcomes = hosts.map(host => installForHost(
|
|
213
|
+
spec,
|
|
182
214
|
host,
|
|
183
215
|
homeDirectories[host],
|
|
184
216
|
environment,
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import {
|
|
4
5
|
PSTACK_MARKETPLACE_REPOSITORY,
|
|
5
6
|
PSTACK_PLUGIN_HOSTS,
|
|
6
7
|
PSTACK_PLUGIN_IDENTIFIER,
|
|
8
|
+
PSTACK_PLUGIN_SPEC,
|
|
9
|
+
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
7
10
|
hostCommandInvocation,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
installMarketplacePlugin,
|
|
12
|
+
marketplacePluginPlan,
|
|
13
|
+
shouldInstallMarketplacePlugin,
|
|
11
14
|
} from './install-pstack-plugin.mjs';
|
|
12
15
|
|
|
13
16
|
function recordingRunner(outcomeForCommand = () => ({ status: 0, stderr: '' })) {
|
|
@@ -30,7 +33,7 @@ const ROOTS = Object.freeze({
|
|
|
30
33
|
const WINDOWS_ENVIRONMENT = Object.freeze({ ComSpec: 'C:\\Windows\\system32\\cmd.exe' });
|
|
31
34
|
|
|
32
35
|
test('the Claude plan carries the two documented plugin commands', () => {
|
|
33
|
-
const plan =
|
|
36
|
+
const plan = marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'claude');
|
|
34
37
|
assert.equal(plan.executable, 'claude');
|
|
35
38
|
assert.deepEqual(plan.commands, [
|
|
36
39
|
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
@@ -39,7 +42,7 @@ test('the Claude plan carries the two documented plugin commands', () => {
|
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
test('the Codex plan adds the plugin with the Codex verb', () => {
|
|
42
|
-
const plan =
|
|
45
|
+
const plan = marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'codex');
|
|
43
46
|
assert.equal(plan.executable, 'codex');
|
|
44
47
|
assert.deepEqual(plan.commands, [
|
|
45
48
|
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
@@ -48,13 +51,13 @@ test('the Codex plan adds the plugin with the Codex verb', () => {
|
|
|
48
51
|
});
|
|
49
52
|
|
|
50
53
|
test('an unsupported host names the hosts this installer knows', () => {
|
|
51
|
-
assert.throws(() =>
|
|
54
|
+
assert.throws(() => marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'cursor'), /cursor/);
|
|
52
55
|
assert.deepEqual(PSTACK_PLUGIN_HOSTS, ['claude', 'codex']);
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
test('each host installs into the managed root this run wrote', () => {
|
|
56
59
|
const runner = recordingRunner();
|
|
57
|
-
const outcome =
|
|
60
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
58
61
|
assert.equal(outcome.status, 'installed');
|
|
59
62
|
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
60
63
|
['claude', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
@@ -70,7 +73,7 @@ test('an absent host command skips that host and leaves the other installed', ()
|
|
|
70
73
|
const runner = recordingRunner((executable) => (executable === 'codex'
|
|
71
74
|
? { status: null, error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }) }
|
|
72
75
|
: { status: 0, stderr: '' }));
|
|
73
|
-
const outcome =
|
|
76
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
74
77
|
assert.equal(outcome.hosts.find(host => host.host === 'claude').status, 'installed');
|
|
75
78
|
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
76
79
|
assert.equal(codex.status, 'skipped');
|
|
@@ -83,7 +86,7 @@ test('a failing command reports that host and still installs the next one', () =
|
|
|
83
86
|
executable === 'claude' && commandArguments[1] === 'marketplace'
|
|
84
87
|
? { status: 1, stderr: 'marketplace unreachable' }
|
|
85
88
|
: { status: 0, stderr: '' }));
|
|
86
|
-
const outcome =
|
|
89
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
87
90
|
assert.equal(outcome.status, 'failed');
|
|
88
91
|
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
89
92
|
assert.equal(claude.status, 'failed');
|
|
@@ -94,7 +97,8 @@ test('a failing command reports that host and still installs the next one', () =
|
|
|
94
97
|
|
|
95
98
|
test('an executable override replaces the host command for that host only', () => {
|
|
96
99
|
const runner = recordingRunner();
|
|
97
|
-
|
|
100
|
+
installMarketplacePlugin(
|
|
101
|
+
PSTACK_PLUGIN_SPEC,
|
|
98
102
|
{ ...ROOTS, environment: { CDE_CODEX_EXECUTABLE: '/opt/codex/bin/codex' } },
|
|
99
103
|
runner,
|
|
100
104
|
);
|
|
@@ -105,15 +109,15 @@ test('an executable override replaces the host command for that host only', () =
|
|
|
105
109
|
});
|
|
106
110
|
|
|
107
111
|
test('the opt-out flag and variable both turn the step off', () => {
|
|
108
|
-
assert.equal(
|
|
109
|
-
assert.equal(
|
|
110
|
-
assert.equal(
|
|
111
|
-
assert.equal(
|
|
112
|
+
assert.equal(shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC, [], {}), true);
|
|
113
|
+
assert.equal(shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC, ['--no-pstack'], {}), false);
|
|
114
|
+
assert.equal(shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC, [], { CDE_INSTALL_PSTACK: '0' }), false);
|
|
115
|
+
assert.equal(shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC, [], { CDE_INSTALL_PSTACK: '1' }), true);
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
test('a selected host list installs only that host', () => {
|
|
115
119
|
const runner = recordingRunner();
|
|
116
|
-
const outcome =
|
|
120
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['codex'] }, runner);
|
|
117
121
|
assert.deepEqual(outcome.hosts.map(host => host.host), ['codex']);
|
|
118
122
|
assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], ['codex']);
|
|
119
123
|
});
|
|
@@ -166,7 +170,7 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
|
|
|
166
170
|
stderr: "'codex' is not recognized as an internal or external command,\noperable program or batch file.",
|
|
167
171
|
}
|
|
168
172
|
: { status: 0, stderr: '' }));
|
|
169
|
-
const outcome =
|
|
173
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
170
174
|
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
171
175
|
assert.equal(codex.status, 'skipped');
|
|
172
176
|
assert.equal(outcome.status, 'installed');
|
|
@@ -175,7 +179,79 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
|
|
|
175
179
|
|
|
176
180
|
test('a non-zero exit that is not command-not-found still reads as a failure', () => {
|
|
177
181
|
const runner = recordingRunner(() => ({ status: 9009, stderr: 'the marketplace rejected the catalog' }));
|
|
178
|
-
const outcome =
|
|
182
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['claude'] }, runner);
|
|
179
183
|
assert.equal(outcome.hosts[0].status, 'failed');
|
|
180
184
|
assert.match(outcome.hosts[0].warning, /rejected the catalog/);
|
|
181
185
|
});
|
|
186
|
+
|
|
187
|
+
const USAGE_WRAPUP_PINNED_COMMIT = 'd78c314e5787786e866bf5c4967107365854dc78';
|
|
188
|
+
|
|
189
|
+
test('the usage-wrapup plan adds this repository marketplace and installs the plugin on Claude', () => {
|
|
190
|
+
const plan = marketplacePluginPlan(USAGE_WRAPUP_PLUGIN_SPEC, 'claude');
|
|
191
|
+
assert.equal(plan.executable, 'claude');
|
|
192
|
+
assert.equal(plan.homeVariable, 'CLAUDE_CONFIG_DIR');
|
|
193
|
+
assert.deepEqual(plan.commands, [
|
|
194
|
+
['plugin', 'marketplace', 'add', 'jl-cmd/claude-dev-env', '--sparse', '.claude-plugin'],
|
|
195
|
+
['plugin', 'install', 'usage-wrapup@claude-dev-env'],
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('usage-wrapup is a Claude-only plugin and refuses a Codex plan', () => {
|
|
200
|
+
assert.deepEqual(USAGE_WRAPUP_PLUGIN_SPEC.hosts, ['claude']);
|
|
201
|
+
assert.throws(() => marketplacePluginPlan(USAGE_WRAPUP_PLUGIN_SPEC, 'codex'), /usage-wrapup plugin host codex/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('installing usage-wrapup runs only the Claude commands, inside the managed Claude root', () => {
|
|
205
|
+
const runner = recordingRunner();
|
|
206
|
+
const outcome = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ROOTS, runner);
|
|
207
|
+
assert.equal(outcome.status, 'installed');
|
|
208
|
+
assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
|
|
209
|
+
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
210
|
+
['claude', 'plugin', 'marketplace', 'add', 'jl-cmd/claude-dev-env', '--sparse', '.claude-plugin'],
|
|
211
|
+
['claude', 'plugin', 'install', 'usage-wrapup@claude-dev-env'],
|
|
212
|
+
]);
|
|
213
|
+
for (const call of runner.calls) {
|
|
214
|
+
assert.deepEqual(call.environment, { CLAUDE_CONFIG_DIR: ROOTS.claudeRoot });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('an absent claude command skips usage-wrapup and names the plugin in the warning', () => {
|
|
219
|
+
const runner = recordingRunner(() => ({
|
|
220
|
+
status: null,
|
|
221
|
+
error: Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' }),
|
|
222
|
+
}));
|
|
223
|
+
const outcome = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ROOTS, runner);
|
|
224
|
+
assert.equal(outcome.status, 'skipped');
|
|
225
|
+
assert.match(outcome.warning, /usage-wrapup was not installed for claude/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('the usage-wrapup opt-out flag and variable turn off only usage-wrapup', () => {
|
|
229
|
+
assert.equal(shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, [], {}), true);
|
|
230
|
+
assert.equal(shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ['--no-usage-wrapup'], {}), false);
|
|
231
|
+
assert.equal(
|
|
232
|
+
shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, [], { CDE_INSTALL_USAGE_WRAPUP: '0' }),
|
|
233
|
+
false,
|
|
234
|
+
);
|
|
235
|
+
assert.equal(shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ['--no-pstack'], { CDE_INSTALL_PSTACK: '0' }), true);
|
|
236
|
+
assert.equal(
|
|
237
|
+
shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC, ['--no-usage-wrapup'], { CDE_INSTALL_USAGE_WRAPUP: '0' }),
|
|
238
|
+
true,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('the repository marketplace lists usage-wrapup from the fork, pinned to the reviewed commit', () => {
|
|
243
|
+
const marketplace = JSON.parse(readFileSync(
|
|
244
|
+
new URL('../../../.claude-plugin/marketplace.json', import.meta.url),
|
|
245
|
+
'utf8',
|
|
246
|
+
));
|
|
247
|
+
const [pluginName, marketplaceName] = USAGE_WRAPUP_PLUGIN_SPEC.pluginIdentifier.split('@');
|
|
248
|
+
assert.equal(marketplace.name, marketplaceName);
|
|
249
|
+
const entry = marketplace.plugins.find(plugin => plugin.name === pluginName);
|
|
250
|
+
assert.ok(entry, `the marketplace lists ${pluginName}`);
|
|
251
|
+
assert.deepEqual(entry.source, {
|
|
252
|
+
source: 'github',
|
|
253
|
+
repo: 'jl-cmd/usage-wrapup',
|
|
254
|
+
ref: 'main',
|
|
255
|
+
sha: USAGE_WRAPUP_PINNED_COMMIT,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -51,6 +51,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
51
51
|
const installerEnvironment = {
|
|
52
52
|
...process.env,
|
|
53
53
|
CDE_INSTALL_PSTACK: '0',
|
|
54
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
54
55
|
HOME: homeDirectory,
|
|
55
56
|
USERPROFILE: homeDirectory,
|
|
56
57
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -57,6 +57,7 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
57
57
|
env: {
|
|
58
58
|
...process.env,
|
|
59
59
|
CDE_INSTALL_PSTACK: '0',
|
|
60
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
60
61
|
HOME: homeDirectory,
|
|
61
62
|
USERPROFILE: homeDirectory,
|
|
62
63
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -85,6 +86,7 @@ function scanTextWithProductionPiiScanner(scannedText) {
|
|
|
85
86
|
env: {
|
|
86
87
|
...process.env,
|
|
87
88
|
CDE_INSTALL_PSTACK: '0',
|
|
89
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
88
90
|
PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
|
|
89
91
|
CLAUDE_LOCAL_IDENTITY_PATH: join(
|
|
90
92
|
temporaryDirectory,
|
|
@@ -32,6 +32,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
32
32
|
env: {
|
|
33
33
|
...process.env,
|
|
34
34
|
CDE_INSTALL_PSTACK: '0',
|
|
35
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
35
36
|
HOME: homeDirectory,
|
|
36
37
|
USERPROFILE: homeDirectory,
|
|
37
38
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
package/bin/install.mjs
CHANGED
|
@@ -48,8 +48,10 @@ import {
|
|
|
48
48
|
ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS,
|
|
49
49
|
} from './install-constants.mjs';
|
|
50
50
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
PSTACK_PLUGIN_SPEC,
|
|
52
|
+
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
53
|
+
installMarketplacePlugin,
|
|
54
|
+
shouldInstallMarketplacePlugin,
|
|
53
55
|
} from './install-pstack-plugin.mjs';
|
|
54
56
|
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
55
57
|
import {
|
|
@@ -404,12 +406,20 @@ export function pythonFileAndPrefixArguments(pythonCommand) {
|
|
|
404
406
|
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome.
|
|
405
407
|
*/
|
|
406
408
|
export function installPstackPluginForHosts() {
|
|
407
|
-
return
|
|
409
|
+
return installMarketplacePlugin(PSTACK_PLUGIN_SPEC, {
|
|
408
410
|
claudeRoot: CLAUDE_HOME,
|
|
409
411
|
codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
410
412
|
});
|
|
411
413
|
}
|
|
412
414
|
|
|
415
|
+
function printPluginHostOutcomes(spec, pluginOutcome) {
|
|
416
|
+
for (const eachHost of pluginOutcome.hosts) {
|
|
417
|
+
console.log(eachHost.warning
|
|
418
|
+
? ` ${spec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
|
|
419
|
+
: ` ${spec.label} (${eachHost.host}): ${eachHost.status}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
413
423
|
function seedSubagentModelPolicy() {
|
|
414
424
|
const policyDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
415
425
|
const policyPath = join(policyDirectory, SUBAGENT_MODEL_POLICY_FILE_NAME);
|
|
@@ -2762,7 +2772,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2762
2772
|
}
|
|
2763
2773
|
const packageGuidanceText = selectedGroups ? '' : readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8');
|
|
2764
2774
|
const allPackageGuidancePaths = [join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')];
|
|
2765
|
-
if (!selectedGroups &&
|
|
2775
|
+
if (!selectedGroups && shouldInstallMarketplacePlugin(PSTACK_PLUGIN_SPEC)) {
|
|
2766
2776
|
const pstackPlugin = installPstackPluginForHosts();
|
|
2767
2777
|
summary.pstackPlugin = pstackPlugin;
|
|
2768
2778
|
if (pstackPlugin.hosts.some(host => host.host === 'codex' && host.status === 'installed')) {
|
|
@@ -2784,11 +2794,14 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2784
2794
|
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
|
2785
2795
|
}
|
|
2786
2796
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2797
|
+
printPluginHostOutcomes(PSTACK_PLUGIN_SPEC, pstackPlugin);
|
|
2798
|
+
}
|
|
2799
|
+
if (!selectedGroups && shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC)) {
|
|
2800
|
+
const usageWrapupPlugin = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, {
|
|
2801
|
+
claudeRoot: CLAUDE_HOME,
|
|
2802
|
+
});
|
|
2803
|
+
summary.usageWrapupPlugin = usageWrapupPlugin;
|
|
2804
|
+
printPluginHostOutcomes(USAGE_WRAPUP_PLUGIN_SPEC, usageWrapupPlugin);
|
|
2792
2805
|
}
|
|
2793
2806
|
if (!selectedGroups) {
|
|
2794
2807
|
const packageGuidancePath = writeCodexPackageGuidance(
|
|
@@ -3324,6 +3337,7 @@ Usage:
|
|
|
3324
3337
|
npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
|
|
3325
3338
|
npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
|
|
3326
3339
|
npx ${PACKAGE_NAME} --no-pstack Full install without the pstack plugin (also CDE_INSTALL_PSTACK=0)
|
|
3340
|
+
npx ${PACKAGE_NAME} --no-usage-wrapup Full install without the usage-wrapup plugin (also CDE_INSTALL_USAGE_WRAPUP=0)
|
|
3327
3341
|
npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
|
|
3328
3342
|
npx ${PACKAGE_NAME} --help Show this help
|
|
3329
3343
|
|
|
@@ -260,6 +260,7 @@ function resolveInstallerInvocation(homeDirectory) {
|
|
|
260
260
|
const childEnvironment = {
|
|
261
261
|
...process.env,
|
|
262
262
|
CDE_INSTALL_PSTACK: '0',
|
|
263
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
263
264
|
HOME: homeDirectory,
|
|
264
265
|
USERPROFILE: homeDirectory,
|
|
265
266
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -23,6 +23,7 @@ function installInScratchHome(homeDirectory, argumentsList = [], extraEnvironmen
|
|
|
23
23
|
env: {
|
|
24
24
|
...process.env,
|
|
25
25
|
CDE_INSTALL_PSTACK: '0',
|
|
26
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
26
27
|
HOME: homeDirectory,
|
|
27
28
|
USERPROFILE: homeDirectory,
|
|
28
29
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
@@ -177,6 +178,7 @@ test('a successful pstack install retains its skill block and gains the question
|
|
|
177
178
|
|
|
178
179
|
const installation = installInScratchHome(homeDirectory, [], {
|
|
179
180
|
CDE_INSTALL_PSTACK: '1',
|
|
181
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
180
182
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
181
183
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
182
184
|
});
|
|
@@ -203,6 +205,7 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
|
|
|
203
205
|
|
|
204
206
|
const secondInstallation = installInScratchHome(homeDirectory, [], {
|
|
205
207
|
CDE_INSTALL_PSTACK: '1',
|
|
208
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
206
209
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
207
210
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
208
211
|
});
|
|
@@ -230,6 +233,7 @@ test('a failed pstack transition restores question-only guidance and removes its
|
|
|
230
233
|
|
|
231
234
|
const failedInstallation = installInScratchHome(homeDirectory, [], {
|
|
232
235
|
CDE_INSTALL_PSTACK: '1',
|
|
236
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
233
237
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
234
238
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
235
239
|
CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
|
package/bin/install.test.mjs
CHANGED
|
@@ -3034,6 +3034,7 @@ function runPstackInstaller(homeDirectory, extraArguments, environmentOverrides
|
|
|
3034
3034
|
CLAUDE_CONFIG_DIR: join(homeDirectory, '.claude'),
|
|
3035
3035
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3036
3036
|
CDE_INSTALL_PSTACK: '1',
|
|
3037
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3037
3038
|
...environmentOverrides,
|
|
3038
3039
|
},
|
|
3039
3040
|
});
|
|
@@ -3267,6 +3268,81 @@ test('a pstack install after a --no-pstack install seeds the Codex model sheet',
|
|
|
3267
3268
|
assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
|
|
3268
3269
|
});
|
|
3269
3270
|
|
|
3271
|
+
const USAGE_WRAPUP_CLAUDE_COMMANDS = Object.freeze([
|
|
3272
|
+
'claude plugin marketplace add jl-cmd/claude-dev-env --sparse .claude-plugin',
|
|
3273
|
+
'claude plugin install usage-wrapup@claude-dev-env',
|
|
3274
|
+
]);
|
|
3275
|
+
|
|
3276
|
+
function usageWrapupCommands(allRecordedCommands) {
|
|
3277
|
+
return allRecordedCommands.filter(eachCommand => eachCommand.includes('claude-dev-env'));
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
test('a full install adds this marketplace and installs usage-wrapup on Claude only', t => {
|
|
3281
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3282
|
+
|
|
3283
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], {
|
|
3284
|
+
...sandbox.environment,
|
|
3285
|
+
CDE_INSTALL_USAGE_WRAPUP: '1',
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
assert.deepEqual(usageWrapupCommands(sandbox.recordedCommands()), USAGE_WRAPUP_CLAUDE_COMMANDS);
|
|
3289
|
+
assert.match(installerOutput, /Usage-wrapup \(claude\): installed/);
|
|
3290
|
+
assert.doesNotMatch(installerOutput, /Usage-wrapup \(codex\)/);
|
|
3291
|
+
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
3292
|
+
});
|
|
3293
|
+
|
|
3294
|
+
for (const [caseName, extraArguments, environmentOverrides] of [
|
|
3295
|
+
['--no-usage-wrapup', ['--no-usage-wrapup'], { CDE_INSTALL_USAGE_WRAPUP: '1' }],
|
|
3296
|
+
['CDE_INSTALL_USAGE_WRAPUP=0', [], { CDE_INSTALL_USAGE_WRAPUP: '0' }],
|
|
3297
|
+
['an --only run', ['--only', 'core'], { CDE_INSTALL_USAGE_WRAPUP: '1' }],
|
|
3298
|
+
]) {
|
|
3299
|
+
test(`${caseName} skips usage-wrapup`, t => {
|
|
3300
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3301
|
+
|
|
3302
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, extraArguments, {
|
|
3303
|
+
...sandbox.environment,
|
|
3304
|
+
...environmentOverrides,
|
|
3305
|
+
});
|
|
3306
|
+
|
|
3307
|
+
assert.deepEqual(usageWrapupCommands(sandbox.recordedCommands()), []);
|
|
3308
|
+
assert.doesNotMatch(installerOutput, /Usage-wrapup \(/);
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
test('a failing usage-wrapup command is reported and the install still succeeds', t => {
|
|
3313
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3314
|
+
const failingCommandPath = join(
|
|
3315
|
+
sandbox.homeDirectory,
|
|
3316
|
+
process.platform === 'win32' ? 'failing-claude.cmd' : 'failing-claude',
|
|
3317
|
+
);
|
|
3318
|
+
writeFileSync(
|
|
3319
|
+
failingCommandPath,
|
|
3320
|
+
process.platform === 'win32'
|
|
3321
|
+
? '@echo off\r\necho marketplace unreachable 1>&2\r\nexit /b 1\r\n'
|
|
3322
|
+
: '#!/bin/sh\necho marketplace unreachable >&2\nexit 1\n',
|
|
3323
|
+
{ mode: 0o755 },
|
|
3324
|
+
);
|
|
3325
|
+
|
|
3326
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], {
|
|
3327
|
+
...sandbox.environment,
|
|
3328
|
+
CDE_CLAUDE_EXECUTABLE: failingCommandPath,
|
|
3329
|
+
CDE_INSTALL_USAGE_WRAPUP: '1',
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
assert.match(installerOutput, /Usage-wrapup \(claude\): failed .*marketplace unreachable/);
|
|
3333
|
+
assert.ok(existsSync(join(sandbox.homeDirectory, '.claude', '.claude-dev-env-manifest.json')));
|
|
3334
|
+
});
|
|
3335
|
+
|
|
3336
|
+
test('the help output names the usage-wrapup opt-out beside the pstack one', () => {
|
|
3337
|
+
const helpRun = spawnSync(process.execPath, [PSTACK_TEST_INSTALLER_PATH, '--help'], {
|
|
3338
|
+
encoding: 'utf8',
|
|
3339
|
+
env: process.env,
|
|
3340
|
+
});
|
|
3341
|
+
|
|
3342
|
+
assert.equal(helpRun.status, 0, helpRun.stderr);
|
|
3343
|
+
assert.match(helpRun.stdout, /--no-pstack .*\n.*--no-usage-wrapup .*CDE_INSTALL_USAGE_WRAPUP=0/);
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3270
3346
|
function continuityCommandCount(configurationPath) {
|
|
3271
3347
|
if (!existsSync(configurationPath)) return 0;
|
|
3272
3348
|
const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
|
|
@@ -84,6 +84,7 @@ test('capture and restore recover settings, manifest, files, and hooksPath', ()
|
|
|
84
84
|
const env = {
|
|
85
85
|
...process.env,
|
|
86
86
|
CDE_INSTALL_PSTACK: '0',
|
|
87
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
87
88
|
HOME: box.root,
|
|
88
89
|
USERPROFILE: box.root,
|
|
89
90
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -145,6 +146,7 @@ test('capture and restore recover an additional host settings file', () => {
|
|
|
145
146
|
env: {
|
|
146
147
|
...process.env,
|
|
147
148
|
CDE_INSTALL_PSTACK: '0',
|
|
149
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
148
150
|
HOME: box.root,
|
|
149
151
|
USERPROFILE: box.root,
|
|
150
152
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -179,6 +181,7 @@ test('runWithInstallTransaction restores prior state on injected fault', () => {
|
|
|
179
181
|
const env = {
|
|
180
182
|
...process.env,
|
|
181
183
|
CDE_INSTALL_PSTACK: '0',
|
|
184
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
182
185
|
HOME: box.root,
|
|
183
186
|
USERPROFILE: box.root,
|
|
184
187
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -232,6 +235,7 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
|
|
|
232
235
|
env: {
|
|
233
236
|
...process.env,
|
|
234
237
|
CDE_INSTALL_PSTACK: '0',
|
|
238
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
235
239
|
HOME: box.root,
|
|
236
240
|
USERPROFILE: box.root,
|
|
237
241
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -274,6 +278,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
274
278
|
const childEnvironment = {
|
|
275
279
|
...process.env,
|
|
276
280
|
CDE_INSTALL_PSTACK: '0',
|
|
281
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
277
282
|
HOME: homeDirectory,
|
|
278
283
|
USERPROFILE: homeDirectory,
|
|
279
284
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -308,6 +313,7 @@ test('a later install fault removes newly seeded Codex pstack files', () => {
|
|
|
308
313
|
faultPhase: FAULT_PHASES.AFTER_MANIFEST_WRITE,
|
|
309
314
|
environment: {
|
|
310
315
|
CDE_INSTALL_PSTACK: '1',
|
|
316
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
311
317
|
CDE_CODEX_EXECUTABLE: codexCommandPath,
|
|
312
318
|
},
|
|
313
319
|
});
|
|
@@ -450,6 +456,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
450
456
|
env: {
|
|
451
457
|
...process.env,
|
|
452
458
|
CDE_INSTALL_PSTACK: '0',
|
|
459
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
453
460
|
HOME: homeDirectory,
|
|
454
461
|
USERPROFILE: homeDirectory,
|
|
455
462
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -466,6 +473,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
466
473
|
env: {
|
|
467
474
|
...process.env,
|
|
468
475
|
CDE_INSTALL_PSTACK: '0',
|
|
476
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
469
477
|
HOME: homeDirectory,
|
|
470
478
|
USERPROFILE: homeDirectory,
|
|
471
479
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -72,6 +72,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
72
72
|
const childEnvironment = {
|
|
73
73
|
...process.env,
|
|
74
74
|
CDE_INSTALL_PSTACK: '0',
|
|
75
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
75
76
|
HOME: homeDirectory,
|
|
76
77
|
USERPROFILE: homeDirectory,
|
|
77
78
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -305,6 +306,7 @@ test('uninstall fault after_git_config restores prior core.hooksPath', () => {
|
|
|
305
306
|
env: {
|
|
306
307
|
...process.env,
|
|
307
308
|
CDE_INSTALL_PSTACK: '0',
|
|
309
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
308
310
|
HOME: homeDirectory,
|
|
309
311
|
USERPROFILE: homeDirectory,
|
|
310
312
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -59,6 +59,7 @@ try:
|
|
|
59
59
|
ALL_FAILING_CHECK_CONCLUSIONS,
|
|
60
60
|
ALL_PASSING_CHECK_CONCLUSIONS,
|
|
61
61
|
CHECK_COMPLETED_STATUS,
|
|
62
|
+
CHECKS_NOT_REPORTED_TEXT,
|
|
62
63
|
DONE_LABEL_NAME,
|
|
63
64
|
GH_PR_CREATE_ACTION,
|
|
64
65
|
GH_PR_SUBCOMMAND,
|
|
@@ -164,7 +165,7 @@ def _check_counts(all_pr_fields: dict[str, object]) -> dict[str, int]:
|
|
|
164
165
|
|
|
165
166
|
def _checks_line(all_check_counts: dict[str, int]) -> str:
|
|
166
167
|
if all_check_counts["total"] == 0:
|
|
167
|
-
return
|
|
168
|
+
return CHECKS_NOT_REPORTED_TEXT
|
|
168
169
|
return (
|
|
169
170
|
f"{all_check_counts['failing']} failing, "
|
|
170
171
|
f"{all_check_counts['pending']} pending, "
|
|
@@ -185,6 +186,7 @@ def _has_done_label(all_pr_fields: dict[str, object]) -> bool:
|
|
|
185
186
|
def _is_done(mergeable_value: str, all_check_counts: dict[str, int]) -> bool:
|
|
186
187
|
return (
|
|
187
188
|
mergeable_value == MERGEABLE_CLEAN_VALUE
|
|
189
|
+
and all_check_counts["total"] > 0
|
|
188
190
|
and all_check_counts["failing"] == 0
|
|
189
191
|
and all_check_counts["pending"] == 0
|
|
190
192
|
)
|
|
@@ -152,6 +152,58 @@ def test_should_say_done_when_mergeable_and_checks_pass() -> None:
|
|
|
152
152
|
assert "NOT DONE" not in context
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def test_should_say_not_done_when_no_check_has_reported_on_the_head() -> None:
|
|
156
|
+
no_checks_yet = {**_CLEAN_PR_OBJECT, "statusCheckRollup": []}
|
|
157
|
+
|
|
158
|
+
context = pr_done_reminder.build_reminder_context(no_checks_yet)
|
|
159
|
+
|
|
160
|
+
assert "CI checks: none reported yet on this head." in context
|
|
161
|
+
assert "NOT DONE" in context
|
|
162
|
+
assert "gh pr edit 42 --add-label" not in context
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_should_say_not_done_when_the_rollup_is_missing() -> None:
|
|
166
|
+
no_rollup = {key: value for key, value in _CLEAN_PR_OBJECT.items() if key != "statusCheckRollup"}
|
|
167
|
+
|
|
168
|
+
context = pr_done_reminder.build_reminder_context(no_rollup)
|
|
169
|
+
|
|
170
|
+
assert "none reported yet on this head." in context
|
|
171
|
+
assert "NOT DONE" in context
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_should_say_not_done_while_a_check_is_pending_on_an_otherwise_green_head() -> None:
|
|
175
|
+
one_pending = {
|
|
176
|
+
**_CLEAN_PR_OBJECT,
|
|
177
|
+
"statusCheckRollup": [
|
|
178
|
+
{"name": "pytest", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
179
|
+
{"name": "mypy", "status": "QUEUED", "conclusion": ""},
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
context = pr_done_reminder.build_reminder_context(one_pending)
|
|
184
|
+
|
|
185
|
+
assert "1 pending" in context
|
|
186
|
+
assert "NOT DONE" in context
|
|
187
|
+
assert "gh pr edit 42 --add-label" not in context
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_should_say_done_when_every_reported_check_passed() -> None:
|
|
191
|
+
all_green = {
|
|
192
|
+
**_CLEAN_PR_OBJECT,
|
|
193
|
+
"statusCheckRollup": [
|
|
194
|
+
{"name": "pytest", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
195
|
+
{"name": "docs", "status": "COMPLETED", "conclusion": "SKIPPED"},
|
|
196
|
+
{"context": "lint", "state": "SUCCESS"},
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
context = pr_done_reminder.build_reminder_context(all_green)
|
|
201
|
+
|
|
202
|
+
assert "CI checks: 0 failing, 0 pending, 3 total" in context
|
|
203
|
+
assert "Verdict: DONE. Add the label now:" in context
|
|
204
|
+
assert "NOT DONE" not in context
|
|
205
|
+
|
|
206
|
+
|
|
155
207
|
def test_should_keep_the_checklist_order_verdict_then_recheck_then_footer() -> None:
|
|
156
208
|
all_lines = pr_done_reminder.build_reminder_context(_CLEAN_PR_OBJECT).split("\n")
|
|
157
209
|
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"REMINDER_FOOTER",
|
|
29
29
|
"REMINDER_LINE_SEPARATOR",
|
|
30
30
|
"ALL_REMINDER_HINTS_BY_MERGEABLE",
|
|
31
|
+
"CHECKS_NOT_REPORTED_TEXT",
|
|
31
32
|
"VERDICT_DONE",
|
|
32
33
|
"VERDICT_NOT_DONE",
|
|
33
34
|
"NO_PULL_REQUEST_REMINDER",
|
|
@@ -75,6 +76,9 @@ ALL_REMINDER_HINTS_BY_MERGEABLE: dict[str, str] = {
|
|
|
75
76
|
),
|
|
76
77
|
MERGEABLE_UNKNOWN_VALUE: "GitHub is still computing. Re-run the re-check command in a minute.",
|
|
77
78
|
}
|
|
79
|
+
CHECKS_NOT_REPORTED_TEXT: str = (
|
|
80
|
+
"none reported yet on this head. Wait for the checks, then run the re-check command."
|
|
81
|
+
)
|
|
78
82
|
VERDICT_DONE: str = "DONE. Add the label now:"
|
|
79
83
|
VERDICT_NOT_DONE: str = "NOT DONE. Do not add the done label yet."
|
|
80
84
|
NO_PULL_REQUEST_REMINDER: str = (
|