@elmundi/ship-cli 0.14.2 → 0.15.4
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 +17 -16
- package/bin/shipctl.mjs +4 -80
- package/lib/commands/feedback.mjs +1 -1
- package/lib/commands/help.mjs +47 -131
- package/lib/commands/init.mjs +17 -250
- package/lib/commands/knowledge.mjs +25 -328
- package/lib/commands/preflight.mjs +213 -0
- package/lib/commands/run.mjs +298 -119
- package/lib/commands/trigger.mjs +95 -10
- package/lib/config/schema.mjs +73 -11
- package/lib/http.mjs +0 -2
- package/lib/runtime/routines.mjs +39 -0
- package/lib/templates.mjs +2 -2
- package/lib/verify/checks/agents-on-disk.mjs +5 -28
- package/lib/verify/registry.mjs +7 -8
- package/package.json +1 -1
- package/lib/artifacts/fs-index.mjs +0 -230
- package/lib/cache/store.mjs +0 -422
- package/lib/commands/bootstrap.mjs +0 -4
- package/lib/commands/callback.mjs +0 -742
- package/lib/commands/docs.mjs +0 -90
- package/lib/commands/kickoff.mjs +0 -192
- package/lib/commands/lanes.mjs +0 -566
- package/lib/commands/manifest-catalog.mjs +0 -251
- package/lib/commands/migrate.mjs +0 -204
- package/lib/commands/new.mjs +0 -452
- package/lib/commands/patterns.mjs +0 -160
- package/lib/commands/process.mjs +0 -388
- package/lib/commands/search.mjs +0 -43
- package/lib/commands/sync.mjs +0 -824
- package/lib/config/migrate.mjs +0 -223
- package/lib/find-ship-root.mjs +0 -75
- package/lib/process/specialist-prompt-contract.mjs +0 -171
- package/lib/state/lockfile.mjs +0 -180
- package/lib/vendor/run-agent.workflow.yml +0 -254
- package/lib/verify/checks/artifacts-up-to-date.mjs +0 -78
- package/lib/verify/checks/cache-integrity.mjs +0 -51
- package/lib/verify/checks/gitignore-cache.mjs +0 -51
- package/lib/verify/checks/rules-markers.mjs +0 -135
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
# ship-cli: run-agent v1 — generated by `shipctl lanes install`.
|
|
2
|
-
# Regenerate via: shipctl lanes install
|
|
3
|
-
# Template ships with @elmundi/ship-cli; hand edits outside this banner may be overwritten.
|
|
4
|
-
name: Ship · run-agent (reusable)
|
|
5
|
-
# RFC-0007 Phase 3 + RFC-0008 C3.2 — reusable workflow for lane wrappers.
|
|
6
|
-
# Caller repos use: uses: ./.github/workflows/run-agent.yml (same repository).
|
|
7
|
-
|
|
8
|
-
on:
|
|
9
|
-
workflow_call:
|
|
10
|
-
inputs:
|
|
11
|
-
lane:
|
|
12
|
-
description: "Lane id as declared in .ship/config.yml"
|
|
13
|
-
type: string
|
|
14
|
-
required: true
|
|
15
|
-
trigger:
|
|
16
|
-
description: "Override trigger (once|event|schedule|manual). Defaults to github.event_name."
|
|
17
|
-
type: string
|
|
18
|
-
required: false
|
|
19
|
-
default: ""
|
|
20
|
-
shipctl_version:
|
|
21
|
-
description: "npm dist-tag or semver for @elmundi/ship-cli. Default: latest."
|
|
22
|
-
type: string
|
|
23
|
-
required: false
|
|
24
|
-
default: "latest"
|
|
25
|
-
node_version:
|
|
26
|
-
description: "Node.js major version for the runner"
|
|
27
|
-
type: string
|
|
28
|
-
required: false
|
|
29
|
-
default: "20"
|
|
30
|
-
dry_run:
|
|
31
|
-
description: "Resolve the lane but neither call the callback nor write the idempotency marker."
|
|
32
|
-
type: boolean
|
|
33
|
-
required: false
|
|
34
|
-
default: false
|
|
35
|
-
offline:
|
|
36
|
-
description: "Do not talk to the methodology API; read patterns from the local .ship/cache."
|
|
37
|
-
type: boolean
|
|
38
|
-
required: false
|
|
39
|
-
default: false
|
|
40
|
-
ship_run_id:
|
|
41
|
-
description: "Pipeline-run id the callback should report against (set by Ship dispatch)."
|
|
42
|
-
type: string
|
|
43
|
-
required: false
|
|
44
|
-
default: ""
|
|
45
|
-
ship_callback_url:
|
|
46
|
-
description: "Ship callback URL (set by Ship dispatch)."
|
|
47
|
-
type: string
|
|
48
|
-
required: false
|
|
49
|
-
default: ""
|
|
50
|
-
upload_prompt:
|
|
51
|
-
description: "Upload the rendered prompt as a workflow artifact named `ship-prompt`."
|
|
52
|
-
type: boolean
|
|
53
|
-
required: false
|
|
54
|
-
default: true
|
|
55
|
-
secrets:
|
|
56
|
-
SHIP_API_TOKEN:
|
|
57
|
-
description: "Methodology API token (optional; required for private patterns / callbacks)."
|
|
58
|
-
required: false
|
|
59
|
-
SHIP_API_BASE:
|
|
60
|
-
description: "Ship API origin for shipctl (e.g. https://api.example.com). Wizard seeds this from SHIP_PUBLIC_URL."
|
|
61
|
-
required: false
|
|
62
|
-
SHIP_RUN_TOKEN:
|
|
63
|
-
description: "Short-lived bearer token Ship issued when dispatching this run."
|
|
64
|
-
required: false
|
|
65
|
-
|
|
66
|
-
permissions:
|
|
67
|
-
contents: read
|
|
68
|
-
|
|
69
|
-
jobs:
|
|
70
|
-
plan:
|
|
71
|
-
name: "ship plan --lane ${{ inputs.lane }}"
|
|
72
|
-
runs-on: ubuntu-latest
|
|
73
|
-
timeout-minutes: 5
|
|
74
|
-
outputs:
|
|
75
|
-
matrix: ${{ steps.plan.outputs.matrix }}
|
|
76
|
-
patterns: ${{ steps.plan.outputs.patterns }}
|
|
77
|
-
fanout: ${{ steps.plan.outputs.fanout }}
|
|
78
|
-
steps:
|
|
79
|
-
- name: Checkout caller repo
|
|
80
|
-
uses: actions/checkout@v4
|
|
81
|
-
|
|
82
|
-
- name: Setup Node.js
|
|
83
|
-
uses: actions/setup-node@v4
|
|
84
|
-
with:
|
|
85
|
-
node-version: ${{ inputs.node_version }}
|
|
86
|
-
|
|
87
|
-
- name: Install shipctl
|
|
88
|
-
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
89
|
-
|
|
90
|
-
- name: Resolve patterns + fanout
|
|
91
|
-
id: plan
|
|
92
|
-
env:
|
|
93
|
-
LANE: ${{ inputs.lane }}
|
|
94
|
-
run: |
|
|
95
|
-
set -euo pipefail
|
|
96
|
-
JSON=$(shipctl lanes list --json)
|
|
97
|
-
ROW=$(echo "$JSON" | jq -c --arg lane "$LANE" '.lanes[] | select(.lane == $lane)')
|
|
98
|
-
if [ -z "$ROW" ] || [ "$ROW" = "null" ]; then
|
|
99
|
-
echo "::error::lane '$LANE' not declared in .ship/config.yml"
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
PATTERNS=$(echo "$ROW" | jq -c '.patterns // []')
|
|
103
|
-
FANOUT=$(echo "$ROW" | jq -r '.fanout // "matrix"')
|
|
104
|
-
N=$(echo "$PATTERNS" | jq 'length')
|
|
105
|
-
if [ "$N" -lt 1 ]; then
|
|
106
|
-
echo "::error::lane '$LANE' declares no patterns"
|
|
107
|
-
exit 1
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
if [ "$FANOUT" = "matrix" ] && [ "$N" -gt 1 ]; then
|
|
111
|
-
MATRIX=$(echo "$PATTERNS" | jq -c '[.[] | {pattern: ., fanout: "matrix"}]')
|
|
112
|
-
elif [ "$N" -gt 1 ]; then
|
|
113
|
-
MATRIX=$(jq -cn --arg fanout "$FANOUT" '[{pattern: "", fanout: $fanout}]')
|
|
114
|
-
else
|
|
115
|
-
MATRIX='[{"pattern": "", "fanout": "matrix"}]'
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
echo "patterns=$PATTERNS" >> "$GITHUB_OUTPUT"
|
|
119
|
-
echo "fanout=$FANOUT" >> "$GITHUB_OUTPUT"
|
|
120
|
-
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
|
121
|
-
{
|
|
122
|
-
echo "### Plan"
|
|
123
|
-
echo "- lane: \`$LANE\`"
|
|
124
|
-
echo "- patterns: $(echo "$PATTERNS" | jq -r '. | join(", ")')"
|
|
125
|
-
echo "- fanout: $FANOUT"
|
|
126
|
-
} >> "$GITHUB_STEP_SUMMARY"
|
|
127
|
-
|
|
128
|
-
run:
|
|
129
|
-
name: "ship run --lane ${{ inputs.lane }} (pattern=${{ matrix.entry.pattern }} fanout=${{ matrix.entry.fanout }})"
|
|
130
|
-
needs: plan
|
|
131
|
-
runs-on: ubuntu-latest
|
|
132
|
-
timeout-minutes: 20
|
|
133
|
-
strategy:
|
|
134
|
-
fail-fast: false
|
|
135
|
-
matrix:
|
|
136
|
-
entry: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
|
137
|
-
steps:
|
|
138
|
-
- name: Checkout caller repo
|
|
139
|
-
uses: actions/checkout@v4
|
|
140
|
-
|
|
141
|
-
- name: Setup Node.js
|
|
142
|
-
uses: actions/setup-node@v4
|
|
143
|
-
with:
|
|
144
|
-
node-version: ${{ inputs.node_version }}
|
|
145
|
-
|
|
146
|
-
- name: Install shipctl
|
|
147
|
-
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
148
|
-
|
|
149
|
-
- name: Resolve trigger
|
|
150
|
-
id: trigger
|
|
151
|
-
env:
|
|
152
|
-
OVERRIDE: ${{ inputs.trigger }}
|
|
153
|
-
EVENT: ${{ github.event_name }}
|
|
154
|
-
run: |
|
|
155
|
-
set -euo pipefail
|
|
156
|
-
if [ -n "${OVERRIDE:-}" ]; then
|
|
157
|
-
echo "value=$OVERRIDE" >> "$GITHUB_OUTPUT"
|
|
158
|
-
else
|
|
159
|
-
case "$EVENT" in
|
|
160
|
-
workflow_dispatch) echo "value=manual" >> "$GITHUB_OUTPUT" ;;
|
|
161
|
-
schedule) echo "value=schedule" >> "$GITHUB_OUTPUT" ;;
|
|
162
|
-
*) echo "value=event" >> "$GITHUB_OUTPUT" ;;
|
|
163
|
-
esac
|
|
164
|
-
fi
|
|
165
|
-
|
|
166
|
-
- name: shipctl run
|
|
167
|
-
id: run
|
|
168
|
-
env:
|
|
169
|
-
SHIP_API_TOKEN: ${{ secrets.SHIP_API_TOKEN }}
|
|
170
|
-
SHIP_API_BASE: ${{ secrets.SHIP_API_BASE }}
|
|
171
|
-
SHIP_RUN_TOKEN: ${{ secrets.SHIP_RUN_TOKEN }}
|
|
172
|
-
SHIP_RUN_ID: ${{ inputs.ship_run_id }}
|
|
173
|
-
MATRIX_PATTERN: ${{ matrix.entry.pattern }}
|
|
174
|
-
MATRIX_FANOUT: ${{ matrix.entry.fanout }}
|
|
175
|
-
run: |
|
|
176
|
-
set -euo pipefail
|
|
177
|
-
mkdir -p .ship/run-output
|
|
178
|
-
ARGS=(run --lane "${{ inputs.lane }}" --trigger "${{ steps.trigger.outputs.value }}")
|
|
179
|
-
[ "${{ inputs.dry_run }}" = "true" ] && ARGS+=(--dry-run)
|
|
180
|
-
[ "${{ inputs.offline }}" = "true" ] && ARGS+=(--offline)
|
|
181
|
-
[ -n "${MATRIX_PATTERN:-}" ] && ARGS+=(--pattern "$MATRIX_PATTERN")
|
|
182
|
-
[ -n "${MATRIX_FANOUT:-}" ] && ARGS+=(--fanout "$MATRIX_FANOUT")
|
|
183
|
-
[ -n "${SHIP_RUN_ID:-}" ] && ARGS+=(--ship-run-id "$SHIP_RUN_ID")
|
|
184
|
-
|
|
185
|
-
set +e
|
|
186
|
-
shipctl "${ARGS[@]}" \
|
|
187
|
-
1> .ship/run-output/prompt.md \
|
|
188
|
-
2> .ship/run-output/shipctl.log
|
|
189
|
-
STATUS=$?
|
|
190
|
-
set -e
|
|
191
|
-
|
|
192
|
-
cat .ship/run-output/shipctl.log >&2 || true
|
|
193
|
-
echo "status=$STATUS" >> "$GITHUB_OUTPUT"
|
|
194
|
-
if [ "$STATUS" -ne 0 ]; then
|
|
195
|
-
echo "::error::shipctl run exited with $STATUS"
|
|
196
|
-
exit "$STATUS"
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
- name: Upload prompt artifact
|
|
200
|
-
if: ${{ inputs.upload_prompt && (success() || failure()) }}
|
|
201
|
-
uses: actions/upload-artifact@v4
|
|
202
|
-
with:
|
|
203
|
-
name: ship-prompt-${{ inputs.lane }}${{ matrix.entry.pattern != '' && format('-{0}', matrix.entry.pattern) || '' }}
|
|
204
|
-
path: .ship/run-output/
|
|
205
|
-
if-no-files-found: warn
|
|
206
|
-
retention-days: 14
|
|
207
|
-
|
|
208
|
-
aggregate:
|
|
209
|
-
name: "ship aggregate --lane ${{ inputs.lane }}"
|
|
210
|
-
if: ${{ always() && inputs.ship_callback_url != '' }}
|
|
211
|
-
needs: [plan, run]
|
|
212
|
-
runs-on: ubuntu-latest
|
|
213
|
-
timeout-minutes: 5
|
|
214
|
-
steps:
|
|
215
|
-
- name: Checkout caller repo
|
|
216
|
-
uses: actions/checkout@v4
|
|
217
|
-
|
|
218
|
-
- name: Setup Node.js
|
|
219
|
-
uses: actions/setup-node@v4
|
|
220
|
-
with:
|
|
221
|
-
node-version: ${{ inputs.node_version }}
|
|
222
|
-
|
|
223
|
-
- name: Install shipctl
|
|
224
|
-
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
225
|
-
|
|
226
|
-
- name: Report rollup to Ship
|
|
227
|
-
env:
|
|
228
|
-
SHIP_RUN_ID: ${{ inputs.ship_run_id }}
|
|
229
|
-
SHIP_CALLBACK_URL: ${{ inputs.ship_callback_url }}
|
|
230
|
-
SHIP_RUN_TOKEN: ${{ secrets.SHIP_RUN_TOKEN }}
|
|
231
|
-
GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
232
|
-
RUN_RESULT: ${{ needs.run.result }}
|
|
233
|
-
PLAN_PATTERNS: ${{ needs.plan.outputs.patterns }}
|
|
234
|
-
PLAN_FANOUT: ${{ needs.plan.outputs.fanout }}
|
|
235
|
-
run: |
|
|
236
|
-
set -euo pipefail
|
|
237
|
-
case "$RUN_RESULT" in
|
|
238
|
-
success)
|
|
239
|
-
CB=ok
|
|
240
|
-
SUMMARY="Ship lane ${{ inputs.lane }} completed ($PLAN_FANOUT over $PLAN_PATTERNS)."
|
|
241
|
-
;;
|
|
242
|
-
failure|cancelled|skipped|*)
|
|
243
|
-
CB=fail
|
|
244
|
-
SUMMARY="Ship lane ${{ inputs.lane }} $RUN_RESULT ($PLAN_FANOUT over $PLAN_PATTERNS)."
|
|
245
|
-
;;
|
|
246
|
-
esac
|
|
247
|
-
shipctl callback --status "$CB" \
|
|
248
|
-
--summary "$SUMMARY" \
|
|
249
|
-
--metric "lane_id=${{ inputs.lane }}" \
|
|
250
|
-
--metric "fanout=$PLAN_FANOUT" \
|
|
251
|
-
--metric "patterns=$(echo "$PLAN_PATTERNS" | jq -r '. | join(",")')" \
|
|
252
|
-
--metric "gh_workflow_run_id=${{ github.run_id }}" \
|
|
253
|
-
--metric "gh_html_url=$GH_RUN_URL" \
|
|
254
|
-
--metric "gh_event=${{ github.event_name }}" || true
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { fetchManifest } from "../../http.mjs";
|
|
2
|
-
import { listCached } from "../../cache/store.mjs";
|
|
3
|
-
|
|
4
|
-
export const id = "artifacts-up-to-date";
|
|
5
|
-
export const category = "network";
|
|
6
|
-
export const description = "Local cache matches latest manifest on the configured channel";
|
|
7
|
-
|
|
8
|
-
function newestVersion(versions) {
|
|
9
|
-
if (!Array.isArray(versions) || !versions.length) return null;
|
|
10
|
-
// Manifest entries are usually already sorted descending; treat the first as latest.
|
|
11
|
-
return versions[0];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {import("../registry.mjs").CheckContext} ctx
|
|
16
|
-
*/
|
|
17
|
-
export async function run(ctx) {
|
|
18
|
-
let cache;
|
|
19
|
-
try {
|
|
20
|
-
cache = listCached(ctx.cwd);
|
|
21
|
-
} catch {
|
|
22
|
-
cache = [];
|
|
23
|
-
}
|
|
24
|
-
if (!cache.length) {
|
|
25
|
-
return { status: "skip", detail: "no cached artifacts to compare" };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const baseUrl = ctx.baseUrl
|
|
29
|
-
|| (ctx.config && ctx.config.api && ctx.config.api.base_url)
|
|
30
|
-
|| process.env.SHIP_API_BASE
|
|
31
|
-
|| "https://ship.elmundi.com";
|
|
32
|
-
const channel = (ctx.config && ctx.config.api && ctx.config.api.channel) || "stable";
|
|
33
|
-
|
|
34
|
-
let manifest;
|
|
35
|
-
try {
|
|
36
|
-
manifest = await fetchManifest(baseUrl, { channel });
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return { status: "warn", detail: `manifest fetch failed: ${e.message}` };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const index = new Map();
|
|
42
|
-
for (const m of manifest || []) {
|
|
43
|
-
const k = `${m.kind}/${m.id}`;
|
|
44
|
-
if (!index.has(k)) index.set(k, m);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const stale = [];
|
|
48
|
-
for (const entry of cache) {
|
|
49
|
-
const key = `${entry.kind}/${entry.id}`;
|
|
50
|
-
const m = index.get(key);
|
|
51
|
-
if (!m) continue;
|
|
52
|
-
const latest = m.version || (m.versions ? newestVersion(m.versions) : null);
|
|
53
|
-
if (latest && entry.version && latest !== entry.version) {
|
|
54
|
-
stale.push({
|
|
55
|
-
kind: entry.kind,
|
|
56
|
-
id: entry.id,
|
|
57
|
-
cached: entry.version,
|
|
58
|
-
latest,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (stale.length) {
|
|
64
|
-
const summary = stale
|
|
65
|
-
.slice(0, 3)
|
|
66
|
-
.map((s) => `${s.kind}/${s.id} ${s.cached}→${s.latest}`)
|
|
67
|
-
.join(", ");
|
|
68
|
-
return {
|
|
69
|
-
status: "warn",
|
|
70
|
-
detail: `${stale.length} stale artifact(s): ${summary}${stale.length > 3 ? "…" : ""} — run 'shipctl sync'`,
|
|
71
|
-
data: { stale, channel },
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
status: "pass",
|
|
76
|
-
detail: `all ${cache.length} cached entries are current on channel=${channel}`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { listCached, verifyCached } from "../../cache/store.mjs";
|
|
2
|
-
|
|
3
|
-
export const id = "cache-integrity";
|
|
4
|
-
export const category = "local";
|
|
5
|
-
export const description = "Cached artifact bodies match their .meta.json sha256";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {import("../registry.mjs").CheckContext} ctx
|
|
9
|
-
*/
|
|
10
|
-
export async function run(ctx) {
|
|
11
|
-
let entries = [];
|
|
12
|
-
try {
|
|
13
|
-
entries = listCached(ctx.cwd);
|
|
14
|
-
} catch (e) {
|
|
15
|
-
return { status: "fail", detail: `failed to read cache index: ${e.message}` };
|
|
16
|
-
}
|
|
17
|
-
if (!entries.length) {
|
|
18
|
-
return { status: "skip", detail: "no cached artifacts (.ship/cache/)" };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const tampered = [];
|
|
22
|
-
for (const e of entries) {
|
|
23
|
-
const res = verifyCached(ctx.cwd, e.kind, e.id, e.version);
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
tampered.push({
|
|
26
|
-
kind: e.kind,
|
|
27
|
-
id: e.id,
|
|
28
|
-
version: e.version,
|
|
29
|
-
expected: res.expected,
|
|
30
|
-
actual: res.actual,
|
|
31
|
-
reason: res.reason || "sha256 mismatch",
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (tampered.length) {
|
|
37
|
-
return {
|
|
38
|
-
status: "fail",
|
|
39
|
-
detail: `${tampered.length}/${entries.length} cached entries tampered: ${tampered
|
|
40
|
-
.slice(0, 3)
|
|
41
|
-
.map((t) => `${t.kind}/${t.id}@${t.version}`)
|
|
42
|
-
.join(", ")}${tampered.length > 3 ? "…" : ""}`,
|
|
43
|
-
data: { tampered, total: entries.length },
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
status: "pass",
|
|
48
|
-
detail: `${entries.length} cached entries verified (sha256 ok)`,
|
|
49
|
-
data: { total: entries.length },
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export const id = "gitignore-cache";
|
|
5
|
-
export const category = "local";
|
|
6
|
-
export const description = ".gitignore contains .ship/cache/";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {import("../registry.mjs").CheckContext} ctx
|
|
10
|
-
*/
|
|
11
|
-
export async function run(ctx) {
|
|
12
|
-
const cacheTracked =
|
|
13
|
-
!!(ctx.config && ctx.config.cache && ctx.config.cache.vcs_tracked === true);
|
|
14
|
-
const giPath = path.join(ctx.cwd, ".gitignore");
|
|
15
|
-
if (!fs.existsSync(giPath)) {
|
|
16
|
-
if (cacheTracked) {
|
|
17
|
-
return {
|
|
18
|
-
status: "pass",
|
|
19
|
-
detail: ".gitignore absent but cache.vcs_tracked=true — cache is intentionally committed",
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
status: "warn",
|
|
24
|
-
detail: ".gitignore not found; add `.ship/cache/` to keep cached artifacts out of git",
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
const body = fs.readFileSync(giPath, "utf8");
|
|
28
|
-
const lines = new Set(body.split(/\r?\n/).map((l) => l.trim()));
|
|
29
|
-
const listed = lines.has(".ship/cache/") || lines.has(".ship/cache");
|
|
30
|
-
|
|
31
|
-
if (cacheTracked && listed) {
|
|
32
|
-
return {
|
|
33
|
-
status: "warn",
|
|
34
|
-
detail:
|
|
35
|
-
".ship/cache/ listed in .gitignore but cache.vcs_tracked=true — entries will be ignored by git",
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (cacheTracked) {
|
|
39
|
-
return {
|
|
40
|
-
status: "pass",
|
|
41
|
-
detail: "cache.vcs_tracked=true; .gitignore does not exclude .ship/cache/",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (!listed) {
|
|
45
|
-
return {
|
|
46
|
-
status: "warn",
|
|
47
|
-
detail: ".ship/cache/ not listed in .gitignore — add it to avoid committing cached bodies",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return { status: "pass", detail: ".ship/cache/ listed in .gitignore" };
|
|
51
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { KNOWN_AGENTS } from "../../detect.mjs";
|
|
4
|
-
import { listCached, readCachedArtifact } from "../../cache/store.mjs";
|
|
5
|
-
|
|
6
|
-
export const id = "rules-markers";
|
|
7
|
-
export const category = "local";
|
|
8
|
-
export const description = "Agent rule files contain current artifacts-protocol markers";
|
|
9
|
-
|
|
10
|
-
const RULE_MARKER = "<!-- ship-cli: artifacts-protocol v1 -->";
|
|
11
|
-
const FOOTER_PREFIX = "<!-- ship-cli: installed-from ";
|
|
12
|
-
const FOOTER_SUFFIX = " -->";
|
|
13
|
-
|
|
14
|
-
function readFooter(body) {
|
|
15
|
-
const idx = body.lastIndexOf(FOOTER_PREFIX);
|
|
16
|
-
if (idx < 0) return null;
|
|
17
|
-
const end = body.indexOf(FOOTER_SUFFIX, idx);
|
|
18
|
-
if (end < 0) return null;
|
|
19
|
-
return body.slice(idx + FOOTER_PREFIX.length, end).trim();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {import("../registry.mjs").CheckContext} ctx
|
|
24
|
-
*/
|
|
25
|
-
export async function run(ctx) {
|
|
26
|
-
const agents = (ctx.config && ctx.config.stack && Array.isArray(ctx.config.stack.agents))
|
|
27
|
-
? ctx.config.stack.agents
|
|
28
|
-
: [];
|
|
29
|
-
if (!agents.length) {
|
|
30
|
-
return { status: "skip", detail: "stack.agents is empty" };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let cache = [];
|
|
34
|
-
try {
|
|
35
|
-
cache = listCached(ctx.cwd);
|
|
36
|
-
} catch {
|
|
37
|
-
cache = [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const rows = [];
|
|
41
|
-
let hasFail = false;
|
|
42
|
-
let hasWarn = false;
|
|
43
|
-
|
|
44
|
-
for (const agent of agents) {
|
|
45
|
-
const spec = KNOWN_AGENTS[agent];
|
|
46
|
-
// Prefer the install_target declared by the cached agent-rules artifact;
|
|
47
|
-
// fall back to the hardcoded KNOWN_AGENTS mapping for graceful
|
|
48
|
-
// degradation when the cache hasn't been populated yet (RFC-0004).
|
|
49
|
-
let rel = null;
|
|
50
|
-
let cachedFm = null;
|
|
51
|
-
try {
|
|
52
|
-
cachedFm = readCachedArtifact(ctx.cwd, "collection", `agent-rules-${agent}`);
|
|
53
|
-
} catch {
|
|
54
|
-
cachedFm = null;
|
|
55
|
-
}
|
|
56
|
-
if (!cachedFm) {
|
|
57
|
-
rows.push({
|
|
58
|
-
agent,
|
|
59
|
-
status: "warn",
|
|
60
|
-
detail: `no cached agent-rules-${agent}; run shipctl sync`,
|
|
61
|
-
});
|
|
62
|
-
hasWarn = true;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
// v1 single-file artifacts kept install_target at the top level; v2
|
|
66
|
-
// moved it under `spec:`. Honour both.
|
|
67
|
-
const topLevel = cachedFm.fm && typeof cachedFm.fm.install_target === "string"
|
|
68
|
-
? cachedFm.fm.install_target.trim()
|
|
69
|
-
: "";
|
|
70
|
-
const nested = cachedFm.spec && typeof cachedFm.spec.install_target === "string"
|
|
71
|
-
? cachedFm.spec.install_target.trim()
|
|
72
|
-
: "";
|
|
73
|
-
const installTarget = topLevel || nested;
|
|
74
|
-
if (installTarget) {
|
|
75
|
-
rel = installTarget;
|
|
76
|
-
} else if (spec) {
|
|
77
|
-
rel = path.join(...spec.targetRel);
|
|
78
|
-
} else {
|
|
79
|
-
rows.push({ agent, status: "warn", detail: `unknown agent id '${agent}' (no install_target and no KNOWN_AGENTS entry)` });
|
|
80
|
-
hasWarn = true;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const abs = path.join(ctx.cwd, rel);
|
|
85
|
-
if (!fs.existsSync(abs)) {
|
|
86
|
-
rows.push({ agent, status: "fail", detail: `missing rule file ${rel}` });
|
|
87
|
-
hasFail = true;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const body = fs.readFileSync(abs, "utf8");
|
|
91
|
-
if (!body.includes(RULE_MARKER)) {
|
|
92
|
-
rows.push({ agent, status: "fail", detail: `${rel} has no '${RULE_MARKER}' marker` });
|
|
93
|
-
hasFail = true;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const footer = readFooter(body);
|
|
97
|
-
if (!footer) {
|
|
98
|
-
rows.push({ agent, status: "fail", detail: `${rel} has no 'installed-from' footer` });
|
|
99
|
-
hasFail = true;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// footer looks like "collection/agent-rules-cursor@1.0.1"
|
|
104
|
-
const m = /^collection\/agent-rules-[a-z0-9-]+@([0-9A-Za-z.\-+]+)$/.exec(footer);
|
|
105
|
-
if (!m) {
|
|
106
|
-
rows.push({ agent, status: "warn", detail: `${rel} footer is '${footer}' (non-standard format)` });
|
|
107
|
-
hasWarn = true;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const installedVersion = m[1];
|
|
111
|
-
const cached = cache.find(
|
|
112
|
-
(c) => c.kind === "collection" && c.id === `agent-rules-${agent}`,
|
|
113
|
-
);
|
|
114
|
-
if (cached && cached.version && cached.version !== installedVersion) {
|
|
115
|
-
rows.push({
|
|
116
|
-
agent,
|
|
117
|
-
status: "warn",
|
|
118
|
-
detail: `${rel} footer @${installedVersion}, cache has @${cached.version} — run 'shipctl init --copy-rules'`,
|
|
119
|
-
});
|
|
120
|
-
hasWarn = true;
|
|
121
|
-
} else {
|
|
122
|
-
rows.push({ agent, status: "pass", detail: `${rel} @${installedVersion}` });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const overall = hasFail ? "fail" : hasWarn ? "warn" : "pass";
|
|
127
|
-
const summary = rows
|
|
128
|
-
.filter((r) => r.status !== "pass")
|
|
129
|
-
.map((r) => `${r.agent}: ${r.detail}`)
|
|
130
|
-
.join("; ");
|
|
131
|
-
const detail = overall === "pass"
|
|
132
|
-
? `all ${rows.length} agent rule files have correct markers`
|
|
133
|
-
: summary || `${rows.length} agent rule files inspected`;
|
|
134
|
-
return { status: overall, detail, data: { rows } };
|
|
135
|
-
}
|