@remits/remits-cli 0.1.122 → 0.1.124
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/index.js +112 -20
- package/package.json +1 -1
- package/skills/remits-cli/references/development-loop.md +5 -0
package/index.js
CHANGED
|
@@ -1819,6 +1819,19 @@ function resolveSessionContext(cwd, flags) {
|
|
|
1819
1819
|
};
|
|
1820
1820
|
}
|
|
1821
1821
|
|
|
1822
|
+
/**
|
|
1823
|
+
* Record the FILE identity of a component (type + filename id, or type + filename stem) on an object without
|
|
1824
|
+
* sending it anywhere. Non-enumerable, so JSON payloads and the content hash never see it.
|
|
1825
|
+
*
|
|
1826
|
+
* It exists because a component is named twice: by its files, and by its `.meta.yml` `name:`. For a `new_`
|
|
1827
|
+
* file there is no id, so the two can disagree (`new_PDFStatement` vs `name: PDF Statement`), and the file
|
|
1828
|
+
* identity is the only thing both the payload and the git changed set agree on.
|
|
1829
|
+
*/
|
|
1830
|
+
function withFileKey(target, key) {
|
|
1831
|
+
Object.defineProperty(target, 'fileKey', { value: key, enumerable: false, configurable: true });
|
|
1832
|
+
return target;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1822
1835
|
function collectComponents(cwd) {
|
|
1823
1836
|
const mapping = {
|
|
1824
1837
|
schemas: 'schema',
|
|
@@ -1866,7 +1879,7 @@ function collectComponents(cwd) {
|
|
|
1866
1879
|
//
|
|
1867
1880
|
// A partial stage (mcp_component_edit writing one field) deliberately does NOT set this and
|
|
1868
1881
|
// keeps the merge semantics it needs.
|
|
1869
|
-
byKey.set(key, { type, id, name, metadataAuthoritative: true });
|
|
1882
|
+
byKey.set(key, withFileKey({ type, id, name, metadataAuthoritative: true }, key));
|
|
1870
1883
|
}
|
|
1871
1884
|
const component = byKey.get(key);
|
|
1872
1885
|
const filePath = path.join(dir, fileName);
|
|
@@ -2033,11 +2046,11 @@ function changedComponentsFromWorkingTree(cwd) {
|
|
|
2033
2046
|
if (!entry.statuses.includes(status)) entry.statuses.push(status);
|
|
2034
2047
|
if (!entry.paths.includes(info.path)) entry.paths.push(info.path);
|
|
2035
2048
|
}
|
|
2036
|
-
return Array.from(byKey.
|
|
2049
|
+
return Array.from(byKey.entries()).map(([key, entry]) => withFileKey({
|
|
2037
2050
|
...entry,
|
|
2038
2051
|
fields: entry.fields.sort(),
|
|
2039
2052
|
paths: entry.paths.sort()
|
|
2040
|
-
})).sort((a, b) => {
|
|
2053
|
+
}, key)).sort((a, b) => {
|
|
2041
2054
|
const typeCmp = String(a.type).localeCompare(String(b.type));
|
|
2042
2055
|
if (typeCmp !== 0) return typeCmp;
|
|
2043
2056
|
return String(a.id || a.name || '').localeCompare(String(b.id || b.name || ''));
|
|
@@ -2071,7 +2084,7 @@ function changedComponentsSinceRef(cwd, ref) {
|
|
|
2071
2084
|
const info = componentPathInfo(cwd, filePath);
|
|
2072
2085
|
if (!info) continue;
|
|
2073
2086
|
if (!byKey.has(info.key)) {
|
|
2074
|
-
byKey.set(info.key, { type: info.type, id: info.id, name: info.name, fields: [], paths: [] });
|
|
2087
|
+
byKey.set(info.key, withFileKey({ type: info.type, id: info.id, name: info.name, fields: [], paths: [] }, info.key));
|
|
2075
2088
|
}
|
|
2076
2089
|
const entry = byKey.get(info.key);
|
|
2077
2090
|
if (!entry.fields.includes(info.field)) entry.fields.push(info.field);
|
|
@@ -2759,7 +2772,7 @@ async function pushComponentsCommand(flags) {
|
|
|
2759
2772
|
const dataMode = resolveDataMode(flags, session);
|
|
2760
2773
|
const requestedMode = String(flags.mode || 'stage').toLowerCase();
|
|
2761
2774
|
const mode = requestedMode === 'push' ? 'stage' : requestedMode;
|
|
2762
|
-
const
|
|
2775
|
+
const changedFromGit = changedComponentsFromWorkingTree(cwd);
|
|
2763
2776
|
|
|
2764
2777
|
// WHAT this stage is. Three shapes, and the difference between them is the difference between a lane
|
|
2765
2778
|
// that reads as "7 components in flight" and one that reads as "115 staged":
|
|
@@ -2779,6 +2792,8 @@ async function pushComponentsCommand(flags) {
|
|
|
2779
2792
|
const emptyWorksetPolicy = normalizeEmptyWorksetPolicy(flags);
|
|
2780
2793
|
|
|
2781
2794
|
let components = collectComponents(cwd);
|
|
2795
|
+
// Named the way the payload is named, BEFORE anything compares the two — see alignChangedSetWithComponents.
|
|
2796
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedFromGit, components);
|
|
2782
2797
|
// Changes git reported that a component payload cannot carry — a deleted component file has nothing to
|
|
2783
2798
|
// stage, and Redis staging has no way to say "hide this during CLI-scoped runs". Removing its staged
|
|
2784
2799
|
// entry falls back to the committed row, so the component still resolves. Reported out loud rather than
|
|
@@ -2788,10 +2803,8 @@ async function pushComponentsCommand(flags) {
|
|
|
2788
2803
|
if (changedFromWorkingTree === null) {
|
|
2789
2804
|
throw new Error('--changed-only needs a git working tree to establish the changed set, and this directory is not one.');
|
|
2790
2805
|
}
|
|
2791
|
-
const wanted = new Set(changedFromWorkingTree.map(
|
|
2792
|
-
|
|
2793
|
-
components = components.filter((component) => wanted.has(
|
|
2794
|
-
component.type + ':' + (component.id ? 'id:' + component.id : 'name:' + String(component.name || '').toLowerCase())));
|
|
2806
|
+
const wanted = new Set(changedFromWorkingTree.map(stageIdentityKey));
|
|
2807
|
+
components = components.filter((component) => wanted.has(stageIdentityKey(component)));
|
|
2795
2808
|
if (!components.length && !(worksetReplace && emptyWorksetPolicy === 'clear')) {
|
|
2796
2809
|
// Deliberately NOT an error, and deliberately not a clear. An agent that has not edited anything yet
|
|
2797
2810
|
// is in an ordinary state; failing its loop teaches it nothing, and reconciling the lane to an empty
|
|
@@ -2943,16 +2956,71 @@ function normalizeEmptyWorksetPolicy(flags) {
|
|
|
2943
2956
|
*
|
|
2944
2957
|
* Returns [] when git could not answer, because "nothing is unrepresentable" would be a claim.
|
|
2945
2958
|
*/
|
|
2959
|
+
/**
|
|
2960
|
+
* The ONE identity a stage compares components by: type + id, or type + name for an id-less `new_` file.
|
|
2961
|
+
* The workset filter and the unrepresentable check used to spell this out separately.
|
|
2962
|
+
*/
|
|
2963
|
+
function stageIdentityKey(entry) {
|
|
2964
|
+
return entry.type + ':' + (entry.id ? 'id:' + entry.id : 'name:' + String(entry.name || '').toLowerCase());
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/**
|
|
2968
|
+
* Name each git-changed component the way its payload is named.
|
|
2969
|
+
*
|
|
2970
|
+
* The changed set is built from FILENAMES; the payload takes `name` from the `.meta.yml`. For a `new_` file
|
|
2971
|
+
* there is no id, so `new_PDFStatement.groovy` + `name: PDF Statement` produced two identities for one
|
|
2972
|
+
* component: `--workset` filtered it out, it was reported as "no-local-files", and the platform (which
|
|
2973
|
+
* matches the changed set against the stored name) never marked it as workset — so a full snapshot staged
|
|
2974
|
+
* it WITHOUT compiling it. Both sides carry the file identity (`fileKey`), so align on that.
|
|
2975
|
+
*/
|
|
2976
|
+
function alignChangedSetWithComponents(changed, components) {
|
|
2977
|
+
if (!Array.isArray(changed)) return changed;
|
|
2978
|
+
const byFileKey = new Map();
|
|
2979
|
+
(components || []).forEach((component) => {
|
|
2980
|
+
if (component && component.fileKey) byFileKey.set(component.fileKey, component);
|
|
2981
|
+
});
|
|
2982
|
+
return changed.map((entry) => {
|
|
2983
|
+
const component = entry && entry.fileKey ? byFileKey.get(entry.fileKey) : null;
|
|
2984
|
+
if (!component || entry.id || !component.name || component.name === entry.name) return entry;
|
|
2985
|
+
return withFileKey(Object.assign({}, entry, { name: component.name }), entry.fileKey);
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* The local components, for identity alignment ONLY on paths that did not already collect them (the sync
|
|
2991
|
+
* gates). Best-effort: a sidecar that does not parse must not stop a sync the server is about to judge.
|
|
2992
|
+
*/
|
|
2993
|
+
function collectComponentsForIdentity(cwd) {
|
|
2994
|
+
try {
|
|
2995
|
+
return collectComponents(cwd);
|
|
2996
|
+
} catch (_) {
|
|
2997
|
+
return [];
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2946
3001
|
function unrepresentableChanges(changedFromWorkingTree, collected) {
|
|
2947
3002
|
if (!Array.isArray(changedFromWorkingTree)) return [];
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
.
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3003
|
+
const contentFields = ['source', 'prompt', 'html', 'javascript', 'previewData', 'inputSchema', 'messages', 'schema'];
|
|
3004
|
+
const present = new Map((collected || []).map((component) => [stageIdentityKey(component), component]));
|
|
3005
|
+
const report = [];
|
|
3006
|
+
changedFromWorkingTree.forEach((entry) => {
|
|
3007
|
+
const deleted = (entry.statuses || []).some((s) => String(s).includes('D'));
|
|
3008
|
+
const component = present.get(stageIdentityKey(entry));
|
|
3009
|
+
if (!component) {
|
|
3010
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: deleted ? 'deleted' : 'no-local-files' }));
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
// The component is still present (its other files remain), so it IS staged — but a content file git
|
|
3014
|
+
// reports as deleted has nothing to send, and staged content fields LAYER: the lane keeps whatever it
|
|
3015
|
+
// held before. Reported alongside the component, which stays in the workset.
|
|
3016
|
+
const deletedFields = deleted
|
|
3017
|
+
? (entry.fields || []).filter((field) => contentFields.includes(field) && component[field] == null)
|
|
3018
|
+
: [];
|
|
3019
|
+
if (deletedFields.length) {
|
|
3020
|
+
report.push(Object.assign({}, entry, { unrepresentable: true, reason: 'content-file-deleted', fields: deletedFields }));
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
return report;
|
|
2956
3024
|
}
|
|
2957
3025
|
|
|
2958
3026
|
function printUnrepresentableChanges(unstageable) {
|
|
@@ -2960,12 +3028,23 @@ function printUnrepresentableChanges(unstageable) {
|
|
|
2960
3028
|
console.log('');
|
|
2961
3029
|
console.log('NOT REPRESENTABLE IN REDIS STAGING — ' + unstageable.length + ' change(s):');
|
|
2962
3030
|
unstageable.slice(0, 20).forEach((entry) => {
|
|
3031
|
+
const fields = Array.isArray(entry.fields) && entry.reason === 'content-file-deleted' ? ': ' + entry.fields.join(', ') : '';
|
|
2963
3032
|
console.log(' ' + entry.type + ' ' + (entry.id || entry.name || '(unknown)') +
|
|
2964
|
-
' (' + entry.reason + ') ' + (entry.paths || []).join(', '));
|
|
3033
|
+
' (' + entry.reason + fields + ') ' + (entry.paths || []).join(', '));
|
|
2965
3034
|
});
|
|
2966
3035
|
console.log(' A staged entry cannot hide a component. Clearing it falls back to the committed row, so');
|
|
2967
3036
|
console.log(' the component still resolves in a CLI-scoped run. Prove a deletion through the durable');
|
|
2968
3037
|
console.log(' plan instead: remits-cli components sync --dry-run --summary --fail-on-errors');
|
|
3038
|
+
const partial = unstageable.filter((entry) => entry.reason === 'content-file-deleted');
|
|
3039
|
+
if (partial.length) {
|
|
3040
|
+
console.log(' A deleted CONTENT file of a component that is still present is not a staged removal: the');
|
|
3041
|
+
console.log(' lane keeps the content it already held, and compile validation judges THAT. Drop it with:');
|
|
3042
|
+
partial.slice(0, 5).forEach((entry) => {
|
|
3043
|
+
console.log(entry.id
|
|
3044
|
+
? ' remits-cli components clear --component-type ' + entry.type + ' --component-id ' + entry.id
|
|
3045
|
+
: ' remits-cli components clear --all (' + entry.type + ' ' + entry.name + ' has no id; this clears only your lane)');
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
2969
3048
|
}
|
|
2970
3049
|
|
|
2971
3050
|
/**
|
|
@@ -2995,6 +3074,10 @@ async function stageOrRefuse(api, cwd, payload) {
|
|
|
2995
3074
|
// over a stage with nine broken components fixes one, re-stages, and pays nine round trips. Render
|
|
2996
3075
|
// the whole list HERE, where the body is still in hand, then throw the one-line summary.
|
|
2997
3076
|
printCompileValidation(body.compileValidation, { stderr: true });
|
|
3077
|
+
// A refusal can be ABOUT an unrepresentable change: a deleted source file leaves the lane holding the
|
|
3078
|
+
// content it had, and that is what was judged. Without this the refusal names a file that no longer
|
|
3079
|
+
// exists and "fix it and re-stage" cannot work. Read from the payload, which still carries `fields`.
|
|
3080
|
+
printUnrepresentableChanges((payload.changedSet || []).filter((entry) => entry && entry.unrepresentable));
|
|
2998
3081
|
const failure = new Error(body.message);
|
|
2999
3082
|
failure.responseBody = body;
|
|
3000
3083
|
failure.status = status;
|
|
@@ -3052,6 +3135,13 @@ function printCompileValidation(validation, options) {
|
|
|
3052
3135
|
emit('Compile validation: ' + status + ' — ' + validation.passed + '/' + validation.attempted +
|
|
3053
3136
|
' compiled in ' + validation.durationMs + 'ms' +
|
|
3054
3137
|
(validation.concurrency ? ' (' + validation.concurrency + ' parallel)' : ''));
|
|
3138
|
+
// A skip is neither a pass nor a failure, and it used to be printed as a pass ("1/1 compiled" over a
|
|
3139
|
+
// component whose source was blank). Named, so the reader knows exactly what was NOT looked at.
|
|
3140
|
+
const unchecked = Array.isArray(validation.unchecked) ? validation.unchecked : [];
|
|
3141
|
+
if (unchecked.length) {
|
|
3142
|
+
emit(' ' + unchecked.length + ' NOT compile-checked: ' + unchecked.map((entry) =>
|
|
3143
|
+
(entry.type || 'component') + ' ' + (entry.id || entry.name || '?') + ' (' + (entry.reason || 'unchecked') + ')').join(', '));
|
|
3144
|
+
}
|
|
3055
3145
|
const shown = failures.slice(0, COMPILE_FAILURES_SHOWN);
|
|
3056
3146
|
shown.forEach((failure) => {
|
|
3057
3147
|
emit(' ' + (failure.type || 'component') + ' ' + (failure.id || failure.name || '') +
|
|
@@ -3692,7 +3782,9 @@ async function syncComponentsCommand(rawFlags) {
|
|
|
3692
3782
|
const forceTombstones = flagEnabled(flags['force-tombstones']) || flagEnabled(flags.forceTombstones);
|
|
3693
3783
|
const dryRun = flagEnabled(flags['dry-run']) || flagEnabled(flags.dryRun);
|
|
3694
3784
|
const api = buildAxios(baseUrl, session.token);
|
|
3695
|
-
|
|
3785
|
+
// Aligned for the same reason a stage is: the server's plan names a `new_` component by its sidecar
|
|
3786
|
+
// `name:`, and the changed set would otherwise name it by its filename and the gate would refuse it.
|
|
3787
|
+
const changedFromWorkingTree = alignChangedSetWithComponents(changedComponentsFromWorkingTree(cwd), collectComponentsForIdentity(cwd));
|
|
3696
3788
|
const preflightRequested = syncPreflightRequested(flags);
|
|
3697
3789
|
const namesOnly = flagEnabled(flags['names-only']) || flagEnabled(flags.namesOnly);
|
|
3698
3790
|
|
|
@@ -4100,7 +4192,7 @@ function evaluateSyncGates(response, flags, changedFromWorkingTree, cwd) {
|
|
|
4100
4192
|
|
|
4101
4193
|
if (flagEnabled(flags['changed-only']) || flagEnabled(flags.changedOnly)) {
|
|
4102
4194
|
const since = flags['changed-since'] || flags.changedSince;
|
|
4103
|
-
const fromRef = since ? changedComponentsSinceRef(cwd, String(since)) : null;
|
|
4195
|
+
const fromRef = since ? alignChangedSetWithComponents(changedComponentsSinceRef(cwd, String(since)), collectComponentsForIdentity(cwd)) : null;
|
|
4104
4196
|
if (since && fromRef === null) {
|
|
4105
4197
|
violations.push('--changed-since: could not diff against "' + since + '". Check the ref exists (git fetch first for a remote ref).');
|
|
4106
4198
|
checks.changedOnly = false;
|
package/package.json
CHANGED
|
@@ -235,6 +235,11 @@ Two things to read correctly when it refuses:
|
|
|
235
235
|
lease) and writes nothing at all.
|
|
236
236
|
- **Every failure is printed, not just the first.** The one-line error is the first failure; the full list
|
|
237
237
|
(identified by type and id) follows it. Fix them in one pass rather than one round trip each.
|
|
238
|
+
- **`N NOT compile-checked` is not a pass.** A `new_` component whose source file is empty or blank cannot
|
|
239
|
+
be judged, so it is listed by name with its reason instead of being counted as compiled. It will not run.
|
|
240
|
+
- **Deleting a component's source file while its sidecar stays is `content-file-deleted`.** The lane keeps
|
|
241
|
+
the source it already held, and that is what gets compiled and run. Clear that one component with the
|
|
242
|
+
command the CLI prints (`components clear --component-type <type> --component-id <id>`).
|
|
238
243
|
|
|
239
244
|
If git cannot identify the changed set — you are not in a working tree, or git failed — there is no
|
|
240
245
|
workset to validate, and the CLI prints `Compile validation: NOT RUN — workset-unknown` rather than
|