@c4a/context-cli 0.6.0-beta.6 → 0.6.0-beta.7
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/README.md +7 -2
- package/cli.js +2305 -1570
- package/package.json +2 -2
- package/plugin/skills/skill-continue-workflow/SKILL.md +4 -0
- package/plugin/skills/skill-prose-align/SKILL.md +7 -6
- package/plugin/skills/skill-prose-compile/SKILL.md +10 -8
- package/plugins/VERSION +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/skill-continue-workflow/SKILL.md +4 -0
- package/plugins/claude/skills/skill-prose-align/SKILL.md +7 -6
- package/plugins/claude/skills/skill-prose-compile/SKILL.md +10 -8
- package/plugins/codex/.codex-plugin/plugin.json +2 -2
- package/plugins/codex/skills/continue/references/internal-procedures/skill-continue-workflow.md +4 -0
- package/plugins/codex/skills/continue/references/internal-procedures/skill-prose-align.md +7 -6
- package/plugins/codex/skills/continue/references/internal-procedures/skill-prose-compile.md +10 -8
- package/plugins/codex/skills/init/references/internal-procedures/skill-continue-workflow.md +4 -0
- package/plugins/codex/skills/init/references/internal-procedures/skill-prose-align.md +7 -6
- package/plugins/codex/skills/init/references/internal-procedures/skill-prose-compile.md +10 -8
- package/plugins/cursor/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor/skills/skill-continue-workflow/SKILL.md +4 -0
- package/plugins/cursor/skills/skill-prose-align/SKILL.md +7 -6
- package/plugins/cursor/skills/skill-prose-compile/SKILL.md +10 -8
- package/plugins/skills/context-continue/references/internal-procedures/skill-continue-workflow.md +4 -0
- package/plugins/skills/context-continue/references/internal-procedures/skill-prose-align.md +7 -6
- package/plugins/skills/context-continue/references/internal-procedures/skill-prose-compile.md +10 -8
- package/plugins/skills/context-init/references/internal-procedures/skill-continue-workflow.md +4 -0
- package/plugins/skills/context-init/references/internal-procedures/skill-prose-align.md +7 -6
- package/plugins/skills/context-init/references/internal-procedures/skill-prose-compile.md +10 -8
- package/scripts/postinstall.mjs +68 -129
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,106 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* plugin, the `context` bin should end up on PATH so `/context:*` slash
|
|
7
|
-
* commands can Bash out to it. In every other install scenario (library
|
|
8
|
-
* dependency, `bun install -g`, CI, local `bun link`) we do nothing.
|
|
9
|
-
*
|
|
10
|
-
* Dangerous things this script used to do (review findings 高-1 / 高-2):
|
|
11
|
-
* - Recursion: the child `bun install -g @c4a/context-cli` inherits the
|
|
12
|
-
* same Claude env vars that triggered us, runs postinstall again, and
|
|
13
|
-
* loops. Fixed by (a) hard re-entry guard via CONTEXT_CLI_POSTINSTALL
|
|
14
|
-
* in the child env, and (b) skipping entirely when we are already
|
|
15
|
-
* inside a global install (`npm_config_global === "true"`).
|
|
16
|
-
* - Dev mode break: pinning the registry spec `@c4a/context-cli@<version>`
|
|
17
|
-
* ignores the actual install source (local tarball, file:, github:,
|
|
18
|
-
* unpublished pre-release). Fixed by never spawning a child install;
|
|
19
|
-
* we print a single actionable hint with the registry fallback command.
|
|
20
|
-
* The plugin tarball already contains the whole CLI, so the user can
|
|
21
|
-
* just `bun install -g <same source>` themselves — we can't guess the
|
|
22
|
-
* source reliably from inside postinstall.
|
|
23
|
-
*
|
|
24
|
-
* Structure: all decision logic lives in the pure, exported `decide()`
|
|
25
|
-
* function. The top-level side-effect wrapper is the only thing that reads
|
|
26
|
-
* `process.env`, spawns the PATH probe, or writes to stderr. This split
|
|
27
|
-
* lets a sibling `.test.ts` exercise every branch without spawning
|
|
28
|
-
* subprocesses (Bun test's subprocess sandboxing has been unreliable in
|
|
29
|
-
* this package, so in-process tests are the practical way to cover the
|
|
30
|
-
* script).
|
|
3
|
+
* Best-effort global agent plugin refresh after a global context-cli install.
|
|
4
|
+
* Local project dependencies and CI installs must not mutate user-level agent
|
|
5
|
+
* configuration. Link development performs the same refresh in cliLink.ts.
|
|
31
6
|
*/
|
|
32
7
|
|
|
33
8
|
import { spawnSync } from "node:child_process";
|
|
34
|
-
import { realpathSync } from "node:fs";
|
|
9
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
35
10
|
import process from "node:process";
|
|
36
|
-
import {
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
13
|
|
|
38
14
|
export const RE_ENTRY_ENV = "CONTEXT_CLI_POSTINSTALL";
|
|
15
|
+
export const SKIP_PLUGIN_INSTALL_ENV = "CONTEXT_CLI_SKIP_PLUGIN_INSTALL";
|
|
16
|
+
export const FORCE_PLUGIN_INSTALL_ENV = "CONTEXT_CLI_AUTO_PLUGIN_INSTALL";
|
|
17
|
+
|
|
18
|
+
export function looksLikeClaudePluginInstall(env) {
|
|
19
|
+
const hints = ["CLAUDE_PLUGIN_INSTALL", "CLAUDE_CODE_PLUGIN", "npm_config_user_agent"];
|
|
20
|
+
return hints.some((key) => env[key]?.toLowerCase().includes("claude") === true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isGlobalPackageInstall(env) {
|
|
24
|
+
return env.npm_config_global === "true" ||
|
|
25
|
+
env.npm_config_global === "1" ||
|
|
26
|
+
env.npm_config_location === "global";
|
|
27
|
+
}
|
|
39
28
|
|
|
40
|
-
/**
|
|
41
|
-
* Pure decision function. Given an env snapshot and a "does `context` bin
|
|
42
|
-
* exist on PATH?" predicate result, return either a hint to print or a
|
|
43
|
-
* "do nothing" sentinel.
|
|
44
|
-
*
|
|
45
|
-
* Shape:
|
|
46
|
-
* { action: "skip", reason: "<why>" }
|
|
47
|
-
* | { action: "hint", lines: string[] }
|
|
48
|
-
*
|
|
49
|
-
* Every branch that reads process state in the real run is modeled here so
|
|
50
|
-
* tests can pass synthetic envs. The caller decides whether to actually
|
|
51
|
-
* print or set the re-entry guard.
|
|
52
|
-
*/
|
|
53
29
|
export function decide(env, hasContextBin) {
|
|
54
|
-
if (env[RE_ENTRY_ENV] === "1") {
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
if (env
|
|
58
|
-
return { action: "
|
|
59
|
-
}
|
|
60
|
-
if (env.npm_config_global === "true") {
|
|
61
|
-
return { action: "skip", reason: "already global install" };
|
|
62
|
-
}
|
|
63
|
-
if (env.CONTEXT_CLI_SKIP_AUTO_LINK === "1") {
|
|
64
|
-
return { action: "skip", reason: "explicit opt-out" };
|
|
65
|
-
}
|
|
66
|
-
if (!looksLikeClaudePluginInstall(env)) {
|
|
67
|
-
return { action: "skip", reason: "not a Claude plugin install" };
|
|
68
|
-
}
|
|
69
|
-
if (hasContextBin) {
|
|
70
|
-
return { action: "skip", reason: "context already on PATH" };
|
|
30
|
+
if (env[RE_ENTRY_ENV] === "1") return { action: "skip", reason: "re-entry guard" };
|
|
31
|
+
if (env.CI === "true" || env.CI === "1") return { action: "skip", reason: "CI" };
|
|
32
|
+
if (env[SKIP_PLUGIN_INSTALL_ENV] === "1") return { action: "skip", reason: "explicit opt-out" };
|
|
33
|
+
if (env[FORCE_PLUGIN_INSTALL_ENV] === "1" || isGlobalPackageInstall(env)) {
|
|
34
|
+
return { action: "install" };
|
|
71
35
|
}
|
|
36
|
+
if (!looksLikeClaudePluginInstall(env)) return { action: "skip", reason: "local dependency install" };
|
|
37
|
+
if (hasContextBin) return { action: "skip", reason: "context already on PATH" };
|
|
72
38
|
const version = env.npm_package_version ?? "latest";
|
|
73
39
|
return {
|
|
74
40
|
action: "hint",
|
|
75
41
|
lines: [
|
|
76
|
-
"Claude plugin installed
|
|
77
|
-
|
|
78
|
-
` bun install -g @c4a/context-cli@${version} # install from npm registry`,
|
|
79
|
-
" bun link # if installing a local build",
|
|
42
|
+
"Claude plugin installed without a global context CLI. Install it with:",
|
|
43
|
+
` npm install -g @c4a/context-cli@${version}`,
|
|
80
44
|
],
|
|
81
45
|
};
|
|
82
46
|
}
|
|
83
47
|
|
|
84
|
-
/**
|
|
85
|
-
* Heuristic: does the caller's env smell like a Claude plugin install?
|
|
86
|
-
* We require the env var's *value* to contain "claude" (case-insensitive),
|
|
87
|
-
* not just the var to be set — `npm_config_user_agent` is set by every
|
|
88
|
-
* install, and `CLAUDE_PLUGIN_INSTALL=0` should not trigger us either.
|
|
89
|
-
*/
|
|
90
|
-
export function looksLikeClaudePluginInstall(env) {
|
|
91
|
-
const hints = ["CLAUDE_PLUGIN_INSTALL", "CLAUDE_CODE_PLUGIN", "npm_config_user_agent"];
|
|
92
|
-
for (const key of hints) {
|
|
93
|
-
const value = env[key];
|
|
94
|
-
if (!value) continue;
|
|
95
|
-
if (value.toLowerCase().includes("claude")) return true;
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Real-world `hasContextBin` probe: shell out to `command -v` / `where`.
|
|
102
|
-
* Kept separate from `decide()` so tests can inject a synthetic value.
|
|
103
|
-
*/
|
|
104
48
|
export function probeContextBin() {
|
|
105
49
|
const lookup = process.platform === "win32"
|
|
106
50
|
? spawnSync("where", ["context"], { stdio: "ignore" })
|
|
@@ -108,55 +52,52 @@ export function probeContextBin() {
|
|
|
108
52
|
return lookup.status === 0;
|
|
109
53
|
}
|
|
110
54
|
|
|
111
|
-
function
|
|
112
|
-
|
|
55
|
+
export function resolveBundledCliEntry(metaUrl) {
|
|
56
|
+
const scriptDir = dirname(fileURLToPath(metaUrl));
|
|
57
|
+
const candidates = [
|
|
58
|
+
join(scriptDir, "..", "cli.js"),
|
|
59
|
+
join(scriptDir, "..", "dist", "cli.js"),
|
|
60
|
+
];
|
|
61
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function log(message) {
|
|
65
|
+
process.stderr.write(`[context-cli postinstall] ${message}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installPlugin(env) {
|
|
69
|
+
const cliEntry = resolveBundledCliEntry(import.meta.url);
|
|
70
|
+
if (cliEntry === null) {
|
|
71
|
+
log("plugin refresh skipped: bundled cli.js was not found; run `context plugin install` later.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
log("refreshing global Context agent plugins...");
|
|
75
|
+
const result = spawnSync(process.execPath, [cliEntry, "plugin", "install"], {
|
|
76
|
+
env: { ...env, [RE_ENTRY_ENV]: "1" },
|
|
77
|
+
stdio: "inherit",
|
|
78
|
+
});
|
|
79
|
+
if (result.error !== undefined) {
|
|
80
|
+
log(`plugin refresh failed: ${result.error.message}; run \`context plugin install\` later.`);
|
|
81
|
+
} else if (result.status !== 0) {
|
|
82
|
+
log(`plugin refresh exited with code ${result.status ?? "unknown"}; run \`context plugin install\` later.`);
|
|
83
|
+
} else {
|
|
84
|
+
log("global Context agent plugins refreshed.");
|
|
85
|
+
}
|
|
113
86
|
}
|
|
114
87
|
|
|
115
88
|
function main() {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
89
|
+
const provisional = decide(process.env, false);
|
|
90
|
+
if (provisional.action === "install") {
|
|
91
|
+
installPlugin(process.env);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
121
94
|
if (provisional.action === "skip") return;
|
|
122
|
-
|
|
123
|
-
// Decision said "hint" — now probe the real PATH. If context IS on PATH,
|
|
124
|
-
// the user doesn't need the hint.
|
|
125
|
-
const final = decide(env, probeContextBin());
|
|
95
|
+
const final = decide(process.env, probeContextBin());
|
|
126
96
|
if (final.action === "hint") {
|
|
127
97
|
for (const line of final.lines) log(line);
|
|
128
98
|
}
|
|
129
|
-
// Set the re-entry guard so any child process we may spawn in the future
|
|
130
|
-
// (we currently don't, but belt-and-suspenders against recursion) will
|
|
131
|
-
// inherit it and skip their own postinstall.
|
|
132
|
-
env[RE_ENTRY_ENV] = "1";
|
|
133
99
|
}
|
|
134
100
|
|
|
135
|
-
// Side-effect wrapper only runs when this file is executed directly as a
|
|
136
|
-
// script (the postinstall lifecycle does that). When the file is imported
|
|
137
|
-
// by a test, `main()` is not invoked.
|
|
138
|
-
//
|
|
139
|
-
// IMPORTANT — two quirks that the obvious
|
|
140
|
-
// `import.meta.url === \`file://${process.argv[1]}\``
|
|
141
|
-
// check silently fails on, producing an invisible-no-op postinstall:
|
|
142
|
-
//
|
|
143
|
-
// 1. **Percent-encoding**: `import.meta.url` is always WHATWG-encoded
|
|
144
|
-
// (`%20` for spaces, %-hex for many punctuation chars, uppercase
|
|
145
|
-
// Windows drive letters, UNC `\\?\` prefixes). Raw `argv[1]` isn't.
|
|
146
|
-
// A package install under `/Users/Dev Tools/...` reproducibly no-ops
|
|
147
|
-
// with the interpolated check; going through `pathToFileURL` fixes
|
|
148
|
-
// it.
|
|
149
|
-
// 2. **Symlink resolution**: on macOS `/tmp` is a symlink to
|
|
150
|
-
// `/private/tmp`, and `import.meta.url` resolves that; `argv[1]`
|
|
151
|
-
// does not. npm stashes packages under symlinked paths on some
|
|
152
|
-
// setups (npm's cache, nvm prefixes, corporate homedir mounts). We
|
|
153
|
-
// realpath `argv[1]` before URL-encoding so both sides compare the
|
|
154
|
-
// canonical target.
|
|
155
|
-
//
|
|
156
|
-
// Either quirk alone turns the postinstall script into a silent no-op —
|
|
157
|
-
// exactly the invisible-failure mode npm postinstall scripts are
|
|
158
|
-
// infamous for. The combined check below is validated by the subprocess
|
|
159
|
-
// tests in postinstall.test.ts.
|
|
160
101
|
export function isDirectRun(metaUrl, argv1) {
|
|
161
102
|
if (!argv1) return false;
|
|
162
103
|
try {
|
|
@@ -164,9 +105,7 @@ export function isDirectRun(metaUrl, argv1) {
|
|
|
164
105
|
try {
|
|
165
106
|
resolved = realpathSync(argv1);
|
|
166
107
|
} catch {
|
|
167
|
-
//
|
|
168
|
-
// lifecycle timing). Fall back to the raw path — pathToFileURL
|
|
169
|
-
// still normalizes encoding, so spaces-but-no-symlinks still works.
|
|
108
|
+
// Keep the raw path when lifecycle timing makes argv[1] unavailable.
|
|
170
109
|
}
|
|
171
110
|
return metaUrl === pathToFileURL(resolved).href;
|
|
172
111
|
} catch {
|
|
@@ -177,7 +116,7 @@ export function isDirectRun(metaUrl, argv1) {
|
|
|
177
116
|
if (isDirectRun(import.meta.url, process.argv[1])) {
|
|
178
117
|
try {
|
|
179
118
|
main();
|
|
180
|
-
} catch (
|
|
181
|
-
log(`postinstall error: ${
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log(`postinstall error: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
121
|
}
|
|
183
122
|
}
|