@topy-ai/maggie 0.7.42 → 0.7.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh-TW.md +6 -4
- package/README.md +6 -1
- package/bin/maggie.js +2 -0
- package/bundled-contracts/maggie-marketplace/cloudflare-pages-v1.schema.json +90 -0
- package/bundled-contracts/maggie-marketplace/enrichment-v1.schema.json +17 -0
- package/bundled-contracts/maggie-marketplace/github-repository-v1.schema.json +50 -0
- package/bundled-contracts/maggie-marketplace/starter-provisioning-v1.schema.json +72 -0
- package/bundled-contracts/maggie-marketplace/starter-release-v1.schema.json +71 -0
- package/bundled-contracts/maggie-marketplace/starter-site-v1.schema.json +56 -0
- package/bundled-skills/maggie-deployment/SKILL.md +18 -0
- package/bundled-tools/clis/maggie_deployment.py +106 -0
- package/bundled-tools/clis/maggie_marketplace.py +143 -0
- package/package.json +1 -1
package/README-zh-TW.md
CHANGED
|
@@ -8,11 +8,13 @@ Codex、Claude Code 與相容的 coding agents。
|
|
|
8
8
|
## 安裝
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npx @topy-ai/maggie@0.7.
|
|
12
|
-
npx @topy-ai/maggie@0.7.
|
|
11
|
+
npx @topy-ai/maggie@0.7.43 init --agent all
|
|
12
|
+
npx @topy-ai/maggie@0.7.43 doctor --project .
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
目前 release 是 `0.7.
|
|
15
|
+
目前 release 是 `0.7.43`。新增 secret-free Starter marketplace provisioning
|
|
16
|
+
plan、versioned GitHub/Cloudflare/Starter contracts,以及 deterministic
|
|
17
|
+
`maggie marketplace plan` dry-run CLI。0.7.42 新增 redacted Gemini video-provider evidence gate,
|
|
16
18
|
驗證 provider acknowledgement、idempotency、moderation、object-storage
|
|
17
19
|
readback、bounded retry 與 git provenance,並拒絕 raw provider payload 或
|
|
18
20
|
input asset 欄位。0.7.41 完成 feedback batches 22、23:加入 declarative
|
|
@@ -78,7 +80,7 @@ structure drift 或重複 treatment product 時 fail closed。
|
|
|
78
80
|
deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
|
|
79
81
|
publish 與 production deployment 需要明確確認。
|
|
80
82
|
|
|
81
|
-
目前 source tree 的正式 release 是 `0.7.
|
|
83
|
+
目前 source tree 的正式 release 是 `0.7.43`。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
|
|
82
84
|
dispatch、`maggie --version`、feedback batch review 聚合與重複偵測、marketplace enrichment evidence、booking
|
|
83
85
|
worker/resolver evidence,以及 read-only migration preflight。它也包含 host-owned mobile app surface contract、signed-in
|
|
84
86
|
camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
|
package/README.md
CHANGED
|
@@ -60,7 +60,12 @@ Maggie keeps the existing project foundation and asks for decisions before
|
|
|
60
60
|
shared routes, analytics, or publishing boundaries change. The current
|
|
61
61
|
package ships 20 installable skills and a local-first MaggieDash foundation.
|
|
62
62
|
|
|
63
|
-
The current release is `0.7.
|
|
63
|
+
The current release is `0.7.43`. It adds a secret-free Starter marketplace
|
|
64
|
+
provisioning plan and bundles versioned GitHub repository, Cloudflare Pages,
|
|
65
|
+
Starter site, and Starter release contracts. `maggie marketplace plan` emits a
|
|
66
|
+
deterministic dry-run plan for a public Cal.com URL, static Cloudflare Pages,
|
|
67
|
+
no database, and ordered idempotent steps; it never stores credentials or
|
|
68
|
+
mutates provider state. The 0.7.42 release adds a redacted Gemini video-provider
|
|
64
69
|
evidence gate for provider acknowledgement, idempotency, moderation,
|
|
65
70
|
object-storage readback, bounded retry, and git provenance; unknown raw
|
|
66
71
|
provider payload and input-asset fields fail validation. The 0.7.41 release
|
package/bin/maggie.js
CHANGED
|
@@ -80,6 +80,7 @@ Usage:
|
|
|
80
80
|
maggie remove [SKILL ...] [--project PATH] [--agent codex|claude|all]
|
|
81
81
|
maggie cleanup --project PATH [--confirm]
|
|
82
82
|
maggie marketplace add <template-url> clone <preview-url> --rename <id>
|
|
83
|
+
maggie marketplace plan --workspace-id ID --merchant-id ID --site-slug SLUG --cal-url URL [--output FILE]
|
|
83
84
|
maggie marketplace list
|
|
84
85
|
maggie marketplace suggest "premium London medical spa"
|
|
85
86
|
maggie marketplace inspect <id>
|
|
@@ -124,6 +125,7 @@ Usage:
|
|
|
124
125
|
maggie service convert-page <page-path> --project PATH
|
|
125
126
|
maggie service match-pages --project PATH --pages-dir src/pages
|
|
126
127
|
maggie deployment --project PATH --target vps-with-cloudflare-dns
|
|
128
|
+
maggie deployment worker-secrets --project PATH --environment staging --required RESEND_API_KEY
|
|
127
129
|
maggie deployment readiness --project PATH
|
|
128
130
|
maggie deployment parity --project PATH --evidence FILE --output FILE
|
|
129
131
|
maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/cloudflare-pages-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace Cloudflare Pages adapter result v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "provider", "state", "sourceRevision", "retry", "privacy"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "maggie-marketplace-cloudflare-pages.v1"},
|
|
10
|
+
"provider": {"const": "cloudflare-pages"},
|
|
11
|
+
"state": {"enum": ["succeeded", "retryable", "dead-letter"]},
|
|
12
|
+
"projectOutcome": {"enum": ["created", "reused", "updated"]},
|
|
13
|
+
"domainOutcome": {"enum": ["created", "reused", "updated"]},
|
|
14
|
+
"project": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["externalReference", "name", "productionBranch", "repositoryOwner", "repositoryName", "sourceType"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"externalReference": {"type": "string", "minLength": 1, "maxLength": 200},
|
|
20
|
+
"name": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$"},
|
|
21
|
+
"productionBranch": {"type": "string", "pattern": "^[A-Za-z0-9._/-]+$"},
|
|
22
|
+
"repositoryOwner": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
23
|
+
"repositoryName": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
24
|
+
"sourceType": {"enum": ["github", "direct-upload"]},
|
|
25
|
+
"canonicalUrl": {"type": ["string", "null"], "format": "uri", "pattern": "^https://"}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"domain": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["externalReference", "name", "status", "validationStatus", "verificationStatus"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"externalReference": {"type": "string", "minLength": 1, "maxLength": 255},
|
|
34
|
+
"name": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.bea\\.gift$"},
|
|
35
|
+
"status": {"enum": ["initializing", "pending", "active", "deactivated", "blocked", "error", "unknown"]},
|
|
36
|
+
"validationStatus": {"type": "string", "minLength": 1, "maxLength": 80},
|
|
37
|
+
"verificationStatus": {"type": "string", "minLength": 1, "maxLength": 80}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"deployment": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["externalReference", "environment", "stageStatus", "skipped"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"externalReference": {"type": "string", "minLength": 1, "maxLength": 200},
|
|
46
|
+
"environment": {"enum": ["production", "preview", "unknown"]},
|
|
47
|
+
"sourceRevision": {"type": ["string", "null"], "pattern": "^[A-Za-z0-9._:/-]{1,200}$"},
|
|
48
|
+
"branch": {"type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$"},
|
|
49
|
+
"stageStatus": {"enum": ["success", "idle", "active", "failure", "canceled", "unknown"]},
|
|
50
|
+
"url": {"type": ["string", "null"], "format": "uri", "pattern": "^https://"},
|
|
51
|
+
"skipped": {"type": "boolean"}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"publicReadback": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["url", "status", "https", "canonical"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"url": {"type": "string", "format": "uri", "pattern": "^https://"},
|
|
60
|
+
"status": {"type": "integer", "minimum": 100, "maximum": 599},
|
|
61
|
+
"https": {"const": true},
|
|
62
|
+
"canonical": {"const": true},
|
|
63
|
+
"contentType": {"type": ["string", "null"], "maxLength": 160}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"sourceRevision": {"type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,200}$"},
|
|
67
|
+
"errorCode": {"type": "string", "pattern": "^cloudflare_[a-z0-9_]{1,80}$"},
|
|
68
|
+
"retry": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["allowed", "attempt", "maxAttempts"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"allowed": {"type": "boolean"},
|
|
74
|
+
"attempt": {"type": "integer", "minimum": 1, "maximum": 3},
|
|
75
|
+
"maxAttempts": {"const": 3},
|
|
76
|
+
"afterSeconds": {"type": "integer", "minimum": 0, "maximum": 900}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"privacy": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"required": ["tokenIncluded", "rawResponseIncluded", "secretFree"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"tokenIncluded": {"const": false},
|
|
85
|
+
"rawResponseIncluded": {"const": false},
|
|
86
|
+
"secretFree": {"const": true}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/enrichment-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace enrichment evidence v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "candidateId", "status", "passed", "errors", "crawl", "audit", "snapshot", "seoBackfill", "retry", "privacy"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": {"const": "maggie-marketplace-enrichment.v1"},
|
|
9
|
+
"candidateId": {"type": "string", "minLength": 1},
|
|
10
|
+
"status": {"enum": ["passed", "failed"]}, "passed": {"type": "boolean"},
|
|
11
|
+
"errors": {"type": "array", "items": {"type": "string"}},
|
|
12
|
+
"crawl": {"type": "object"}, "audit": {"type": "object"}, "snapshot": {"type": "object"}, "seoBackfill": {"type": "object"},
|
|
13
|
+
"retry": {"type": "object", "required": ["allowed"]},
|
|
14
|
+
"privacy": {"type": "object", "required": ["rawHtmlIncluded", "responseBodiesIncluded", "localPathsIncluded", "secretsRedacted"]}
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/github-repository-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace GitHub repository adapter result v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "provider", "state", "sourceRevision", "retry", "privacy"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "maggie-marketplace-github-repository.v1"},
|
|
10
|
+
"provider": {"const": "github"},
|
|
11
|
+
"state": {"enum": ["succeeded", "retryable", "dead-letter"]},
|
|
12
|
+
"outcome": {"enum": ["created", "reused", "updated"]},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["externalReference", "owner", "name", "private", "defaultBranch", "htmlUrl"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"externalReference": {"type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"},
|
|
19
|
+
"owner": {"type": "string", "pattern": "^[A-Za-z0-9-]{1,39}$"},
|
|
20
|
+
"name": {"type": "string", "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$"},
|
|
21
|
+
"private": {"type": "boolean"},
|
|
22
|
+
"defaultBranch": {"type": "string", "pattern": "^[A-Za-z0-9._/-]+$"},
|
|
23
|
+
"htmlUrl": {"type": "string", "format": "uri", "pattern": "^https://github\\.com/"}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sourceRevision": {"type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,200}$"},
|
|
27
|
+
"errorCode": {"type": "string", "pattern": "^github_[a-z0-9_]{1,80}$"},
|
|
28
|
+
"retry": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["allowed", "attempt", "maxAttempts"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"allowed": {"type": "boolean"},
|
|
34
|
+
"attempt": {"type": "integer", "minimum": 1, "maximum": 3},
|
|
35
|
+
"maxAttempts": {"const": 3},
|
|
36
|
+
"afterSeconds": {"type": "integer", "minimum": 0, "maximum": 900}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"privacy": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["tokenIncluded", "rawResponseIncluded", "secretFree"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"tokenIncluded": {"const": false},
|
|
45
|
+
"rawResponseIncluded": {"const": false},
|
|
46
|
+
"secretFree": {"const": true}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/starter-provisioning-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace Starter provisioning plan v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "plan", "mode", "workspace", "site", "booking", "steps", "security", "nextActions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "maggie-marketplace-starter-provisioning.v1"},
|
|
10
|
+
"plan": {"const": "starter"},
|
|
11
|
+
"mode": {"const": "dry-run"},
|
|
12
|
+
"workspace": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["workspaceId", "merchantId"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"workspaceId": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9_-]+$"},
|
|
18
|
+
"merchantId": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9_-]+$"}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"site": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["slug", "hostname", "repository", "branch", "runtime", "database"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"slug": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"},
|
|
27
|
+
"hostname": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.bea\\.gift$"},
|
|
28
|
+
"repository": {"type": "string", "pattern": "^bea-merchant-[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"},
|
|
29
|
+
"templateId": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$"},
|
|
30
|
+
"branch": {"type": "string", "pattern": "^[A-Za-z0-9._/-]+$"},
|
|
31
|
+
"runtime": {"const": "cloudflare-pages-static"},
|
|
32
|
+
"database": {"const": "none"}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"booking": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": ["mode", "sourceOfTruth", "publicUrl", "embed"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"mode": {"const": "cal-public"},
|
|
41
|
+
"sourceOfTruth": {"const": "cal.com"},
|
|
42
|
+
"publicUrl": {"type": "string", "format": "uri", "pattern": "^https://"},
|
|
43
|
+
"embed": {"enum": ["link", "iframe"]}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"steps": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"minItems": 6,
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["id", "status", "idempotencyKey"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"id": {"enum": ["entitlement", "repository", "site-deploy", "hostname", "booking", "release-readback"]},
|
|
55
|
+
"status": {"const": "planned"},
|
|
56
|
+
"idempotencyKey": {"type": "string", "pattern": "^starter:[a-f0-9]{64}$"}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"security": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"required": ["secretsInArtifact", "calCredentialsStored", "databaseRequired"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"secretsInArtifact": {"const": false},
|
|
66
|
+
"calCredentialsStored": {"const": false},
|
|
67
|
+
"databaseRequired": {"const": false}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"nextActions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/starter-release-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace Starter release evidence v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "status", "checkedAt", "sourceRevision", "site", "booking", "artifact", "repository", "cloudflare", "errors", "security"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "maggie-marketplace-starter-release.v1"},
|
|
10
|
+
"status": {"enum": ["passed", "blocked"]},
|
|
11
|
+
"checkedAt": {"type": "string", "format": "date-time"},
|
|
12
|
+
"sourceRevision": {"type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,200}$"},
|
|
13
|
+
"site": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["slug", "hostname", "templateId"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"slug": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"},
|
|
19
|
+
"hostname": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.bea\\.gift$"},
|
|
20
|
+
"templateId": {"type": "string", "minLength": 1, "maxLength": 120}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"booking": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["mode", "publicUrl", "embed", "readback"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"mode": {"const": "cal-public"},
|
|
29
|
+
"publicUrl": {"type": "string", "format": "uri", "pattern": "^https://"},
|
|
30
|
+
"embed": {"enum": ["link", "iframe"]},
|
|
31
|
+
"readback": {"enum": ["passed", "failed"]}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"artifact": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["readback", "fileCount"],
|
|
38
|
+
"properties": {"readback": {"enum": ["passed", "failed"]}, "fileCount": {"type": "integer", "minimum": 1}}
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["readback", "externalReference", "branch"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"readback": {"enum": ["passed", "failed"]},
|
|
46
|
+
"externalReference": {"type": ["string", "null"], "maxLength": 200},
|
|
47
|
+
"branch": {"type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$"},
|
|
48
|
+
"commitSha": {"type": ["string", "null"], "pattern": "^[A-Fa-f0-9]{7,64}$"}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"cloudflare": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": ["readback", "projectReference", "deploymentReference", "hostnameReference", "publicUrl"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"readback": {"enum": ["passed", "failed"]},
|
|
57
|
+
"projectReference": {"type": ["string", "null"], "maxLength": 200},
|
|
58
|
+
"deploymentReference": {"type": ["string", "null"], "maxLength": 200},
|
|
59
|
+
"hostnameReference": {"type": ["string", "null"], "maxLength": 255},
|
|
60
|
+
"publicUrl": {"type": ["string", "null"], "format": "uri", "pattern": "^https://"}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"errors": {"type": "array", "items": {"type": "string", "pattern": "^[a-z0-9_]{1,100}$"}},
|
|
64
|
+
"security": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["secretsInEvidence", "databaseRequired"],
|
|
68
|
+
"properties": {"secretsInEvidence": {"const": false}, "databaseRequired": {"const": false}}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://maggie.noblox.app/contracts/maggie-marketplace/starter-site-v1.schema.json",
|
|
4
|
+
"title": "Maggie marketplace Starter site artifact v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "templateId", "sourceRevision", "site", "booking", "files", "security"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "maggie-marketplace-starter-site.v1"},
|
|
10
|
+
"templateId": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
11
|
+
"sourceRevision": {"type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,200}$"},
|
|
12
|
+
"site": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["slug", "hostname"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"slug": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"},
|
|
18
|
+
"hostname": {"type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.bea\\.gift$"}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"booking": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["mode", "publicUrl", "embed"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"mode": {"const": "cal-public"},
|
|
27
|
+
"publicUrl": {"type": "string", "format": "uri", "pattern": "^https://"},
|
|
28
|
+
"embed": {"enum": ["link", "iframe"]}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"minItems": 1,
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["path", "sha256", "bytes"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"path": {"type": "string", "pattern": "^(?!/)(?!.*\\.\\.)[A-Za-z0-9_./-]+$"},
|
|
40
|
+
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
41
|
+
"bytes": {"type": "integer", "minimum": 0}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"security": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["runtimeSecrets", "credentialsStored", "databaseRequired"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"runtimeSecrets": {"const": false},
|
|
51
|
+
"credentialsStored": {"const": false},
|
|
52
|
+
"databaseRequired": {"const": false}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -145,6 +145,24 @@ This command never deploys. Production execution still requires the shared
|
|
|
145
145
|
decision loop, explicit confirmation, migration/rollback details, and the
|
|
146
146
|
provider-specific deploy command.
|
|
147
147
|
|
|
148
|
+
For Cloudflare Workers, check the required secret *names* before a canary. The
|
|
149
|
+
command never reads, prints, uploads, or changes secret values:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
maggie deployment worker-secrets --project . \
|
|
153
|
+
--environment staging \
|
|
154
|
+
--required DATABASE_URL \
|
|
155
|
+
--required BOOKING_TOKEN_SECRET \
|
|
156
|
+
--required RESEND_API_KEY \
|
|
157
|
+
--required RESEND_FROM_EMAIL \
|
|
158
|
+
--output .maggie/deployment/worker-secrets.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It calls `wrangler secret list --format json` and stores only configured and
|
|
162
|
+
missing binding names. A missing required name fails closed. Use a redacted
|
|
163
|
+
JSON name snapshot with `--snapshot` for CI or a host adapter that cannot
|
|
164
|
+
expose Cloudflare credentials to Maggie; the snapshot must contain names only.
|
|
165
|
+
|
|
148
166
|
For a VPS adapter, check the project `.env` by variable name before reporting
|
|
149
167
|
credentials as unavailable:
|
|
150
168
|
|
|
@@ -77,6 +77,110 @@ def credential_preflight_main(argv: list[str]) -> int:
|
|
|
77
77
|
return 0 if result["passed"] else 1
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def _secret_names(payload: object) -> list[str]:
|
|
81
|
+
"""Extract only binding names from a Wrangler secret-list response."""
|
|
82
|
+
entries: object = payload
|
|
83
|
+
if isinstance(payload, dict):
|
|
84
|
+
entries = payload.get("secrets", payload.get("result", []))
|
|
85
|
+
if not isinstance(entries, list):
|
|
86
|
+
return []
|
|
87
|
+
names: set[str] = set()
|
|
88
|
+
for entry in entries:
|
|
89
|
+
if isinstance(entry, str):
|
|
90
|
+
name = entry
|
|
91
|
+
elif isinstance(entry, dict):
|
|
92
|
+
name = entry.get("name", entry.get("key", ""))
|
|
93
|
+
else:
|
|
94
|
+
continue
|
|
95
|
+
if isinstance(name, str) and re.fullmatch(r"[A-Z][A-Z0-9_]{1,127}", name):
|
|
96
|
+
names.add(name)
|
|
97
|
+
return sorted(names)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def worker_secret_preflight(
|
|
101
|
+
project: Path,
|
|
102
|
+
environment: str,
|
|
103
|
+
required: list[str],
|
|
104
|
+
snapshot: Path | None = None,
|
|
105
|
+
worker_name: str | None = None,
|
|
106
|
+
wrangler_command: list[str] | None = None,
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""Read Worker secret names without reading or mutating secret values."""
|
|
109
|
+
required_names = sorted({name.strip() for name in required if name.strip()})
|
|
110
|
+
errors: list[str] = []
|
|
111
|
+
if not required_names:
|
|
112
|
+
errors.append("at least one --required secret name is required")
|
|
113
|
+
if any(not re.fullmatch(r"[A-Z][A-Z0-9_]{1,127}", name) for name in required_names):
|
|
114
|
+
errors.append("required secret names must match [A-Z][A-Z0-9_]{1,127}")
|
|
115
|
+
|
|
116
|
+
source = "cloudflare-wrangler-secret-list"
|
|
117
|
+
configured: list[str] = []
|
|
118
|
+
if not errors and snapshot:
|
|
119
|
+
source = "redacted-secret-name-snapshot"
|
|
120
|
+
try:
|
|
121
|
+
configured = _secret_names(json.loads(snapshot.read_text(encoding="utf-8")))
|
|
122
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
123
|
+
errors.append(f"secret-name snapshot is unreadable: {error}")
|
|
124
|
+
elif not errors:
|
|
125
|
+
command = list(wrangler_command or ["npx", "wrangler"])
|
|
126
|
+
command.extend(["secret", "list", "--env", environment, "--format", "json"])
|
|
127
|
+
if worker_name:
|
|
128
|
+
command.extend(["--name", worker_name])
|
|
129
|
+
try:
|
|
130
|
+
completed = subprocess.run(command, cwd=project, capture_output=True, text=True, check=False, timeout=30)
|
|
131
|
+
except (OSError, subprocess.SubprocessError):
|
|
132
|
+
completed = None
|
|
133
|
+
if completed is None or completed.returncode != 0:
|
|
134
|
+
errors.append("Wrangler secret-list command failed; inspect authentication and Worker target")
|
|
135
|
+
else:
|
|
136
|
+
try:
|
|
137
|
+
configured = _secret_names(json.loads(completed.stdout))
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
errors.append("Wrangler secret-list response was not valid JSON")
|
|
140
|
+
|
|
141
|
+
missing = sorted(set(required_names) - set(configured))
|
|
142
|
+
errors.extend(f"required Worker secret is missing: {name}" for name in missing)
|
|
143
|
+
return {
|
|
144
|
+
"schemaVersion": "maggie-worker-secrets.v1",
|
|
145
|
+
"project": str(project),
|
|
146
|
+
"provider": "cloudflare-workers",
|
|
147
|
+
"environment": environment,
|
|
148
|
+
"worker": worker_name,
|
|
149
|
+
"required": required_names,
|
|
150
|
+
"configured": configured,
|
|
151
|
+
"missing": missing,
|
|
152
|
+
"source": source,
|
|
153
|
+
"valuesReturned": False,
|
|
154
|
+
"passed": not errors,
|
|
155
|
+
"errors": errors,
|
|
156
|
+
"mutation": "not executed",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def worker_secret_preflight_main(argv: list[str]) -> int:
|
|
161
|
+
parser = argparse.ArgumentParser(description="Check Cloudflare Worker secret names without reading or changing secret values.")
|
|
162
|
+
parser.add_argument("--project", default=".")
|
|
163
|
+
parser.add_argument("--environment", choices=("staging", "production"), default="staging")
|
|
164
|
+
parser.add_argument("--required", action="append", default=[], help="required secret name; repeat for each binding")
|
|
165
|
+
parser.add_argument("--worker-name")
|
|
166
|
+
parser.add_argument("--snapshot", help="offline JSON containing only secret names")
|
|
167
|
+
parser.add_argument("--output")
|
|
168
|
+
args = parser.parse_args(argv)
|
|
169
|
+
result = worker_secret_preflight(
|
|
170
|
+
Path(args.project).expanduser().resolve(),
|
|
171
|
+
args.environment,
|
|
172
|
+
args.required,
|
|
173
|
+
Path(args.snapshot).expanduser().resolve() if args.snapshot else None,
|
|
174
|
+
args.worker_name,
|
|
175
|
+
)
|
|
176
|
+
if args.output:
|
|
177
|
+
output = Path(args.output).resolve()
|
|
178
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
180
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
181
|
+
return 0 if result["passed"] else 1
|
|
182
|
+
|
|
183
|
+
|
|
80
184
|
def preflight(project: Path, target: str, environment: str) -> dict:
|
|
81
185
|
package_path = project / "package.json"
|
|
82
186
|
package = json.loads(package_path.read_text(encoding="utf-8")) if package_path.exists() else {}
|
|
@@ -390,6 +494,8 @@ def write_release_runner(path: Path, plan: dict) -> None:
|
|
|
390
494
|
def main() -> int:
|
|
391
495
|
if sys.argv[1:2] == ["credential-preflight"]:
|
|
392
496
|
return credential_preflight_main(sys.argv[2:])
|
|
497
|
+
if sys.argv[1:2] == ["worker-secrets"]:
|
|
498
|
+
return worker_secret_preflight_main(sys.argv[2:])
|
|
393
499
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
394
500
|
parser.add_argument("project", nargs="?", default=".")
|
|
395
501
|
parser.add_argument("--target", choices=("detected", "cloudflare", "vps", "vps-with-cloudflare-dns", "gcp", "aws"), default="detected")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import re
|
|
8
9
|
import shutil
|
|
@@ -10,6 +11,7 @@ import subprocess
|
|
|
10
11
|
import sys
|
|
11
12
|
import urllib.error
|
|
12
13
|
import urllib.request
|
|
14
|
+
from urllib.parse import urlparse
|
|
13
15
|
from datetime import datetime, timezone
|
|
14
16
|
from html.parser import HTMLParser
|
|
15
17
|
from html import escape, unescape
|
|
@@ -57,6 +59,137 @@ def now() -> str:
|
|
|
57
59
|
return datetime.now(timezone.utc).isoformat()
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
STARTER_PLAN_SCHEMA_VERSION = "maggie-marketplace-starter-provisioning.v1"
|
|
63
|
+
STARTER_STEP_IDS = (
|
|
64
|
+
"entitlement",
|
|
65
|
+
"repository",
|
|
66
|
+
"site-deploy",
|
|
67
|
+
"hostname",
|
|
68
|
+
"booking",
|
|
69
|
+
"release-readback",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _starter_slug(value: str) -> str:
|
|
74
|
+
slug = str(value or "").strip().lower()
|
|
75
|
+
if not re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", slug):
|
|
76
|
+
raise ValueError("Starter site slug must be 1-63 lowercase letters, numbers, or hyphens")
|
|
77
|
+
return slug
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _starter_public_url(value: str) -> str:
|
|
81
|
+
url = str(value or "").strip()
|
|
82
|
+
parsed = urlparse(url)
|
|
83
|
+
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.fragment:
|
|
84
|
+
raise ValueError("Starter Cal public URL must be an https URL without credentials or a fragment")
|
|
85
|
+
return url
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_starter_plan(
|
|
89
|
+
workspace_id: str,
|
|
90
|
+
merchant_id: str,
|
|
91
|
+
slug: str,
|
|
92
|
+
cal_url: str,
|
|
93
|
+
embed: str = "link",
|
|
94
|
+
branch: str = "main",
|
|
95
|
+
) -> dict:
|
|
96
|
+
workspace = str(workspace_id or "").strip()
|
|
97
|
+
merchant = str(merchant_id or "").strip()
|
|
98
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", workspace) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", merchant):
|
|
99
|
+
raise ValueError("workspace and merchant IDs must use letters, numbers, underscores, or hyphens")
|
|
100
|
+
site_slug = _starter_slug(slug)
|
|
101
|
+
public_url = _starter_public_url(cal_url)
|
|
102
|
+
if embed not in {"link", "iframe"}:
|
|
103
|
+
raise ValueError("Starter booking embed must be link or iframe")
|
|
104
|
+
if not re.fullmatch(r"[A-Za-z0-9._/-]+", branch or ""):
|
|
105
|
+
raise ValueError("Starter branch contains unsupported characters")
|
|
106
|
+
fingerprint = hashlib.sha256(
|
|
107
|
+
"|".join((workspace, merchant, site_slug, public_url, embed, branch)).encode("utf-8")
|
|
108
|
+
).hexdigest()
|
|
109
|
+
return {
|
|
110
|
+
"schemaVersion": STARTER_PLAN_SCHEMA_VERSION,
|
|
111
|
+
"plan": "starter",
|
|
112
|
+
"mode": "dry-run",
|
|
113
|
+
"workspace": {"workspaceId": workspace, "merchantId": merchant},
|
|
114
|
+
"site": {
|
|
115
|
+
"slug": site_slug,
|
|
116
|
+
"hostname": f"{site_slug}.bea.gift",
|
|
117
|
+
"repository": f"bea-merchant-{site_slug}",
|
|
118
|
+
"branch": branch,
|
|
119
|
+
"runtime": "cloudflare-pages-static",
|
|
120
|
+
"database": "none",
|
|
121
|
+
},
|
|
122
|
+
"booking": {"mode": "cal-public", "sourceOfTruth": "cal.com", "publicUrl": public_url, "embed": embed},
|
|
123
|
+
"steps": [
|
|
124
|
+
{"id": step_id, "status": "planned", "idempotencyKey": f"starter:{fingerprint}"}
|
|
125
|
+
for step_id in STARTER_STEP_IDS
|
|
126
|
+
],
|
|
127
|
+
"security": {
|
|
128
|
+
"secretsInArtifact": False,
|
|
129
|
+
"calCredentialsStored": False,
|
|
130
|
+
"databaseRequired": False,
|
|
131
|
+
},
|
|
132
|
+
"nextActions": [
|
|
133
|
+
"Confirm the merchant owns the supplied Cal public booking URL",
|
|
134
|
+
"Create or reconcile the GitHub repository and static site artifact",
|
|
135
|
+
"Deploy the generated site to Cloudflare Pages and read back the hostname",
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_starter_plan(plan: dict) -> list[str]:
|
|
141
|
+
errors = []
|
|
142
|
+
if not isinstance(plan, dict):
|
|
143
|
+
return ["plan must be an object"]
|
|
144
|
+
if plan.get("schemaVersion") != STARTER_PLAN_SCHEMA_VERSION:
|
|
145
|
+
errors.append("unsupported Starter plan schema")
|
|
146
|
+
if plan.get("plan") != "starter" or plan.get("mode") != "dry-run":
|
|
147
|
+
errors.append("Starter plan must be a dry-run")
|
|
148
|
+
site = plan.get("site") if isinstance(plan.get("site"), dict) else {}
|
|
149
|
+
if site.get("runtime") != "cloudflare-pages-static" or site.get("database") != "none":
|
|
150
|
+
errors.append("Starter site must be static Cloudflare Pages with no database")
|
|
151
|
+
booking = plan.get("booking") if isinstance(plan.get("booking"), dict) else {}
|
|
152
|
+
if booking.get("mode") != "cal-public" or booking.get("sourceOfTruth") != "cal.com":
|
|
153
|
+
errors.append("Starter booking must use a public Cal URL")
|
|
154
|
+
try:
|
|
155
|
+
_starter_public_url(booking.get("publicUrl", ""))
|
|
156
|
+
except ValueError as error:
|
|
157
|
+
errors.append(str(error))
|
|
158
|
+
security = plan.get("security") if isinstance(plan.get("security"), dict) else {}
|
|
159
|
+
for field in ("secretsInArtifact", "calCredentialsStored", "databaseRequired"):
|
|
160
|
+
if security.get(field) is not False:
|
|
161
|
+
errors.append(f"security.{field} must be false")
|
|
162
|
+
steps = plan.get("steps")
|
|
163
|
+
if not isinstance(steps, list) or [item.get("id") for item in steps if isinstance(item, dict)] != list(STARTER_STEP_IDS):
|
|
164
|
+
errors.append("Starter steps must be ordered and complete")
|
|
165
|
+
else:
|
|
166
|
+
for item in steps:
|
|
167
|
+
if item.get("status") != "planned" or not re.fullmatch(r"starter:[a-f0-9]{64}", str(item.get("idempotencyKey", ""))):
|
|
168
|
+
errors.append(f"invalid Starter step: {item.get('id')}")
|
|
169
|
+
return errors
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def starter_plan(args) -> int:
|
|
173
|
+
plan = build_starter_plan(
|
|
174
|
+
args.workspace_id,
|
|
175
|
+
args.merchant_id,
|
|
176
|
+
args.site_slug,
|
|
177
|
+
args.cal_url,
|
|
178
|
+
args.embed,
|
|
179
|
+
args.branch,
|
|
180
|
+
)
|
|
181
|
+
errors = validate_starter_plan(plan)
|
|
182
|
+
if errors:
|
|
183
|
+
raise ValueError("invalid generated Starter plan: " + "; ".join(errors))
|
|
184
|
+
rendered = json.dumps(plan, indent=2, ensure_ascii=False) + "\n"
|
|
185
|
+
if args.output:
|
|
186
|
+
output = args.output.resolve()
|
|
187
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
output.write_text(rendered, encoding="utf-8")
|
|
189
|
+
print(rendered, end="")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
60
193
|
def repo_root() -> Path:
|
|
61
194
|
return Path(__file__).resolve().parents[2]
|
|
62
195
|
|
|
@@ -2140,6 +2273,16 @@ def main() -> int:
|
|
|
2140
2273
|
add.add_argument("--sample", type=Path)
|
|
2141
2274
|
add.add_argument("--force", action="store_true")
|
|
2142
2275
|
add.set_defaults(func=lambda args: run_marketplace_job(argparse.Namespace(**{**vars(args), "template_url": args.template_url, "preview_url": args.preview_url})))
|
|
2276
|
+
plan = sub.add_parser("plan", help="build a secret-free Starter provisioning plan")
|
|
2277
|
+
plan.add_argument("--project", type=Path, default=Path.cwd(), help="host project path; planning is read-only")
|
|
2278
|
+
plan.add_argument("--workspace-id", required=True)
|
|
2279
|
+
plan.add_argument("--merchant-id", required=True)
|
|
2280
|
+
plan.add_argument("--site-slug", required=True)
|
|
2281
|
+
plan.add_argument("--cal-url", required=True)
|
|
2282
|
+
plan.add_argument("--embed", choices=("link", "iframe"), default="link")
|
|
2283
|
+
plan.add_argument("--branch", default="main")
|
|
2284
|
+
plan.add_argument("--output", type=Path)
|
|
2285
|
+
plan.set_defaults(func=starter_plan)
|
|
2143
2286
|
val = sub.add_parser("validate")
|
|
2144
2287
|
val.add_argument("template", type=Path)
|
|
2145
2288
|
val.add_argument(
|