@mnstry/atelier 0.2.0-alpha.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/CHANGELOG.md +383 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +262 -0
- package/SECURITY.md +93 -0
- package/TRADEMARKS.md +88 -0
- package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
- package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
- package/bin/atelier.mjs +2 -0
- package/bin/mnstry-atelier.mjs +3 -0
- package/contracts/analysis-adapter.v1.schema.json +248 -0
- package/contracts/atelier-action-intent.v1.schema.json +143 -0
- package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
- package/contracts/atelier-attestation.v1.schema.json +239 -0
- package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
- package/contracts/atelier-claim.v1.schema.json +97 -0
- package/contracts/atelier-export.v1.schema.json +1015 -0
- package/contracts/atelier-extension-pack.v1.schema.json +290 -0
- package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
- package/contracts/atelier-lock.v1.schema.json +364 -0
- package/contracts/atelier-migration.v1.schema.json +162 -0
- package/contracts/atelier-project-config.v1.schema.json +352 -0
- package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
- package/contracts/atelier-readiness-run.v1.schema.json +229 -0
- package/contracts/atelier-readiness.v1.schema.json +457 -0
- package/contracts/compat-baseline.json +10 -0
- package/contracts/git-promote-event.v1.schema.json +145 -0
- package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
- package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
- package/docs/announcements.md +101 -0
- package/docs/atelier-runtime.md +34 -0
- package/docs/attestation.md +201 -0
- package/docs/continuity.md +55 -0
- package/docs/contract-stability.md +111 -0
- package/docs/distributions.md +220 -0
- package/docs/install.md +140 -0
- package/docs/knowledge-graph.md +140 -0
- package/docs/ontology.md +206 -0
- package/docs/release-engineering.md +151 -0
- package/docs/repo-boundary-guard.md +199 -0
- package/docs/tenant-readiness.md +64 -0
- package/docs/upgrade.md +124 -0
- package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
- package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
- package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
- package/fixtures/analysis/valid/output.claims.v1.json +17 -0
- package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
- package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
- package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
- package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
- package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
- package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
- package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
- package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
- package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
- package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
- package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
- package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
- package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
- package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
- package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
- package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
- package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
- package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
- package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
- package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
- package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
- package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
- package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
- package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
- package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
- package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
- package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
- package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
- package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
- package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
- package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
- package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
- package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
- package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
- package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
- package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
- package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
- package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
- package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
- package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
- package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
- package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
- package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
- package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
- package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
- package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
- package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
- package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
- package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
- package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
- package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
- package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
- package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
- package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
- package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
- package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
- package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
- package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
- package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
- package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
- package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
- package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
- package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
- package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
- package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
- package/fixtures/git-promote/private-to-team.v1.json +29 -0
- package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
- package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
- package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
- package/fixtures/projects/neutral-project/atelier.project.json +15 -0
- package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
- package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
- package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
- package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
- package/fixtures/projects/sample-workspace/content/README.md +16 -0
- package/fixtures/projects/sample-workspace/content/source.html +5 -0
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
- package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
- package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
- package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
- package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
- package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
- package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
- package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
- package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
- package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
- package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
- package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
- package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
- package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
- package/fixtures/support/invalid/leaky-support.v1.json +13 -0
- package/fixtures/support/valid/minimal-safe.v1.json +20 -0
- package/package.json +98 -0
- package/skills/claude/atelier-open/SKILL.md +68 -0
- package/skills/claude/mnstry-readiness/SKILL.md +41 -0
- package/skills/codex/atelier-open/SKILL.md +68 -0
- package/skills/codex/mnstry-readiness/SKILL.md +41 -0
- package/src/analysis/adapter.mjs +20 -0
- package/src/analysis/analysis-claim-contract.mjs +335 -0
- package/src/attestation/jcs.mjs +64 -0
- package/src/attestation/sign.mjs +305 -0
- package/src/boundary/content-rules.mjs +262 -0
- package/src/boundary/policy.mjs +806 -0
- package/src/check-atelier-export-contract.mjs +116 -0
- package/src/cli/run.mjs +283 -0
- package/src/collaboration/proposals.mjs +262 -0
- package/src/commands/analysis.mjs +8 -0
- package/src/commands/announcements.mjs +308 -0
- package/src/commands/attestation.mjs +198 -0
- package/src/commands/boundary.mjs +25 -0
- package/src/commands/config.mjs +4 -0
- package/src/commands/context.mjs +3 -0
- package/src/commands/distribution.mjs +224 -0
- package/src/commands/egress.mjs +11 -0
- package/src/commands/extension-pack.mjs +209 -0
- package/src/commands/feedback.mjs +3 -0
- package/src/commands/graph.mjs +3 -0
- package/src/commands/init.mjs +130 -0
- package/src/commands/lock.mjs +3 -0
- package/src/commands/project.mjs +3 -0
- package/src/commands/promote.mjs +4 -0
- package/src/commands/readiness.mjs +3 -0
- package/src/commands/server.mjs +6 -0
- package/src/commands/setup.mjs +250 -0
- package/src/commands/support.mjs +3 -0
- package/src/commands/upgrade.mjs +3 -0
- package/src/contracts/corpus.mjs +171 -0
- package/src/egress/check.mjs +45 -0
- package/src/egress/forbidden-egress.mjs +323 -0
- package/src/export/atelier-export-contract.mjs +722 -0
- package/src/extension-packs/loader.mjs +491 -0
- package/src/graph/graph.mjs +370 -0
- package/src/graph/knowledge-graph.mjs +887 -0
- package/src/harness/context-client.mjs +144 -0
- package/src/harness/context.mjs +27 -0
- package/src/index.mjs +88 -0
- package/src/project/config.mjs +557 -0
- package/src/project/file-class.mjs +113 -0
- package/src/project/git-ignore.mjs +31 -0
- package/src/project/path-match.mjs +30 -0
- package/src/project/repo-identity.mjs +176 -0
- package/src/projection/alignment-projection.mjs +169 -0
- package/src/projection/policy.mjs +90 -0
- package/src/projection/project.mjs +152 -0
- package/src/readiness/readiness.mjs +312 -0
- package/src/readiness-protocols/bundled-pack.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +419 -0
- package/src/server/local-sidecar.mjs +640 -0
- package/src/server/security.mjs +229 -0
- package/src/server/server.mjs +42 -0
- package/src/support/feedback-report.mjs +414 -0
- package/src/support/support-bundle.mjs +226 -0
- package/src/ui/html-primitives.mjs +104 -0
- package/src/upgrade/upgrade.mjs +701 -0
- package/src/validate-atelier-export-dry-run.mjs +75 -0
- package/templates/atelier.project.example.json +23 -0
- package/templates/distribution-workspace/.claude/launch.json +12 -0
- package/templates/distribution-workspace/README.md +86 -0
- package/templates/distribution-workspace/atelier.project.json +36 -0
- package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
- package/templates/distribution-workspace/gitignore +29 -0
- package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/distribution-workspace/project/README.md +19 -0
- package/templates/distribution-workspace/repo-access.v1.json +9 -0
- package/templates/launch.json +12 -0
- package/templates/private-domain-workspace/.claude/launch.json +12 -0
- package/templates/private-domain-workspace/README.md +50 -0
- package/templates/private-domain-workspace/atelier.project.json +28 -0
- package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
- package/templates/private-domain-workspace/domain/README.md +19 -0
- package/templates/private-domain-workspace/gitignore +29 -0
- package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
- package/templates/private-domain-workspace/repo-access.v1.json +9 -0
- package/templates/shared-project-workspace/.claude/launch.json +12 -0
- package/templates/shared-project-workspace/README.md +51 -0
- package/templates/shared-project-workspace/atelier.project.json +28 -0
- package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
- package/templates/shared-project-workspace/gitignore +29 -0
- package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/shared-project-workspace/project/README.md +19 -0
- package/templates/shared-project-workspace/repo-access.v1.json +9 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// atelier distribution check — verifies that a distribution package carries
|
|
3
|
+
// the required MNSTRY attribution markers. The normative attribution policy
|
|
4
|
+
// lives in TRADEMARKS.md under "Required attribution"; this command checks
|
|
5
|
+
// only the mechanical markers and points there rather than restating policy.
|
|
6
|
+
//
|
|
7
|
+
// Exit codes: 0 = checks passed, 1 = blocking attribution check failed,
|
|
8
|
+
// 2 = usage error (unknown subcommand or unusable target).
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { parseArgs } from '../project/config.mjs'
|
|
13
|
+
|
|
14
|
+
const ATTRIBUTION_BYTES = 'powered by MNSTRY Atelier'
|
|
15
|
+
const ATTRIBUTION_EXT_KEY = 'mnstry.atelier/attribution'
|
|
16
|
+
const PACK_MANIFEST_NAME = 'atelier.pack.json'
|
|
17
|
+
const PREFIX = '[atelier-distribution:check]'
|
|
18
|
+
const POLICY_POINTER = 'see TRADEMARKS.md ("Required attribution") and docs/attestation.md'
|
|
19
|
+
|
|
20
|
+
const log = (message) => console.log(`${PREFIX} ${message}`)
|
|
21
|
+
const logError = (message) => console.error(`${PREFIX} ${message}`)
|
|
22
|
+
|
|
23
|
+
// Paths in output stay relative to the checked distribution so logs never
|
|
24
|
+
// leak machine-local absolute paths.
|
|
25
|
+
const displayPath = (target, file) => path.relative(target, file) || path.basename(file)
|
|
26
|
+
|
|
27
|
+
function checkReadmeAttribution(target) {
|
|
28
|
+
const readmePath = path.join(target, 'README.md')
|
|
29
|
+
let bytes
|
|
30
|
+
try {
|
|
31
|
+
bytes = fs.readFileSync(readmePath)
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false, reason: 'README.md is missing from the distribution root' }
|
|
34
|
+
}
|
|
35
|
+
if (!bytes.includes(ATTRIBUTION_BYTES)) {
|
|
36
|
+
return { ok: false, reason: `README.md does not contain the exact byte string "${ATTRIBUTION_BYTES}"` }
|
|
37
|
+
}
|
|
38
|
+
return { ok: true }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function manifestIn(dir) {
|
|
42
|
+
const file = path.join(dir, PACK_MANIFEST_NAME)
|
|
43
|
+
return fs.existsSync(file) ? file : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findPackManifests(target, packOverride) {
|
|
47
|
+
if (typeof packOverride === 'string' && packOverride.trim()) {
|
|
48
|
+
const resolved = path.resolve(packOverride)
|
|
49
|
+
let stat = null
|
|
50
|
+
try {
|
|
51
|
+
stat = fs.statSync(resolved)
|
|
52
|
+
} catch {
|
|
53
|
+
return { manifests: [], note: `--pack path does not exist: ${packOverride}` }
|
|
54
|
+
}
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
const manifest = manifestIn(resolved)
|
|
57
|
+
if (manifest) return { manifests: [manifest], note: null }
|
|
58
|
+
return { manifests: [], note: `--pack directory has no ${PACK_MANIFEST_NAME}: ${packOverride}` }
|
|
59
|
+
}
|
|
60
|
+
return { manifests: [resolved], note: null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const packsRoot = path.join(target, 'packs')
|
|
64
|
+
let entries = []
|
|
65
|
+
try {
|
|
66
|
+
entries = fs.readdirSync(packsRoot, { withFileTypes: true })
|
|
67
|
+
} catch {
|
|
68
|
+
return { manifests: [], note: `no packs/ directory found; nothing to check for the pack-manifest attribution key` }
|
|
69
|
+
}
|
|
70
|
+
const manifests = entries
|
|
71
|
+
.filter((entry) => entry.isDirectory())
|
|
72
|
+
.map((entry) => manifestIn(path.join(packsRoot, entry.name)))
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
if (!manifests.length) {
|
|
75
|
+
return { manifests: [], note: `no ${PACK_MANIFEST_NAME} found under packs/` }
|
|
76
|
+
}
|
|
77
|
+
return { manifests, note: null }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Advisory only: reported, never blocking, regardless of what it finds.
|
|
81
|
+
function reportPackAttribution(target, packOverride) {
|
|
82
|
+
const { manifests, note } = findPackManifests(target, packOverride)
|
|
83
|
+
if (note) log(`advisory: ${note}`)
|
|
84
|
+
for (const manifest of manifests) {
|
|
85
|
+
const shown = displayPath(target, manifest)
|
|
86
|
+
let doc
|
|
87
|
+
try {
|
|
88
|
+
doc = JSON.parse(fs.readFileSync(manifest, 'utf8'))
|
|
89
|
+
} catch {
|
|
90
|
+
log(`advisory: ${shown} is not readable JSON; attribution key not verified`)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if (doc?.ext?.[ATTRIBUTION_EXT_KEY] === ATTRIBUTION_BYTES) {
|
|
94
|
+
log(`advisory: ${shown} declares ext["${ATTRIBUTION_EXT_KEY}"] — satisfied`)
|
|
95
|
+
} else {
|
|
96
|
+
log(`advisory: ${shown} does not declare ext["${ATTRIBUTION_EXT_KEY}"] — ${POLICY_POINTER}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Blocking CLI probe. runCli accepts injected stdout/stderr writers, so a
|
|
102
|
+
// wrapper can swallow the attribution that help/version output carries. The
|
|
103
|
+
// probe therefore spawns the distribution's own declared bin with --version
|
|
104
|
+
// through real pipes (not the wrapper's writers) and requires the exact
|
|
105
|
+
// attribution byte string in the child's stdout.
|
|
106
|
+
//
|
|
107
|
+
// The probe is opt-out-proof for anything that looks like a distribution:
|
|
108
|
+
// a target that depends on @mnstry/atelier, ships packs/, or carries an
|
|
109
|
+
// atelier.project.json MUST declare a probe-able bin — a missing or empty
|
|
110
|
+
// bin, or a package.json that does not parse, is a blocking failure there.
|
|
111
|
+
// Only a target with none of those markers gets the advisory skip (packages
|
|
112
|
+
// without a CLI exist). A declared bin that cannot be resolved, or whose
|
|
113
|
+
// --version stdout lacks the attribution bytes, is always blocking.
|
|
114
|
+
function looksLikeDistribution(target, pkg) {
|
|
115
|
+
if (pkg && (pkg.dependencies?.['@mnstry/atelier'] || pkg.peerDependencies?.['@mnstry/atelier'] || pkg.devDependencies?.['@mnstry/atelier'])) return true
|
|
116
|
+
if (fs.existsSync(path.join(target, 'packs'))) return true
|
|
117
|
+
if (fs.existsSync(path.join(target, 'atelier.project.json'))) return true
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function declaredBinEntries(target) {
|
|
122
|
+
let pkg
|
|
123
|
+
try {
|
|
124
|
+
pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8'))
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error?.code === 'ENOENT') return { pkg: null, entries: null }
|
|
127
|
+
// A manifest that exists but does not parse must never be
|
|
128
|
+
// indistinguishable from an absent one.
|
|
129
|
+
return { pkg: null, entries: null, malformed: true }
|
|
130
|
+
}
|
|
131
|
+
const bin = pkg?.bin
|
|
132
|
+
if (typeof bin === 'string' && bin.trim()) {
|
|
133
|
+
return { pkg, entries: [{ name: typeof pkg.name === 'string' ? pkg.name : 'bin', file: bin.trim() }] }
|
|
134
|
+
}
|
|
135
|
+
if (bin && typeof bin === 'object' && !Array.isArray(bin)) {
|
|
136
|
+
const entries = Object.entries(bin)
|
|
137
|
+
.filter(([, file]) => typeof file === 'string' && file.trim())
|
|
138
|
+
.map(([name, file]) => ({ name, file: file.trim() }))
|
|
139
|
+
if (entries.length) return { pkg, entries }
|
|
140
|
+
}
|
|
141
|
+
return { pkg, entries: null }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function checkBinAttribution(target) {
|
|
145
|
+
const { pkg, entries, malformed } = declaredBinEntries(target)
|
|
146
|
+
if (malformed) {
|
|
147
|
+
return { ok: false, reason: 'package.json exists but does not parse; the CLI attribution probe cannot be skipped by shipping a malformed manifest' }
|
|
148
|
+
}
|
|
149
|
+
if (entries === null) {
|
|
150
|
+
if (looksLikeDistribution(target, pkg)) {
|
|
151
|
+
return { ok: false, reason: 'target looks like a distribution (declares @mnstry/atelier, ships packs/, or carries atelier.project.json) but declares no probe-able bin; a distribution CLI must exist and carry the attribution' }
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, note: 'no package.json bin declared and no distribution markers; CLI attribution probe skipped' }
|
|
154
|
+
}
|
|
155
|
+
for (const { name, file } of entries) {
|
|
156
|
+
const resolved = path.resolve(target, file)
|
|
157
|
+
if (!fs.existsSync(resolved)) {
|
|
158
|
+
return { ok: false, reason: `declared bin "${name}" cannot be resolved: ${displayPath(target, resolved)}` }
|
|
159
|
+
}
|
|
160
|
+
const result = spawnSync(process.execPath, [resolved, '--version'], {
|
|
161
|
+
cwd: target,
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
timeout: 30_000,
|
|
164
|
+
})
|
|
165
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : ''
|
|
166
|
+
if (result.error || !stdout.includes(ATTRIBUTION_BYTES)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason: `declared bin "${name}" --version stdout does not contain the exact byte string "${ATTRIBUTION_BYTES}"`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, note: null }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function usageExit(message) {
|
|
177
|
+
logError(message)
|
|
178
|
+
logError('Usage: distribution check [--target DIR] [--pack DIR]')
|
|
179
|
+
process.exit(2)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runCheck(args) {
|
|
183
|
+
if (args.target === true) usageExit('--target requires a value')
|
|
184
|
+
if (args.pack === true) usageExit('--pack requires a value')
|
|
185
|
+
const target = path.resolve(typeof args.target === 'string' ? args.target : '.')
|
|
186
|
+
let stat = null
|
|
187
|
+
try {
|
|
188
|
+
stat = fs.statSync(target)
|
|
189
|
+
} catch {
|
|
190
|
+
stat = null
|
|
191
|
+
}
|
|
192
|
+
if (!stat?.isDirectory()) {
|
|
193
|
+
logError(`target is not a directory: ${typeof args.target === 'string' ? args.target : target}`)
|
|
194
|
+
process.exit(2)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const readme = checkReadmeAttribution(target)
|
|
198
|
+
const binProbe = checkBinAttribution(target)
|
|
199
|
+
reportPackAttribution(target, args.pack)
|
|
200
|
+
if (binProbe.note) log(`advisory: ${binProbe.note}`)
|
|
201
|
+
|
|
202
|
+
if (!readme.ok) {
|
|
203
|
+
logError(`${readme.reason}; distributions must carry the MNSTRY attribution — ${POLICY_POINTER}`)
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|
|
206
|
+
if (!binProbe.ok) {
|
|
207
|
+
logError(`${binProbe.reason}; distributions must carry the MNSTRY attribution — ${POLICY_POINTER}`)
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
log('required README.md attribution present')
|
|
211
|
+
if (!binProbe.note) log('declared bin --version output carries the required attribution')
|
|
212
|
+
log('distribution check passed')
|
|
213
|
+
process.exit(0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const args = parseArgs(process.argv.slice(2))
|
|
217
|
+
const subcommand = args._[0] || 'check'
|
|
218
|
+
if (subcommand === 'check') {
|
|
219
|
+
runCheck(args)
|
|
220
|
+
} else {
|
|
221
|
+
logError(`Unknown distribution command: ${subcommand}`)
|
|
222
|
+
logError('Usage: distribution check [--target DIR] [--pack DIR]')
|
|
223
|
+
process.exit(2)
|
|
224
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { checkForbiddenEgress } from '../egress/forbidden-egress.mjs'
|
|
3
|
+
|
|
4
|
+
const findings = checkForbiddenEgress()
|
|
5
|
+
if (findings.length) {
|
|
6
|
+
for (const finding of findings) {
|
|
7
|
+
console.error(`[atelier-egress] ${finding.file}:${finding.line} ${finding.type}: ${finding.detail}`)
|
|
8
|
+
}
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
console.log('[atelier-egress] no forbidden network egress found')
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// atelier extension-pack validate|list
|
|
3
|
+
//
|
|
4
|
+
// Self-contained subcommand parser (no shared CLI plumbing) so the module can
|
|
5
|
+
// be invoked directly: node src/commands/extension-pack.mjs <subcommand> ...
|
|
6
|
+
//
|
|
7
|
+
// Exit codes: 0 every enabled pack loads clean (warnings do not fail),
|
|
8
|
+
// 1 an enabled pack failed to load, 2 usage or project resolution error.
|
|
9
|
+
// No network access anywhere in this command. Log-safety contract: output
|
|
10
|
+
// names pack ids, declared paths, and loader messages; it never echoes pack
|
|
11
|
+
// or protocol file contents.
|
|
12
|
+
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { ATELIER_EXT_NAMESPACE, loadExtensionPacks } from '../extension-packs/loader.mjs'
|
|
15
|
+
import { asObject, commandProject } from '../project/config.mjs'
|
|
16
|
+
|
|
17
|
+
const REPORT_SCHEMA = 'mnstry.atelier-extension-pack-report@v1'
|
|
18
|
+
const LIST_SCHEMA = 'mnstry.atelier-extension-pack-list@v1'
|
|
19
|
+
|
|
20
|
+
const USAGE = `Usage: atelier extension-pack <subcommand>
|
|
21
|
+
|
|
22
|
+
Subcommands:
|
|
23
|
+
validate [--json] [--project ./atelier.project.json]
|
|
24
|
+
Load every declared extension pack in report mode and print one line
|
|
25
|
+
per pack (ok, error, or skipped) with errors and warnings indented.
|
|
26
|
+
--json emits { schema: "${REPORT_SCHEMA}", packs, ok }.
|
|
27
|
+
|
|
28
|
+
list [--json] [--project ./atelier.project.json]
|
|
29
|
+
Show declared packs with id, version, enabled, config-relative path, digest,
|
|
30
|
+
protocol count, and lock status (locked, unlocked, or mismatch).
|
|
31
|
+
This is the default subcommand.
|
|
32
|
+
|
|
33
|
+
Packs are declared in the tracked project config under
|
|
34
|
+
ext["${ATELIER_EXT_NAMESPACE}"].extensionPacks. Pack protocols are resolvable
|
|
35
|
+
by full namespaced id only; the machine-local overlay can disable a declared
|
|
36
|
+
pack but can never enable an undeclared one.
|
|
37
|
+
|
|
38
|
+
Exit codes: 0 every enabled pack loads clean (warnings do not fail),
|
|
39
|
+
1 an enabled pack failed to load, 2 usage or project resolution error.`
|
|
40
|
+
|
|
41
|
+
function fail(message) {
|
|
42
|
+
console.error(message)
|
|
43
|
+
process.exit(2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --project / --project-config (both --flag VALUE and --flag=VALUE forms) are
|
|
47
|
+
// recognized here for the unknown-option check only; commandProject reads the
|
|
48
|
+
// same process.argv itself, so the values need no forwarding.
|
|
49
|
+
const OPTION_SPEC = { json: 'flag', project: 'value', 'project-config': 'value' }
|
|
50
|
+
|
|
51
|
+
function parseOptions(argv) {
|
|
52
|
+
const options = { json: false }
|
|
53
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
54
|
+
const arg = argv[i]
|
|
55
|
+
if (arg === '--help' || arg === '-h') {
|
|
56
|
+
console.log(USAGE)
|
|
57
|
+
process.exit(0)
|
|
58
|
+
}
|
|
59
|
+
if (!arg.startsWith('--')) {
|
|
60
|
+
fail(`unexpected argument: ${arg}\n\n${USAGE}`)
|
|
61
|
+
}
|
|
62
|
+
const name = arg.slice(2).split('=')[0]
|
|
63
|
+
if (!(name in OPTION_SPEC)) fail(`unknown option --${name}\n\n${USAGE}`)
|
|
64
|
+
if (OPTION_SPEC[name] === 'flag') {
|
|
65
|
+
options[name] = true
|
|
66
|
+
} else if (!arg.includes('=')) {
|
|
67
|
+
i += 1
|
|
68
|
+
if (argv[i] === undefined) fail(`--${name} requires a value`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return options
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveProject() {
|
|
75
|
+
try {
|
|
76
|
+
return commandProject()
|
|
77
|
+
} catch (error) {
|
|
78
|
+
fail(error.message)
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function declaredEntries(project) {
|
|
84
|
+
const container = asObject(asObject(asObject(project?.config).ext)[ATELIER_EXT_NAMESPACE])
|
|
85
|
+
return Array.isArray(container.extensionPacks) ? container.extensionPacks : []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const LOCK_MISMATCH_PATTERN = /^lock (version|digest) mismatch/
|
|
89
|
+
|
|
90
|
+
// One row per declared entry, in declaration order, joining the loader report
|
|
91
|
+
// back onto the declaration. Loader messages without a usable packId (project-
|
|
92
|
+
// level problems and malformed entries) surface separately as project rows.
|
|
93
|
+
function buildRows(project, result) {
|
|
94
|
+
const entries = declaredEntries(project)
|
|
95
|
+
const attributed = new Set()
|
|
96
|
+
const rows = entries.map((entry, index) => {
|
|
97
|
+
const id = typeof entry?.id === 'string' && entry.id ? entry.id : null
|
|
98
|
+
const label = id ?? `extensionPacks[${index}]`
|
|
99
|
+
const record = id ? result.packs.find((pack) => pack.id === id) ?? null : null
|
|
100
|
+
const skippedEntry = id ? result.skipped.find((item) => item.packId === id) ?? null : null
|
|
101
|
+
const errors = id ? result.errors.filter((item) => item.packId === id).map((item) => item.message) : []
|
|
102
|
+
const warnings = id ? result.warnings.filter((item) => item.packId === id).map((item) => item.message) : []
|
|
103
|
+
if (id) attributed.add(id)
|
|
104
|
+
const status = record ? 'ok' : skippedEntry ? 'skipped' : 'error'
|
|
105
|
+
const declaredPath = typeof entry?.path === 'string' && entry.path ? entry.path : null
|
|
106
|
+
// Log-safety: paths in output stay relative to the project config dir
|
|
107
|
+
// (same rationale as distribution.mjs) so neither text nor --json output
|
|
108
|
+
// ever leaks machine-local absolute paths.
|
|
109
|
+
const resolvedPath = record?.path ?? (declaredPath ? path.resolve(project.configDir, declaredPath) : null)
|
|
110
|
+
const displayPath = resolvedPath ? path.relative(project.configDir, resolvedPath) || '.' : null
|
|
111
|
+
return {
|
|
112
|
+
id: label,
|
|
113
|
+
status,
|
|
114
|
+
// Effective enablement: the declared flag minus any disable (config or
|
|
115
|
+
// machine-local overlay); skippedReason names which disable applied.
|
|
116
|
+
enabled: entry?.enabled === true && !skippedEntry,
|
|
117
|
+
skippedReason: skippedEntry?.reason ?? null,
|
|
118
|
+
version: record?.version ?? (typeof entry?.version === 'string' ? entry.version : null),
|
|
119
|
+
path: displayPath,
|
|
120
|
+
digest: record?.digest ?? null,
|
|
121
|
+
protocolCount: record ? record.protocols.length : null,
|
|
122
|
+
lock: record ? record.lock : errors.some((message) => LOCK_MISMATCH_PATTERN.test(message)) ? 'mismatch' : null,
|
|
123
|
+
errors,
|
|
124
|
+
warnings,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
const projectErrors = result.errors.filter((item) => !item.packId || !attributed.has(item.packId)).map((item) => item.message)
|
|
128
|
+
const projectWarnings = result.warnings.filter((item) => !item.packId || !attributed.has(item.packId)).map((item) => item.message)
|
|
129
|
+
return { rows, projectErrors, projectWarnings }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadReport() {
|
|
133
|
+
const project = resolveProject()
|
|
134
|
+
const result = loadExtensionPacks(project, { report: true })
|
|
135
|
+
return { project, result, ...buildRows(project, result) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printIndented(row) {
|
|
139
|
+
for (const message of row.errors) console.log(` error: ${message}`)
|
|
140
|
+
for (const message of row.warnings) console.log(` warning: ${message}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function runValidate(argv) {
|
|
144
|
+
const options = parseOptions(argv)
|
|
145
|
+
const { result, rows, projectErrors, projectWarnings } = loadReport()
|
|
146
|
+
const ok = result.errors.length === 0
|
|
147
|
+
if (options.json) {
|
|
148
|
+
const packs = rows.map(({ id, status, enabled, skippedReason, version, path: packPath, digest, protocolCount, lock, errors, warnings }) => ({
|
|
149
|
+
id, status, enabled, skippedReason, version, path: packPath, digest, protocolCount, lock, errors, warnings,
|
|
150
|
+
}))
|
|
151
|
+
console.log(JSON.stringify({ schema: REPORT_SCHEMA, ok, packs, errors: projectErrors, warnings: projectWarnings }, null, 2))
|
|
152
|
+
} else {
|
|
153
|
+
if (!rows.length && !projectErrors.length) console.log('no extension packs declared')
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const detail = row.status === 'ok'
|
|
156
|
+
? ` (${row.version}, ${row.protocolCount} ${row.protocolCount === 1 ? 'protocol' : 'protocols'}, ${row.lock})`
|
|
157
|
+
: row.status === 'skipped' ? ` (${row.skippedReason})` : ''
|
|
158
|
+
console.log(`${row.id} ${row.status}${detail}`)
|
|
159
|
+
printIndented(row)
|
|
160
|
+
}
|
|
161
|
+
if (projectErrors.length || projectWarnings.length) {
|
|
162
|
+
console.log('project')
|
|
163
|
+
for (const message of projectErrors) console.log(` error: ${message}`)
|
|
164
|
+
for (const message of projectWarnings) console.log(` warning: ${message}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
process.exit(ok ? 0 : 1)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runList(argv) {
|
|
171
|
+
const options = parseOptions(argv)
|
|
172
|
+
const { result, rows, projectErrors, projectWarnings } = loadReport()
|
|
173
|
+
const ok = result.errors.length === 0
|
|
174
|
+
if (options.json) {
|
|
175
|
+
const packs = rows.map(({ id, status, enabled, skippedReason, version, path: packPath, digest, protocolCount, lock }) => ({
|
|
176
|
+
id, status, enabled, skippedReason, version, path: packPath, digest, protocolCount, lock,
|
|
177
|
+
}))
|
|
178
|
+
console.log(JSON.stringify({ schema: LIST_SCHEMA, ok, packs, errors: projectErrors, warnings: projectWarnings }, null, 2))
|
|
179
|
+
} else {
|
|
180
|
+
if (!rows.length) console.log('no extension packs declared')
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
console.log(row.id)
|
|
183
|
+
console.log(` status: ${row.status}${row.skippedReason ? ` (${row.skippedReason})` : ''}`)
|
|
184
|
+
console.log(` version: ${row.version ?? 'unknown'}`)
|
|
185
|
+
console.log(` enabled: ${row.enabled ? 'yes' : 'no'}`)
|
|
186
|
+
if (row.path) console.log(` path: ${row.path}`)
|
|
187
|
+
if (row.digest) console.log(` digest: ${row.digest}`)
|
|
188
|
+
if (row.protocolCount !== null) console.log(` protocols: ${row.protocolCount}`)
|
|
189
|
+
if (row.lock) console.log(` lock: ${row.lock}`)
|
|
190
|
+
}
|
|
191
|
+
if (projectErrors.length || projectWarnings.length) {
|
|
192
|
+
console.log('project')
|
|
193
|
+
for (const message of projectErrors) console.log(` error: ${message}`)
|
|
194
|
+
for (const message of projectWarnings) console.log(` warning: ${message}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
process.exit(ok ? 0 : 1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const argv = process.argv.slice(2)
|
|
201
|
+
const first = argv[0]
|
|
202
|
+
if (first === '--help' || first === '-h' || first === 'help') {
|
|
203
|
+
console.log(USAGE)
|
|
204
|
+
process.exit(0)
|
|
205
|
+
}
|
|
206
|
+
if (first === 'validate') runValidate(argv.slice(1))
|
|
207
|
+
else if (first === 'list') runList(argv.slice(1))
|
|
208
|
+
else if (first === undefined || first.startsWith('--')) runList(argv)
|
|
209
|
+
else fail(`unknown extension-pack subcommand: ${first}\n\n${USAGE}`)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { parseArgs, resolveProjectConfig, writeJson } from '../project/config.mjs'
|
|
6
|
+
import { writeAtelierLock } from '../upgrade/upgrade.mjs'
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(new URL('../..', import.meta.url).pathname)
|
|
9
|
+
|
|
10
|
+
function slug(value) {
|
|
11
|
+
return String(value || '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
15
|
+
.replace(/^-+|-+$/g, '') || 'owner'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function gitConfig(name) {
|
|
19
|
+
const result = spawnSync('git', ['config', '--get', name], { encoding: 'utf8' })
|
|
20
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(file) {
|
|
24
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyDir(source, target, { renameGitignore = true } = {}) {
|
|
28
|
+
fs.mkdirSync(target, { recursive: true })
|
|
29
|
+
for (const ent of fs.readdirSync(source, { withFileTypes: true })) {
|
|
30
|
+
const from = path.join(source, ent.name)
|
|
31
|
+
// npm pack strips files named .gitignore, so templates ship the file as
|
|
32
|
+
// `gitignore` at the template root; nested files keep their literal names.
|
|
33
|
+
const rename = renameGitignore && ent.name === 'gitignore' && ent.isFile()
|
|
34
|
+
const to = path.join(target, rename ? '.gitignore' : ent.name)
|
|
35
|
+
if (ent.isDirectory()) copyDir(from, to, { renameGitignore: false })
|
|
36
|
+
else fs.copyFileSync(from, to)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function personalizeBoundaryPolicy(target, { actor, githubLogin, gitEmail } = {}) {
|
|
41
|
+
const policyPath = path.join(target, 'boundary-policy.v1.json')
|
|
42
|
+
if (!fs.existsSync(policyPath)) return
|
|
43
|
+
const policy = readJson(policyPath)
|
|
44
|
+
const actorId = slug(actor || 'owner')
|
|
45
|
+
const privateDomainRepo = Object.entries(policy.repos || {}).find(([, repo]) => repo?.kind === 'private_domain')?.[0] || `mnstry-private-${actorId}`
|
|
46
|
+
const login = githubLogin || process.env.GITHUB_ACTOR || actorId
|
|
47
|
+
const email = gitEmail || gitConfig('user.email') || `${actorId}@example.invalid`
|
|
48
|
+
|
|
49
|
+
policy.actors = {
|
|
50
|
+
[actorId]: {
|
|
51
|
+
githubLogin: login,
|
|
52
|
+
gitEmails: [email],
|
|
53
|
+
privateDomainRepo,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
if (policy.repos?.[privateDomainRepo]) policy.repos[privateDomainRepo].ownerActor = actorId
|
|
57
|
+
writeJson(policyPath, policy)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const args = parseArgs(process.argv.slice(2))
|
|
61
|
+
const target = path.resolve(args.target || process.cwd())
|
|
62
|
+
const template = args.template || args.fixture
|
|
63
|
+
let templateId = template || 'default'
|
|
64
|
+
if (template === 'sample-workspace') {
|
|
65
|
+
copyDir(path.join(packageRoot, 'fixtures/projects/sample-workspace'), target)
|
|
66
|
+
console.log(`created sample Atelier workspace at ${target}`)
|
|
67
|
+
} else if (template === 'private-domain') {
|
|
68
|
+
copyDir(path.join(packageRoot, 'templates/private-domain-workspace'), target)
|
|
69
|
+
personalizeBoundaryPolicy(target, {
|
|
70
|
+
actor: args.actor,
|
|
71
|
+
githubLogin: args['github-login'],
|
|
72
|
+
gitEmail: args['git-email'],
|
|
73
|
+
})
|
|
74
|
+
console.log(`created private domain Atelier workspace at ${target}`)
|
|
75
|
+
} else if (template === 'shared-project') {
|
|
76
|
+
copyDir(path.join(packageRoot, 'templates/shared-project-workspace'), target)
|
|
77
|
+
personalizeBoundaryPolicy(target, {
|
|
78
|
+
actor: args.actor,
|
|
79
|
+
githubLogin: args['github-login'],
|
|
80
|
+
gitEmail: args['git-email'],
|
|
81
|
+
})
|
|
82
|
+
console.log(`created shared project Atelier workspace at ${target}`)
|
|
83
|
+
} else if (template === 'distribution') {
|
|
84
|
+
copyDir(path.join(packageRoot, 'templates/distribution-workspace'), target)
|
|
85
|
+
personalizeBoundaryPolicy(target, {
|
|
86
|
+
actor: args.actor,
|
|
87
|
+
githubLogin: args['github-login'],
|
|
88
|
+
gitEmail: args['git-email'],
|
|
89
|
+
})
|
|
90
|
+
console.log(`created distribution Atelier workspace at ${target}`)
|
|
91
|
+
} else if (template) {
|
|
92
|
+
// Unknown template names fail closed: a typo must not silently produce a
|
|
93
|
+
// blank scaffold that lacks the boundary policy the caller asked for.
|
|
94
|
+
console.error(`Unknown template: ${template}. Valid templates: private-domain, shared-project, sample-workspace, distribution.`)
|
|
95
|
+
process.exit(1)
|
|
96
|
+
} else {
|
|
97
|
+
fs.mkdirSync(target, { recursive: true })
|
|
98
|
+
fs.mkdirSync(path.join(target, 'content'), { recursive: true })
|
|
99
|
+
writeJson(path.join(target, 'atelier.project.json'), {
|
|
100
|
+
schema: 'mnstry.atelier-project-config@v1',
|
|
101
|
+
name: path.basename(target),
|
|
102
|
+
roots: { workspace: '.' },
|
|
103
|
+
graph: { repoAccessPath: 'repo-access.v1.json', outputPath: 'atelier-output/knowledge.graph.json' },
|
|
104
|
+
projection: { outputRoot: 'atelier-output', readinessPath: 'atelier-output/atelier-readiness.json' },
|
|
105
|
+
repos: [{ name: 'content', path: 'content', readBoundary: 'private' }],
|
|
106
|
+
})
|
|
107
|
+
writeJson(path.join(target, 'repo-access.v1.json'), {
|
|
108
|
+
schema: 'mnstry.atelier-repo-access@v1',
|
|
109
|
+
defaultReadBoundary: 'private',
|
|
110
|
+
repos: { content: { readBoundary: 'private' } },
|
|
111
|
+
})
|
|
112
|
+
console.log(`created Atelier project scaffold at ${target}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Every workspace gets a preview launch config, whichever template made it.
|
|
116
|
+
// autoPort matters: a workspace usually keeps a long-lived instance on the
|
|
117
|
+
// canonical port, and a preview that cannot take a free port evicts it.
|
|
118
|
+
const launchTemplate = path.join(packageRoot, 'templates/launch.json')
|
|
119
|
+
const launchTarget = path.join(target, '.claude/launch.json')
|
|
120
|
+
if (fs.existsSync(launchTemplate) && !fs.existsSync(launchTarget)) {
|
|
121
|
+
fs.mkdirSync(path.dirname(launchTarget), { recursive: true })
|
|
122
|
+
fs.copyFileSync(launchTemplate, launchTarget)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const projectPath = path.join(target, 'atelier.project.json')
|
|
126
|
+
if (fs.existsSync(projectPath)) {
|
|
127
|
+
const project = resolveProjectConfig({ cwd: target, argv: ['--project', projectPath] })
|
|
128
|
+
writeAtelierLock({ project, templateId })
|
|
129
|
+
console.log(`wrote Atelier lock at ${path.join(target, 'atelier.lock.json')}`)
|
|
130
|
+
}
|