c8ctl-plugin-nano 1.33.0 → 1.33.2
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/c8ctl-plugin.js +121 -16
- package/package.json +8 -8
package/c8ctl-plugin.js
CHANGED
|
@@ -161,6 +161,17 @@ function getLogger() {
|
|
|
161
161
|
warn: console.warn,
|
|
162
162
|
error: console.error,
|
|
163
163
|
debug: () => {},
|
|
164
|
+
// Primary command output, written to stdout as-is (mirrors the c8ctl host
|
|
165
|
+
// logger's `output()`). Used for preformatted, non-structured content such
|
|
166
|
+
// as the supervisor status table, whose newlines must survive verbatim.
|
|
167
|
+
// Uses `process.stdout.write` (not `console.log`) so the content is emitted
|
|
168
|
+
// literally — `console.log` applies `util.format` (mangling stray `%`
|
|
169
|
+
// sequences) and would append its own newline; here we add exactly one
|
|
170
|
+
// trailing newline when the text lacks one.
|
|
171
|
+
output: (msg) => {
|
|
172
|
+
const s = typeof msg === 'string' ? msg : String(msg);
|
|
173
|
+
process.stdout.write(s.endsWith('\n') ? s : s + '\n');
|
|
174
|
+
},
|
|
164
175
|
};
|
|
165
176
|
}
|
|
166
177
|
|
|
@@ -1244,7 +1255,7 @@ function logsCluster(req) {
|
|
|
1244
1255
|
const proc = spawn('tail', tailArgs, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
1245
1256
|
proc.on('error', (err) => {
|
|
1246
1257
|
logger.error(`Failed to read logs: ${err.message}`);
|
|
1247
|
-
logger.
|
|
1258
|
+
logger.output(`Log files:\n ${files.join('\n ')}`);
|
|
1248
1259
|
});
|
|
1249
1260
|
}
|
|
1250
1261
|
|
|
@@ -2853,9 +2864,55 @@ function ghUserIdentity() {
|
|
|
2853
2864
|
}
|
|
2854
2865
|
}
|
|
2855
2866
|
|
|
2867
|
+
// Reject a commit-author email that can't be attributed to a real account and
|
|
2868
|
+
// can't receive mail — a `*@nano.local` (or other non-routable) placeholder
|
|
2869
|
+
// injected by the launch environment. Such an address produces UNVERIFIED
|
|
2870
|
+
// commits that look like a person but map to no GitHub user, so the harness must
|
|
2871
|
+
// never stamp it onto a commit; it falls through to the next identity source
|
|
2872
|
+
// instead. An EMPTY email is NOT a placeholder — it is an absent field handled
|
|
2873
|
+
// by ordinary per-field fallthrough, so it does not invalidate its source.
|
|
2874
|
+
// Matching is trim + case-insensitive.
|
|
2875
|
+
function isPlaceholderEmail(email) {
|
|
2876
|
+
const e = String(email || '').trim().toLowerCase();
|
|
2877
|
+
if (!e) return false; // absent — handled by per-field fallthrough, not a placeholder
|
|
2878
|
+
const at = e.lastIndexOf('@');
|
|
2879
|
+
if (at < 0) return true; // no domain at all — not a routable address
|
|
2880
|
+
const local = e.slice(0, at);
|
|
2881
|
+
const domain = e.slice(at + 1);
|
|
2882
|
+
// Malformed addresses missing a local part (`@example.com`) or a domain
|
|
2883
|
+
// (`user@`) can't be routed or attributed either — reject them too.
|
|
2884
|
+
if (!local || !domain) return true;
|
|
2885
|
+
// Non-routable mDNS/host-local TLDs and the loopback host: unattributable and
|
|
2886
|
+
// undeliverable, so never a legitimate commit author.
|
|
2887
|
+
return domain === 'localhost'
|
|
2888
|
+
|| domain.endsWith('.local')
|
|
2889
|
+
|| domain.endsWith('.internal');
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Coerce one identity source into a usable { name, email }. When the source's
|
|
2893
|
+
// email is a non-routable placeholder we discard the WHOLE candidate (both
|
|
2894
|
+
// fields) rather than just the email — otherwise a placeholder-derived name
|
|
2895
|
+
// (e.g. `trial-merge`) would be stitched onto a borrowed email from a lower
|
|
2896
|
+
// source, forging a Frankenstein author. An empty email is preserved as-is so
|
|
2897
|
+
// ordinary per-field fill still works (e.g. git supplies a name, gh the email).
|
|
2898
|
+
// Fields are trimmed so a whitespace-only/space-padded name or email behaves
|
|
2899
|
+
// like "absent" (empty) rather than a truthy value that would block per-field
|
|
2900
|
+
// fallthrough and get stamped as an invalid commit identity — this matches
|
|
2901
|
+
// isPlaceholderEmail, which already normalizes via trim().
|
|
2902
|
+
function sanitizeIdentity(id) {
|
|
2903
|
+
const name = String((id && id.name) || '').trim();
|
|
2904
|
+
const email = String((id && id.email) || '').trim();
|
|
2905
|
+
if (isPlaceholderEmail(email)) return { name: '', email: '' };
|
|
2906
|
+
return { name, email };
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2856
2909
|
// Resolve the committer identity the harness stamps onto the cloned workspace.
|
|
2857
|
-
//
|
|
2910
|
+
// Source precedence: explicit GIT_AUTHOR_* env → the operator's global git
|
|
2858
2911
|
// config → the gh-authenticated GitHub user → the `nano-agent` fallback.
|
|
2912
|
+
// Precedence is per-field only for ABSENT fields (an empty name/email falls
|
|
2913
|
+
// through to the next source); a source whose email is a non-routable
|
|
2914
|
+
// placeholder is discarded WHOLE by sanitizeIdentity (name included), so in that
|
|
2915
|
+
// case its name does not participate in per-field fill (see sanitizeIdentity).
|
|
2859
2916
|
// Preferring the operator's real identity means autonomous commits are authored
|
|
2860
2917
|
// by the human running the fleet (who has signed any CLA) rather than an
|
|
2861
2918
|
// anonymous bot that hasn't; the agent's own authorship is recorded as a PR
|
|
@@ -2864,20 +2921,22 @@ function ghUserIdentity() {
|
|
|
2864
2921
|
// when a higher-precedence source didn't already supply the field — so explicit
|
|
2865
2922
|
// GIT_AUTHOR_* env fully short-circuits them (no `git config`/`gh` spawns, hence
|
|
2866
2923
|
// no added latency or failure modes when the override is present).
|
|
2867
|
-
//
|
|
2924
|
+
// Every candidate source is passed through sanitizeIdentity, so a non-routable
|
|
2925
|
+
// `*@nano.local` placeholder from ANY source (env, git-global) is discarded and
|
|
2926
|
+
// falls through to the gh identity / marked bot fallback — never stamped onto a
|
|
2927
|
+
// commit. `gitIdentity`/`ghIdentity` are injectable for testing.
|
|
2868
2928
|
function resolveCommitterIdentity({ gitIdentity = hostGitIdentity, ghIdentity = ghUserIdentity } = {}) {
|
|
2869
|
-
const
|
|
2870
|
-
const envEmail = process.env.GIT_AUTHOR_EMAIL || '';
|
|
2929
|
+
const env = sanitizeIdentity({ name: process.env.GIT_AUTHOR_NAME || '', email: process.env.GIT_AUTHOR_EMAIL || '' });
|
|
2871
2930
|
let g = null;
|
|
2872
|
-
const gitOnce = () => (g ??= (gitIdentity() || {
|
|
2931
|
+
const gitOnce = () => (g ??= sanitizeIdentity(gitIdentity() || {}));
|
|
2873
2932
|
let gh = null;
|
|
2874
|
-
const ghOnce = () => (gh ??= (ghIdentity() || {
|
|
2933
|
+
const ghOnce = () => (gh ??= sanitizeIdentity(ghIdentity() || {}));
|
|
2875
2934
|
|
|
2876
|
-
const name =
|
|
2877
|
-
const email =
|
|
2935
|
+
const name = env.name || gitOnce().name || ghOnce().name || 'nano-agent';
|
|
2936
|
+
const email = env.email || gitOnce().email || ghOnce().email || 'nano-agent@users.noreply.github.com';
|
|
2878
2937
|
|
|
2879
2938
|
const source =
|
|
2880
|
-
(
|
|
2939
|
+
(env.name || env.email) ? 'env'
|
|
2881
2940
|
: (g && (g.name || g.email)) ? 'git-global'
|
|
2882
2941
|
: (gh && (gh.name || gh.email)) ? 'gh'
|
|
2883
2942
|
: 'fallback';
|
|
@@ -2983,6 +3042,20 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
2983
3042
|
const committer = resolveCommitterIdentity();
|
|
2984
3043
|
runGit(['config', 'user.name', committer.name], { cwd: workspaceDir, env: gitEnv });
|
|
2985
3044
|
runGit(['config', 'user.email', committer.email], { cwd: workspaceDir, env: gitEnv });
|
|
3045
|
+
// Config alone is not enough: git honours GIT_AUTHOR_*/GIT_COMMITTER_* OVER
|
|
3046
|
+
// user.name/user.email config, so a placeholder GIT_AUTHOR_EMAIL inherited from
|
|
3047
|
+
// the launch environment (e.g. `trial-merge@nano.local`) would still be stamped
|
|
3048
|
+
// onto commits even though we just wrote a clean identity into config. Pin all
|
|
3049
|
+
// four env vars to the resolved (already placeholder-sanitized) identity so
|
|
3050
|
+
// EVERY commit — finalizeGit's own rebase commits (which run with gitEnv) and
|
|
3051
|
+
// the harness's commits (extraEnv below carries these into harnessEnv) — uses
|
|
3052
|
+
// it deterministically, and a non-routable `*@nano.local` author can never be
|
|
3053
|
+
// written. When GIT_AUTHOR_* already held a real identity, resolveCommitterIdentity
|
|
3054
|
+
// returned it verbatim, so this is a no-op in that case.
|
|
3055
|
+
gitEnv.GIT_AUTHOR_NAME = committer.name;
|
|
3056
|
+
gitEnv.GIT_AUTHOR_EMAIL = committer.email;
|
|
3057
|
+
gitEnv.GIT_COMMITTER_NAME = committer.name;
|
|
3058
|
+
gitEnv.GIT_COMMITTER_EMAIL = committer.email;
|
|
2986
3059
|
|
|
2987
3060
|
// Determine the working branch. With branch.create we make a real branch.
|
|
2988
3061
|
// Otherwise we're on whatever the clone checked out: a branch only if
|
|
@@ -3003,7 +3076,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
3003
3076
|
// `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
|
|
3004
3077
|
// non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
|
|
3005
3078
|
// commit" (empty startSha) rather than a bogus revision.
|
|
3006
|
-
return { workspaceDir, gitEnv, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
|
|
3079
|
+
return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
|
|
3007
3080
|
}
|
|
3008
3081
|
|
|
3009
3082
|
// Look up a PR for this branch (2a does NOT open it — the harness does, driven
|
|
@@ -4239,6 +4312,15 @@ async function workAgent(req, flags) {
|
|
|
4239
4312
|
AGENT_REPO_URL: provisioned.remote,
|
|
4240
4313
|
AGENT_REPO_BRANCH: provisioned.workingBranch || '',
|
|
4241
4314
|
AGENT_REPO_REF: provisioned.ref || '',
|
|
4315
|
+
// Pin the harness's commit identity to the resolved (placeholder-
|
|
4316
|
+
// sanitized) committer so the agent's own `git commit` can't be
|
|
4317
|
+
// hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
|
|
4318
|
+
// (git honours these over user.name/user.email config). Layered via
|
|
4319
|
+
// extraEnv so they override any inherited placeholder in harnessEnv.
|
|
4320
|
+
GIT_AUTHOR_NAME: provisioned.committer.name,
|
|
4321
|
+
GIT_AUTHOR_EMAIL: provisioned.committer.email,
|
|
4322
|
+
GIT_COMMITTER_NAME: provisioned.committer.name,
|
|
4323
|
+
GIT_COMMITTER_EMAIL: provisioned.committer.email,
|
|
4242
4324
|
};
|
|
4243
4325
|
} catch (err) {
|
|
4244
4326
|
if (runDir) { try { rmSync(runDir, { recursive: true, force: true }); } catch { /* best effort */ } liveRunDirs.delete(runDir); }
|
|
@@ -5023,6 +5105,27 @@ function formatSupervisorStatus(status) {
|
|
|
5023
5105
|
return lines.join('\n');
|
|
5024
5106
|
}
|
|
5025
5107
|
|
|
5108
|
+
/**
|
|
5109
|
+
* Print a preformatted supervisor status table as primary command output.
|
|
5110
|
+
*
|
|
5111
|
+
* Preformatted, multi-line text MUST go through the logger's `output()`
|
|
5112
|
+
* channel, never `info()`. In `--output json` mode the c8ctl host logger wraps
|
|
5113
|
+
* an `info()` message in a JSON envelope (`{"status":"info","message":"…"}`),
|
|
5114
|
+
* which escapes every newline to a literal `\n` and collapses the aligned table
|
|
5115
|
+
* onto a single line — the exact breakage this guards against. `output()` writes
|
|
5116
|
+
* the content to stdout verbatim in every output mode (like `raw` command
|
|
5117
|
+
* output), so the table renders correctly regardless of mode. Falls back to
|
|
5118
|
+
* `info()` for a logger without `output()`, and to `console.log` if `logger`
|
|
5119
|
+
* is null/undefined or lacks `info()` (defensive; both the c8ctl host logger
|
|
5120
|
+
* and this plugin's fallback logger provide `output()`).
|
|
5121
|
+
*/
|
|
5122
|
+
function printSupervisorStatus(logger, status) {
|
|
5123
|
+
const text = formatSupervisorStatus(status);
|
|
5124
|
+
if (logger && typeof logger.output === 'function') logger.output(text);
|
|
5125
|
+
else if (logger && typeof logger.info === 'function') logger.info(text);
|
|
5126
|
+
else console.log(text);
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5026
5129
|
function readSupervisorState() {
|
|
5027
5130
|
const file = getSupervisorStateFile();
|
|
5028
5131
|
if (!existsSync(file)) return null;
|
|
@@ -5656,7 +5759,7 @@ async function supervisorStatusCmd() {
|
|
|
5656
5759
|
const res = await supervisorRequest({ op: 'status' }, { socketPath: getSupervisorSocketPath(), timeoutMs: 500, responseTimeoutMs: SUPERVISOR_PROBE_RESPONSE_TIMEOUT_MS });
|
|
5657
5760
|
if (res && res.ok) {
|
|
5658
5761
|
try { writeSupervisorState(stateFromStatus(res, getSupervisorSocketPath())); } catch { /* best effort */ }
|
|
5659
|
-
logger
|
|
5762
|
+
printSupervisorStatus(logger, res);
|
|
5660
5763
|
return;
|
|
5661
5764
|
}
|
|
5662
5765
|
} catch { /* no live daemon on the socket — genuinely down */ }
|
|
@@ -5672,13 +5775,13 @@ async function supervisorStatusCmd() {
|
|
|
5672
5775
|
}
|
|
5673
5776
|
try {
|
|
5674
5777
|
const res = await supervisorRequest({ op: 'status' });
|
|
5675
|
-
if (res.ok) { logger
|
|
5778
|
+
if (res.ok) { printSupervisorStatus(logger, res); return; }
|
|
5676
5779
|
} catch { /* fall back to state file below */ }
|
|
5677
5780
|
// Socket unreachable but pid alive — render from the last persisted state.
|
|
5678
|
-
logger
|
|
5781
|
+
printSupervisorStatus(logger, {
|
|
5679
5782
|
daemon: { pid: running.pid, startedAt: running.startedAt, socket: running.socket },
|
|
5680
5783
|
workers: (running.workers || []).map((w) => summarizeSupervisorWorker(w)),
|
|
5681
|
-
})
|
|
5784
|
+
});
|
|
5682
5785
|
}
|
|
5683
5786
|
|
|
5684
5787
|
async function supervisorAddCmd(req, flags) {
|
|
@@ -5791,7 +5894,7 @@ function supervisorLogsCmd(req) {
|
|
|
5791
5894
|
if (follow) logger.warn('`--follow` is not supported without `tail` on this platform; printing the current tail only.');
|
|
5792
5895
|
try {
|
|
5793
5896
|
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
5794
|
-
logger.
|
|
5897
|
+
logger.output(lines.slice(-200).join('\n'));
|
|
5795
5898
|
} catch (err) { logger.error(`Could not read ${file}: ${err.message}`); }
|
|
5796
5899
|
});
|
|
5797
5900
|
}
|
|
@@ -7506,6 +7609,7 @@ export {
|
|
|
7506
7609
|
finalizeGit,
|
|
7507
7610
|
reconcileAgentPr,
|
|
7508
7611
|
resolveCommitterIdentity,
|
|
7612
|
+
isPlaceholderEmail,
|
|
7509
7613
|
postAgentAttribution,
|
|
7510
7614
|
reapAgentRunDirs,
|
|
7511
7615
|
authUrl,
|
|
@@ -7552,6 +7656,7 @@ export {
|
|
|
7552
7656
|
formatDuration,
|
|
7553
7657
|
summarizeSupervisorWorker,
|
|
7554
7658
|
formatSupervisorStatus,
|
|
7659
|
+
printSupervisorStatus,
|
|
7555
7660
|
supervisorStatusSignature,
|
|
7556
7661
|
supervisorJobCell,
|
|
7557
7662
|
supervisorWorkerActivityFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"node-pty": "^1.0.0",
|
|
60
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.33.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.33.2",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.2",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.2",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.2",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.2",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.2",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.2"
|
|
67
67
|
}
|
|
68
68
|
}
|