@t275005746/gse 0.1.0 → 0.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +42 -42
- package/.github/ISSUE_TEMPLATE/change_request.yml +50 -50
- package/.github/ISSUE_TEMPLATE/config.yml +5 -5
- package/.github/PULL_REQUEST_TEMPLATE.md +38 -38
- package/.github/workflows/validate-gse.yml +33 -33
- package/.gse/README.md +18 -18
- package/.gse/gse-development-protocol.md +50 -50
- package/.gse/project-profile.md +29 -29
- package/.gse/quality-gates.md +25 -25
- package/.gse/releases/public-registry-publication-npm.md +49 -0
- package/.gse/releases/public-release-owner-required.md +65 -65
- package/.gse/releases/public-security-contact-owner-required.md +45 -45
- package/.gse/state.json +3 -4
- package/CHANGELOG.md +24 -24
- package/CONTRIBUTING.md +64 -64
- package/LICENSE +21 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +41 -41
- package/SUPPORT.md +38 -38
- package/assets/marketplace/README.md +7 -7
- package/assets/marketplace/gse-listing.json +75 -75
- package/assets/templates/acceptance-execution-packet.md +73 -73
- package/assets/templates/adr.md +14 -14
- package/assets/templates/change-brief.md +16 -16
- package/assets/templates/design.md +18 -18
- package/assets/templates/dispatch-packet.md +104 -104
- package/assets/templates/evidence.md +14 -14
- package/assets/templates/execution-quality-pack.md +65 -65
- package/assets/templates/goal-map.md +21 -21
- package/assets/templates/host-adapter.md +48 -48
- package/assets/templates/host-ui-invocation-record.md +42 -42
- package/assets/templates/incident-review.md +60 -60
- package/assets/templates/public-channel-publication-record.md +49 -49
- package/assets/templates/public-ci-run-record.md +53 -53
- package/assets/templates/public-release-record.md +65 -65
- package/assets/templates/public-repository-settings-record.md +59 -59
- package/assets/templates/public-security-contact-record.md +45 -45
- package/assets/templates/release-trust-record.md +32 -32
- package/assets/templates/review.md +18 -18
- package/assets/templates/spec.md +16 -16
- package/assets/templates/target-adoption-evidence.md +29 -29
- package/assets/templates/tasks.md +17 -17
- package/assets/templates/update-release-acceptance-record.md +58 -58
- package/examples/README.md +22 -22
- package/examples/agent-runtime-host/.claude/gse-adapter.md +6 -6
- package/examples/agent-runtime-host/.codex/gse-adapter.md +7 -7
- package/examples/agent-runtime-host/.gse/goal-map.md +7 -7
- package/examples/agent-runtime-host/.gse/project-profile.md +27 -27
- package/examples/agent-runtime-host/.gse/tooling.md +5 -5
- package/examples/agent-runtime-host/.mcp.json +9 -9
- package/examples/agent-runtime-host/AGENTS.md +5 -5
- package/examples/agent-runtime-host/README.md +10 -10
- package/examples/agent-runtime-host/docs/model-routing.md +5 -5
- package/examples/cli-tool/.gse/project-profile.md +21 -21
- package/examples/cli-tool/AGENTS.md +5 -5
- package/examples/cli-tool/README.md +12 -12
- package/examples/cli-tool/package.json +21 -21
- package/examples/small-app/.env.example +2 -2
- package/examples/small-app/.github/workflows/ci.yml +8 -8
- package/examples/small-app/AGENTS.md +5 -5
- package/examples/small-app/README.md +12 -12
- package/examples/small-app/package.json +26 -26
- package/examples/small-app/playwright.config.ts +4 -4
- package/package.json +53 -53
- package/references/adoption-recipes.md +171 -171
- package/references/agent-roles.md +49 -49
- package/references/architecture-health.md +101 -101
- package/references/benchmark-audit.md +73 -73
- package/references/community-channels.md +46 -46
- package/references/compatibility.md +70 -70
- package/references/design-basis.md +38 -38
- package/references/domain-model.md +129 -129
- package/references/domain-quality-gates.md +75 -75
- package/references/drift-audit.md +81 -81
- package/references/evidence-taxonomy.md +123 -123
- package/references/file-ownership.md +107 -107
- package/references/final-readiness.md +84 -84
- package/references/forward-test.md +133 -133
- package/references/goal-map.md +36 -36
- package/references/host-adapters.md +119 -119
- package/references/learning-system.md +35 -35
- package/references/marketplace-discovery.md +46 -46
- package/references/model-routing.md +103 -103
- package/references/open-source-defaults.md +39 -39
- package/references/operating-model.md +52 -52
- package/references/packaging.md +277 -277
- package/references/project-agent-workspace.md +89 -89
- package/references/project-bootstrap.md +122 -122
- package/references/project-profile.md +57 -57
- package/references/public-release.md +174 -174
- package/references/quality-gates.md +100 -100
- package/references/recovery.md +176 -176
- package/references/release-trust.md +43 -43
- package/references/release.md +126 -126
- package/references/review.md +141 -141
- package/references/router.md +90 -90
- package/references/spec-workflow.md +66 -66
- package/references/task-levels.md +81 -81
- package/references/tool-adapters.md +73 -73
- package/scripts/audit-acceptance-execution-packet.mjs +133 -133
- package/scripts/audit-adoption-recipes.mjs +99 -99
- package/scripts/audit-change-lifecycle.mjs +77 -77
- package/scripts/audit-change-system.mjs +134 -134
- package/scripts/audit-ci-readiness.mjs +107 -107
- package/scripts/audit-close-gate.mjs +323 -323
- package/scripts/audit-command-adapters.mjs +91 -91
- package/scripts/audit-command-execution.mjs +210 -210
- package/scripts/audit-compatibility.mjs +149 -149
- package/scripts/audit-completion-readiness.mjs +292 -292
- package/scripts/audit-distribution.mjs +251 -251
- package/scripts/audit-domain-quality-gates.mjs +108 -108
- package/scripts/audit-evidence-placeholders.mjs +126 -126
- package/scripts/audit-final-readiness-promotion.mjs +255 -255
- package/scripts/audit-final-readiness.mjs +226 -226
- package/scripts/audit-fixtures.mjs +154 -154
- package/scripts/audit-fresh-session-readiness.mjs +177 -177
- package/scripts/audit-host-runtime-evidence-handoff.mjs +159 -159
- package/scripts/audit-host-runtime-invocation-drill.mjs +240 -240
- package/scripts/audit-host-runtime-invocations.mjs +254 -254
- package/scripts/audit-host-ui-invocation.mjs +132 -132
- package/scripts/audit-marketplace-discovery.mjs +122 -122
- package/scripts/audit-npm-package-metadata.mjs +155 -155
- package/scripts/audit-npm-publish-dry-run.mjs +191 -169
- package/scripts/audit-npm-tarball-install.mjs +191 -191
- package/scripts/audit-open-source-defaults.mjs +92 -92
- package/scripts/audit-open-source-readiness.mjs +97 -97
- package/scripts/audit-project.mjs +138 -138
- package/scripts/audit-public-acceptance-command-dry-run-drill.mjs +203 -203
- package/scripts/audit-public-acceptance-readiness.mjs +224 -224
- package/scripts/audit-public-channel-publication.mjs +248 -248
- package/scripts/audit-public-ci-run.mjs +184 -184
- package/scripts/audit-public-collaboration-templates.mjs +98 -98
- package/scripts/audit-public-external-gate-probe.mjs +206 -206
- package/scripts/audit-public-release-decision.mjs +201 -201
- package/scripts/audit-public-release-metadata.mjs +176 -176
- package/scripts/audit-public-repository-settings.mjs +237 -237
- package/scripts/audit-public-security-contact.mjs +171 -171
- package/scripts/audit-readme-docs.mjs +6 -4
- package/scripts/audit-recovery-readiness.mjs +98 -98
- package/scripts/audit-release-readiness.mjs +106 -106
- package/scripts/audit-release-trust.mjs +62 -62
- package/scripts/audit-remote-distribution.mjs +266 -266
- package/scripts/audit-roadmap-consistency.mjs +235 -235
- package/scripts/audit-signing.mjs +147 -147
- package/scripts/audit-state-freshness.mjs +14 -9
- package/scripts/audit-target-adoption-evidence.mjs +117 -117
- package/scripts/audit-target-project.mjs +507 -507
- package/scripts/audit-update-release-acceptance.mjs +136 -136
- package/scripts/audit-v1-target-validation.mjs +269 -269
- package/scripts/audit-validation-profiles.mjs +125 -125
- package/scripts/close-change.mjs +116 -116
- package/scripts/discover-project-profile.mjs +307 -307
- package/scripts/generate-command-adapter.mjs +231 -231
- package/scripts/generate-final-acceptance-packet.mjs +181 -181
- package/scripts/generate-final-form-progress-report.mjs +205 -205
- package/scripts/generate-host-runtime-evidence-handoff.mjs +206 -206
- package/scripts/generate-owner-external-gate-kit.mjs +295 -295
- package/scripts/generate-public-acceptance-handoff.mjs +168 -168
- package/scripts/generate-public-release-checklist.mjs +207 -207
- package/scripts/generate-release-bundle.mjs +505 -505
- package/scripts/generate-release-owner-action-plan.mjs +172 -172
- package/scripts/generate-release-status-manifest.mjs +200 -200
- package/scripts/generate-session-prompt.mjs +188 -188
- package/scripts/gse.mjs +67 -67
- package/scripts/init-change.mjs +265 -265
- package/scripts/init-project.mjs +785 -785
- package/scripts/install-gse.mjs +234 -234
- package/scripts/lib/evidence-placeholders.mjs +28 -28
- package/scripts/package-gse.mjs +174 -174
- package/scripts/probe-public-external-gates.mjs +167 -167
- package/scripts/record-host-invocation.mjs +151 -151
- package/scripts/record-public-channel-publication.mjs +178 -178
- package/scripts/record-public-ci-run.mjs +180 -180
- package/scripts/record-public-release.mjs +175 -175
- package/scripts/record-public-repository-settings.mjs +209 -209
- package/scripts/record-public-security-contact.mjs +157 -157
- package/scripts/sign-gse-package.mjs +83 -83
- package/scripts/update-project-state.mjs +223 -223
- package/scripts/verify-gse-package.mjs +85 -85
package/scripts/install-gse.mjs
CHANGED
|
@@ -1,234 +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)
|
|
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)
|
|
@@ -1,28 +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
|
-
}
|
|
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
|
+
}
|