@topy-ai/maggie 0.7.53 → 0.7.54
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 +28 -50
- package/README.md +85 -64
- package/bin/maggie.js +51 -7
- package/bundled-references/maggiedash-booking/QA-TEST-PLAN.md +1 -1
- package/bundled-references/maggiedash-booking/RUNTIME-OPERATIONS.md +2 -2
- package/bundled-skills/maggie-blog/SKILL.md +8 -0
- package/bundled-skills/maggie-booking/SKILL.md +20 -12
- package/bundled-skills/maggie-content-localization/SKILL.md +26 -0
- package/bundled-skills/maggie-dash/SKILL.md +28 -2
- package/bundled-skills/maggie-deployment/SKILL.md +27 -0
- package/bundled-skills/maggie-design/SKILL.md +81 -0
- package/bundled-skills/maggie-feedback/SKILL.md +10 -2
- package/bundled-skills/maggie-marketplace/SKILL.md +1 -1
- package/bundled-skills/maggie-marketplace/references/directory-search.md +1 -1
- package/bundled-skills/maggie-marketplace/references/directory.mjs +2 -2
- package/bundled-skills/maggie-mobile-app/SKILL.md +29 -0
- package/bundled-skills/maggie-social-share/SKILL.md +30 -0
- package/bundled-tools/clis/maggie_booking.py +9 -8
- package/bundled-tools/clis/maggie_dash.py +173 -3
- package/bundled-tools/clis/maggie_deployment_ops.py +64 -1
- package/bundled-tools/clis/maggie_design.py +7 -1
- package/bundled-tools/clis/maggie_design_bulk.py +290 -0
- package/bundled-tools/clis/maggie_design_lint.py +131 -1
- package/bundled-tools/clis/maggie_feedback.py +41 -13
- package/bundled-tools/clis/maggie_live_urls.py +138 -0
- package/bundled-tools/clis/maggie_localization.py +3 -0
- package/bundled-tools/clis/maggie_localization_keys.py +151 -0
- package/bundled-tools/clis/maggie_media_generate.py +174 -0
- package/bundled-tools/clis/maggie_mobile.py +84 -1
- package/bundled-tools/clis/maggie_product_release.py +49 -1
- package/bundled-tools/clis/maggie_social_share.py +122 -1
- package/bundled-tools/integrations/maggie-api-pull.md +22 -0
- package/package.json +1 -1
- package/bundled-contracts/maggiedash/execution-board.json +0 -5463
- package/bundled-references/maggiedash-booking/CURRENT-STATE.md +0 -210
- package/bundled-references/maggiedash-booking/EXECUTION-BOARD.json +0 -5463
- package/bundled-references/maggiedash-booking/ORAWELLNESS-INTEGRATION-AUDIT.md +0 -270
- package/bundled-references/maggiedash-booking/PROGRESS.md +0 -279
- package/bundled-references/maggiedash-booking/README.md +0 -288
- package/bundled-references/maggiedash-booking/SKILLS-AND-CLI.md +0 -551
- package/bundled-references/maggiedash-booking/TASK-RUNBOOK.md +0 -107
- package/bundled-references/maggiedash-booking/TASKS.md +0 -178
|
@@ -70,6 +70,32 @@ Norwegian, Finnish, Greek, Hebrew, Czech, Romanian, Hungarian, Thai, Persian,
|
|
|
70
70
|
and Swahili. Use BCP 47 regions such as `es-MX`, `pt-BR`, `ar-SA`, or `ja-JP`
|
|
71
71
|
when market-specific copy is required.
|
|
72
72
|
|
|
73
|
+
## Text keys are site-wide; generated pages need position keys
|
|
74
|
+
|
|
75
|
+
A translation stored under its source text (a text hash) applies to **every
|
|
76
|
+
page** that shows that text. That is right for a shared header and wrong for
|
|
77
|
+
generic phrases on generated pages: "your practice" is 诊所 on a dental page
|
|
78
|
+
and 事务所 on an accountants page, and only one translation can exist per
|
|
79
|
+
text. For generated or templated pages (one page per industry, city or
|
|
80
|
+
product), render the source page, then store each node's translation under a
|
|
81
|
+
**per-page position key** (page + node position, with the source text hash so
|
|
82
|
+
a changed source shows as stale). Keep text keys only as a fallback for text
|
|
83
|
+
that is genuinely the same everywhere.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
maggie localization key-check rows.json [--locale zh-hans]
|
|
87
|
+
maggie localization fragments --html dist --brand "Maggie"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`key-check` reads per-page rows (`page`, `position`, `text`, `translation`)
|
|
91
|
+
and lists source texts translated differently on different pages — each one
|
|
92
|
+
needs position keys. `fragments` reads rendered HTML and lists text split
|
|
93
|
+
around an inline brand span into one- or two-word pieces ("Can", "No.",
|
|
94
|
+
"What") that cannot be translated alone; translate the whole sentence with
|
|
95
|
+
the span as a placeholder instead. Agents writing in parallel must use one
|
|
96
|
+
glossary for short shared fragments, or the same piece gets different
|
|
97
|
+
translations from different writers.
|
|
98
|
+
|
|
73
99
|
## Email and lifecycle email
|
|
74
100
|
|
|
75
101
|
Localized email is a content surface with its own checks. Resolve the
|
|
@@ -31,9 +31,35 @@ must already be configured for the project owner.
|
|
|
31
31
|
maggie dash install --project . --confirm
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
This installs **only the core**: the dashboard, the auth boundary
|
|
35
|
+
(email/password sign-in, first-owner registration, password reset), the
|
|
36
|
+
`/_maggie/admin` route, navigation and the core schema. It then reads the host
|
|
37
|
+
(framework, existing sign-in, database, payments, whether the site sells
|
|
38
|
+
services or publishes a blog, locales) and prints `recommendations`, one per
|
|
39
|
+
optional module or companion skill with a reason. Install a module only after
|
|
40
|
+
the user agrees:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
maggie dash recommend --project . # analysis only, writes nothing
|
|
44
|
+
maggie dash add booking --project . --confirm # same as: maggie booking install --confirm
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Booking is the optional code module MaggieDash ships (workspace, backend,
|
|
48
|
+
schema, Stripe adapter, worker, env example). Blog, localization, social
|
|
49
|
+
sharing and ops are Maggie skills that use the core panels; the
|
|
50
|
+
recommendations name them. Never pass `--with booking` or `--all-modules` for
|
|
51
|
+
a site that has not asked for bookings: a content-only site must not receive
|
|
52
|
+
payment and booking files. A release without the module split (MaggieDash
|
|
53
|
+
0.2.13 or older) still installs everything and says so in `notices`.
|
|
54
|
+
|
|
55
|
+
Server settings in the Astro scaffold are read at run time
|
|
56
|
+
(`src/lib/maggie/env.ts`). Never read a secret through `import.meta.env.X` in
|
|
57
|
+
host code: Vite inlines it into the build output.
|
|
58
|
+
|
|
34
59
|
The command is idempotent, preserves existing files unless `--force` is
|
|
35
|
-
explicitly supplied, and records the resolved revision
|
|
36
|
-
`.maggie/dash-install.json
|
|
60
|
+
explicitly supplied, and records the resolved revision and installed
|
|
61
|
+
`modules` in `.maggie/dash-install.json` (a later `install` keeps refreshing
|
|
62
|
+
modules that were added). It never accepts or stores a GitHub token as a
|
|
37
63
|
CLI argument. For local development or a pinned checkout, use
|
|
38
64
|
`--source /path/to/MaggieDash` or `--source URL --ref REF`.
|
|
39
65
|
|
|
@@ -25,6 +25,33 @@ metadata:
|
|
|
25
25
|
- Product CLI and MCP packages: `maggie product release-check|smoke|mcp-check`
|
|
26
26
|
([product release](../maggie-assistant-tools/references/product-release.md)).
|
|
27
27
|
|
|
28
|
+
## Downstream surfaces after a major product release
|
|
29
|
+
|
|
30
|
+
A product change is not released while an agent-facing surface still
|
|
31
|
+
describes the old product. After any major change to the app, the dashboard or
|
|
32
|
+
the backend API (a new feature or channel, a renamed tab, a changed rule or
|
|
33
|
+
limit), walk every surface below and either update it or record "no change":
|
|
34
|
+
|
|
35
|
+
| Surface | Check | Evidence |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Remote MCP tools | New or changed API params and response fields have tools; descriptions use the product's words | `tools/list` on the live server: count and the new names |
|
|
38
|
+
| Agent system prompts | In-app assistant and any hosted bridge/agent prompt name tabs, channels and rules as the app does | One question about the change answered correctly |
|
|
39
|
+
| Skills catalog and marketplace | Grep skills for the old wording; `mcp_tools` lists only real tool names; any tool-name allowlist is in sync with the tool source | Validator passes; marketplace repo pushed; agent hosts re-synced and recycled |
|
|
40
|
+
| CLI and MCP npm packages | Commands/columns for the new data; both package versions bumped together | `npm view <pkg> version` |
|
|
41
|
+
| Third-party plugins (ChatGPT, IDE) | Usually nothing if they use the remote MCP; re-review when tool names or auth change | Plugin lists the new tools |
|
|
42
|
+
| SDK / API client types | New response fields typed | Workspace typecheck |
|
|
43
|
+
| Mobile app | Web deploy plus OTA when the native fingerprint matches; a new build when it does not | Fingerprint compare; update id |
|
|
44
|
+
| Assistant memory | A new channel or surface gets its own memory channel | Memory read for that channel |
|
|
45
|
+
| Docs | PRD task list, progress log and the deployment runbook | — |
|
|
46
|
+
| Preview host protection | Before (or after) adding basic auth to a preview host, every workflow, script or webhook that sends to it is repointed to production or sends the preview's basic-auth credentials from a secret — a bearer token does not pass basic auth | `maggie deployment preview-auth-check --host preview.example.com --repo . --live` passes; the next run of each listed workflow is green and the public surfaces it feeds show the new version |
|
|
47
|
+
| npm staged publishes | A just-published version can stay invisible as `latest` for minutes; do not report it until it is. `Cannot publish over previously staged version` means the earlier attempt was already accepted, not a failure | `maggie product published --package-name NAME --version X --publish-log publish.log` reports `live` |
|
|
48
|
+
|
|
49
|
+
Run the walk in the release order (schema → backend → dashboard → app →
|
|
50
|
+
MCP/agent → bridge → npm → skills) and record which surfaces changed in the
|
|
51
|
+
release record. Keep publish tokens in an env file and pass them through a
|
|
52
|
+
temporary npmrc (`NPM_CONFIG_USERCONFIG`) that is deleted afterwards; never
|
|
53
|
+
print them.
|
|
54
|
+
|
|
28
55
|
## Edge and rendered canary
|
|
29
56
|
|
|
30
57
|
After staging or production deployment, verify that the edge serves the
|
|
@@ -262,6 +262,59 @@ maggie design step author-<id> --project . \
|
|
|
262
262
|
An author plan is not complete until every `requiredSteps` entry has matching
|
|
263
263
|
evidence and the explicit approval step is recorded.
|
|
264
264
|
|
|
265
|
+
### Bulk author mode (many similar pages)
|
|
266
|
+
|
|
267
|
+
For a family of similar pages (one use-case page per industry, one page per
|
|
268
|
+
city), use one bulk job instead of hand-built scripts:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
maggie design bulk plan --project . --targets targets.json --brief BRIEF.md --facts FACTS.md \
|
|
272
|
+
--validator '["python3", "scripts/validate-page.py"]' --check links --check prices --confirm
|
|
273
|
+
maggie design bulk next <job> --stage write # next batch (default 8) with each page's currentHash
|
|
274
|
+
maggie design bulk record <job> --target ID --stage written --expect-hash HASH
|
|
275
|
+
maggie design bulk validate <job> # project validator + built-in checks
|
|
276
|
+
maggie design bulk next <job> --stage picture # only pages validated and unchanged since
|
|
277
|
+
maggie design bulk record <job> --target ID --stage pictured
|
|
278
|
+
maggie design bulk status <job>
|
|
279
|
+
maggie design bulk seed <job> --confirm # only when every page passed
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The job enforces what goes wrong in a parallel run: a picture is drawn only
|
|
283
|
+
for a page that validated and has not changed since (a picture worker must
|
|
284
|
+
never rewrite a page a writer is still editing); a writer records with the
|
|
285
|
+
hash it read and is refused if the page changed meanwhile (re-read, re-apply,
|
|
286
|
+
record again); changing the brief, facts or validator mid-run sends every
|
|
287
|
+
earlier pass back to `revalidate`; nothing is seeded until all pages are
|
|
288
|
+
validated and pictured. Built-in checks: every internal link points at a page
|
|
289
|
+
of the job or a `--known-routes` entry, and every price appears in the facts
|
|
290
|
+
file. The project validator checks structure, counts, facts and translation
|
|
291
|
+
coverage. Share one glossary between writers for short repeated fragments
|
|
292
|
+
(see maggie-content-localization, position keys).
|
|
293
|
+
|
|
294
|
+
### Page pictures: choose the engine before a large run
|
|
295
|
+
|
|
296
|
+
`maggie media generate` draws one picture with either engine; `maggie media
|
|
297
|
+
engine-plan` states time and cost first. Show the plan and wait for a yes
|
|
298
|
+
before a run of more than a handful of pictures.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
maggie media engine-plan --count 510 --size 2400x1000 --gemini-unit-cost <price>
|
|
302
|
+
maggie media generate --engine gemini --size 2400x1000 --prompt "..." --output hero.png --confirm
|
|
303
|
+
maggie media generate --engine codex --image screen.png --prompt "Redraw as a clean step picture" --output step.png --confirm
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
| Engine | Billing | Speed (observed) | Reference pictures |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| `codex` | the ChatGPT plan's quota via the local `codex` CLI, no per-picture charge | about 75 s | yes (`--image`, before `--`) |
|
|
309
|
+
| `gemini` | the project's own `GEMINI_API_KEY` (its Google account) | about 19 s at 2K | no |
|
|
310
|
+
|
|
311
|
+
For 510 pictures that is hours versus minutes. Gemini takes the nearest
|
|
312
|
+
aspect ratio and a size from the target (2400x1000 → 21:9 at 2K; 1K up to
|
|
313
|
+
1300 px, 2K up to 2600 px, else 4K); crop to the exact size and convert to
|
|
314
|
+
WebP afterwards. `engine-plan` leaves the Gemini cost empty until you pass
|
|
315
|
+
`--gemini-unit-cost` from the current price list. Only transient Gemini
|
|
316
|
+
errors (429, 5xx, network) are retried.
|
|
317
|
+
|
|
265
318
|
## Content UI initialization
|
|
266
319
|
|
|
267
320
|
Use content UI initialization when `maggie-blog` or
|
|
@@ -398,6 +451,34 @@ repeated sheet and modal changes. Add `maggie design textinput-check`: a
|
|
|
398
451
|
after collapsing; see the `chat-composer` pattern). `// maggie-allow-textinput`
|
|
399
452
|
marks a reviewed exception.
|
|
400
453
|
|
|
454
|
+
Closing a modal sheet is the third iOS-only trap. Closing a sheet and, in the
|
|
455
|
+
same tick, showing a toast, refreshing or writing query data, navigating,
|
|
456
|
+
opening another sheet, or changing what the closing sheet shows can leave the
|
|
457
|
+
sheet's transparent window on top of the app: it looks fine but takes no
|
|
458
|
+
touches until the app is killed. Close first, then do the rest once it has
|
|
459
|
+
slid away:
|
|
460
|
+
|
|
461
|
+
export const SHEET_CLOSE_MS = 450; // the slide-out duration
|
|
462
|
+
export function afterSheetCloses(fn: () => void) {
|
|
463
|
+
if (Platform.OS === "web") fn(); else setTimeout(fn, SHEET_CLOSE_MS);
|
|
464
|
+
}
|
|
465
|
+
// setSheet(null); afterSheetCloses(() => { toast(...); refresh(); });
|
|
466
|
+
|
|
467
|
+
maggie design sheet-check --project apps/app
|
|
468
|
+
|
|
469
|
+
`sheet-check` finds closes (`setSheet(null)`, `setXOpen(false)`, `onClose()`,
|
|
470
|
+
and the setter of any state used in a sheet's `open`/`visible` prop) followed
|
|
471
|
+
in the same block by a toast, a refetch/invalidate/`setQueryData`,
|
|
472
|
+
`router.*`/`*.open(…)` or another sheet opening; work inside
|
|
473
|
+
`afterSheetCloses`, `setTimeout`, `requestAnimationFrame`, a local helper that
|
|
474
|
+
wraps them, or after `await new Promise(r => setTimeout(r, …))` is deferred and
|
|
475
|
+
passes. Errors fail the check. Warnings: a parent callback (`onDone()`) right
|
|
476
|
+
after a close, and a sheet whose content reads the same state as its `open`
|
|
477
|
+
prop (`open={Boolean(item)} title={item?.label}`), which empties the sheet
|
|
478
|
+
while it slides away — keep a separate open flag. `// maggie-allow-sheet-close`
|
|
479
|
+
marks a reviewed exception. Keep it with pressable-check, safe-area-check and
|
|
480
|
+
textinput-check in the app's lint loop.
|
|
481
|
+
|
|
401
482
|
### Onboarding wizards
|
|
402
483
|
|
|
403
484
|
Use one pattern for multi-step setup (brand, logo, voice, posting hours, and
|
|
@@ -223,10 +223,18 @@ leaving contradictory claims current.
|
|
|
223
223
|
The CLI never submits automatically. After reviewing the draft:
|
|
224
224
|
|
|
225
225
|
```bash
|
|
226
|
-
maggie feedback submit .maggie/feedback/<feedback-id>.json
|
|
227
|
-
--endpoint https://feedback.noblox.app/api/feedback --confirm
|
|
226
|
+
maggie feedback submit .maggie/feedback/<feedback-id>.json --confirm
|
|
228
227
|
```
|
|
229
228
|
|
|
229
|
+
Without `--endpoint` the draft goes to `https://feedback.noblox.app/api/feedback`
|
|
230
|
+
and, only when that host fails before storing it (no connection, timeout, or a
|
|
231
|
+
5xx such as Cloudflare's 526), to `https://feedback.cmo.so/api/feedback`. Both
|
|
232
|
+
accept the same contract and de-duplicate on `feedbackId`, so a retry never
|
|
233
|
+
creates a second record. A 4xx is the host rejecting the draft and is not
|
|
234
|
+
retried elsewhere. The saved `submission` records the `endpoint` that accepted
|
|
235
|
+
the draft and any `failedEndpoints`. Repeat `--endpoint URL` to give your own
|
|
236
|
+
ordered list. Do not hand-edit drafts to point at a different host.
|
|
237
|
+
|
|
230
238
|
Only HTTPS endpoints are accepted. Project paths, file contents, secrets, and
|
|
231
239
|
logs are excluded by default. `--allow-project-context` is an explicit opt-in;
|
|
232
240
|
even then, send only a short public-safe context note. After submission, the
|
|
@@ -231,7 +231,7 @@ imported by the server and every node script (town pages, sitemaps, maps) —
|
|
|
231
231
|
never a copy. It parses UK post towns and US "City, ST", falls back to the
|
|
232
232
|
town field plus the state from the ZIP prefix when a US profile saved only
|
|
233
233
|
the street, keeps non-US places in a US market ("Kingston, Jamaica"), and
|
|
234
|
-
gives a normalised search key so "
|
|
234
|
+
gives a normalised search key so "lakeview" finds "Lake View Spa", ranked
|
|
235
235
|
exact → prefix → word prefix → contains. See [directory towns and
|
|
236
236
|
search](references/directory-search.md) for the generated-column SQL.
|
|
237
237
|
|
|
@@ -27,7 +27,7 @@ counts per town.
|
|
|
27
27
|
|
|
28
28
|
## Name search
|
|
29
29
|
|
|
30
|
-
People type "
|
|
30
|
+
People type "lakeview" for "Lake View Spa". Compare a normalised key —
|
|
31
31
|
lower case, accents removed, letters and digits only (`searchKey`) — as well
|
|
32
32
|
as the plain `ILIKE` match, and rank exact and prefix matches first
|
|
33
33
|
(`nameMatchRank`). In Postgres keep the key in a generated column with an
|
|
@@ -85,8 +85,8 @@ export function parseTown(address, market, locality, postcode) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Name search key: lower case, accents removed, letters and digits only, so "
|
|
89
|
-
* "
|
|
88
|
+
* Name search key: lower case, accents removed, letters and digits only, so "lakeview", "Lake-View" and
|
|
89
|
+
* "Lake View Spa" meet. Store it in a generated column (see directory-search.md) and compare both forms.
|
|
90
90
|
* @param {string | null | undefined} value
|
|
91
91
|
*/
|
|
92
92
|
export const searchKey = (value) => (value || "").normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
@@ -129,6 +129,35 @@ always restores the working copies — checked by hash — even when a step
|
|
|
129
129
|
fails. A mismatch with the build's own files means the change needs a new
|
|
130
130
|
native build. `MAGGIE_MOBILE_EAS` overrides the `npx eas-cli` invocation.
|
|
131
131
|
|
|
132
|
+
### Monorepos: publish only to a runtime an installed build has
|
|
133
|
+
|
|
134
|
+
In a pnpm monorepo the fingerprint also hashes pnpm's virtual-store paths
|
|
135
|
+
(`node_modules/.pnpm/<pkg>@<version>_<peer hash>`), so a dependency change in
|
|
136
|
+
**any** workspace package (React added to the website) can move the app's
|
|
137
|
+
runtime. `eas update` then publishes to a runtime no build has — silently.
|
|
138
|
+
`ignorePaths` and `fileHookTransform` cannot fix it: `@expo/fingerprint` adds
|
|
139
|
+
each native module folder by its real path. Keep the installed builds'
|
|
140
|
+
runtimes in a file and publish through the guard:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{"ios": [{"build": 7, "eas_build_id": "…", "runtime": "19d7a260e8e2…"}], "android": []}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
maggie mobile ota-guard --project . --app-dir apps/app --platform ios --runtimes scripts/ota-runtimes.json
|
|
148
|
+
maggie mobile ota-guard … --publish --message "Fix copy" --confirm
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It computes the fingerprint (`expo-updates fingerprint:generate`), refuses
|
|
152
|
+
when no installed build has it — naming the two ways out (a native build, or
|
|
153
|
+
publishing from a worktree with the build's lockfile and workspace file after a
|
|
154
|
+
frozen install) — and with `--publish --confirm` runs `eas update` and checks
|
|
155
|
+
that the published runtime is the one it checked. Add a build's runtime to the
|
|
156
|
+
file when it ships; drop it when nobody runs it. At the next native build set
|
|
157
|
+
an explicit `runtimeVersion` with a native-change check, so unrelated
|
|
158
|
+
workspace changes cannot move it. `preflight` warns about a fingerprint
|
|
159
|
+
runtime in a pnpm workspace.
|
|
160
|
+
|
|
132
161
|
Show the plan's `costNotice` and wait for confirmation before step 1.
|
|
133
162
|
|
|
134
163
|
## 5. Store-review rules
|
|
@@ -36,6 +36,36 @@ URL, channel limits, UTM links, approval state, and provider post state.
|
|
|
36
36
|
draft without `workspace_domain`. Apply the same rule in source selection:
|
|
37
37
|
an account that manages several sites must not pick another site's
|
|
38
38
|
articles for this workspace's plan.
|
|
39
|
+
- **Live links only.** Before planning posts from articles, drop the ones
|
|
40
|
+
whose link is gone. A site that moved platforms leaves old URLs that answer
|
|
41
|
+
404; posts scheduled with them reach readers as dead links.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
maggie tool maggie_social_share.py link-check .maggie/candidate-articles.json
|
|
45
|
+
maggie tool maggie_social_share.py plan content.json --workspace-domain example.com --check-link
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Only a definite 404 or 410 (also after redirects) drops an article. Blocked
|
|
49
|
+
(403/429), failing (5xx) or unreachable pages are kept, so a firewall or an
|
|
50
|
+
outage cannot empty the list; verdicts are cached for 6 hours
|
|
51
|
+
(`.maggie/social-link-cache.json`, `--ttl-hours`). Prefer the URL the site
|
|
52
|
+
reports for a delivered post over an older stored one (maggie-blog, "Where a
|
|
53
|
+
delivered post went live").
|
|
54
|
+
- **One picture per idea.** Posts written together for several channels from
|
|
55
|
+
one idea share one picture on every channel, and a later picture change
|
|
56
|
+
(redraw, replace, rewrite of an idea that had one) is applied to every
|
|
57
|
+
channel of that idea, not only some. Write all channels of an idea through
|
|
58
|
+
the same path — an article-sourced channel (often LinkedIn) must join the
|
|
59
|
+
other channels' idea instead of being written separately. Check with:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
maggie tool maggie_social_share.py pictures .maggie/social-drafts.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It groups drafts by `idea_id` and fails a group whose channels carry
|
|
66
|
+
different pictures or one without; a channel whose picture a person chose
|
|
67
|
+
records `image_source: "user"`. `validate` applies the same rule to the
|
|
68
|
+
channels of one draft.
|
|
39
69
|
- **Image posts by default.** When a post has its own image (`image_url`),
|
|
40
70
|
each channel gets `publish_mode: "image"` with the link in the text;
|
|
41
71
|
`link_preview` is the default only without an image. A person may switch a
|
|
@@ -32,11 +32,9 @@ DEFAULT_OPS_EVIDENCE_CONTRACT = CONTRACTS_ROOT / "maggiedash" / "booking-ops-evi
|
|
|
32
32
|
DEFAULT_BROWSER_EVIDENCE_CONTRACT = CONTRACTS_ROOT / "maggiedash" / "booking-browser-evidence-v2.json"
|
|
33
33
|
DEFAULT_STRIPE_CONTRACT = CONTRACTS_ROOT / "maggiedash" / "stripe-booking-capabilities-v1.json"
|
|
34
34
|
DEFAULT_VOUCHER_CONTRACT = CONTRACTS_ROOT / "maggiedash" / "booking-voucher-lifecycle-v1.json"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else REPO_ROOT / "docs" / "maggiedash-booking" / "EXECUTION-BOARD.json"
|
|
39
|
-
)
|
|
35
|
+
# The Booking delivery board is a maintainer document of the Maggie source
|
|
36
|
+
# repository; the npm package does not ship it (config/release-privacy.json).
|
|
37
|
+
EXECUTION_BOARD = REPO_ROOT / "docs" / "maggiedash-booking" / "EXECUTION-BOARD.json"
|
|
40
38
|
|
|
41
39
|
|
|
42
40
|
def load_json(path: Path) -> dict:
|
|
@@ -57,6 +55,8 @@ def load_gate_json(path: Path, label: str) -> tuple[dict | None, str | None]:
|
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
def load_execution_board(path: Path = EXECUTION_BOARD) -> dict:
|
|
58
|
+
if not path.is_file():
|
|
59
|
+
raise RuntimeError("the Booking delivery board lives in the Maggie source repository (docs/maggiedash-booking/EXECUTION-BOARD.json); pass --board PATH")
|
|
60
60
|
board = load_json(path)
|
|
61
61
|
if board.get("schemaVersion") != "maggiedash-booking-execution-board.v1":
|
|
62
62
|
raise RuntimeError("unsupported Booking execution board schema")
|
|
@@ -661,9 +661,9 @@ def booking_setup_result(root: Path, environment: dict[str, str] | None = None)
|
|
|
661
661
|
stripe_webhook = has("STRIPE_WEBHOOK_SECRET")
|
|
662
662
|
stripe_complete = stripe_secret and stripe_webhook
|
|
663
663
|
stripe_partial = stripe_secret != stripe_webhook
|
|
664
|
-
stripe_key_help = "STRIPE_SECRET_KEY (or
|
|
664
|
+
stripe_key_help = "STRIPE_SECRET_KEY (or the legacy alias ST_SECRECT_KEY)"
|
|
665
665
|
stripe_detail = (
|
|
666
|
-
"Recognizes
|
|
666
|
+
"Recognizes the legacy ST_SECRECT_KEY alias; prefer STRIPE_SECRET_KEY for new deployments. Verify the connection in Booking Overview."
|
|
667
667
|
if stripe_legacy_alias and stripe_complete
|
|
668
668
|
else f"Both {stripe_key_help} and STRIPE_WEBHOOK_SECRET are required for online payments."
|
|
669
669
|
if stripe_partial
|
|
@@ -872,7 +872,8 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
872
872
|
return 2
|
|
873
873
|
root = project_path(args)
|
|
874
874
|
dash = Path(__file__).with_name("maggie_dash.py")
|
|
875
|
-
|
|
875
|
+
# Booking is an optional MaggieDash module: install the core and Booking.
|
|
876
|
+
command = [sys.executable, str(dash), "install", "--project", str(root), "--host", args.host, "--with", "booking"]
|
|
876
877
|
if args.source:
|
|
877
878
|
command.extend(["--source", args.source])
|
|
878
879
|
if args.ref:
|
|
@@ -252,7 +252,7 @@ def merge_astro_middleware(path: Path) -> dict[str, object]:
|
|
|
252
252
|
return {"status": "updated", "reason": "inserted idempotent Maggie rewrite block"}
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
def manifest_host_file_pairs(source: Path, manifest: dict[str, object], root: Path, framework: str | None) -> list[tuple[Path, Path, Path]]:
|
|
255
|
+
def manifest_host_file_pairs(source: Path, manifest: dict[str, object], root: Path, framework: str | None, required: bool = True) -> list[tuple[Path, Path, Path]]:
|
|
256
256
|
"""Return non-destructive host files for a declared framework adapter."""
|
|
257
257
|
if not framework:
|
|
258
258
|
return []
|
|
@@ -260,6 +260,8 @@ def manifest_host_file_pairs(source: Path, manifest: dict[str, object], root: Pa
|
|
|
260
260
|
if not isinstance(bootstrap, dict):
|
|
261
261
|
return []
|
|
262
262
|
spec = bootstrap.get(framework)
|
|
263
|
+
if not isinstance(spec, dict) and not required:
|
|
264
|
+
return []
|
|
263
265
|
if not isinstance(spec, dict):
|
|
264
266
|
raise RuntimeError(
|
|
265
267
|
f"MaggieDash distribution at {source} does not provide a {framework} host bootstrap; "
|
|
@@ -288,6 +290,128 @@ def manifest_host_file_pairs(source: Path, manifest: dict[str, object], root: Pa
|
|
|
288
290
|
return pairs
|
|
289
291
|
|
|
290
292
|
|
|
293
|
+
CORE_MODULE = "core"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def module_names(manifest: dict[str, object]) -> list[str]:
|
|
297
|
+
modules = manifest.get("modules")
|
|
298
|
+
return sorted(modules) if isinstance(modules, dict) else []
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def effective_manifest(manifest: dict[str, object], modules: list[str]) -> dict[str, object]:
|
|
302
|
+
"""Core entries plus the root files and workspaces of the chosen modules."""
|
|
303
|
+
result = dict(manifest)
|
|
304
|
+
specs = manifest.get("modules") if isinstance(manifest.get("modules"), dict) else {}
|
|
305
|
+
root_files = list(manifest.get("rootFiles", []))
|
|
306
|
+
workspaces = list(manifest.get("workspaces", []))
|
|
307
|
+
for name in modules:
|
|
308
|
+
spec = specs.get(name) or {}
|
|
309
|
+
root_files.extend(spec.get("rootFiles", []))
|
|
310
|
+
workspaces.extend(spec.get("workspaces", []))
|
|
311
|
+
result["rootFiles"] = root_files
|
|
312
|
+
result["workspaces"] = workspaces
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def resolve_modules(manifest: dict[str, object], requested: list[str], previous: list[str]) -> list[str]:
|
|
317
|
+
available = module_names(manifest)
|
|
318
|
+
if requested and not available:
|
|
319
|
+
# A release without the module split installs everything already.
|
|
320
|
+
return []
|
|
321
|
+
unknown = sorted(set(requested) - set(available))
|
|
322
|
+
if unknown:
|
|
323
|
+
raise RuntimeError(f"unknown MaggieDash module(s): {', '.join(unknown)}; this release offers: {', '.join(available) or 'none'}")
|
|
324
|
+
# Re-running install keeps refreshing modules that were added before.
|
|
325
|
+
return sorted((set(requested) | set(previous)) & set(available))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def installed_state(root: Path) -> dict[str, object]:
|
|
329
|
+
try:
|
|
330
|
+
value = json.loads((root / ".maggie" / "dash-install.json").read_text(encoding="utf-8"))
|
|
331
|
+
except (OSError, json.JSONDecodeError):
|
|
332
|
+
return {}
|
|
333
|
+
return value if isinstance(value, dict) else {}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def package_dependencies(root: Path) -> set[str]:
|
|
337
|
+
try:
|
|
338
|
+
value = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
339
|
+
except (OSError, json.JSONDecodeError):
|
|
340
|
+
return set()
|
|
341
|
+
return set(value.get("dependencies") or {}) | set(value.get("devDependencies") or {})
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
SERVICE_WORDS = re.compile(r"\b(book (?:now|an? (?:appointment|session|treatment|table))|appointments?|treatments?|reservations?|book online|opening hours)\b", re.I)
|
|
345
|
+
LOCALE_DIR = re.compile(r"^(?:[a-z]{2}(?:-[a-z]{2,4})?)$")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def analyse_host(root: Path, framework: str | None) -> dict[str, object]:
|
|
349
|
+
"""What the host already has, read from files only (nothing is executed)."""
|
|
350
|
+
deps = package_dependencies(root)
|
|
351
|
+
auth = sorted(name for name in deps if name in {"firebase", "firebase-admin", "next-auth", "lucia", "better-auth", "@supabase/supabase-js", "@supabase/ssr", "auth-astro"} or name.startswith(("@clerk/", "@auth/")))
|
|
352
|
+
database = sorted(name for name in deps if name in {"pg", "postgres", "@neondatabase/serverless", "drizzle-orm", "prisma", "@prisma/client", "mysql2", "better-sqlite3", "@libsql/client", "kysely"})
|
|
353
|
+
for config in ("wrangler.json", "wrangler.jsonc", "wrangler.toml"):
|
|
354
|
+
path = root / config
|
|
355
|
+
if path.is_file() and "d1_databases" in path.read_text(encoding="utf-8", errors="replace"):
|
|
356
|
+
database.append("cloudflare-d1")
|
|
357
|
+
payments = sorted(name for name in deps if name in {"stripe", "@stripe/stripe-js", "@paypal/checkout-server-sdk", "@lemonsqueezy/lemonsqueezy.js"})
|
|
358
|
+
source = root / "src"
|
|
359
|
+
service_hits: list[str] = []
|
|
360
|
+
if source.is_dir():
|
|
361
|
+
for path in sorted(source.rglob("*")):
|
|
362
|
+
if len(service_hits) >= 5:
|
|
363
|
+
break
|
|
364
|
+
if path.is_file() and path.suffix in {".astro", ".md", ".mdx", ".tsx", ".jsx", ".ts", ".json", ".html", ".vue", ".svelte"} and "_maggie" not in path.parts and "maggie" not in path.parts:
|
|
365
|
+
try:
|
|
366
|
+
if SERVICE_WORDS.search(path.read_text(encoding="utf-8", errors="replace")[:200_000]):
|
|
367
|
+
service_hits.append(str(path.relative_to(root)))
|
|
368
|
+
except OSError:
|
|
369
|
+
continue
|
|
370
|
+
blog = any((root / candidate).exists() for candidate in ("src/content/blog", "src/content/posts", "src/pages/blog", "src/pages/blog.astro", "src/pages/posts", "content/blog", "app/blog"))
|
|
371
|
+
locales: set[str] = set()
|
|
372
|
+
for pages in (root / "src" / "pages", root / "app"):
|
|
373
|
+
if pages.is_dir():
|
|
374
|
+
locales.update(child.name for child in pages.iterdir() if child.is_dir() and LOCALE_DIR.match(child.name) and child.name not in {"api", "og"})
|
|
375
|
+
astro_config = next((path for path in root.glob("astro.config.*")), None)
|
|
376
|
+
if astro_config:
|
|
377
|
+
match = re.search(r"locales\s*:\s*\[([^\]]+)\]", astro_config.read_text(encoding="utf-8", errors="replace"))
|
|
378
|
+
if match:
|
|
379
|
+
locales.update(re.findall(r"[\"']([A-Za-z-]+)[\"']", match.group(1)))
|
|
380
|
+
return {
|
|
381
|
+
"framework": framework,
|
|
382
|
+
"auth": auth,
|
|
383
|
+
"database": sorted(set(database)),
|
|
384
|
+
"payments": payments,
|
|
385
|
+
"sellsServices": bool(service_hits),
|
|
386
|
+
"serviceEvidence": service_hits,
|
|
387
|
+
"blog": blog,
|
|
388
|
+
"locales": sorted(locales),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def recommendations(analysis: dict[str, object], available: list[str], installed: list[str]) -> list[dict[str, str]]:
|
|
393
|
+
result: list[dict[str, str]] = []
|
|
394
|
+
if "booking" in available and "booking" not in installed:
|
|
395
|
+
if analysis.get("sellsServices"):
|
|
396
|
+
notes = []
|
|
397
|
+
if not any(name in {"pg", "postgres", "@neondatabase/serverless"} for name in analysis.get("database", [])):
|
|
398
|
+
notes.append("it needs PostgreSQL, which this project does not use yet")
|
|
399
|
+
if analysis.get("auth"):
|
|
400
|
+
notes.append(f"the site already signs people in with {', '.join(analysis['auth'])}; MaggieDash keeps its own staff sign-in")
|
|
401
|
+
reason = "The site offers appointments or services (" + ", ".join(analysis.get("serviceEvidence", [])[:3]) + ")" + ("; " + "; ".join(notes) if notes else "")
|
|
402
|
+
result.append({"kind": "module", "module": "booking", "recommended": "yes", "reason": reason, "command": "maggie dash add booking --confirm"})
|
|
403
|
+
else:
|
|
404
|
+
result.append({"kind": "module", "module": "booking", "recommended": "no", "reason": "No appointments or bookable services found in src/; add it only if the business takes bookings.", "command": "maggie dash add booking --confirm"})
|
|
405
|
+
if analysis.get("blog"):
|
|
406
|
+
result.append({"kind": "skill", "skill": "maggie-blog", "recommended": "yes", "reason": "The site has a blog; manage posts from the dashboard Blog panel.", "command": "use the maggie-blog skill"})
|
|
407
|
+
result.append({"kind": "skill", "skill": "maggie-social-share", "recommended": "maybe", "reason": "Blog posts can be shared to social channels after publishing.", "command": "use the maggie-social-share skill"})
|
|
408
|
+
else:
|
|
409
|
+
result.append({"kind": "skill", "skill": "maggie-blog-bootstrap", "recommended": "maybe", "reason": "No blog found; add one only if the site will publish articles.", "command": "use the maggie-blog-bootstrap skill"})
|
|
410
|
+
if len(analysis.get("locales", [])) > 1:
|
|
411
|
+
result.append({"kind": "skill", "skill": "maggie-content-localization", "recommended": "yes", "reason": f"The site serves {', '.join(analysis['locales'])}; keep translations in the Languages panel.", "command": "use the maggie-content-localization skill"})
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
291
415
|
def command_install_diff(source: Path, manifest: dict[str, object], target: Path, compare_dir: Path | None, force: bool, host_pairs: list[tuple[Path, Path, Path]] | None = None, host_framework: str | None = None) -> dict[str, object]:
|
|
292
416
|
pairs = manifest_file_pairs(source, manifest, target)
|
|
293
417
|
root_pairs = manifest_root_file_pairs(source, manifest, target.parents[1])
|
|
@@ -386,7 +510,16 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
386
510
|
raise RuntimeError("unsupported MaggieDash distribution schema")
|
|
387
511
|
if manifest.get("target") != "_maggie/admin" and args.target == "_maggie/admin":
|
|
388
512
|
raise RuntimeError("MaggieDash manifest target is not ./_maggie/admin")
|
|
513
|
+
previous = installed_state(root)
|
|
514
|
+
legacy = not module_names(manifest)
|
|
515
|
+
modules = resolve_modules(manifest, list(getattr(args, "with_modules", None) or []) + (module_names(manifest) if getattr(args, "all_modules", False) else []), [name for name in previous.get("modules", []) if name != CORE_MODULE])
|
|
516
|
+
module_specs = manifest.get("modules") if isinstance(manifest.get("modules"), dict) else {}
|
|
517
|
+
manifest = effective_manifest(manifest, modules)
|
|
389
518
|
host_pairs = manifest_host_file_pairs(source, manifest, root, host_framework)
|
|
519
|
+
for name in modules:
|
|
520
|
+
host_pairs += manifest_host_file_pairs(source, module_specs[name], root, host_framework, required=False)
|
|
521
|
+
analysis = analyse_host(root, host_framework)
|
|
522
|
+
advice = recommendations(analysis, module_names({"modules": module_specs}), modules)
|
|
390
523
|
compare_dir = None
|
|
391
524
|
if args.existing_dir:
|
|
392
525
|
compare_dir = Path(args.existing_dir).expanduser()
|
|
@@ -394,7 +527,9 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
394
527
|
compare_dir = root / compare_dir
|
|
395
528
|
compare_dir = compare_dir.resolve()
|
|
396
529
|
if args.dry_run or args.diff:
|
|
397
|
-
|
|
530
|
+
plan = command_install_diff(source, manifest, target, compare_dir, args.force, host_pairs, host_framework)
|
|
531
|
+
plan.update({"modules": [CORE_MODULE, *modules] if not legacy else ["all (release without modules)"], "availableModules": module_names({"modules": module_specs}), "host": analysis, "recommendations": advice})
|
|
532
|
+
emit(plan)
|
|
398
533
|
return 0
|
|
399
534
|
installed: list[str] = []
|
|
400
535
|
preserved: list[str] = []
|
|
@@ -471,18 +606,39 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
471
606
|
"hostPreserved": sorted(set(host_preserved)),
|
|
472
607
|
"hostUpdated": sorted(set(host_updated)),
|
|
473
608
|
"hostWarnings": sorted(set(host_warnings)),
|
|
609
|
+
"modules": [CORE_MODULE, *modules] if not legacy else ["legacy-all"],
|
|
474
610
|
"updatedAt": utc_now(),
|
|
475
611
|
}
|
|
476
612
|
state_path = root / ".maggie" / "dash-install.json"
|
|
477
613
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
478
614
|
state_path.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
479
|
-
|
|
615
|
+
notices = ["this MaggieDash release has no module split, so its Booking workspace was installed with the core; use MaggieDash 0.2.14 or later to install the core first"] if legacy and manifest.get("workspaces") else []
|
|
616
|
+
emit({"status": "needs-attention" if host_warnings else "installed", "version": manifest.get("version"), "modules": state["modules"], "notices": notices, "recommendations": advice, "next": "Add a recommended module with `maggie dash add <module> --confirm`; nothing optional is installed without that step." if advice and not legacy else None, "target": str(target), "hostFramework": host_framework, "hostInstalled": len(set(host_installed)), "hostPreserved": len(set(host_preserved)), "hostUpdated": len(set(host_updated)), "hostWarnings": host_warnings, "installed": len(set(installed)), "preserved": len(set(preserved)), "state": str(state_path)})
|
|
480
617
|
return 1 if host_warnings else 0
|
|
481
618
|
finally:
|
|
482
619
|
if temporary is not None:
|
|
483
620
|
temporary.cleanup()
|
|
484
621
|
|
|
485
622
|
|
|
623
|
+
def command_add(args: argparse.Namespace) -> int:
|
|
624
|
+
"""Install one optional module next to an existing (or new) core install."""
|
|
625
|
+
args.with_modules = list(args.modules)
|
|
626
|
+
args.all_modules = False
|
|
627
|
+
return command_install(args)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def command_recommend(args: argparse.Namespace) -> int:
|
|
631
|
+
root = project_root(args)
|
|
632
|
+
framework = detect_host_framework(root, args.host)
|
|
633
|
+
state = installed_state(root)
|
|
634
|
+
installed = [name for name in state.get("modules", []) if name != CORE_MODULE]
|
|
635
|
+
analysis = analyse_host(root, framework)
|
|
636
|
+
# The module list comes from the installed release when known; Booking is
|
|
637
|
+
# the optional code module MaggieDash ships today.
|
|
638
|
+
emit({"status": "recommendations", "installed": state.get("modules", []), "host": analysis, "recommendations": recommendations(analysis, ["booking"], installed)})
|
|
639
|
+
return 0
|
|
640
|
+
|
|
641
|
+
|
|
486
642
|
def command_init(args: argparse.Namespace) -> int:
|
|
487
643
|
require_confirm(args)
|
|
488
644
|
root = project_root(args)
|
|
@@ -832,8 +988,22 @@ def parser() -> argparse.ArgumentParser:
|
|
|
832
988
|
install.add_argument("--dry-run", action="store_true", help="show the install plan without writing files")
|
|
833
989
|
install.add_argument("--diff", action="store_true", help="show the install plan and compare with an existing workspace")
|
|
834
990
|
install.add_argument("--existing-dir", help="existing dashboard source directory to compare during --diff")
|
|
991
|
+
install.add_argument("--with", dest="with_modules", action="append", default=[], metavar="MODULE", help="also install an optional module (repeatable), e.g. --with booking")
|
|
992
|
+
install.add_argument("--all-modules", action="store_true", help="install every optional module (the pre-0.2.14 behaviour)")
|
|
835
993
|
install.add_argument("--confirm", action="store_true")
|
|
836
994
|
install.set_defaults(func=command_install)
|
|
995
|
+
add = sub.add_parser("add", help="add an optional MaggieDash module (e.g. booking) to an installed core")
|
|
996
|
+
add.add_argument("modules", nargs="+", metavar="MODULE")
|
|
997
|
+
for flag, options in (("--project", {"default": "."}), ("--source", {"help": "local checkout or Git URL; defaults to MaggieDash repository"}), ("--ref", {"default": "main"}), ("--target", {"default": "_maggie/admin"}), ("--existing-dir", {})):
|
|
998
|
+
add.add_argument(flag, **options)
|
|
999
|
+
add.add_argument("--host", choices=["auto", "astro", "none"], default="auto")
|
|
1000
|
+
for flag in ("--force", "--dry-run", "--diff", "--confirm"):
|
|
1001
|
+
add.add_argument(flag, action="store_true")
|
|
1002
|
+
add.set_defaults(func=command_add)
|
|
1003
|
+
recommend = sub.add_parser("recommend", help="analyse the host project and recommend optional modules and skills")
|
|
1004
|
+
recommend.add_argument("--project", default=".")
|
|
1005
|
+
recommend.add_argument("--host", choices=["auto", "astro", "none"], default="auto")
|
|
1006
|
+
recommend.set_defaults(func=command_recommend)
|
|
837
1007
|
status = sub.add_parser("status", help="inspect project and documents")
|
|
838
1008
|
status.add_argument("--project", default=".")
|
|
839
1009
|
status.add_argument("--project-id")
|