@aperant/framework 0.9.0 → 0.10.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/CHANGELOG.md +68 -0
- package/agents/apt-verifier.md +6 -3
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +17 -1
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +5 -3
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +10 -8
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/install/mcp-provision.d.mts +26 -0
- package/dist/cli/install/mcp-provision.d.mts.map +1 -0
- package/dist/cli/install/mcp-provision.mjs +281 -0
- package/dist/cli/install/mcp-provision.mjs.map +1 -0
- package/dist/cli/install/mcp-server-specs.d.mts +80 -0
- package/dist/cli/install/mcp-server-specs.d.mts.map +1 -0
- package/dist/cli/install/mcp-server-specs.mjs +102 -0
- package/dist/cli/install/mcp-server-specs.mjs.map +1 -0
- package/dist/cli/install/runtime-detect.d.mts +1 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +5 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/toml-merge.d.mts +40 -0
- package/dist/cli/install/toml-merge.d.mts.map +1 -0
- package/dist/cli/install/toml-merge.mjs +134 -0
- package/dist/cli/install/toml-merge.mjs.map +1 -0
- package/dist/cli/util/events-path.d.mts +8 -0
- package/dist/cli/util/events-path.d.mts.map +1 -0
- package/dist/cli/util/events-path.mjs +23 -0
- package/dist/cli/util/events-path.mjs.map +1 -0
- package/dist/cli/util/runtime-capabilities.d.mts +30 -0
- package/dist/cli/util/runtime-capabilities.d.mts.map +1 -0
- package/dist/cli/util/runtime-capabilities.mjs +214 -0
- package/dist/cli/util/runtime-capabilities.mjs.map +1 -0
- package/dist/cli/verify-proof/audit.d.mts +2 -7
- package/dist/cli/verify-proof/audit.d.mts.map +1 -1
- package/dist/cli/verify-proof/audit.mjs +11 -17
- package/dist/cli/verify-proof/audit.mjs.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.d.mts +2 -29
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.mjs +8 -203
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-verifier.md +6 -3
- package/dist/plugin/skills/apt-pr-review/SKILL.md +54 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +1 -1
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +25 -1
- package/drivers/browser/skill/agent-browser/SKILL.md +51 -0
- package/drivers/tauri/README.md +107 -0
- package/drivers/tauri/driver.mjs +88 -0
- package/drivers/tauri/manifest.json +37 -0
- package/drivers/tauri/scripts/tauri-mcp.sh +41 -0
- package/package.json +1 -1
- package/skills/apt-pr-review/SKILL.md +54 -0
- package/skills/apt-verify/SKILL.md +1 -1
- package/skills/apt-verify-proof/SKILL.md +25 -1
- package/src/cli/commands/init.mjs +19 -0
- package/src/cli/consistency/parse-review.mjs +5 -3
- package/src/cli/consistency/rules/r5-verdict-consistency.mjs +14 -9
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/install/mcp-provision.mjs +293 -0
- package/src/cli/install/mcp-server-specs.mjs +101 -0
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/install/toml-merge.mjs +139 -0
- package/src/cli/util/events-path.mjs +24 -0
- package/src/cli/util/runtime-capabilities.mjs +216 -0
- package/src/cli/verify-proof/audit.mjs +11 -17
- package/src/cli/verify-proof/runtime-detect.mjs +11 -204
- package/templates/config.json +2 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install/mcp-server-specs.mjs — single-sourced install-time provision specs.
|
|
3
|
+
*
|
|
4
|
+
* One registry, two kinds (ID-03):
|
|
5
|
+
* - kind 'mcp-server' (electron, tauri): the {command, args, env?} the
|
|
6
|
+
* installer writes into each host CLI's MCP config. Keyed by the SAME
|
|
7
|
+
* `transport.mcp_server_id` string the driver manifests carry.
|
|
8
|
+
* - kind 'skill' (agent-browser): the vendored SKILL.md the installer
|
|
9
|
+
* copies into each host CLI's skill dir. Keyed by the SAME
|
|
10
|
+
* `transport.skill_id` the browser driver manifest carries.
|
|
11
|
+
*
|
|
12
|
+
* We do NOT derive commands from the driver manifests — manifests are
|
|
13
|
+
* transport-agnostic by design (ID-01 keeps the verb→tool mapping in the
|
|
14
|
+
* driver; this is a separate install-only transport/skill concern). We do
|
|
15
|
+
* NOT add a per-driver mcp-server.json (over-engineering for 3 specs —
|
|
16
|
+
* CLAUDE.md Simplicity First).
|
|
17
|
+
*
|
|
18
|
+
* Pure module — no fs, no exec.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} McpServerSpec
|
|
23
|
+
* @property {'mcp-server'} kind
|
|
24
|
+
* @property {string} command
|
|
25
|
+
* @property {string[]} args
|
|
26
|
+
* @property {Record<string, string>} [env]
|
|
27
|
+
* @property {boolean} [requiresLauncherScript] — tauri: copy scripts/tauri-mcp.sh first
|
|
28
|
+
*
|
|
29
|
+
* @typedef {Object} SkillSpec
|
|
30
|
+
* @property {'skill'} kind
|
|
31
|
+
* @property {string} skillId
|
|
32
|
+
* @property {string} vendoredSkillRelPath — relative to the framework root
|
|
33
|
+
*
|
|
34
|
+
* @typedef {McpServerSpec | SkillSpec} ProvisionSpec
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The install-time provision specs, keyed by spec id.
|
|
39
|
+
*
|
|
40
|
+
* `electron` / `tauri` ids match each driver manifest's
|
|
41
|
+
* `transport.mcp_server_id`; `agent-browser` matches the browser driver
|
|
42
|
+
* manifest's `transport.skill_id`.
|
|
43
|
+
*
|
|
44
|
+
* @type {Record<string, ProvisionSpec>}
|
|
45
|
+
*/
|
|
46
|
+
export const PROVISION_SPECS = {
|
|
47
|
+
// kind 'mcp-server'
|
|
48
|
+
electron: {
|
|
49
|
+
kind: 'mcp-server',
|
|
50
|
+
command: 'npx',
|
|
51
|
+
args: ['-y', 'electron-mcp-server'],
|
|
52
|
+
// No SCREENSHOT_ENCRYPTION_KEY — left for the user to add per ID-09.
|
|
53
|
+
env: {},
|
|
54
|
+
},
|
|
55
|
+
tauri: {
|
|
56
|
+
kind: 'mcp-server',
|
|
57
|
+
// The tauri-plugin-mcp-server npm bin ships without a shebang, so it
|
|
58
|
+
// cannot be invoked via `npx -y`. We launch it through a generated
|
|
59
|
+
// scripts/tauri-mcp.sh (ID-04) — the proven racing-ai shape.
|
|
60
|
+
command: 'bash',
|
|
61
|
+
args: ['./scripts/tauri-mcp.sh'],
|
|
62
|
+
requiresLauncherScript: true,
|
|
63
|
+
},
|
|
64
|
+
// kind 'skill'
|
|
65
|
+
'agent-browser': {
|
|
66
|
+
kind: 'skill',
|
|
67
|
+
skillId: 'agent-browser',
|
|
68
|
+
// Vendored UNDER the browser driver (NOT skills/agent-browser/) so the
|
|
69
|
+
// install pipeline's discoverCanonicalFiles does not auto-install it
|
|
70
|
+
// into every project unconditionally — that would break the
|
|
71
|
+
// verification.provision_mcp opt-out (AC7) and the web-frontend runtime
|
|
72
|
+
// gate. The provisioner (mcp-provision.mjs) is the sole installer.
|
|
73
|
+
vendoredSkillRelPath: 'drivers/browser/skill/agent-browser/SKILL.md',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Map a project's runtime capabilities to the provision spec ids it
|
|
79
|
+
* triggers. A multi-surface monorepo (e.g. electron + nextjs) returns
|
|
80
|
+
* multiple ids.
|
|
81
|
+
*
|
|
82
|
+
* is_electron → ['electron']
|
|
83
|
+
* is_tauri → ['tauri']
|
|
84
|
+
* is_web_frontend|is_nextjs|is_nuxt → ['agent-browser']
|
|
85
|
+
*
|
|
86
|
+
* Accepts a partial capability record — only the flags it reads need be
|
|
87
|
+
* present (the rest default to absent/false).
|
|
88
|
+
*
|
|
89
|
+
* @param {Partial<import('../util/runtime-capabilities.mjs').RuntimeCapabilities>} runtimeCaps
|
|
90
|
+
* @returns {string[]}
|
|
91
|
+
*/
|
|
92
|
+
export function capabilityToProvisionIds(runtimeCaps) {
|
|
93
|
+
const ids = []
|
|
94
|
+
if (!runtimeCaps || typeof runtimeCaps !== 'object') return ids
|
|
95
|
+
if (runtimeCaps.is_electron) ids.push('electron')
|
|
96
|
+
if (runtimeCaps.is_tauri) ids.push('tauri')
|
|
97
|
+
if (runtimeCaps.is_web_frontend || runtimeCaps.is_nextjs || runtimeCaps.is_nuxt) {
|
|
98
|
+
ids.push('agent-browser')
|
|
99
|
+
}
|
|
100
|
+
return ids
|
|
101
|
+
}
|
|
@@ -18,6 +18,15 @@ import { existsSync, statSync } from 'node:fs'
|
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { MANIFEST_FILENAME } from './manifest.mjs'
|
|
20
20
|
|
|
21
|
+
// Package.json-shape capability detection lives in util/ (shared with
|
|
22
|
+
// verify-proof/runtime-detect.mjs). Re-exported here so install-layer callers
|
|
23
|
+
// (e.g. install/mcp-provision.mjs) consume it from their OWN layer instead of
|
|
24
|
+
// reaching across into verify-proof/.
|
|
25
|
+
export {
|
|
26
|
+
detectFromPackageJson,
|
|
27
|
+
loadRuntimeCapabilities,
|
|
28
|
+
} from '../util/runtime-capabilities.mjs'
|
|
29
|
+
|
|
21
30
|
/**
|
|
22
31
|
* @typedef {Object} RuntimeDescriptor
|
|
23
32
|
* @property {string} id Stable identifier (`claude`, `cursor`, …).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install/toml-merge.mjs — dependency-free minimal TOML upsert for the
|
|
3
|
+
* Codex MCP-server config table.
|
|
4
|
+
*
|
|
5
|
+
* Codex's MCP config lives at project-root `.codex/config.toml` as:
|
|
6
|
+
*
|
|
7
|
+
* [mcp_servers.<id>]
|
|
8
|
+
* command = "bash"
|
|
9
|
+
* args = ["./scripts/tauri-mcp.sh"]
|
|
10
|
+
*
|
|
11
|
+
* [mcp_servers.<id>.env]
|
|
12
|
+
* FOO = "bar"
|
|
13
|
+
*
|
|
14
|
+
* No TOML serializer ships in the framework and `@iarna/toml` is not a
|
|
15
|
+
* dependency, so we hand-roll the MINIMAL upsert covering ONLY this shape
|
|
16
|
+
* (string command, string[] args, optional nested env table of
|
|
17
|
+
* string=string). Presence is detected by scanning for the
|
|
18
|
+
* `[mcp_servers.<id>]` header line; if present, we SKIP unchanged (TOFU
|
|
19
|
+
* per ID-05) — never diff/merge/overwrite a hand-authored table. All other
|
|
20
|
+
* content is preserved verbatim; the new table block is appended at EOF.
|
|
21
|
+
*
|
|
22
|
+
* Pure module — string in, string out.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Escape a string for a TOML basic (double-quoted) string. Only the
|
|
27
|
+
* characters that can occur in a command/arg/env value: backslash and
|
|
28
|
+
* double-quote.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} s
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function tomlString(s) {
|
|
34
|
+
return `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render a TOML array of double-quoted strings: `["a", "b"]`.
|
|
39
|
+
*
|
|
40
|
+
* @param {string[]} arr
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function tomlStringArray(arr) {
|
|
44
|
+
return `[${arr.map((a) => tomlString(a)).join(', ')}]`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render a TOML key. A bare key (TOML `[A-Za-z0-9_-]+`) is emitted verbatim;
|
|
49
|
+
* any other key is quoted as a TOML basic string so a non-bare env name
|
|
50
|
+
* (e.g. one containing `.`, a space, or `"`) cannot break the table or inject
|
|
51
|
+
* extra structure.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} key
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function tomlKey(key) {
|
|
57
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether `existingToml` already declares a `[mcp_servers.<id>]` table.
|
|
62
|
+
* Matches the header at the start of a line (ignoring leading whitespace),
|
|
63
|
+
* permitting an optional TOML trailing comment (` # …`) after the header —
|
|
64
|
+
* a hand-authored `[mcp_servers.tauri] # my server` must still count as
|
|
65
|
+
* present, otherwise the upsert would append a DUPLICATE table and produce
|
|
66
|
+
* an unparseable config (two same-named tables). The id is taken literally
|
|
67
|
+
* (dotted-key TOML ids do not occur for our server ids — electron/tauri are
|
|
68
|
+
* bare) and regex-escaped defensively. A `#`-prefixed comment LINE that
|
|
69
|
+
* merely mentions the header is NOT a match (the header must be the first
|
|
70
|
+
* non-whitespace token).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} existingToml
|
|
73
|
+
* @param {string} serverId
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function hasMcpServerTable(existingToml, serverId) {
|
|
77
|
+
if (typeof existingToml !== 'string' || existingToml.length === 0) return false
|
|
78
|
+
const escapedId = serverId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
79
|
+
// `[mcp_servers.<id>]` at line start (after optional indent), optionally
|
|
80
|
+
// followed by whitespace and/or a `#` trailing comment, to EOL.
|
|
81
|
+
const headerRe = new RegExp(`^\\s*\\[mcp_servers\\.${escapedId}\\]\\s*(#.*)?$`)
|
|
82
|
+
const lines = existingToml.split(/\r?\n/)
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (headerRe.test(line)) return true
|
|
85
|
+
}
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Upsert a `[mcp_servers.<id>]` table into a Codex config.toml string.
|
|
91
|
+
*
|
|
92
|
+
* TOFU (ID-05): if a `[mcp_servers.<id>]` table already exists, returns the
|
|
93
|
+
* input unchanged with `action: 'skipped'`. Otherwise appends the new table
|
|
94
|
+
* (and a nested `[mcp_servers.<id>.env]` table when `env` is non-empty) at
|
|
95
|
+
* EOF, preserving all existing content verbatim, and returns the new
|
|
96
|
+
* content with `action: 'written'`.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} existingToml — current file contents ('' when the file is absent)
|
|
99
|
+
* @param {string} serverId
|
|
100
|
+
* @param {{command: string, args: string[], env?: Record<string, string>}} spec
|
|
101
|
+
* @returns {{content: string, action: 'written'|'skipped'}}
|
|
102
|
+
*/
|
|
103
|
+
export function upsertMcpServerToml(existingToml, serverId, spec) {
|
|
104
|
+
const existing = typeof existingToml === 'string' ? existingToml : ''
|
|
105
|
+
if (hasMcpServerTable(existing, serverId)) {
|
|
106
|
+
return { content: existing, action: 'skipped' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = []
|
|
110
|
+
lines.push(`[mcp_servers.${serverId}]`)
|
|
111
|
+
lines.push(`command = ${tomlString(spec.command)}`)
|
|
112
|
+
lines.push(`args = ${tomlStringArray(spec.args ?? [])}`)
|
|
113
|
+
|
|
114
|
+
const env = spec.env && typeof spec.env === 'object' ? spec.env : null
|
|
115
|
+
const envKeys = env ? Object.keys(env) : []
|
|
116
|
+
if (envKeys.length > 0) {
|
|
117
|
+
lines.push('')
|
|
118
|
+
lines.push(`[mcp_servers.${serverId}.env]`)
|
|
119
|
+
for (const key of envKeys) {
|
|
120
|
+
lines.push(`${tomlKey(key)} = ${tomlString(env[key])}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const block = lines.join('\n')
|
|
125
|
+
|
|
126
|
+
// Preserve existing content; separate the new block with exactly one blank
|
|
127
|
+
// line. An empty existing file yields just the block + trailing newline.
|
|
128
|
+
// Strip the existing file's trailing newline run first so a file that
|
|
129
|
+
// already ends in a blank line (`…\n\n`) does not produce a DOUBLE blank
|
|
130
|
+
// line before the appended block.
|
|
131
|
+
let content
|
|
132
|
+
if (existing.trim().length === 0) {
|
|
133
|
+
content = `${block}\n`
|
|
134
|
+
} else {
|
|
135
|
+
const head = existing.replace(/\n+$/, '')
|
|
136
|
+
content = `${head}\n\n${block}\n`
|
|
137
|
+
}
|
|
138
|
+
return { content, action: 'written' }
|
|
139
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/events-path.mjs — canonical path computation for the per-day event
|
|
3
|
+
* log at `.aperant/events/{YYYY-MM-DD}.jsonl` (ID-05).
|
|
4
|
+
*
|
|
5
|
+
* Lives in util/ so both the install layer (install/mcp-provision.mjs) and the
|
|
6
|
+
* verify-proof layer (verify-proof/audit.mjs) compute the same path WITHOUT
|
|
7
|
+
* install/ importing from verify-proof/ (the layer-boundary rule). Pure
|
|
8
|
+
* function — no fs, no exec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join, resolve } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compute today's events filename relative to projectDir.
|
|
15
|
+
* @param {string} projectDir
|
|
16
|
+
* @param {Date} [now]
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function dailyEventsPath(projectDir, now = new Date()) {
|
|
20
|
+
const yyyy = now.getUTCFullYear()
|
|
21
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
|
|
22
|
+
const dd = String(now.getUTCDate()).padStart(2, '0')
|
|
23
|
+
return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
|
|
24
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/runtime-capabilities.mjs — pure project-shape capability detection.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from packages/core/src/prompts/prompt-loader.ts:572-595 so the
|
|
5
|
+
* framework CLI is self-contained (no @aperant/core dep). Same heuristics:
|
|
6
|
+
* read package.json's dependencies + devDependencies, return a capability
|
|
7
|
+
* record the resolver can score against.
|
|
8
|
+
*
|
|
9
|
+
* Lives in util/ so BOTH the verify-proof layer (verify-proof/runtime-detect.mjs
|
|
10
|
+
* re-exports it) AND the install layer (install/runtime-detect.mjs re-exports
|
|
11
|
+
* it) can share one implementation without install/ importing from
|
|
12
|
+
* verify-proof/ (the layer-boundary rule).
|
|
13
|
+
*
|
|
14
|
+
* Pattern: pure data in → pure data out. No filesystem walking, no exec.
|
|
15
|
+
* Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
|
|
16
|
+
*
|
|
17
|
+
* v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
|
|
18
|
+
* (configurable via the second arg `{monorepo_globs}`) and OR-merges
|
|
19
|
+
* each match's `package.json` capabilities with the root. The default
|
|
20
|
+
* `['apps/*']` matches the canonical Aperant monorepo layout; adopters
|
|
21
|
+
* whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
|
|
22
|
+
* custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
|
|
23
|
+
* walk entirely (root-only escape hatch).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
27
|
+
import { join, resolve } from 'node:path'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} RuntimeCapabilities
|
|
31
|
+
* @property {boolean} is_electron
|
|
32
|
+
* @property {boolean} is_tauri
|
|
33
|
+
* @property {boolean} is_nextjs
|
|
34
|
+
* @property {boolean} is_nuxt
|
|
35
|
+
* @property {boolean} is_web_frontend
|
|
36
|
+
* @property {boolean} is_expo
|
|
37
|
+
* @property {boolean} is_react_native
|
|
38
|
+
* @property {boolean} has_node
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** A capability record with every flag defaulted to false. */
|
|
42
|
+
function emptyCapabilities() {
|
|
43
|
+
return {
|
|
44
|
+
is_electron: false,
|
|
45
|
+
is_tauri: false,
|
|
46
|
+
is_nextjs: false,
|
|
47
|
+
is_nuxt: false,
|
|
48
|
+
is_web_frontend: false,
|
|
49
|
+
is_expo: false,
|
|
50
|
+
is_react_native: false,
|
|
51
|
+
has_node: false,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect runtime capabilities from a parsed package.json object.
|
|
57
|
+
*
|
|
58
|
+
* @param {Record<string, unknown> | null} packageJson
|
|
59
|
+
* @returns {RuntimeCapabilities}
|
|
60
|
+
*/
|
|
61
|
+
export function detectFromPackageJson(packageJson) {
|
|
62
|
+
const capabilities = emptyCapabilities()
|
|
63
|
+
if (packageJson === null || typeof packageJson !== 'object') return capabilities
|
|
64
|
+
|
|
65
|
+
const allDeps = {
|
|
66
|
+
...(packageJson.dependencies ?? {}) /** @type {Record<string,string>} */,
|
|
67
|
+
...(packageJson.devDependencies ?? {}) /** @type {Record<string,string>} */,
|
|
68
|
+
}
|
|
69
|
+
const deps = new Set(Object.keys(allDeps).map((k) => k.toLowerCase()))
|
|
70
|
+
|
|
71
|
+
if (deps.has('electron') || [...deps].some((d) => d.startsWith('@electron'))) {
|
|
72
|
+
capabilities.is_electron = true
|
|
73
|
+
}
|
|
74
|
+
if (deps.has('@tauri-apps/api') || deps.has('tauri')) {
|
|
75
|
+
capabilities.is_tauri = true
|
|
76
|
+
}
|
|
77
|
+
if (deps.has('expo')) capabilities.is_expo = true
|
|
78
|
+
if (deps.has('react-native')) capabilities.is_react_native = true
|
|
79
|
+
|
|
80
|
+
// Web frontends — react/vue/svelte/angular/solid → frontend. BUT: an
|
|
81
|
+
// Electron app commonly bundles React as its renderer; in that case the
|
|
82
|
+
// runtime is "electron", not "web_frontend". Electron + react-native +
|
|
83
|
+
// expo + tauri all win over the web flag.
|
|
84
|
+
const isNativeContainer =
|
|
85
|
+
capabilities.is_electron ||
|
|
86
|
+
capabilities.is_tauri ||
|
|
87
|
+
capabilities.is_react_native ||
|
|
88
|
+
capabilities.is_expo
|
|
89
|
+
const webFrameworks = ['react', 'vue', 'svelte', 'angular', 'solid']
|
|
90
|
+
if (!isNativeContainer && webFrameworks.some((f) => deps.has(f))) {
|
|
91
|
+
capabilities.is_web_frontend = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (deps.has('next')) {
|
|
95
|
+
capabilities.is_nextjs = true
|
|
96
|
+
capabilities.is_web_frontend = true
|
|
97
|
+
}
|
|
98
|
+
if (deps.has('nuxt')) {
|
|
99
|
+
capabilities.is_nuxt = true
|
|
100
|
+
capabilities.is_web_frontend = true
|
|
101
|
+
}
|
|
102
|
+
if (deps.has('vite') && !isNativeContainer) {
|
|
103
|
+
capabilities.is_web_frontend = true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Node-ish? Anything with a package.json + an entry point counts.
|
|
107
|
+
if (typeof packageJson.main === 'string' || typeof packageJson.module === 'string') {
|
|
108
|
+
capabilities.has_node = true
|
|
109
|
+
}
|
|
110
|
+
if (!capabilities.has_node && packageJson.scripts && typeof packageJson.scripts === 'object') {
|
|
111
|
+
capabilities.has_node = true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return capabilities
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load and detect capabilities from a project directory's package.json.
|
|
119
|
+
* v0.8.6 — also walks `monorepo_globs` (default `['apps/*']`) and
|
|
120
|
+
* OR-merges each match's capabilities with the root.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} projectDir
|
|
123
|
+
* @param {{monorepo_globs?: string[]}} [opts]
|
|
124
|
+
* @returns {RuntimeCapabilities}
|
|
125
|
+
*/
|
|
126
|
+
export function loadRuntimeCapabilities(projectDir, opts) {
|
|
127
|
+
const root = resolve(projectDir)
|
|
128
|
+
const rootPkgPath = join(root, 'package.json')
|
|
129
|
+
let rootCaps = emptyCapabilities()
|
|
130
|
+
if (existsSync(rootPkgPath)) {
|
|
131
|
+
try {
|
|
132
|
+
rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')))
|
|
133
|
+
} catch {
|
|
134
|
+
/* fall through with empty caps */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Default to the canonical Aperant monorepo layout. Passing `[]`
|
|
139
|
+
// disables the walk entirely (root-only escape hatch per spec ID-05).
|
|
140
|
+
const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*']
|
|
141
|
+
if (globs.length === 0) return rootCaps
|
|
142
|
+
|
|
143
|
+
let merged = rootCaps
|
|
144
|
+
for (const glob of globs) {
|
|
145
|
+
const matches = expandSimpleGlob(root, glob)
|
|
146
|
+
for (const dir of matches) {
|
|
147
|
+
const childPkgPath = join(dir, 'package.json')
|
|
148
|
+
if (!existsSync(childPkgPath)) continue
|
|
149
|
+
try {
|
|
150
|
+
const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')))
|
|
151
|
+
merged = orMergeCapabilities(merged, childCaps)
|
|
152
|
+
} catch {
|
|
153
|
+
/* skip malformed package.json */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return merged
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Expand a simple `prefix/*` glob against the root. Supports ONLY the
|
|
162
|
+
* `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
|
|
163
|
+
* glob syntax is intentionally rejected — v1 keeps the dep surface
|
|
164
|
+
* small. Fails closed on malformed patterns (returns []).
|
|
165
|
+
*
|
|
166
|
+
* @param {string} root
|
|
167
|
+
* @param {string} glob
|
|
168
|
+
* @returns {string[]}
|
|
169
|
+
*/
|
|
170
|
+
function expandSimpleGlob(root, glob) {
|
|
171
|
+
if (typeof glob !== 'string' || !glob.endsWith('/*')) return []
|
|
172
|
+
const prefix = glob.slice(0, -2)
|
|
173
|
+
if (prefix.length === 0 || prefix.includes('*')) return []
|
|
174
|
+
const dir = join(root, prefix)
|
|
175
|
+
if (!existsSync(dir)) return []
|
|
176
|
+
let entries
|
|
177
|
+
try {
|
|
178
|
+
entries = readdirSync(dir)
|
|
179
|
+
} catch {
|
|
180
|
+
return []
|
|
181
|
+
}
|
|
182
|
+
const matches = []
|
|
183
|
+
for (const name of entries) {
|
|
184
|
+
const sub = join(dir, name)
|
|
185
|
+
try {
|
|
186
|
+
if (statSync(sub).isDirectory()) matches.push(sub)
|
|
187
|
+
} catch {
|
|
188
|
+
/* skip unreadable entries */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return matches
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Boolean OR-merge of two capability records. Any flag true in either
|
|
196
|
+
* input is true in the output. The "Electron wins over web" rule from
|
|
197
|
+
* `detectFromPackageJson` is preserved when this is called repeatedly —
|
|
198
|
+
* each child's flags are honest about that child's own shape, and the
|
|
199
|
+
* OR-merge just unions them.
|
|
200
|
+
*
|
|
201
|
+
* @param {RuntimeCapabilities} a
|
|
202
|
+
* @param {RuntimeCapabilities} b
|
|
203
|
+
* @returns {RuntimeCapabilities}
|
|
204
|
+
*/
|
|
205
|
+
function orMergeCapabilities(a, b) {
|
|
206
|
+
return {
|
|
207
|
+
is_electron: a.is_electron || b.is_electron,
|
|
208
|
+
is_tauri: a.is_tauri || b.is_tauri,
|
|
209
|
+
is_nextjs: a.is_nextjs || b.is_nextjs,
|
|
210
|
+
is_nuxt: a.is_nuxt || b.is_nuxt,
|
|
211
|
+
is_web_frontend: a.is_web_frontend || b.is_web_frontend,
|
|
212
|
+
is_expo: a.is_expo || b.is_expo,
|
|
213
|
+
is_react_native: a.is_react_native || b.is_react_native,
|
|
214
|
+
has_node: a.has_node || b.has_node,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -12,21 +12,14 @@
|
|
|
12
12
|
* lines but no line is ever overwritten.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { appendJsonl } from '../util/atomic-write.mjs'
|
|
16
|
+
import { dailyEventsPath } from '../util/events-path.mjs'
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
export function dailyEventsPath(projectDir, now = new Date()) {
|
|
25
|
-
const yyyy = now.getUTCFullYear()
|
|
26
|
-
const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
|
|
27
|
-
const dd = String(now.getUTCDate()).padStart(2, '0')
|
|
28
|
-
return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
|
|
29
|
-
}
|
|
18
|
+
// dailyEventsPath now lives in util/events-path.mjs (so install/ can share it
|
|
19
|
+
// without crossing into verify-proof/). Re-exported here byte-for-byte so
|
|
20
|
+
// every existing `import { dailyEventsPath } from '../verify-proof/audit.mjs'`
|
|
21
|
+
// call site keeps working.
|
|
22
|
+
export { dailyEventsPath }
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
25
|
* Append a driver-invocation audit line.
|
|
@@ -45,11 +38,12 @@ export function dailyEventsPath(projectDir, now = new Date()) {
|
|
|
45
38
|
*/
|
|
46
39
|
export function appendDriverEvent(projectDir, entry) {
|
|
47
40
|
const path = dailyEventsPath(projectDir)
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
// Single canonical jsonl-append primitive for the event log (appendJsonl
|
|
42
|
+
// creates parent dirs, fsyncs, and locks on Windows). Keeps every writer to
|
|
43
|
+
// .aperant/events/{date}.jsonl on the same atomic path.
|
|
44
|
+
appendJsonl(path, {
|
|
50
45
|
ts: new Date().toISOString(),
|
|
51
46
|
kind: 'driver_invocation',
|
|
52
47
|
...entry,
|
|
53
48
|
})
|
|
54
|
-
appendFileSync(path, line + '\n', 'utf-8')
|
|
55
49
|
}
|