agentic-workflow-manager 9.22.0 → 9.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/change/index.js +2 -2
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +103 -57
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +18 -6
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
- package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
- package/dist/tests/structural/sensor-slice-decision-contract.test.js +0 -63
|
@@ -119,7 +119,7 @@ function changeNew(cwd, slug, rawOpts, deps) {
|
|
|
119
119
|
if (!range.ok)
|
|
120
120
|
return (0, command_1.failed)(range.failure);
|
|
121
121
|
const { head } = range;
|
|
122
|
-
const derived = (0, derive_1.deriveFields)(root, range.mergeBase);
|
|
122
|
+
const derived = (0, derive_1.deriveFields)(root, range.mergeBase, head);
|
|
123
123
|
if (!derived.ok)
|
|
124
124
|
return (0, command_1.fail)(derived.diagnostic.code, derived.diagnostic.message);
|
|
125
125
|
const movedAfterDerive = (0, range_1.headMovedFailure)(root, head);
|
|
@@ -255,7 +255,7 @@ function failure(json, code, message, remedy) {
|
|
|
255
255
|
return (0, command_1.fail)(code, message, remedy);
|
|
256
256
|
return render(true, 'invalid', [finish(types_1.RECORD_DIR, [{ code, message: remedy ? `${message}; ${remedy}` : message }])]);
|
|
257
257
|
}
|
|
258
|
-
const deriveOnce = (run) => (run.derived ??= (0, derive_1.deriveFields)(run.root, run.mergeBaseSha));
|
|
258
|
+
const deriveOnce = (run) => (run.derived ??= (0, derive_1.deriveFields)(run.root, run.mergeBaseSha, run.head));
|
|
259
259
|
function lateFiles(run, commit) {
|
|
260
260
|
if (!run.late.has(commit)) {
|
|
261
261
|
const raw = (0, git_1.changedFiles)(run.root, commit, run.head);
|
|
@@ -45,6 +45,7 @@ const doctor_1 = require("./doctor");
|
|
|
45
45
|
const context_1 = require("../core/diagnostics/context");
|
|
46
46
|
const bundles_1 = require("../core/bundles");
|
|
47
47
|
const registries_1 = require("../core/registries");
|
|
48
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
48
49
|
const orchestrator_1 = require("../core/init/orchestrator");
|
|
49
50
|
const steps_1 = require("../core/init/steps");
|
|
50
51
|
const failure_1 = require("../core/init/failure");
|
|
@@ -210,10 +211,13 @@ async function runInit(opts = {}) {
|
|
|
210
211
|
(0, registries_1.seedBaselineRegistry)();
|
|
211
212
|
if ((0, registries_1.registriesNeedSync)()) {
|
|
212
213
|
// `syncRegistries()` reports per-registry failures as RESULTS,
|
|
213
|
-
// never as throws (core/registries.ts)
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
(0,
|
|
214
|
+
// never as throws (core/registries.ts). SG-7: init exits non-zero
|
|
215
|
+
// on any sync/clone failure (usable cache is not enough here).
|
|
216
|
+
const syncResults = (await mergedActions.syncCache()) ?? [];
|
|
217
|
+
if ((0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(syncResults)) {
|
|
218
|
+
throw new Error(`registry sync failed — ${(0, registries_1.describeRegistrySyncErrors)((0, registries_1.registrySyncErrors)(syncResults))}`);
|
|
219
|
+
}
|
|
220
|
+
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
217
221
|
}
|
|
218
222
|
const bundles = (0, bundles_1.discoverAllBundles)(undefined, reporter);
|
|
219
223
|
const ctx = (0, context_1.gatherContext)({ cwd, bundles, agent });
|
|
@@ -8,8 +8,9 @@ exports.addRegistry = addRegistry;
|
|
|
8
8
|
// cli/src/commands/registry/add.ts
|
|
9
9
|
// Logic for `awm registry add`, separated from commander wiring (testable without prompts).
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
-
const simple_git_1 = __importDefault(require("simple-git"));
|
|
12
11
|
const registries_1 = require("../../core/registries");
|
|
12
|
+
const registry_git_1 = require("../../core/registry-git");
|
|
13
|
+
const registry_git_errors_1 = require("../../core/registry-git-errors");
|
|
13
14
|
const discovery_1 = require("../../core/discovery");
|
|
14
15
|
const bundles_1 = require("../../core/bundles");
|
|
15
16
|
const orchestrators_1 = require("../../core/orchestrators");
|
|
@@ -40,12 +41,17 @@ async function addRegistry(remote, nameOverride, reporter) {
|
|
|
40
41
|
return { ok: false, name, error: `Destination already exists on disk: ${dest}` };
|
|
41
42
|
}
|
|
42
43
|
fs_1.default.mkdirSync((0, registries_1.registriesDir)(), { recursive: true });
|
|
44
|
+
(0, registry_git_1.warnIgnoredGitEnvOnce)();
|
|
43
45
|
try {
|
|
44
|
-
await (0,
|
|
46
|
+
await (0, registry_git_1.createRegistryGit)().clone(remote, dest);
|
|
45
47
|
}
|
|
46
48
|
catch (e) {
|
|
47
49
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
48
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
name,
|
|
53
|
+
error: (0, registry_git_errors_1.formatClassifiedRegistryFailure)({ name, remote, err: e }),
|
|
54
|
+
};
|
|
49
55
|
}
|
|
50
56
|
if (!(0, registries_1.validateRegistryLayout)(dest)) {
|
|
51
57
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
@@ -19,6 +19,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
19
19
|
const paths_1 = require("../core/paths");
|
|
20
20
|
const profile_1 = require("../core/profile");
|
|
21
21
|
const registries_1 = require("../core/registries");
|
|
22
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
22
23
|
const bundles_1 = require("../core/bundles");
|
|
23
24
|
const bundle_install_1 = require("../core/bundle-install");
|
|
24
25
|
const profile_pins_1 = require("../core/profile-pins");
|
|
@@ -74,9 +75,14 @@ async function runSyncCore(options, deps = {}) {
|
|
|
74
75
|
}
|
|
75
76
|
const selectedAgents = resolved.targets;
|
|
76
77
|
const syncResults = await d.syncRegistries();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// SG-7: any sync/clone failure → non-zero exit. Keep later profile sync / link
|
|
79
|
+
// repair when usable content remains (audit A1) — aborting here left healthy
|
|
80
|
+
// registries advanced on disk without reconciling the project tree.
|
|
81
|
+
const syncHadErrors = (0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(syncResults);
|
|
82
|
+
if (syncHadErrors) {
|
|
83
|
+
for (const e of (0, registries_1.registrySyncErrors)(syncResults)) {
|
|
84
|
+
console.error(picocolors_1.default.red(` ✖ registry ${e.name}: ${e.error}`));
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
try {
|
|
82
88
|
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
@@ -126,7 +132,7 @@ async function runSyncCore(options, deps = {}) {
|
|
|
126
132
|
reportProjectLinkRepair(d.reconcileProjectSkillLinks(projectRoot, selectedAgents, (0, registries_2.contentRoots)()));
|
|
127
133
|
if (profile.extensions.length === 0) {
|
|
128
134
|
console.log(picocolors_1.default.yellow('No extensions in .awm/profile.json — nothing to sync. Use `awm add <bundle>` first.'));
|
|
129
|
-
return { code: 0, selectedAgents };
|
|
135
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents };
|
|
130
136
|
}
|
|
131
137
|
const method = options.method === 'copy' ? 'copy' : 'symlink';
|
|
132
138
|
let result;
|
|
@@ -150,5 +156,5 @@ async function runSyncCore(options, deps = {}) {
|
|
|
150
156
|
for (const id of result.transactionIds) {
|
|
151
157
|
console.log(picocolors_1.default.dim(` transaction ${id} — undo with \`awm backup restore ${id}\``));
|
|
152
158
|
}
|
|
153
|
-
return { code: 0, selectedAgents, result };
|
|
159
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents, result };
|
|
154
160
|
}
|
|
@@ -26,6 +26,7 @@ const paths_1 = require("../core/paths");
|
|
|
26
26
|
const config_1 = require("../utils/config");
|
|
27
27
|
const agent_targets_1 = require("../core/agent-targets");
|
|
28
28
|
const registries_1 = require("../core/registries");
|
|
29
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
29
30
|
const paths_2 = require("../core/paths");
|
|
30
31
|
const regenerate_1 = require("../core/context/regenerate");
|
|
31
32
|
const reconciliation_1 = require("../core/reconciliation");
|
|
@@ -59,11 +60,14 @@ function updateOutro(result) {
|
|
|
59
60
|
const { registries: reg } = result;
|
|
60
61
|
if (reg.configured === 0)
|
|
61
62
|
return 'Nothing updated — no registries configured on this machine.';
|
|
62
|
-
|
|
63
|
+
// SG-7: sync errors always yield code ≠ 0. When healthy registries still advanced,
|
|
64
|
+
// later stages ran — say so instead of the unreachable pre-SG-7 "stale content / exit 0".
|
|
65
|
+
if (result.code !== 0) {
|
|
66
|
+
if (reg.failed.length > 0 && reg.synced > 0) {
|
|
67
|
+
return (`⚠ Finished with registry sync errors — ${reg.synced}/${reg.configured} registries synced `
|
|
68
|
+
+ `(failed: ${reg.failed.join(', ')}); see errors above.`);
|
|
69
|
+
}
|
|
63
70
|
return 'Update failed — see errors above.';
|
|
64
|
-
if (reg.failed.length > 0) {
|
|
65
|
-
return `⚠ Updated with stale content — ${reg.synced}/${reg.configured} registries synced, `
|
|
66
|
-
+ `kept on-disk content for: ${reg.failed.join(', ')}.`;
|
|
67
71
|
}
|
|
68
72
|
return `✅ ${reg.synced} ${reg.synced === 1 ? 'registry' : 'registries'}, skills and hooks updated.`;
|
|
69
73
|
}
|
|
@@ -90,8 +94,10 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
90
94
|
const selectedAgents = resolved.targets;
|
|
91
95
|
const registryResults = await d.syncRegistries();
|
|
92
96
|
for (const r of registryResults) {
|
|
93
|
-
if (r.action === 'error')
|
|
94
|
-
|
|
97
|
+
if (r.action === 'error') {
|
|
98
|
+
// Printed below as a hard SG-7 failure — skip the stale warn path.
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
95
101
|
else if (r.disposition === 'regressed') {
|
|
96
102
|
const latest = r.availableVersion && r.availableVersion !== r.version
|
|
97
103
|
? ` Latest available: ${r.availableVersion}.`
|
|
@@ -131,11 +137,16 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
131
137
|
console.error(picocolors_1.default.dim(" This machine was never initialized: run 'awm init' first."));
|
|
132
138
|
return { code: 1, selectedAgents, registries };
|
|
133
139
|
}
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
140
|
+
// SG-7: any sync/clone failure → non-zero exit with classified message.
|
|
141
|
+
// Do NOT abort the later stages early when healthy registries already advanced
|
|
142
|
+
// on disk (audit A1): reconcile / hooks / self-update still run against usable
|
|
143
|
+
// content, then the command exits ≠ 0 naming the failed registry.
|
|
144
|
+
const syncHadErrors = (0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(registryResults);
|
|
145
|
+
if (syncHadErrors) {
|
|
146
|
+
for (const e of (0, registries_1.registrySyncErrors)(registryResults)) {
|
|
147
|
+
console.error(picocolors_1.default.red(` ✖ registry ${e.name}: ${e.error}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
139
150
|
try {
|
|
140
151
|
(0, registries_1.assertSyncedRegistriesUsable)(registryResults);
|
|
141
152
|
}
|
|
@@ -188,5 +199,5 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
188
199
|
// pasa `undefined` a propósito, para que la decisión la tome `defaultSelfUpdateMode()`
|
|
189
200
|
// según haya o no un humano en stdin — no este llamador.
|
|
190
201
|
await d.offerSelfUpdate(options.yes === true ? 'assume-yes' : undefined);
|
|
191
|
-
return { code: 0, selectedAgents, registries };
|
|
202
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents, registries };
|
|
192
203
|
}
|
|
@@ -96,13 +96,16 @@ const fail = (code, message) => ({ ok: false, diagnostic: { code, message: (0, t
|
|
|
96
96
|
* conventional commit does), while `semver` and `breaking` cover every commit.
|
|
97
97
|
* `mergeBaseSha` must be a full commit id (the output of `mergeBase`); anything else is `invalid-input`.
|
|
98
98
|
*/
|
|
99
|
-
function deriveFields(root, mergeBaseSha) {
|
|
99
|
+
function deriveFields(root, mergeBaseSha, pinnedHead) {
|
|
100
100
|
if (typeof root !== 'string' || root.length === 0)
|
|
101
101
|
return fail('invalid-input', 'repository root must be a non-empty string');
|
|
102
102
|
if (typeof mergeBaseSha !== 'string' || !types_1.COMMIT_ID.test(mergeBaseSha))
|
|
103
103
|
return fail('invalid-input', `merge-base must be a full commit id, got "${(0, text_safety_1.esc)(mergeBaseSha)}"`);
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (pinnedHead !== undefined && (typeof pinnedHead !== 'string' || !types_1.COMMIT_ID.test(pinnedHead)))
|
|
105
|
+
return fail('invalid-input', `pinned HEAD must be a full commit id, got "${(0, text_safety_1.esc)(pinnedHead)}"`);
|
|
106
|
+
// HEAD is read once: the log and the diff below both use this sha, so a HEAD that moves meanwhile cannot mix snapshots. A
|
|
107
|
+
// command that already pinned HEAD passes it (and checks `headMovedFailure` afterwards), saving one more `rev-parse`.
|
|
108
|
+
const head = pinnedHead ?? (0, git_1.headSha)(root);
|
|
106
109
|
if (head === null)
|
|
107
110
|
return fail('git-failed', 'git could not read HEAD');
|
|
108
111
|
const range = `${mergeBaseSha}..${head}`;
|
|
@@ -171,8 +171,7 @@ function isCommittedClean(root, rel) {
|
|
|
171
171
|
* Unknown state, or an unsafe exclusion, counts as dirty: an unreadable repository must never look clean.
|
|
172
172
|
*/
|
|
173
173
|
function isDirtyOutside(root, extraExcludes) {
|
|
174
|
-
|
|
175
|
-
return state.kind !== 'ok' || state.dirty;
|
|
174
|
+
return (0, worktree_1.treeDirty)(root, true, extraExcludes) !== false;
|
|
176
175
|
}
|
|
177
176
|
/** Repository root with native separators; null outside a repository. Only `rev-parse`: never a (slow, refreshing) `status`. */
|
|
178
177
|
function repoRoot(cwd) {
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REASON_GIT_FAILED = exports.REASON_NOT_REPO = exports.RUNTIME_STATE_EXCLUDES = void 0;
|
|
7
7
|
exports.gitState = gitState;
|
|
8
|
+
exports.treeDirty = treeDirty;
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const git_env_1 = require("./git-env");
|
|
10
11
|
const text_safety_1 = require("./text-safety");
|
|
@@ -39,15 +40,26 @@ function gitState(cwd, untracked, extraExcludes = []) {
|
|
|
39
40
|
catch (error) {
|
|
40
41
|
return gitFailure(error);
|
|
41
42
|
}
|
|
43
|
+
const dirty = treeDirty(root, untracked, extraExcludes);
|
|
44
|
+
return dirty === null ? { kind: 'failed' } : { kind: 'ok', root, commit, dirty };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Only the dirty measure of `gitState`, for a caller that already knows the repository root and does not need HEAD: one
|
|
48
|
+
* `status` instead of three git processes. Null when git fails or an exclusion is unsafe (the caller fails closed). Never throws.
|
|
49
|
+
*/
|
|
50
|
+
function treeDirty(root, untracked, extraExcludes = []) {
|
|
51
|
+
if (typeof root !== 'string' || root.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (!Array.isArray(extraExcludes) || extraExcludes.length > MAX_EXTRA_EXCLUDES || !extraExcludes.every(text_safety_1.isSafeRelativePath))
|
|
54
|
+
return null;
|
|
42
55
|
try {
|
|
43
56
|
// Submodules are never hidden by config (`.gitmodules` ignore=all, diff.ignoreSubmodules). The pre-run measure counts everything
|
|
44
57
|
// (`none`); the post-run recheck is about tracked files only, so it also ignores untracked content inside a submodule
|
|
45
58
|
// (sensor build/coverage output) while still seeing a moved checkout or modified tracked content (`untracked`).
|
|
46
59
|
const args = ['status', '--porcelain', untracked ? '--untracked-files=normal' : '--untracked-files=no', untracked ? '--ignore-submodules=none' : '--ignore-submodules=untracked', '--', '.', ...exports.RUNTIME_STATE_EXCLUDES.map((p) => `:(exclude)${p}`), ...extraExcludes.map((p) => `:(exclude,literal)${p}`)];
|
|
47
|
-
|
|
48
|
-
return { kind: 'ok', root, commit, dirty: status.trim().length > 0 };
|
|
60
|
+
return (0, git_env_1.execGit)(root, args, { stderr: 'ignore', maxBuffer: MAX_GIT_OUTPUT }).trim().length > 0;
|
|
49
61
|
}
|
|
50
62
|
catch {
|
|
51
|
-
return
|
|
63
|
+
return null;
|
|
52
64
|
}
|
|
53
65
|
}
|
|
@@ -162,21 +162,16 @@ async function stepCache(d) {
|
|
|
162
162
|
return failed('machine.cache', 'machine', msg);
|
|
163
163
|
}
|
|
164
164
|
// `syncRegistries()` reports per-registry failures as RESULTS, not throws
|
|
165
|
-
// (registries.ts), so the `try` above catches none of them.
|
|
166
|
-
//
|
|
167
|
-
// later step's failure — with its own cause already gone.
|
|
165
|
+
// (registries.ts), so the `try` above catches none of them. SG-7: init must
|
|
166
|
+
// fail closed on ANY sync/clone failure (usable on-disk cache is not enough).
|
|
168
167
|
const errors = (0, registries_1.registrySyncErrors)(results);
|
|
169
168
|
if (errors.length === 0)
|
|
170
169
|
return ok('machine.cache', 'machine', 'applied');
|
|
171
|
-
// A registry that errored and has no content on disk is unusable, and every
|
|
172
|
-
// later step that reads it will fail for a reason that no longer names this
|
|
173
|
-
// cause — so machine.cache owns it here. One that errored but still has
|
|
174
|
-
// content is stale, not broken: record it and carry on.
|
|
175
170
|
const unusable = (0, registries_1.unusableSyncedRegistries)(results);
|
|
176
171
|
if (unusable.length > 0) {
|
|
177
172
|
return failed('machine.cache', 'machine', `registry unavailable — ${(0, registries_1.describeRegistrySyncErrors)(unusable)}`);
|
|
178
173
|
}
|
|
179
|
-
return
|
|
174
|
+
return failed('machine.cache', 'machine', `registry sync failed — ${(0, registries_1.describeRegistrySyncErrors)(errors)}`);
|
|
180
175
|
}
|
|
181
176
|
/** Step 2 – Install the session-start hook for the target agent. */
|
|
182
177
|
function stepHook(d) {
|
|
@@ -30,8 +30,9 @@ exports.assertRegistryGates = assertRegistryGates;
|
|
|
30
30
|
// src/core/registries.ts
|
|
31
31
|
const fs_1 = __importDefault(require("fs"));
|
|
32
32
|
const path_1 = __importDefault(require("path"));
|
|
33
|
-
const simple_git_1 = __importDefault(require("simple-git"));
|
|
34
33
|
const registry_1 = require("./registry");
|
|
34
|
+
const registry_git_1 = require("./registry-git");
|
|
35
|
+
const registry_git_errors_1 = require("./registry-git-errors");
|
|
35
36
|
const versioning_1 = require("./versioning");
|
|
36
37
|
const cli_version_1 = require("./cli-version");
|
|
37
38
|
const paths_1 = require("./paths");
|
|
@@ -229,8 +230,10 @@ function registriesNeedSync() {
|
|
|
229
230
|
return listRegistries().some((registry) => !validateRegistryLayout(registry.contentRoot));
|
|
230
231
|
}
|
|
231
232
|
/** Sincroniza cada registry configurado al ref resuelto (pin > último tag > HEAD);
|
|
232
|
-
* re-clona si falta el dir. Errores por-registry
|
|
233
|
+
* re-clona si falta el dir. Errores por-registry se reportan en el resultado (clasificados SG-6);
|
|
234
|
+
* si el llamador debe abortar lo decide la política SG-7 del comando. */
|
|
233
235
|
async function syncRegistries() {
|
|
236
|
+
(0, registry_git_1.warnIgnoredGitEnvOnce)();
|
|
234
237
|
const results = [];
|
|
235
238
|
for (const reg of listRegistries()) {
|
|
236
239
|
try {
|
|
@@ -241,7 +244,7 @@ async function syncRegistries() {
|
|
|
241
244
|
if (freshClone) {
|
|
242
245
|
fs_1.default.mkdirSync(registriesDir(), { recursive: true });
|
|
243
246
|
try {
|
|
244
|
-
await (0,
|
|
247
|
+
await (0, registry_git_1.createRegistryGit)().clone(reg.remote, reg.contentRoot);
|
|
245
248
|
}
|
|
246
249
|
catch (e) {
|
|
247
250
|
fs_1.default.rmSync(reg.contentRoot, { recursive: true, force: true });
|
|
@@ -249,9 +252,9 @@ async function syncRegistries() {
|
|
|
249
252
|
}
|
|
250
253
|
}
|
|
251
254
|
else {
|
|
252
|
-
await (0,
|
|
255
|
+
await (0, registry_git_1.createRegistryGit)(reg.contentRoot).reset(['--hard']);
|
|
253
256
|
}
|
|
254
|
-
const git = (0,
|
|
257
|
+
const git = (0, registry_git_1.createRegistryGit)(reg.contentRoot);
|
|
255
258
|
const previousVersion = freshClone ? null : await (0, versioning_1.currentVersion)(reg.contentRoot);
|
|
256
259
|
const versionOpts = (0, versioning_1.machineVersionOpts)(reg.name);
|
|
257
260
|
let resolved;
|
|
@@ -290,7 +293,11 @@ async function syncRegistries() {
|
|
|
290
293
|
});
|
|
291
294
|
}
|
|
292
295
|
catch (e) {
|
|
293
|
-
results.push({
|
|
296
|
+
results.push({
|
|
297
|
+
name: reg.name,
|
|
298
|
+
action: 'error',
|
|
299
|
+
error: (0, registry_git_errors_1.formatClassifiedRegistryFailure)({ name: reg.name, remote: reg.remote, err: e }),
|
|
300
|
+
});
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
return results;
|
|
@@ -309,15 +316,13 @@ function describeRegistrySyncErrors(errors) {
|
|
|
309
316
|
* Registries that both errored during sync AND no longer have a usable content
|
|
310
317
|
* layout afterwards — i.e. genuinely unusable, as opposed to merely stale.
|
|
311
318
|
*
|
|
312
|
-
* `syncRegistries()`
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* not about being a healthy git clone — a seeded content root with no `.git`
|
|
320
|
-
* fails to sync every time and is still perfectly readable.
|
|
319
|
+
* `syncRegistries()` still reports per-registry failures as results rather than
|
|
320
|
+
* throwing (so callers can classify/format each one). Under SG-7, init / update /
|
|
321
|
+
* sync / registry add exit non-zero on ANY sync error; add / list may continue
|
|
322
|
+
* only when cached content remains usable — and this helper is what those soft
|
|
323
|
+
* paths consult. "Usable" is about content on disk, not a healthy git clone —
|
|
324
|
+
* a seeded content root with no `.git` fails to sync every time and is still
|
|
325
|
+
* readable.
|
|
321
326
|
*/
|
|
322
327
|
function unusableSyncedRegistries(results) {
|
|
323
328
|
const errors = registrySyncErrors(results);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactRemoteForDisplay = redactRemoteForDisplay;
|
|
4
|
+
exports.extractFatalLine = extractFatalLine;
|
|
5
|
+
exports.classifyRegistryGitError = classifyRegistryGitError;
|
|
6
|
+
exports.formatClassifiedRegistryFailure = formatClassifiedRegistryFailure;
|
|
7
|
+
const registry_git_1 = require("./registry-git");
|
|
8
|
+
const text_safety_1 = require("./changes/text-safety");
|
|
9
|
+
const REMEDIES = {
|
|
10
|
+
authentication: 'configure credential.helper (awm ignores ambient GIT_ASKPASS)',
|
|
11
|
+
tls: 'git config --global http.sslCAInfo <path> or http.sslCAPath (awm ignores ambient GIT_SSL_*)',
|
|
12
|
+
network: 'check connectivity / proxy via git config --global http.proxy (awm does not pass ambient GIT_* proxy vars)',
|
|
13
|
+
ssh: 'set core.sshCommand or ~/.ssh/config (awm ignores ambient GIT_SSH_COMMAND)',
|
|
14
|
+
'not-found': 'verify the remote URL and that the repository exists and is reachable',
|
|
15
|
+
// simple-git `timeout.block` resets on stdout/stderr activity — idle bound, not wall-clock.
|
|
16
|
+
// Only the plugin's "block timeout reached" is this class (audit M1); git/curl "timed out" → network.
|
|
17
|
+
timeout: `check network connectivity; registry git children are killed after ${registry_git_1.REGISTRY_GIT_TIMEOUT_MS / 1000}s `
|
|
18
|
+
+ 'without stdout/stderr activity',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Strip credential-bearing userinfo from a remote URL (or URL-shaped substring) before
|
|
22
|
+
* echoing it to operators. HTTPS `user:token@host` / `user@host` → scheme://host… only.
|
|
23
|
+
* Does not JSON-escape or clip — callers run `cleanText`/`clipMessage`/`esc` for echo safety.
|
|
24
|
+
*/
|
|
25
|
+
function redactRemoteForDisplay(remote) {
|
|
26
|
+
if (typeof remote !== 'string' || !remote)
|
|
27
|
+
return '';
|
|
28
|
+
// https://user:pass@host/... or https://user@host/...
|
|
29
|
+
return remote.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^/@\s]+)@/gi, '$1');
|
|
30
|
+
}
|
|
31
|
+
function errorText(err) {
|
|
32
|
+
if (err instanceof Error)
|
|
33
|
+
return err.message;
|
|
34
|
+
return String(err);
|
|
35
|
+
}
|
|
36
|
+
/** Extract the last line that looks like git's `fatal: …` diagnostic. */
|
|
37
|
+
function extractFatalLine(text) {
|
|
38
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40
|
+
if (/^fatal:/i.test(lines[i]))
|
|
41
|
+
return lines[i];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Map a registry git failure to a stable class + file-based remedy (SG-6).
|
|
47
|
+
* Heuristics are substring-based on message/stderr; never includes env values.
|
|
48
|
+
*/
|
|
49
|
+
function classifyRegistryGitError(err) {
|
|
50
|
+
const message = errorText(err);
|
|
51
|
+
const fatalLine = extractFatalLine(message);
|
|
52
|
+
let errorClass;
|
|
53
|
+
// Audit M1: only simple-git's idle-block plugin text is `timeout`. Git/curl
|
|
54
|
+
// "Connection timed out" / "timed out" are connectivity → `network`.
|
|
55
|
+
if (/block timeout reached/i.test(message)) {
|
|
56
|
+
errorClass = 'timeout';
|
|
57
|
+
}
|
|
58
|
+
else if (/ssl|tls|certificate|unable to get local issuer|certificate verify failed/i.test(message)) {
|
|
59
|
+
errorClass = 'tls';
|
|
60
|
+
}
|
|
61
|
+
else if (
|
|
62
|
+
// Score repository-shaped not-found BEFORE broad SSH wording — GitHub SSH
|
|
63
|
+
// often pairs "Repository not found" with "Could not read from remote repository".
|
|
64
|
+
/repository not found|remote:\s*.*not found|fatal:\s*repository .+ not found|404\b/i.test(message)) {
|
|
65
|
+
errorClass = 'not-found';
|
|
66
|
+
}
|
|
67
|
+
else if (/host key verification|permission denied \(publickey\)/i.test(message)) {
|
|
68
|
+
errorClass = 'ssh';
|
|
69
|
+
}
|
|
70
|
+
else if (
|
|
71
|
+
// Remaining SSH transport wording — but DNS/timeout under ssh: belongs to network.
|
|
72
|
+
/\bssh\b/i.test(message)
|
|
73
|
+
&& !/could not resolve|name or service not known|timed?\s*out|connection timed out|terminal prompts disabled|authentication failed|could not read username/i.test(message)) {
|
|
74
|
+
errorClass = 'ssh';
|
|
75
|
+
}
|
|
76
|
+
else if (/terminal prompts disabled|authentication failed|could not read username|auth(entication)? (fail|required)|401\b|403\b|invalid credentials/i.test(message)) {
|
|
77
|
+
errorClass = 'authentication';
|
|
78
|
+
}
|
|
79
|
+
else if (/could not resolve host|could not resolve hostname|failed to connect|connection refused|network is unreachable|unable to connect|proxy|name or service not known|timed?\s*out|connection timed out/i.test(message)) {
|
|
80
|
+
errorClass = 'network';
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Default: treat unknown transport failures as network.
|
|
84
|
+
errorClass = 'network';
|
|
85
|
+
}
|
|
86
|
+
const rawFatal = fatalLine ?? (message.trim() ? message.trim().split(/\r?\n/).pop().slice(0, 240) : null);
|
|
87
|
+
return {
|
|
88
|
+
class: errorClass,
|
|
89
|
+
fatalLine: rawFatal === null ? null : redactRemoteForDisplay(rawFatal),
|
|
90
|
+
remedy: REMEDIES[errorClass],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Operator-facing SG-6 line: registry name, remote when known, class, fatal, remedy.
|
|
95
|
+
* Never includes ambient env values or URL userinfo credentials.
|
|
96
|
+
*/
|
|
97
|
+
function formatClassifiedRegistryFailure(opts) {
|
|
98
|
+
const classified = classifyRegistryGitError(opts.err);
|
|
99
|
+
const remotePart = opts.remote
|
|
100
|
+
? `; remote=${(0, text_safety_1.clipMessage)((0, text_safety_1.cleanText)(redactRemoteForDisplay(opts.remote)))}`
|
|
101
|
+
: '';
|
|
102
|
+
const fatalPart = classified.fatalLine
|
|
103
|
+
? `; fatal: ${(0, text_safety_1.clipMessage)((0, text_safety_1.cleanText)(classified.fatalLine.replace(/^fatal:\s*/i, '')))}`
|
|
104
|
+
: '';
|
|
105
|
+
return (`registry ${(0, text_safety_1.esc)(opts.name)} sync failed (class=${classified.class}${remotePart}${fatalPart}); `
|
|
106
|
+
+ `remedy: ${classified.remedy}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REGISTRY_GIT_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.buildRegistryGitEnv = buildRegistryGitEnv;
|
|
5
|
+
exports.listIgnoredGuardedGitEnvNames = listIgnoredGuardedGitEnvNames;
|
|
6
|
+
exports.formatIgnoredGitEnvNotice = formatIgnoredGitEnvNotice;
|
|
7
|
+
exports.resetIgnoredGitEnvNoticeForTests = resetIgnoredGitEnvNoticeForTests;
|
|
8
|
+
exports.warnIgnoredGitEnvOnce = warnIgnoredGitEnvOnce;
|
|
9
|
+
exports.createRegistryGit = createRegistryGit;
|
|
10
|
+
const simple_git_1 = require("simple-git");
|
|
11
|
+
exports.REGISTRY_GIT_TIMEOUT_MS = 60_000;
|
|
12
|
+
/**
|
|
13
|
+
* Non-`GIT_*` keys that simple-git 4 treats as guarded when supplied via `.env()`.
|
|
14
|
+
* Omit them from the copy so `.env(copy)` does not reject (editor/pager/visual family).
|
|
15
|
+
* Kept in sync with `@simple-git/argv-parser` GitEnvKeys non-prefix entries.
|
|
16
|
+
*/
|
|
17
|
+
const NON_PREFIX_GUARDED_ENV = new Set(['editor', 'pager', 'prefix', 'ssh_askpass', 'visual']);
|
|
18
|
+
/**
|
|
19
|
+
* Ambient names that affect registry transport/auth and are stripped by the factory.
|
|
20
|
+
* SG-5 warns only for these (audit M2) — not GIT_EDITOR / GIT_PAGER / GIT_AUTHOR_*.
|
|
21
|
+
* Includes SSH_ASKPASS (audit B2), which simple-git also guards without a GIT_ prefix.
|
|
22
|
+
*/
|
|
23
|
+
function isTransportRelevantIgnoredEnv(key) {
|
|
24
|
+
const lower = key.toLowerCase();
|
|
25
|
+
if (lower === 'ssh_askpass')
|
|
26
|
+
return true;
|
|
27
|
+
if (!lower.startsWith('git_'))
|
|
28
|
+
return false;
|
|
29
|
+
if (lower === 'git_terminal_prompt')
|
|
30
|
+
return false;
|
|
31
|
+
return (lower.startsWith('git_ssl_')
|
|
32
|
+
|| lower === 'git_askpass'
|
|
33
|
+
|| lower.startsWith('git_ssh')
|
|
34
|
+
|| lower.startsWith('git_proxy')
|
|
35
|
+
|| lower.startsWith('git_http_')
|
|
36
|
+
|| lower.startsWith('git_curl_')
|
|
37
|
+
|| lower.startsWith('git_config'));
|
|
38
|
+
}
|
|
39
|
+
/** Copy parent env without guarded GIT_* / editor keys, then force non-interactive prompts. */
|
|
40
|
+
function buildRegistryGitEnv(parent = process.env) {
|
|
41
|
+
const env = {};
|
|
42
|
+
for (const [key, value] of Object.entries(parent)) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
const lower = key.toLowerCase();
|
|
46
|
+
if (lower.startsWith('git_'))
|
|
47
|
+
continue;
|
|
48
|
+
if (NON_PREFIX_GUARDED_ENV.has(lower))
|
|
49
|
+
continue;
|
|
50
|
+
env[key] = value;
|
|
51
|
+
}
|
|
52
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
53
|
+
return env;
|
|
54
|
+
}
|
|
55
|
+
/** Names of stripped transport/auth vars worth warning about (SG-5) — never values. */
|
|
56
|
+
function listIgnoredGuardedGitEnvNames(parent = process.env) {
|
|
57
|
+
return Object.keys(parent)
|
|
58
|
+
.filter((key) => isTransportRelevantIgnoredEnv(key))
|
|
59
|
+
.sort((a, b) => a.localeCompare(b));
|
|
60
|
+
}
|
|
61
|
+
/** Operator notice for SG-5 — names only, never values. */
|
|
62
|
+
function formatIgnoredGitEnvNotice(names) {
|
|
63
|
+
const listed = names.join(', ');
|
|
64
|
+
return (`⚠ Ignoring ambient transport/auth env vars: ${listed}. `
|
|
65
|
+
+ 'Use file-based git config instead '
|
|
66
|
+
+ '(e.g. git config --global http.sslCAInfo <path>, credential.helper, core.sshCommand / ~/.ssh/config).');
|
|
67
|
+
}
|
|
68
|
+
let ignoredGitEnvWarned = false;
|
|
69
|
+
/** Test seam: clear the once-per-process SG-5 flag. */
|
|
70
|
+
function resetIgnoredGitEnvNoticeForTests() {
|
|
71
|
+
ignoredGitEnvWarned = false;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Before the first network registry git op: warn once listing ignored ambient
|
|
75
|
+
* transport/auth env names (except GIT_TERMINAL_PROMPT) and point at file-based remedies (SG-5).
|
|
76
|
+
*/
|
|
77
|
+
function warnIgnoredGitEnvOnce(log = (message) => console.warn(message), parent = process.env) {
|
|
78
|
+
if (ignoredGitEnvWarned)
|
|
79
|
+
return;
|
|
80
|
+
const names = listIgnoredGuardedGitEnvNames(parent);
|
|
81
|
+
if (names.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
ignoredGitEnvWarned = true;
|
|
84
|
+
log(formatIgnoredGitEnvNotice(names));
|
|
85
|
+
}
|
|
86
|
+
function createRegistryGit(baseDir) {
|
|
87
|
+
const git = (0, simple_git_1.simpleGit)({
|
|
88
|
+
...(baseDir !== undefined ? { baseDir } : {}),
|
|
89
|
+
allowEnvironment: ['GIT_TERMINAL_PROMPT'],
|
|
90
|
+
timeout: { block: exports.REGISTRY_GIT_TIMEOUT_MS },
|
|
91
|
+
});
|
|
92
|
+
return git.env(buildRegistryGitEnv());
|
|
93
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatClassifiedRegistryFailure = void 0;
|
|
4
|
+
exports.formatRegistrySyncSoftFailure = formatRegistrySyncSoftFailure;
|
|
5
|
+
exports.hardFailOnAnyRegistrySyncError = hardFailOnAnyRegistrySyncError;
|
|
6
|
+
exports.formatSoftRegistrySyncFailures = formatSoftRegistrySyncFailures;
|
|
7
|
+
exports.reportRegistrySyncSoftFailures = reportRegistrySyncSoftFailures;
|
|
8
|
+
const registries_1 = require("./registries");
|
|
9
|
+
const registry_git_errors_1 = require("./registry-git-errors");
|
|
10
|
+
Object.defineProperty(exports, "formatClassifiedRegistryFailure", { enumerable: true, get: function () { return registry_git_errors_1.formatClassifiedRegistryFailure; } });
|
|
11
|
+
const text_safety_1 = require("./changes/text-safety");
|
|
12
|
+
const versioning_1 = require("./versioning");
|
|
13
|
+
/**
|
|
14
|
+
* Soft path for `awm add` / `awm list` (SG-7): continue on usable cache only after
|
|
15
|
+
* stating the cached version plus the classified cause/remedy.
|
|
16
|
+
* Avoids duplicating "registry X … sync failed" (audit B3); escapes the name like SG-6.
|
|
17
|
+
*/
|
|
18
|
+
function formatRegistrySyncSoftFailure(input) {
|
|
19
|
+
const versionLabel = input.cachedVersion ? `v${input.cachedVersion}` : 'unknown';
|
|
20
|
+
return (`⚠ Continuing with cached version ${versionLabel} for registry ${(0, text_safety_1.esc)(input.name)}. `
|
|
21
|
+
+ input.classifiedMessage);
|
|
22
|
+
}
|
|
23
|
+
/** True when any per-registry sync result is an error (SG-7 hard-fail gate). */
|
|
24
|
+
function hardFailOnAnyRegistrySyncError(results) {
|
|
25
|
+
return results.some((r) => r.action === 'error');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Soft-failure lines for add/list: one per errored registry that still has usable
|
|
29
|
+
* on-disk content. Callers print these then continue (or hard-fail via
|
|
30
|
+
* assertSyncedRegistriesUsable when nothing usable remains).
|
|
31
|
+
*/
|
|
32
|
+
async function formatSoftRegistrySyncFailures(results, opts) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
if (r.action !== 'error')
|
|
36
|
+
continue;
|
|
37
|
+
if (!opts.isUsable(r.name))
|
|
38
|
+
continue;
|
|
39
|
+
lines.push(formatRegistrySyncSoftFailure({
|
|
40
|
+
name: r.name,
|
|
41
|
+
cachedVersion: await opts.cachedVersion(r.name),
|
|
42
|
+
classifiedMessage: r.error,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* SG-7 soft path for `awm add` / `awm list`: warn with cached version + SG-6 text
|
|
49
|
+
* for each failed-but-usable registry; warn with the classified error for
|
|
50
|
+
* unusable ones (caller still runs assertSyncedRegistriesUsable for hard fail).
|
|
51
|
+
*/
|
|
52
|
+
async function reportRegistrySyncSoftFailures(results, log = (m) => console.warn(m)) {
|
|
53
|
+
const roots = new Map((0, registries_1.listRegistries)().map((r) => [r.name, r.contentRoot]));
|
|
54
|
+
const soft = await formatSoftRegistrySyncFailures(results, {
|
|
55
|
+
isUsable: (name) => {
|
|
56
|
+
const root = roots.get(name);
|
|
57
|
+
return root !== undefined && (0, registries_1.validateRegistryLayout)(root);
|
|
58
|
+
},
|
|
59
|
+
cachedVersion: async (name) => {
|
|
60
|
+
const root = roots.get(name);
|
|
61
|
+
return root ? (0, versioning_1.currentVersion)(root) : null;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
for (const line of soft)
|
|
65
|
+
log(line);
|
|
66
|
+
for (const r of results) {
|
|
67
|
+
if (r.action !== 'error')
|
|
68
|
+
continue;
|
|
69
|
+
const root = roots.get(r.name);
|
|
70
|
+
if (root !== undefined && (0, registries_1.validateRegistryLayout)(root))
|
|
71
|
+
continue;
|
|
72
|
+
log(` ⚠ registry ${r.name}: ${r.error}`);
|
|
73
|
+
}
|
|
74
|
+
}
|