@t275005746/gse 0.1.0
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/.github/ISSUE_TEMPLATE/bug_report.yml +42 -0
- package/.github/ISSUE_TEMPLATE/change_request.yml +50 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +38 -0
- package/.github/workflows/validate-gse.yml +33 -0
- package/.gse/README.md +18 -0
- package/.gse/gse-development-protocol.md +50 -0
- package/.gse/project-profile.md +29 -0
- package/.gse/quality-gates.md +25 -0
- package/.gse/releases/public-channel-publication-pending.md +49 -0
- package/.gse/releases/public-ci-run-pending.md +53 -0
- package/.gse/releases/public-release-owner-required.md +65 -0
- package/.gse/releases/public-repository-settings-owner-required.md +59 -0
- package/.gse/releases/public-security-contact-owner-required.md +45 -0
- package/.gse/state.json +27 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/README.zh-CN.md +236 -0
- package/SECURITY.md +41 -0
- package/SKILL.md +148 -0
- package/SUPPORT.md +38 -0
- package/agents/openai.yaml +4 -0
- package/assets/marketplace/README.md +7 -0
- package/assets/marketplace/gse-listing.json +75 -0
- package/assets/templates/acceptance-execution-packet.md +73 -0
- package/assets/templates/adr.md +14 -0
- package/assets/templates/change-brief.md +16 -0
- package/assets/templates/design.md +18 -0
- package/assets/templates/dispatch-packet.md +104 -0
- package/assets/templates/evidence.md +14 -0
- package/assets/templates/execution-quality-pack.md +65 -0
- package/assets/templates/goal-map.md +21 -0
- package/assets/templates/host-adapter.md +48 -0
- package/assets/templates/host-ui-invocation-record.md +42 -0
- package/assets/templates/incident-review.md +60 -0
- package/assets/templates/public-channel-publication-record.md +49 -0
- package/assets/templates/public-ci-run-record.md +53 -0
- package/assets/templates/public-release-record.md +65 -0
- package/assets/templates/public-repository-settings-record.md +59 -0
- package/assets/templates/public-security-contact-record.md +45 -0
- package/assets/templates/release-trust-record.md +32 -0
- package/assets/templates/review.md +18 -0
- package/assets/templates/spec.md +16 -0
- package/assets/templates/target-adoption-evidence.md +29 -0
- package/assets/templates/tasks.md +17 -0
- package/assets/templates/update-release-acceptance-record.md +58 -0
- package/examples/README.md +22 -0
- package/examples/agent-runtime-host/.claude/gse-adapter.md +6 -0
- package/examples/agent-runtime-host/.codex/gse-adapter.md +7 -0
- package/examples/agent-runtime-host/.gse/goal-map.md +7 -0
- package/examples/agent-runtime-host/.gse/project-profile.md +27 -0
- package/examples/agent-runtime-host/.gse/tooling.md +5 -0
- package/examples/agent-runtime-host/.mcp.json +9 -0
- package/examples/agent-runtime-host/AGENTS.md +5 -0
- package/examples/agent-runtime-host/README.md +10 -0
- package/examples/agent-runtime-host/docs/model-routing.md +5 -0
- package/examples/cli-tool/.gse/project-profile.md +21 -0
- package/examples/cli-tool/AGENTS.md +5 -0
- package/examples/cli-tool/README.md +12 -0
- package/examples/cli-tool/package.json +21 -0
- package/examples/small-app/.env.example +2 -0
- package/examples/small-app/.github/workflows/ci.yml +8 -0
- package/examples/small-app/AGENTS.md +5 -0
- package/examples/small-app/README.md +12 -0
- package/examples/small-app/package.json +26 -0
- package/examples/small-app/playwright.config.ts +4 -0
- package/package.json +53 -0
- package/references/adoption-recipes.md +171 -0
- package/references/agent-roles.md +49 -0
- package/references/architecture-health.md +101 -0
- package/references/benchmark-audit.md +73 -0
- package/references/commands.md +295 -0
- package/references/community-channels.md +46 -0
- package/references/compatibility.md +70 -0
- package/references/design-basis.md +38 -0
- package/references/domain-model.md +129 -0
- package/references/domain-quality-gates.md +75 -0
- package/references/drift-audit.md +81 -0
- package/references/evidence-taxonomy.md +123 -0
- package/references/file-ownership.md +107 -0
- package/references/final-readiness.md +84 -0
- package/references/forward-test.md +133 -0
- package/references/goal-map.md +36 -0
- package/references/host-adapters.md +119 -0
- package/references/learning-system.md +35 -0
- package/references/marketplace-discovery.md +46 -0
- package/references/model-routing.md +103 -0
- package/references/open-source-defaults.md +39 -0
- package/references/operating-model.md +52 -0
- package/references/packaging.md +277 -0
- package/references/project-agent-workspace.md +89 -0
- package/references/project-bootstrap.md +122 -0
- package/references/project-profile.md +57 -0
- package/references/public-release.md +174 -0
- package/references/quality-gates.md +100 -0
- package/references/recovery.md +176 -0
- package/references/release-trust.md +43 -0
- package/references/release.md +126 -0
- package/references/review.md +141 -0
- package/references/router.md +90 -0
- package/references/spec-workflow.md +66 -0
- package/references/task-levels.md +81 -0
- package/references/tool-adapters.md +73 -0
- package/scripts/audit-acceptance-execution-packet.mjs +133 -0
- package/scripts/audit-adoption-recipes.mjs +99 -0
- package/scripts/audit-adoption.mjs +154 -0
- package/scripts/audit-change-lifecycle.mjs +77 -0
- package/scripts/audit-change-system.mjs +134 -0
- package/scripts/audit-ci-readiness.mjs +107 -0
- package/scripts/audit-close-gate.mjs +323 -0
- package/scripts/audit-command-adapters.mjs +91 -0
- package/scripts/audit-command-execution.mjs +210 -0
- package/scripts/audit-commands.mjs +117 -0
- package/scripts/audit-compatibility.mjs +149 -0
- package/scripts/audit-completion-readiness.mjs +292 -0
- package/scripts/audit-distribution.mjs +251 -0
- package/scripts/audit-domain-quality-gates.mjs +108 -0
- package/scripts/audit-evidence-placeholders.mjs +126 -0
- package/scripts/audit-final-acceptance-packet.mjs +148 -0
- package/scripts/audit-final-form-progress-report.mjs +161 -0
- package/scripts/audit-final-form-stale-copy.mjs +221 -0
- package/scripts/audit-final-readiness-promotion.mjs +255 -0
- package/scripts/audit-final-readiness.mjs +226 -0
- package/scripts/audit-fixtures.mjs +154 -0
- package/scripts/audit-fresh-session-readiness.mjs +177 -0
- package/scripts/audit-gse.mjs +275 -0
- package/scripts/audit-host-adapters.mjs +119 -0
- package/scripts/audit-host-runtime-evidence-handoff.mjs +159 -0
- package/scripts/audit-host-runtime-invocation-drill.mjs +240 -0
- package/scripts/audit-host-runtime-invocations.mjs +254 -0
- package/scripts/audit-host-ui-invocation.mjs +132 -0
- package/scripts/audit-learning-system.mjs +103 -0
- package/scripts/audit-local-final-form-completion.mjs +131 -0
- package/scripts/audit-marketplace-discovery.mjs +122 -0
- package/scripts/audit-npm-package-metadata.mjs +155 -0
- package/scripts/audit-npm-publish-dry-run.mjs +169 -0
- package/scripts/audit-npm-tarball-install.mjs +191 -0
- package/scripts/audit-open-source-defaults.mjs +92 -0
- package/scripts/audit-open-source-readiness.mjs +97 -0
- package/scripts/audit-owner-external-gate-kit.mjs +218 -0
- package/scripts/audit-project.mjs +323 -0
- package/scripts/audit-public-acceptance-command-dry-run-drill.mjs +203 -0
- package/scripts/audit-public-acceptance-handoff.mjs +142 -0
- package/scripts/audit-public-acceptance-readiness.mjs +224 -0
- package/scripts/audit-public-channel-publication.mjs +248 -0
- package/scripts/audit-public-ci-run.mjs +184 -0
- package/scripts/audit-public-collaboration-templates.mjs +98 -0
- package/scripts/audit-public-external-gate-probe.mjs +206 -0
- package/scripts/audit-public-release-checklist.mjs +133 -0
- package/scripts/audit-public-release-decision.mjs +201 -0
- package/scripts/audit-public-release-metadata.mjs +176 -0
- package/scripts/audit-public-repository-settings.mjs +237 -0
- package/scripts/audit-public-security-contact.mjs +171 -0
- package/scripts/audit-readme-docs.mjs +185 -0
- package/scripts/audit-recovery-readiness.mjs +98 -0
- package/scripts/audit-release-bundle.mjs +251 -0
- package/scripts/audit-release-owner-action-plan-drill.mjs +277 -0
- package/scripts/audit-release-owner-action-plan.mjs +164 -0
- package/scripts/audit-release-readiness.mjs +106 -0
- package/scripts/audit-release-status-manifest.mjs +165 -0
- package/scripts/audit-release-trust.mjs +62 -0
- package/scripts/audit-remote-distribution.mjs +266 -0
- package/scripts/audit-roadmap-consistency.mjs +235 -0
- package/scripts/audit-signing.mjs +147 -0
- package/scripts/audit-state-freshness.mjs +113 -0
- package/scripts/audit-target-adoption-evidence.mjs +117 -0
- package/scripts/audit-target-project.mjs +507 -0
- package/scripts/audit-update-release-acceptance.mjs +136 -0
- package/scripts/audit-v1-target-validation.mjs +269 -0
- package/scripts/audit-validation-profiles.mjs +125 -0
- package/scripts/close-change.mjs +116 -0
- package/scripts/discover-project-profile.mjs +307 -0
- package/scripts/forward-test-gse.mjs +146 -0
- package/scripts/generate-command-adapter.mjs +231 -0
- package/scripts/generate-final-acceptance-packet.mjs +181 -0
- package/scripts/generate-final-form-progress-report.mjs +205 -0
- package/scripts/generate-host-adapter.mjs +73 -0
- package/scripts/generate-host-runtime-evidence-handoff.mjs +206 -0
- package/scripts/generate-owner-external-gate-kit.mjs +295 -0
- package/scripts/generate-public-acceptance-handoff.mjs +168 -0
- package/scripts/generate-public-release-checklist.mjs +207 -0
- package/scripts/generate-release-bundle.mjs +505 -0
- package/scripts/generate-release-owner-action-plan.mjs +172 -0
- package/scripts/generate-release-status-manifest.mjs +200 -0
- package/scripts/generate-session-prompt.mjs +188 -0
- package/scripts/gse.mjs +67 -0
- package/scripts/init-change.mjs +265 -0
- package/scripts/init-project.mjs +785 -0
- package/scripts/install-gse.mjs +234 -0
- package/scripts/lib/evidence-placeholders.mjs +28 -0
- package/scripts/package-gse.mjs +174 -0
- package/scripts/probe-public-external-gates.mjs +167 -0
- package/scripts/record-host-invocation.mjs +151 -0
- package/scripts/record-learning.mjs +137 -0
- package/scripts/record-public-channel-publication.mjs +178 -0
- package/scripts/record-public-ci-run.mjs +180 -0
- package/scripts/record-public-release.mjs +175 -0
- package/scripts/record-public-repository-settings.mjs +209 -0
- package/scripts/record-public-security-contact.mjs +157 -0
- package/scripts/run-gse-command.mjs +538 -0
- package/scripts/run-validation-profile.mjs +146 -0
- package/scripts/sign-gse-package.mjs +83 -0
- package/scripts/update-project-state.mjs +223 -0
- package/scripts/validate-gse.mjs +1168 -0
- package/scripts/verify-gse-package.mjs +85 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2)
|
|
9
|
+
|
|
10
|
+
function readArg(name, fallback = null) {
|
|
11
|
+
const index = args.indexOf(name)
|
|
12
|
+
if (index === -1) return fallback
|
|
13
|
+
return args[index + 1] ?? fallback
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sourceArg = readArg('--source', null)
|
|
17
|
+
const sourceUrlArg = readArg('--source-url', null)
|
|
18
|
+
const manifestUrlArg = readArg('--manifest-url', null)
|
|
19
|
+
const publicKeyArg = readArg('--public-key', null)
|
|
20
|
+
const skipIntegrity = args.includes('--skip-integrity')
|
|
21
|
+
const skipSignature = args.includes('--skip-signature')
|
|
22
|
+
let materializedRemote = null
|
|
23
|
+
let source = path.resolve(sourceArg ?? path.join(import.meta.dirname, '..'))
|
|
24
|
+
const target = path.resolve(readArg('--target', ''))
|
|
25
|
+
const force = args.includes('--force')
|
|
26
|
+
const dryRun = args.includes('--dry-run')
|
|
27
|
+
const jsonOnly = args.includes('--json')
|
|
28
|
+
|
|
29
|
+
const defaultInclude = [
|
|
30
|
+
'package.json',
|
|
31
|
+
'SKILL.md',
|
|
32
|
+
'README.md',
|
|
33
|
+
'README.zh-CN.md',
|
|
34
|
+
'agents',
|
|
35
|
+
'assets',
|
|
36
|
+
'examples',
|
|
37
|
+
'references',
|
|
38
|
+
'scripts',
|
|
39
|
+
'.gse',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function joinUrl(base, relativePath) {
|
|
43
|
+
const cleanBase = base.endsWith('/') ? base : base + '/'
|
|
44
|
+
return new URL(relativePath.split('/').map(encodeURIComponent).join('/'), cleanBase).toString()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchText(url) {
|
|
48
|
+
const response = await fetch(url)
|
|
49
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`)
|
|
50
|
+
return await response.text()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchBytes(url) {
|
|
54
|
+
const response = await fetch(url)
|
|
55
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`)
|
|
56
|
+
return Buffer.from(await response.arrayBuffer())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sha256(filePath) {
|
|
60
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fingerprint(pem) {
|
|
64
|
+
return crypto.createHash('sha256').update(pem).digest('hex')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function verifyPackageSignature(packageDir, publicKeyPath) {
|
|
68
|
+
const signaturePath = path.join(packageDir, 'gse-package-signature.json')
|
|
69
|
+
if (!publicKeyPath) return { status: 'not-requested' }
|
|
70
|
+
if (!fs.existsSync(publicKeyPath)) return { status: 'failed', error: 'public key does not exist', publicKey: publicKeyPath }
|
|
71
|
+
if (!fs.existsSync(signaturePath)) return { status: 'failed', error: 'signature file does not exist', signaturePath }
|
|
72
|
+
try {
|
|
73
|
+
const signatureRecord = JSON.parse(fs.readFileSync(signaturePath, 'utf8').replace(/^\uFEFF/, ''))
|
|
74
|
+
const publicKeyPem = fs.readFileSync(publicKeyPath, 'utf8')
|
|
75
|
+
const manifestPath = path.join(packageDir, 'gse-package-manifest.json')
|
|
76
|
+
const localManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8').replace(/^\uFEFF/, ''))
|
|
77
|
+
const payloadText = JSON.stringify(signatureRecord.payload)
|
|
78
|
+
const signatureOk = crypto.verify(null, Buffer.from(payloadText), publicKeyPem, Buffer.from(signatureRecord.signature, 'base64'))
|
|
79
|
+
const digestOk = signatureRecord.payload?.packageDigest === localManifest.integrity?.packageDigest
|
|
80
|
+
const fingerprintOk = signatureRecord.publicKeyFingerprint === fingerprint(publicKeyPem)
|
|
81
|
+
return signatureOk && digestOk && fingerprintOk
|
|
82
|
+
? { status: 'verified', algorithm: signatureRecord.algorithm, publicKeyFingerprint: signatureRecord.publicKeyFingerprint }
|
|
83
|
+
: { status: 'failed', error: 'signature, digest, or public key fingerprint mismatch' }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveSource() {
|
|
90
|
+
if (!sourceUrlArg && !manifestUrlArg) return { source, sourceMode: 'path', remoteBaseUrl: null }
|
|
91
|
+
const urlText = sourceUrlArg ?? manifestUrlArg
|
|
92
|
+
const parsed = new URL(urlText)
|
|
93
|
+
if (parsed.protocol === 'file:') {
|
|
94
|
+
const resolved = fileURLToPath(parsed)
|
|
95
|
+
const stat = fs.existsSync(resolved) ? fs.statSync(resolved) : null
|
|
96
|
+
if (stat?.isFile()) return { source: path.dirname(resolved), sourceMode: 'file-url', remoteBaseUrl: null }
|
|
97
|
+
return { source: resolved, sourceMode: 'file-url', remoteBaseUrl: null }
|
|
98
|
+
}
|
|
99
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
100
|
+
throw new Error('Unsupported --source-url protocol. Expected file:, http:, or https:.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const manifestUrl = manifestUrlArg ?? joinUrl(sourceUrlArg, 'gse-package-manifest.json')
|
|
104
|
+
const manifestText = await fetchText(manifestUrl)
|
|
105
|
+
const manifest = JSON.parse(manifestText.replace(/^\uFEFF/, ''))
|
|
106
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gse-remote-install-'))
|
|
107
|
+
materializedRemote = tempRoot
|
|
108
|
+
fs.writeFileSync(path.join(tempRoot, 'gse-package-manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
109
|
+
const baseUrl = manifestUrl.slice(0, manifestUrl.lastIndexOf('/') + 1)
|
|
110
|
+
try {
|
|
111
|
+
const signatureText = await fetchText(joinUrl(baseUrl, 'gse-package-signature.json'))
|
|
112
|
+
fs.writeFileSync(path.join(tempRoot, 'gse-package-signature.json'), signatureText, 'utf8')
|
|
113
|
+
} catch {
|
|
114
|
+
// Signature is optional unless --public-key is supplied.
|
|
115
|
+
}
|
|
116
|
+
for (const relativePath of manifest.files ?? []) {
|
|
117
|
+
const fileUrl = joinUrl(baseUrl, relativePath)
|
|
118
|
+
const bytes = await fetchBytes(fileUrl)
|
|
119
|
+
const targetPath = path.join(tempRoot, relativePath)
|
|
120
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
121
|
+
fs.writeFileSync(targetPath, bytes)
|
|
122
|
+
}
|
|
123
|
+
return { source: tempRoot, sourceMode: 'http-url', remoteBaseUrl: baseUrl }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let sourceInfo
|
|
127
|
+
try {
|
|
128
|
+
sourceInfo = await resolveSource()
|
|
129
|
+
source = sourceInfo.source
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const report = {
|
|
132
|
+
source: sourceArg,
|
|
133
|
+
sourceUrl: sourceUrlArg,
|
|
134
|
+
manifestUrl: manifestUrlArg,
|
|
135
|
+
target,
|
|
136
|
+
status: 'failed',
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
}
|
|
139
|
+
console.log(JSON.stringify(report, null, 2))
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const manifestPath = path.join(source, 'gse-package-manifest.json')
|
|
144
|
+
const manifest = fs.existsSync(manifestPath)
|
|
145
|
+
? JSON.parse(fs.readFileSync(manifestPath, 'utf8').replace(/^\uFEFF/, ''))
|
|
146
|
+
: null
|
|
147
|
+
|
|
148
|
+
function walk(itemPath, rootPath = source) {
|
|
149
|
+
const entries = []
|
|
150
|
+
if (!fs.existsSync(itemPath)) return entries
|
|
151
|
+
const stat = fs.statSync(itemPath)
|
|
152
|
+
if (stat.isFile()) {
|
|
153
|
+
entries.push(path.relative(rootPath, itemPath).replace(/\\/g, '/'))
|
|
154
|
+
return entries
|
|
155
|
+
}
|
|
156
|
+
for (const child of fs.readdirSync(itemPath, { withFileTypes: true })) {
|
|
157
|
+
const fullPath = path.join(itemPath, child.name)
|
|
158
|
+
if (child.name === 'node_modules' || child.name === '.git') continue
|
|
159
|
+
if (child.isDirectory()) entries.push(...walk(fullPath, rootPath))
|
|
160
|
+
else if (child.isFile()) entries.push(path.relative(rootPath, fullPath).replace(/\\/g, '/'))
|
|
161
|
+
}
|
|
162
|
+
return entries
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sourceFiles() {
|
|
166
|
+
if (manifest?.files?.length) return manifest.files
|
|
167
|
+
return defaultInclude.flatMap((item) => walk(path.join(source, item)))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function copy(relativePath) {
|
|
171
|
+
const from = path.join(source, relativePath)
|
|
172
|
+
const to = path.join(target, relativePath)
|
|
173
|
+
if (!fs.existsSync(from)) return { relativePath, status: 'missing-source' }
|
|
174
|
+
if (!skipIntegrity && manifest?.fileHashes?.[relativePath]) {
|
|
175
|
+
const actualHash = sha256(from)
|
|
176
|
+
if (actualHash !== manifest.fileHashes[relativePath]) {
|
|
177
|
+
return { relativePath, status: 'integrity-failed', expected: manifest.fileHashes[relativePath], actual: actualHash }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!force && fs.existsSync(to)) return { relativePath, status: 'skipped' }
|
|
181
|
+
if (!dryRun) {
|
|
182
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
183
|
+
fs.copyFileSync(from, to)
|
|
184
|
+
}
|
|
185
|
+
return { relativePath, status: dryRun ? 'would-write' : 'written' }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const files = sourceFiles()
|
|
189
|
+
const signatureStatus = skipSignature ? { status: 'skipped' } : verifyPackageSignature(source, publicKeyArg ? path.resolve(publicKeyArg) : null)
|
|
190
|
+
const report = {
|
|
191
|
+
source,
|
|
192
|
+
sourceUrl: sourceUrlArg,
|
|
193
|
+
manifestUrl: manifestUrlArg,
|
|
194
|
+
sourceMode: sourceInfo.sourceMode,
|
|
195
|
+
target,
|
|
196
|
+
dryRun,
|
|
197
|
+
force,
|
|
198
|
+
integrity: manifest?.integrity ? { algorithm: manifest.integrity.algorithm, packageDigest: manifest.integrity.packageDigest, skipped: skipIntegrity } : null,
|
|
199
|
+
signature: signatureStatus,
|
|
200
|
+
manifest: manifest ? { label: manifest.label, fileCount: manifest.fileCount } : null,
|
|
201
|
+
status: 'passed',
|
|
202
|
+
results: [],
|
|
203
|
+
summary: { written: 0, skipped: 0, missingSource: 0, integrityFailed: 0, total: files.length },
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!target) {
|
|
207
|
+
report.status = 'failed'
|
|
208
|
+
report.error = 'Missing --target <install-skill-dir>.'
|
|
209
|
+
} else if (!fs.existsSync(source)) {
|
|
210
|
+
report.status = 'failed'
|
|
211
|
+
report.error = 'Source does not exist.'
|
|
212
|
+
} else if (signatureStatus.status === 'failed') {
|
|
213
|
+
report.status = 'failed'
|
|
214
|
+
report.error = 'Package signature verification failed.'
|
|
215
|
+
} else {
|
|
216
|
+
for (const file of files) report.results.push(copy(file))
|
|
217
|
+
report.summary.written = report.results.filter((item) => item.status === 'written' || item.status === 'would-write').length
|
|
218
|
+
report.summary.skipped = report.results.filter((item) => item.status === 'skipped').length
|
|
219
|
+
report.summary.missingSource = report.results.filter((item) => item.status === 'missing-source').length
|
|
220
|
+
report.summary.integrityFailed = report.results.filter((item) => item.status === 'integrity-failed').length
|
|
221
|
+
if (report.summary.missingSource > 0 || report.summary.integrityFailed > 0) report.status = 'failed'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (jsonOnly) console.log(JSON.stringify(report, null, 2))
|
|
225
|
+
else {
|
|
226
|
+
console.log('GSE install status: ' + report.status)
|
|
227
|
+
console.log('Source: ' + report.source)
|
|
228
|
+
if (report.sourceUrl) console.log('Source URL: ' + report.sourceUrl)
|
|
229
|
+
console.log('Target: ' + report.target)
|
|
230
|
+
if (report.error) console.log('Error: ' + report.error)
|
|
231
|
+
console.log('Written: ' + report.summary.written + ', skipped: ' + report.summary.skipped + ', missing source: ' + report.summary.missingSource + ', integrity failed: ' + report.summary.integrityFailed)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (report.status !== 'passed') process.exit(1)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function isPlaceholderEvidence(value) {
|
|
2
|
+
const text = String(value ?? '').trim()
|
|
3
|
+
const lower = text.toLowerCase()
|
|
4
|
+
if (!text) return false
|
|
5
|
+
if (lower.includes('__placeholder__') || lower.includes('__') || /<[^>]+>/.test(lower)) return true
|
|
6
|
+
if (['owner', 'fixture', 'fixture-owner', 'todo', 'tbd', 'pending', 'example'].includes(lower)) return true
|
|
7
|
+
if (lower.endsWith('@example.com') || lower.endsWith('@localhost')) return true
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(text)
|
|
10
|
+
const host = url.hostname.toLowerCase()
|
|
11
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') return true
|
|
12
|
+
if (host === 'example.com' || host.endsWith('.example') || host.endsWith('.example.com')) return true
|
|
13
|
+
if (host.endsWith('.local')) return true
|
|
14
|
+
const firstPathSegment = url.pathname.split('/').filter(Boolean)[0]?.toLowerCase()
|
|
15
|
+
if ((host === 'github.com' || host === 'gitlab.com') && firstPathSegment === 'example') return true
|
|
16
|
+
} catch {
|
|
17
|
+
// Non-URL fields are checked by literal placeholder and email rules above.
|
|
18
|
+
}
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function placeholderEvidenceError(label) {
|
|
23
|
+
return `${label} must be real public evidence, not a placeholder, fixture, local, or example value`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function rejectPlaceholderEvidence(errors, value, label) {
|
|
27
|
+
if (isPlaceholderEvidence(value)) errors.push(placeholderEvidenceError(label))
|
|
28
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import crypto from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
function readArg(name, fallback = null) {
|
|
9
|
+
const index = args.indexOf(name)
|
|
10
|
+
if (index === -1) return fallback
|
|
11
|
+
return args[index + 1] ?? fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
|
|
15
|
+
const label = readArg('--label', 'gse-local-' + new Date().toISOString().slice(0, 10))
|
|
16
|
+
const out = path.resolve(readArg('--out', path.join(process.env.TEMP || process.env.TMP || root, label)))
|
|
17
|
+
const force = args.includes('--force')
|
|
18
|
+
const dryRun = args.includes('--dry-run')
|
|
19
|
+
const jsonOnly = args.includes('--json')
|
|
20
|
+
|
|
21
|
+
const include = [
|
|
22
|
+
'package.json',
|
|
23
|
+
'SKILL.md',
|
|
24
|
+
'README.md',
|
|
25
|
+
'README.zh-CN.md',
|
|
26
|
+
'CHANGELOG.md',
|
|
27
|
+
'CONTRIBUTING.md',
|
|
28
|
+
'LICENSE',
|
|
29
|
+
'SECURITY.md',
|
|
30
|
+
'SUPPORT.md',
|
|
31
|
+
'.github',
|
|
32
|
+
'agents',
|
|
33
|
+
'assets',
|
|
34
|
+
'examples',
|
|
35
|
+
'references',
|
|
36
|
+
'scripts',
|
|
37
|
+
'.gse',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const excludeNames = new Set(['.git', 'node_modules', 'dist', '.DS_Store'])
|
|
41
|
+
const packageGseAllowlist = new Set([
|
|
42
|
+
'.gse/README.md',
|
|
43
|
+
'.gse/project-profile.md',
|
|
44
|
+
'.gse/quality-gates.md',
|
|
45
|
+
'.gse/gse-development-protocol.md',
|
|
46
|
+
'.gse/state.json',
|
|
47
|
+
'.gse/releases',
|
|
48
|
+
'.gse/releases/public-release-owner-required.md',
|
|
49
|
+
'.gse/releases/public-security-contact-owner-required.md',
|
|
50
|
+
])
|
|
51
|
+
const excludeRelativePrefixes = [
|
|
52
|
+
'.gse/evidence/',
|
|
53
|
+
'.gse/benchmark-audits/',
|
|
54
|
+
'.gse/acceptance/',
|
|
55
|
+
'.gse/archive/',
|
|
56
|
+
'.gse/changes/',
|
|
57
|
+
'.gse/release-bundles/',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
function exists(relativePath) {
|
|
61
|
+
return fs.existsSync(path.join(root, relativePath))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shouldExclude(relativePath) {
|
|
65
|
+
const normalized = relativePath.replace(/\\/g, '/')
|
|
66
|
+
if (normalized.startsWith('.gse/') && !packageGseAllowlist.has(normalized)) return true
|
|
67
|
+
if (excludeRelativePrefixes.some((prefix) => normalized.startsWith(prefix))) return true
|
|
68
|
+
return normalized.split('/').some((part) => excludeNames.has(part))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function walk(sourcePath, basePath = sourcePath) {
|
|
72
|
+
const entries = []
|
|
73
|
+
if (!fs.existsSync(sourcePath)) return entries
|
|
74
|
+
const stat = fs.statSync(sourcePath)
|
|
75
|
+
if (stat.isFile()) {
|
|
76
|
+
const relativePath = path.relative(root, sourcePath).replace(/\\/g, '/')
|
|
77
|
+
if (!shouldExclude(relativePath)) entries.push({ sourcePath, relativePath, bytes: stat.size })
|
|
78
|
+
return entries
|
|
79
|
+
}
|
|
80
|
+
for (const child of fs.readdirSync(sourcePath, { withFileTypes: true })) {
|
|
81
|
+
const fullPath = path.join(sourcePath, child.name)
|
|
82
|
+
const relativePath = path.relative(root, fullPath).replace(/\\/g, '/')
|
|
83
|
+
if (shouldExclude(relativePath)) continue
|
|
84
|
+
if (child.isDirectory()) entries.push(...walk(fullPath, basePath))
|
|
85
|
+
else if (child.isFile()) entries.push({ sourcePath: fullPath, relativePath, bytes: fs.statSync(fullPath).size })
|
|
86
|
+
}
|
|
87
|
+
return entries
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function copyFile(entry) {
|
|
91
|
+
const targetPath = path.join(out, entry.relativePath)
|
|
92
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
93
|
+
fs.copyFileSync(entry.sourcePath, targetPath)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sha256(filePath) {
|
|
97
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const missing = include.filter((item) => !exists(item))
|
|
101
|
+
const files = include.flatMap((item) => walk(path.join(root, item)))
|
|
102
|
+
const sortedFiles = files.map((item) => item.relativePath).sort()
|
|
103
|
+
const fileHashes = Object.fromEntries(
|
|
104
|
+
sortedFiles.map((relativePath) => [relativePath, sha256(path.join(root, relativePath))]),
|
|
105
|
+
)
|
|
106
|
+
const manifest = {
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
packageName: 'gse',
|
|
109
|
+
label,
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
fileCount: files.length,
|
|
112
|
+
totalBytes: files.reduce((sum, item) => sum + item.bytes, 0),
|
|
113
|
+
includes: include,
|
|
114
|
+
excludes: [...excludeNames, ...excludeRelativePrefixes, '.gse/* except portable package allowlist'],
|
|
115
|
+
packageGseAllowlist: [...packageGseAllowlist].sort(),
|
|
116
|
+
entrypoints: {
|
|
117
|
+
nodePackage: 'package.json',
|
|
118
|
+
skill: 'SKILL.md',
|
|
119
|
+
cli: 'scripts/gse.mjs',
|
|
120
|
+
validate: 'scripts/validate-gse.mjs',
|
|
121
|
+
npmPackageAudit: 'scripts/audit-npm-package-metadata.mjs',
|
|
122
|
+
validationProfile: 'scripts/run-validation-profile.mjs',
|
|
123
|
+
install: 'scripts/install-gse.mjs',
|
|
124
|
+
commands: 'references/commands.md',
|
|
125
|
+
},
|
|
126
|
+
integrity: {
|
|
127
|
+
algorithm: 'sha256',
|
|
128
|
+
},
|
|
129
|
+
files: sortedFiles,
|
|
130
|
+
fileHashes,
|
|
131
|
+
}
|
|
132
|
+
manifest.integrity.packageDigest = crypto
|
|
133
|
+
.createHash('sha256')
|
|
134
|
+
.update(JSON.stringify({ files: manifest.files, fileHashes: manifest.fileHashes }))
|
|
135
|
+
.digest('hex')
|
|
136
|
+
|
|
137
|
+
const report = {
|
|
138
|
+
root,
|
|
139
|
+
out,
|
|
140
|
+
label,
|
|
141
|
+
dryRun,
|
|
142
|
+
status: missing.length === 0 ? 'ready' : 'failed',
|
|
143
|
+
missing,
|
|
144
|
+
fileCount: files.length,
|
|
145
|
+
totalBytes: manifest.totalBytes,
|
|
146
|
+
manifestPath: path.join(out, 'gse-package-manifest.json'),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (missing.length === 0 && !dryRun) {
|
|
150
|
+
if (fs.existsSync(out)) {
|
|
151
|
+
if (!force) {
|
|
152
|
+
report.status = 'exists'
|
|
153
|
+
report.recommendation = 'Use --force or choose another --out directory.'
|
|
154
|
+
} else {
|
|
155
|
+
fs.rmSync(out, { recursive: true, force: true })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (report.status === 'ready') {
|
|
159
|
+
fs.mkdirSync(out, { recursive: true })
|
|
160
|
+
for (const file of files) copyFile(file)
|
|
161
|
+
fs.writeFileSync(path.join(out, 'gse-package-manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
162
|
+
report.status = 'written'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (jsonOnly) console.log(JSON.stringify(report, null, 2))
|
|
167
|
+
else {
|
|
168
|
+
console.log('GSE package status: ' + report.status)
|
|
169
|
+
console.log('Output: ' + report.out)
|
|
170
|
+
console.log('Files: ' + report.fileCount)
|
|
171
|
+
if (report.missing.length) console.log('Missing: ' + report.missing.join(', '))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (report.status === 'failed' || report.status === 'exists') process.exit(1)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { isPlaceholderEvidence } from './lib/evidence-placeholders.mjs'
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
function readArg(name, fallback = '') {
|
|
9
|
+
const index = args.indexOf(name)
|
|
10
|
+
if (index === -1) return fallback
|
|
11
|
+
return args[index + 1] ?? fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hasArg(name) {
|
|
15
|
+
return args.includes(name)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
|
|
19
|
+
const jsonOnly = hasArg('--json')
|
|
20
|
+
const allowLocalFixture = hasArg('--allow-local-fixture')
|
|
21
|
+
const timeoutMs = Number(readArg('--timeout-ms', '5000'))
|
|
22
|
+
|
|
23
|
+
const inputs = [
|
|
24
|
+
{ area: 'Public repository settings', kind: 'url', flag: '--public-repo-url', value: readArg('--public-repo-url') },
|
|
25
|
+
{ area: 'Public security contact', kind: 'url', flag: '--security-contact-url', value: readArg('--security-contact-url') },
|
|
26
|
+
{ area: 'Public CI run', kind: 'url', flag: '--public-ci-run-url', value: readArg('--public-ci-run-url') },
|
|
27
|
+
{ area: 'Public registry publication', kind: 'url', flag: '--registry-package-url', value: readArg('--registry-package-url') },
|
|
28
|
+
{ area: 'Marketplace approval', kind: 'url', flag: '--marketplace-url', value: readArg('--marketplace-url') },
|
|
29
|
+
{ area: 'Native slash command', kind: 'evidence', flag: '--native-host-evidence', value: readArg('--native-host-evidence') },
|
|
30
|
+
{ area: 'Other host runtime invocation', kind: 'evidence', flag: '--other-host-evidence', value: readArg('--other-host-evidence') },
|
|
31
|
+
].filter((item) => String(item.value ?? '').trim())
|
|
32
|
+
|
|
33
|
+
function isLocalUrl(value) {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(value)
|
|
36
|
+
const host = url.hostname.toLowerCase()
|
|
37
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local')
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isHttpUrl(value) {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(value)
|
|
46
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function probeUrl(value) {
|
|
53
|
+
const controller = new AbortController()
|
|
54
|
+
const timeout = setTimeout(() => controller.abort(), Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 5000)
|
|
55
|
+
try {
|
|
56
|
+
let response = await fetch(value, { method: 'HEAD', redirect: 'follow', signal: controller.signal })
|
|
57
|
+
if (response.status === 405 || response.status === 403) {
|
|
58
|
+
response = await fetch(value, { method: 'GET', redirect: 'follow', signal: controller.signal })
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
reachable: response.status >= 200 && response.status < 500,
|
|
62
|
+
statusCode: response.status,
|
|
63
|
+
finalUrl: response.url,
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
reachable: false,
|
|
68
|
+
error: error?.name === 'AbortError' ? 'timeout' : String(error?.message ?? error),
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
clearTimeout(timeout)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function probeEvidence(value) {
|
|
76
|
+
if (isHttpUrl(value)) return null
|
|
77
|
+
const fullPath = path.isAbsolute(value) ? value : path.resolve(root, value)
|
|
78
|
+
return {
|
|
79
|
+
reachable: fs.existsSync(fullPath),
|
|
80
|
+
path: fullPath,
|
|
81
|
+
type: fs.existsSync(fullPath) ? 'file' : 'missing',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const probes = []
|
|
86
|
+
for (const item of inputs) {
|
|
87
|
+
const placeholder = isPlaceholderEvidence(item.value)
|
|
88
|
+
const local = isLocalUrl(item.value)
|
|
89
|
+
const errors = []
|
|
90
|
+
if (placeholder && !(allowLocalFixture && local)) errors.push(`${item.flag} must be real public evidence, not a placeholder, fixture, local, or example value`)
|
|
91
|
+
if (item.kind === 'url' && !isHttpUrl(item.value)) errors.push(`${item.flag} must be an http or https URL`)
|
|
92
|
+
if (item.kind === 'url' && local && !allowLocalFixture) errors.push(`${item.flag} must not be local evidence`)
|
|
93
|
+
if (item.kind === 'evidence' && !isHttpUrl(item.value) && !fs.existsSync(path.isAbsolute(item.value) ? item.value : path.resolve(root, item.value))) {
|
|
94
|
+
errors.push(`${item.flag} must be an http/https URL or an existing evidence file`)
|
|
95
|
+
}
|
|
96
|
+
probes.push({ ...item, placeholder, local, errors })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const probe of probes) {
|
|
100
|
+
if (probe.errors.length) continue
|
|
101
|
+
if (probe.kind === 'url' || isHttpUrl(probe.value)) {
|
|
102
|
+
probe.network = await probeUrl(probe.value)
|
|
103
|
+
if (!probe.network.reachable) probe.errors.push(`${probe.flag} is not reachable`)
|
|
104
|
+
} else {
|
|
105
|
+
probe.file = probeEvidence(probe.value)
|
|
106
|
+
if (!probe.file.reachable) probe.errors.push(`${probe.flag} evidence file is not reachable`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const failed = probes.filter((item) => item.errors.length > 0).length
|
|
111
|
+
const ready = probes.length > 0 && failed === 0
|
|
112
|
+
const report = {
|
|
113
|
+
root,
|
|
114
|
+
generatedAt: new Date().toISOString(),
|
|
115
|
+
status: probes.length === 0 ? 'waiting-for-input' : ready ? 'ready' : 'failed',
|
|
116
|
+
summary: {
|
|
117
|
+
checked: probes.length,
|
|
118
|
+
ready: probes.length - failed,
|
|
119
|
+
failed,
|
|
120
|
+
allowLocalFixture,
|
|
121
|
+
},
|
|
122
|
+
probes: probes.map((item) => ({
|
|
123
|
+
area: item.area,
|
|
124
|
+
flag: item.flag,
|
|
125
|
+
kind: item.kind,
|
|
126
|
+
value: item.value,
|
|
127
|
+
placeholder: item.placeholder,
|
|
128
|
+
local: item.local,
|
|
129
|
+
status: item.errors.length === 0 ? 'ready' : 'failed',
|
|
130
|
+
network: item.network,
|
|
131
|
+
file: item.file,
|
|
132
|
+
errors: item.errors,
|
|
133
|
+
})),
|
|
134
|
+
limits: [
|
|
135
|
+
'This probe checks supplied owner/external evidence locations before record creation.',
|
|
136
|
+
'It does not publish GSE, configure repository settings, approve marketplace listings, or mark final readiness accepted.',
|
|
137
|
+
'Accepted gates still require the matching record script and final readiness promotion.',
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderMarkdown(data) {
|
|
142
|
+
const lines = []
|
|
143
|
+
lines.push('# GSE Public External Gate Probe')
|
|
144
|
+
lines.push('')
|
|
145
|
+
lines.push('Generated: ' + data.generatedAt)
|
|
146
|
+
lines.push('Status: ' + data.status)
|
|
147
|
+
lines.push('')
|
|
148
|
+
lines.push('## Probes')
|
|
149
|
+
lines.push('')
|
|
150
|
+
if (data.probes.length === 0) lines.push('- No evidence inputs supplied.')
|
|
151
|
+
for (const probe of data.probes) {
|
|
152
|
+
lines.push('- ' + probe.area + ': ' + probe.status + ' (' + probe.flag + ')')
|
|
153
|
+
if (probe.network) lines.push(' - HTTP status: ' + probe.network.statusCode)
|
|
154
|
+
if (probe.file) lines.push(' - File: ' + probe.file.path)
|
|
155
|
+
for (const error of probe.errors) lines.push(' - Error: ' + error)
|
|
156
|
+
}
|
|
157
|
+
lines.push('')
|
|
158
|
+
lines.push('## Limits')
|
|
159
|
+
lines.push('')
|
|
160
|
+
for (const item of data.limits) lines.push('- ' + item)
|
|
161
|
+
return lines.join('\n') + '\n'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (jsonOnly) console.log(JSON.stringify(report, null, 2))
|
|
165
|
+
else console.log(renderMarkdown(report))
|
|
166
|
+
|
|
167
|
+
if (report.status === 'failed') process.exit(1)
|