makea-cli 0.2.2__tar.gz → 0.2.5__tar.gz

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.
Files changed (66) hide show
  1. {makea_cli-0.2.2 → makea_cli-0.2.5}/CHANGELOG.md +41 -0
  2. {makea_cli-0.2.2 → makea_cli-0.2.5}/PKG-INFO +7 -1
  3. {makea_cli-0.2.2 → makea_cli-0.2.5}/README.md +6 -0
  4. makea_cli-0.2.5/makea_cli/__init__.py +1 -0
  5. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/api_client.py +51 -2
  6. makea_cli-0.2.5/makea_cli/broker.py +83 -0
  7. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/brand/cmd.py +193 -0
  8. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/supplier/cmd.py +24 -4
  9. {makea_cli-0.2.2 → makea_cli-0.2.5}/pyproject.toml +1 -1
  10. {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/golden_manifest.json +309 -1
  11. makea_cli-0.2.5/tests/test_broker.py +106 -0
  12. {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_persona_commands.py +24 -2
  13. makea_cli-0.2.2/makea_cli/__init__.py +0 -1
  14. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  15. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  16. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  17. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  18. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  19. {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  20. {makea_cli-0.2.2 → makea_cli-0.2.5}/.gitignore +0 -0
  21. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea-collection-upload/SKILL.md +0 -0
  22. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea-collection-upload/scripts/create_products.py +0 -0
  23. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/__main__.py +0 -0
  24. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/auth_pkce.py +0 -0
  25. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/__init__.py +0 -0
  26. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/_util.py +0 -0
  27. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/analytics/__init__.py +0 -0
  28. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/analytics/cmd.py +0 -0
  29. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/auth/__init__.py +0 -0
  30. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/auth/cmd.py +0 -0
  31. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/brand/__init__.py +0 -0
  32. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/designer/__init__.py +0 -0
  33. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/designer/cmd.py +0 -0
  34. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/directory/__init__.py +0 -0
  35. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/directory/cmd.py +0 -0
  36. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/document/__init__.py +0 -0
  37. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/document/cmd.py +0 -0
  38. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/email/__init__.py +0 -0
  39. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/email/cmd.py +0 -0
  40. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/factory/__init__.py +0 -0
  41. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/factory/cmd.py +0 -0
  42. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/library/__init__.py +0 -0
  43. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/library/cmd.py +0 -0
  44. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/metrics/__init__.py +0 -0
  45. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/metrics/cmd.py +0 -0
  46. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/orders/__init__.py +0 -0
  47. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/orders/cmd.py +0 -0
  48. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/product/__init__.py +0 -0
  49. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/product/cmd.py +0 -0
  50. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/__init__.py +0 -0
  51. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/cmd.py +0 -0
  52. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/pricing_spec.py +0 -0
  53. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/supplier/__init__.py +0 -0
  54. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/config.py +0 -0
  55. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/errors.py +0 -0
  56. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/identity.py +0 -0
  57. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/main.py +0 -0
  58. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/output.py +0 -0
  59. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/posthog_client.py +0 -0
  60. {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/registry.py +0 -0
  61. {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/README.md +0 -0
  62. {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/makea-cli.mjs +0 -0
  63. {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/package.json +0 -0
  64. {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_output_contract.py +0 -0
  65. {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_registry.py +0 -0
  66. {makea_cli-0.2.2 → makea_cli-0.2.5}/uv.lock +0 -0
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5
4
+
5
+ The production journey, for the Claude agent on the product channel.
6
+
7
+ - `designer set production-cart <production_order_id> --qty 'SIZE,#RRGGBB=COUNT' …`
8
+ (`auto`): the run's size x colour quantities. Replaces the whole cart, and is
9
+ refused once the run is locked.
10
+ - `designer set production-lock <production_order_id> [--unlock]` (`ask`):
11
+ place the order. Locking is what makes the deposit payable; it can be
12
+ reopened until the deposit is paid.
13
+ - `designer get production-price <production_order_id> [--phase final]`: the
14
+ deposit (or balance) for the quantities on the run now, line by line.
15
+
16
+ ## 0.2.4
17
+
18
+ For Beeboo acting for a brand through the CLI (the Claude agent on the product
19
+ channel).
20
+
21
+ - **Token broker mode.** With `MAKEA_TOKEN_BROKER=<unix socket>` the CLI sends
22
+ its own argv to the broker and uses the token it gets back
23
+ (`x-impersonation-token`) for that one invocation. There is no fallback to a
24
+ cached login in this mode: a command that did not come through the host's
25
+ gate gets no token.
26
+ - `designer submit tech-pack <product_id>` (`ask`): confirm the tech pack.
27
+ - `designer create quote-award <product_id> --quote <supplier link id>` (`ask`):
28
+ choose the quote to sample with. Creates or reuses a DRAFT sampling order and
29
+ prints its id.
30
+ - `designer create checkout --key sampling:<id> [--key …] --accept-terms`
31
+ (`ask`): a Stripe checkout link for one or more charges (sampling,
32
+ production deposit/final, shipping, misc). It is the one command near a
33
+ payment endpoint: it creates the session and nothing is charged until the
34
+ brand pays on Stripe's page. Bank transfers and subscriptions stay out.
35
+
36
+ ## 0.2.3
37
+
38
+ - `admin update quote --ip-schedule-hidden/--ip-schedule-visible`: hide a
39
+ supplier's IP Schedule from the customer, or show it again. Visible unless set.
40
+ - `--ip-schedule-json` help describes the version 2 schedule: two buckets
41
+ (`production_files`, tech pack and BOM included, and `physical_tooling`),
42
+ "not created" by default, and optional.
43
+
3
44
  ## 0.2.2
4
45
 
5
46
  Order lifecycle, exposed in tiers rather than all at once.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.2
3
+ Version: 0.2.5
4
4
  Summary: Makea admin CLI (browser login + Makea admin HTTP API)
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click<9,>=8.1
@@ -144,9 +144,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
144
144
 
145
145
  | 命令 | 读/写 | 后端 | 说明 | 旧名 |
146
146
  |---|---|---|---|---|
147
+ | `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
147
148
  | `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
148
149
  | `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
149
150
  | `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
151
+ | `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
150
152
  | `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
151
153
  | `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
152
154
  | `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
@@ -155,6 +157,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
155
157
  | `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
156
158
  | `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
157
159
  | `makea-cli designer get product` | read | `GET /designer/get_product` | one product: status, documents, prices, links. | — |
160
+ | `makea-cli designer get production-price` | read | `GET /designer/production_orders/{production_order_id}/calculate_price` | what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line. | — |
158
161
  | `makea-cli designer get quote-advice` | read | `GET /designer/product/quote_advice` | which quote to award and why, from cache. Reading never spends a model call; the web app is what asks for a fresh one. | — |
159
162
  | `makea-cli designer get tech-pack` | read | `GET /designer/products/{product_id}/technical-specifications` | the product's tech pack. | — |
160
163
  | `makea-cli designer get tech-pack-history` | read | `GET /designer/products/{product_id}/specification-change-history` | who changed which spec field, when, and from what. | — |
@@ -171,8 +174,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
171
174
  | `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
172
175
  | `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
173
176
  | `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
177
+ | `makea-cli designer set production-cart` | write(auto) | `PUT /designer/production_orders/{production_order_id}/cart` | the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repea… | — |
178
+ | `makea-cli designer set production-lock` | write(ask) | `POST /designer/production_orders/{production_order_id}/configuration-lock` | place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: … | — |
174
179
  | `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
175
180
  | `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
181
+ | `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
176
182
  | `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
177
183
  | `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
178
184
  | `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
@@ -132,9 +132,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
132
132
 
133
133
  | 命令 | 读/写 | 后端 | 说明 | 旧名 |
134
134
  |---|---|---|---|---|
135
+ | `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
135
136
  | `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
136
137
  | `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
137
138
  | `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
139
+ | `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
138
140
  | `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
139
141
  | `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
140
142
  | `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
@@ -143,6 +145,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
143
145
  | `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
144
146
  | `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
145
147
  | `makea-cli designer get product` | read | `GET /designer/get_product` | one product: status, documents, prices, links. | — |
148
+ | `makea-cli designer get production-price` | read | `GET /designer/production_orders/{production_order_id}/calculate_price` | what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line. | — |
146
149
  | `makea-cli designer get quote-advice` | read | `GET /designer/product/quote_advice` | which quote to award and why, from cache. Reading never spends a model call; the web app is what asks for a fresh one. | — |
147
150
  | `makea-cli designer get tech-pack` | read | `GET /designer/products/{product_id}/technical-specifications` | the product's tech pack. | — |
148
151
  | `makea-cli designer get tech-pack-history` | read | `GET /designer/products/{product_id}/specification-change-history` | who changed which spec field, when, and from what. | — |
@@ -159,8 +162,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
159
162
  | `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
160
163
  | `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
161
164
  | `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
165
+ | `makea-cli designer set production-cart` | write(auto) | `PUT /designer/production_orders/{production_order_id}/cart` | the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repea… | — |
166
+ | `makea-cli designer set production-lock` | write(ask) | `POST /designer/production_orders/{production_order_id}/configuration-lock` | place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: … | — |
162
167
  | `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
163
168
  | `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
169
+ | `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
164
170
  | `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
165
171
  | `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
166
172
  | `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
@@ -0,0 +1 @@
1
+ __version__ = "0.2.5"
@@ -12,6 +12,7 @@ import httpx
12
12
  from contextvars import ContextVar
13
13
  from dataclasses import dataclass
14
14
 
15
+ from makea_cli import broker
15
16
  from makea_cli.auth_pkce import ensure_fresh_id_token
16
17
  from makea_cli.config import api_base_url
17
18
  from makea_cli.errors import ApiError, AuthError, UnknownOutcomeError
@@ -53,8 +54,20 @@ def _token() -> str:
53
54
  raise AuthError(str(e)) from e
54
55
 
55
56
 
57
+ def _auth_headers() -> dict[str, str]:
58
+ """Who this request is from.
59
+
60
+ Under a token broker the token is a delegation the backend reads from the
61
+ impersonation header; there is no bearer at all, so a command that did not
62
+ come through the host's gate has nothing to send.
63
+ """
64
+ if broker.broker_path():
65
+ return {"x-impersonation-token": broker.brokered_token() or ""}
66
+ return {"Authorization": f"Bearer {_token()}"}
67
+
68
+
56
69
  def _headers() -> dict[str, str]:
57
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "application/json"}
70
+ headers = {**_auth_headers(), "Accept": "application/json"}
58
71
  if state.idempotency_key:
59
72
  headers["Idempotency-Key"] = state.idempotency_key
60
73
  return headers
@@ -254,7 +267,7 @@ def post_multipart_with_files(
254
267
  def _download(path: str, params: dict[str, Any], *, timeout: float) -> tuple[bytes, str | None]:
255
268
  """GET a file (or follow the redirect to its presigned URL); returns (bytes, filename)."""
256
269
  url = f"{api_base_url()}{path}"
257
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "*/*"}
270
+ headers = {**_auth_headers(), "Accept": "*/*"}
258
271
  try:
259
272
  with httpx.Client(timeout=timeout, follow_redirects=True) as client:
260
273
  r = client.get(url, headers=headers, params=params)
@@ -1185,6 +1198,42 @@ def designer_send_rfq(body: dict[str, Any]) -> Any:
1185
1198
  return post_json("/designer/product/send_rfq", body)
1186
1199
 
1187
1200
 
1201
+ def designer_confirm_tech_spec(product_id: str) -> Any:
1202
+ """POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack."""
1203
+ return post_json(f"/designer/products/{product_id}/technical-specifications/confirm", {})
1204
+
1205
+
1206
+ def designer_award_quotes(body: dict[str, Any]) -> Any:
1207
+ """POST /designer/quotes/group_pay_prepare — DRAFT sampling orders for chosen quotes."""
1208
+ return post_json("/designer/quotes/group_pay_prepare", body)
1209
+
1210
+
1211
+ def designer_create_checkout(body: dict[str, Any]) -> Any:
1212
+ """POST /designer/payments/basket/create-session — a Stripe checkout for charges."""
1213
+ return post_json("/designer/payments/basket/create-session", body)
1214
+
1215
+
1216
+ def designer_production_price(production_order_id: str, phase: str) -> Any:
1217
+ """GET /designer/production_orders/{id}/calculate_price — deposit or balance, itemised."""
1218
+ return get_json(
1219
+ f"/designer/production_orders/{production_order_id}/calculate_price",
1220
+ params={"payment_type": phase},
1221
+ )
1222
+
1223
+
1224
+ def designer_set_production_cart(production_order_id: str, cart: dict[str, int]) -> Any:
1225
+ """PUT /designer/production_orders/{id}/cart — replace the size x colour quantities."""
1226
+ return put_json(f"/designer/production_orders/{production_order_id}/cart", {"cart_items": cart})
1227
+
1228
+
1229
+ def designer_set_production_lock(production_order_id: str, locked: bool) -> Any:
1230
+ """POST /designer/production_orders/{id}/configuration-lock — place or reopen the run."""
1231
+ return post_json(
1232
+ f"/designer/production_orders/{production_order_id}/configuration-lock",
1233
+ {"locked": locked},
1234
+ )
1235
+
1236
+
1188
1237
  def designer_list_collections() -> Any:
1189
1238
  """GET /designer/collections — the brand's collections."""
1190
1239
  return get_json("/designer/collections")
@@ -0,0 +1,83 @@
1
+ """Token broker client: how the CLI acts for someone without holding their token.
2
+
3
+ A host that runs the CLI on a user's behalf (the Beeboo agent runner) does not
4
+ put that user's token in the environment, where any process in the sandbox
5
+ could read it. It runs a broker on a unix socket and names it in
6
+ ``MAKEA_TOKEN_BROKER``. Before a command runs, the host's gate has already
7
+ looked at the exact command line and left a one-time ticket for it; the CLI
8
+ sends its own argv and gets back a token that is good for this invocation.
9
+
10
+ A process that did not come through the gate (``python -c``, ``curl``, a CLI
11
+ call wrapped in a script) has no ticket and gets nothing. That is the whole
12
+ point, so there is deliberately no fallback to a cached login in this mode.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import socket
20
+ import sys
21
+
22
+ from makea_cli.errors import AuthError
23
+
24
+ BROKER_ENV = "MAKEA_TOKEN_BROKER"
25
+
26
+ # A broker lives on the same box; anything slower than this is a broker that
27
+ # is not there.
28
+ _TIMEOUT_SECONDS = 10.0
29
+
30
+ # One invocation spends one ticket. Every request the command makes reuses the
31
+ # token the ticket bought.
32
+ _cached: dict[str, str] | None = None
33
+
34
+
35
+ def broker_path() -> str | None:
36
+ path = (os.environ.get(BROKER_ENV) or "").strip()
37
+ return path or None
38
+
39
+
40
+ def invocation_argv() -> list[str]:
41
+ """This process's command line, as the gate saw it (without the program name)."""
42
+ return list(sys.argv[1:])
43
+
44
+
45
+ def exchange(path: str, argv: list[str]) -> dict[str, str]:
46
+ """Trade this command line for ``{"token", "persona"}``, or raise AuthError."""
47
+ request = json.dumps({"argv": argv}).encode("utf-8") + b"\n"
48
+ try:
49
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
50
+ sock.settimeout(_TIMEOUT_SECONDS)
51
+ sock.connect(path)
52
+ sock.sendall(request)
53
+ chunks = []
54
+ while True:
55
+ chunk = sock.recv(65536)
56
+ if not chunk:
57
+ break
58
+ chunks.append(chunk)
59
+ if chunk.endswith(b"\n"):
60
+ break
61
+ except OSError as e:
62
+ raise AuthError(f"token broker unavailable ({e.__class__.__name__})") from e
63
+ try:
64
+ reply = json.loads(b"".join(chunks).decode("utf-8") or "{}")
65
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
66
+ raise AuthError("token broker sent an unreadable reply") from e
67
+ if not reply.get("ok") or not reply.get("token"):
68
+ raise AuthError(
69
+ str(reply.get("error") or "the token broker refused this command"),
70
+ detail={"broker": True},
71
+ )
72
+ return {"token": str(reply["token"]), "persona": str(reply.get("persona") or "")}
73
+
74
+
75
+ def brokered_token() -> str | None:
76
+ """The token for this invocation when a broker is configured, else None."""
77
+ global _cached
78
+ path = broker_path()
79
+ if not path:
80
+ return None
81
+ if _cached is None:
82
+ _cached = exchange(path, invocation_argv())
83
+ return _cached["token"]
@@ -817,3 +817,196 @@ def register(app: typer.Typer) -> None:
817
817
  if makea_sourcing:
818
818
  body["makea_sourcing"] = True
819
819
  print_json_or_exit(console, lambda: api_client.designer_send_rfq(body))
820
+
821
+ # --- the sampling journey: confirm the pack, award a quote, pay -------------
822
+
823
+ @command(
824
+ app,
825
+ "designer",
826
+ "submit",
827
+ "tech-pack",
828
+ api="POST /designer/products/{product_id}/technical-specifications/confirm",
829
+ side_effects=[
830
+ "locks the tech pack as the version suppliers quote against",
831
+ "opens the product for sourcing",
832
+ ],
833
+ approval="ask",
834
+ help=(
835
+ "POST /designer/products/{id}/technical-specifications/confirm — confirm the "
836
+ "tech pack, the step before anyone is asked to quote."
837
+ ),
838
+ )
839
+ def submit_tech_pack(
840
+ product_id: str = typer.Argument(..., help="Product id."),
841
+ ) -> None:
842
+ """Confirm a product's tech pack."""
843
+ print_json_or_exit(console, lambda: api_client.designer_confirm_tech_spec(product_id))
844
+
845
+ @command(
846
+ app,
847
+ "designer",
848
+ "create",
849
+ "quote-award",
850
+ api="POST /designer/quotes/group_pay_prepare",
851
+ side_effects=[
852
+ "creates (or reuses) a DRAFT sampling order on each chosen quote",
853
+ "nothing is charged until that order is paid",
854
+ ],
855
+ approval="ask",
856
+ help=(
857
+ "POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample "
858
+ "with. --quote is the supplier link id from `designer list supplier-link`; "
859
+ "repeat --option for the quote's optional extras. Prints the sampling_order_ids "
860
+ "to pay with `designer create checkout --key sampling:<id>`."
861
+ ),
862
+ )
863
+ def create_quote_award(
864
+ product_id: str = typer.Argument(..., help="Product id."),
865
+ quote: str = typer.Option(..., "--quote", help="Supplier link id of the chosen quote."),
866
+ option: list[str] = typer.Option(
867
+ [], "--option", help="Optional extra on the quote to include (repeat)."
868
+ ),
869
+ no_base_sampling: bool = typer.Option(
870
+ False, "--no-base-sampling", help="Leave the quote's base sample out."
871
+ ),
872
+ notes: str | None = typer.Option(None, "--notes", help="Note for the supplier."),
873
+ ) -> None:
874
+ """Award a quote: the sampling order that pays for it."""
875
+ selection: dict[str, Any] = {"product_id": product_id, "link_id": quote}
876
+ if option:
877
+ selection["selected_option_ids"] = list(option)
878
+ if no_base_sampling:
879
+ selection["include_base_sampling"] = False
880
+ if notes:
881
+ selection["notes"] = notes
882
+ print_json_or_exit(
883
+ console, lambda: api_client.designer_award_quotes({"selections": [selection]})
884
+ )
885
+
886
+ @command(
887
+ app,
888
+ "designer",
889
+ "create",
890
+ "checkout",
891
+ api="POST /designer/payments/basket/create-session",
892
+ side_effects=[
893
+ "records the brand's acceptance of the sampling/production terms",
894
+ "creates a Stripe checkout session; nothing is charged until it is paid there",
895
+ ],
896
+ approval="ask",
897
+ help=(
898
+ "POST /designer/payments/basket/create-session — a Stripe checkout link for one "
899
+ "or more charges. --key repeats: sampling:<sampling_order_id>, "
900
+ "production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> "
901
+ "(the `key` of each payable in `designer list payment`). Prints checkout_url."
902
+ ),
903
+ )
904
+ def create_checkout(
905
+ key: list[str] = typer.Option(..., "--key", help="Charge key to pay (repeat)."),
906
+ accept_terms: bool = typer.Option(
907
+ False,
908
+ "--accept-terms",
909
+ help="The brand accepts the sampling/production terms for these charges.",
910
+ ),
911
+ ) -> None:
912
+ """Create a Stripe checkout link for charges."""
913
+ body: dict[str, Any] = {"keys": list(key)}
914
+ if accept_terms:
915
+ body["terms_accepted"] = True
916
+ print_json_or_exit(console, lambda: api_client.designer_create_checkout(body))
917
+
918
+ # --- the production journey: quantities, place the order, pay the deposit ---
919
+
920
+ @command(
921
+ app,
922
+ "designer",
923
+ "get",
924
+ "production-price",
925
+ api="GET /designer/production_orders/{production_order_id}/calculate_price",
926
+ help=(
927
+ "GET /designer/production_orders/{id}/calculate_price — what the deposit (or the "
928
+ "balance, --phase final) comes to for the quantities on the run right now, line "
929
+ "by line."
930
+ ),
931
+ )
932
+ def get_production_price(
933
+ production_order_id: str = typer.Argument(..., help="Production order id."),
934
+ phase: str = typer.Option("deposit", "--phase", help="deposit or final."),
935
+ ) -> None:
936
+ """Price a production order's deposit or balance."""
937
+ if phase not in ("deposit", "final"):
938
+ raise typer.BadParameter("--phase is deposit or final")
939
+ print_json_or_exit(
940
+ console,
941
+ lambda: api_client.designer_production_price(production_order_id, phase),
942
+ )
943
+
944
+ @command(
945
+ app,
946
+ "designer",
947
+ "set",
948
+ "production-cart",
949
+ api="PUT /designer/production_orders/{production_order_id}/cart",
950
+ side_effects=["replaces the run's quantities; nothing is ordered until it is locked"],
951
+ approval="auto",
952
+ help=(
953
+ "PUT /designer/production_orders/{id}/cart — the quantity of every size and "
954
+ "colour on the run. Replaces the whole cart: pass every cell you want kept. "
955
+ "--qty repeats as 'SIZE,#RRGGBB=COUNT' (quote it: # starts a shell comment), "
956
+ "with the sizes and colour hex codes from "
957
+ "the order's available_sizes / available_colors (`designer list "
958
+ "production-order --product <id>`). Refused once the run is locked."
959
+ ),
960
+ )
961
+ def set_production_cart(
962
+ production_order_id: str = typer.Argument(..., help="Production order id."),
963
+ qty: list[str] = typer.Option(
964
+ ..., "--qty", help="SIZE,#RRGGBB=COUNT for one cell (repeat)."
965
+ ),
966
+ ) -> None:
967
+ """Set a production order's size x colour quantities."""
968
+ cart: dict[str, int] = {}
969
+ for cell in qty:
970
+ key, sep, count = cell.rpartition("=")
971
+ if not sep or "," not in key:
972
+ raise typer.BadParameter(f"--qty {cell!r}: expected SIZE,#RRGGBB=COUNT")
973
+ try:
974
+ n = int(count)
975
+ except ValueError as e:
976
+ raise typer.BadParameter(f"--qty {cell!r}: COUNT must be a whole number") from e
977
+ if n < 0:
978
+ raise typer.BadParameter(f"--qty {cell!r}: COUNT cannot be negative")
979
+ size, _, colour = key.partition(",")
980
+ cart[f"{size.strip()},{colour.strip()}"] = n
981
+ print_json_or_exit(
982
+ console,
983
+ lambda: api_client.designer_set_production_cart(production_order_id, cart),
984
+ )
985
+
986
+ @command(
987
+ app,
988
+ "designer",
989
+ "set",
990
+ "production-lock",
991
+ api="POST /designer/production_orders/{production_order_id}/configuration-lock",
992
+ side_effects=[
993
+ "places the order: the quantities are final and the deposit becomes payable",
994
+ "can be unlocked again until the deposit is paid",
995
+ ],
996
+ approval="ask",
997
+ help=(
998
+ "POST /designer/production_orders/{id}/configuration-lock — place the order "
999
+ "with the quantities on the run (--unlock to reopen them). Locking is what makes "
1000
+ "the deposit payable: `designer create checkout --key "
1001
+ "production:<id>:deposit --accept-terms`."
1002
+ ),
1003
+ )
1004
+ def set_production_lock(
1005
+ production_order_id: str = typer.Argument(..., help="Production order id."),
1006
+ unlock: bool = typer.Option(False, "--unlock", help="Reopen the quantities instead."),
1007
+ ) -> None:
1008
+ """Lock (place) or unlock a production order's configuration."""
1009
+ print_json_or_exit(
1010
+ console,
1011
+ lambda: api_client.designer_set_production_lock(production_order_id, not unlock),
1012
+ )
@@ -688,7 +688,8 @@ def register(app: typer.Typer) -> None:
688
688
  Read one quote request in full.
689
689
  """
690
690
  print_json_or_exit(
691
- console, lambda: api_client.get_quote_request(quote_request_id=quote_request_id)
691
+ console,
692
+ lambda: api_client.get_quote_request(quote_request_id=quote_request_id),
692
693
  )
693
694
 
694
695
  @command(
@@ -988,9 +989,18 @@ def register(app: typer.Typer) -> None:
988
989
  help=(
989
990
  "ip_schedule: who owns each asset the job creates (optional, JSON object). "
990
991
  "Omit when the factory did not state it — makea must not invent an answer "
991
- "on its behalf. If supplied it has to be COMPLETE (every one of the 12 "
992
- "assets answered, plus production location and subcontractors on a "
993
- "PRODUCTION link) or the API returns 400 listing what is missing."
992
+ "on its behalf. If supplied it has to be COMPLETE or the API returns 400 "
993
+ "listing what is missing. Schema v2: "
994
+ '{"schema_version":"2","background_ip_used":false,"assets":['
995
+ '{"asset_type":"production_files","applicable":true,'
996
+ '"transfer":"transfers_to_client","delivery":"on_full_payment"},'
997
+ '{"asset_type":"physical_tooling","applicable":false}]} '
998
+ "-- both asset types answered (applicable false, the default, is a valid "
999
+ "answer; production_files includes the tech pack and BOM), "
1000
+ "background_ip_used true needs background_ip_declaration, and a "
1001
+ "PRODUCTION link also needs production_location and subcontractors_declared. "
1002
+ "To hide it from the customer afterwards, update the quote with "
1003
+ "--ip-schedule-hidden."
994
1004
  ),
995
1005
  ),
996
1006
  resample_request_id: str | None = typer.Option(
@@ -1250,6 +1260,14 @@ def register(app: typer.Typer) -> None:
1250
1260
  "the factory could not have submitted itself."
1251
1261
  ),
1252
1262
  ),
1263
+ ip_schedule_hidden: bool | None = typer.Option(
1264
+ None,
1265
+ "--ip-schedule-hidden/--ip-schedule-visible",
1266
+ help=(
1267
+ "ip_schedule_hidden_from_customer: hide this quote's IP Schedule from "
1268
+ "the customer, or show it again. Visible unless set."
1269
+ ),
1270
+ ),
1253
1271
  available_sizes_json: str | None = typer.Option(
1254
1272
  None,
1255
1273
  "--available-sizes-json",
@@ -1350,6 +1368,8 @@ def register(app: typer.Typer) -> None:
1350
1368
  )
1351
1369
  if ip_schedule_json is not None:
1352
1370
  override["ip_schedule"] = _parse_json_option(ip_schedule_json, "--ip-schedule-json")
1371
+ if ip_schedule_hidden is not None:
1372
+ override["ip_schedule_hidden_from_customer"] = ip_schedule_hidden
1353
1373
  if refundable_terms_json is not None:
1354
1374
  override["refundable_terms"] = _parse_json_option(
1355
1375
  refundable_terms_json, "--refundable-terms-json"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "makea-cli"
7
- version = "0.2.2"
7
+ version = "0.2.5"
8
8
  description = "Makea admin CLI (browser login + Makea admin HTTP API)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2890,7 +2890,7 @@
2890
2890
  "required": false,
2891
2891
  "multiple": false,
2892
2892
  "default": null,
2893
- "help": "ip_schedule: who owns each asset the job creates (optional, JSON object). Omit when the factory did not state it — makea must not invent an answer on its behalf. If supplied it has to be COMPLETE (every one of the 12 assets answered, plus production location and subcontractors on a PRODUCTION link) or the API returns 400 listing what is missing."
2893
+ "help": "ip_schedule: who owns each asset the job creates (optional, JSON object). Omit when the factory did not state it — makea must not invent an answer on its behalf. If supplied it has to be COMPLETE or the API returns 400 listing what is missing. Schema v2: {\"schema_version\":\"2\",\"background_ip_used\":false,\"assets\":[{\"asset_type\":\"production_files\",\"applicable\":true,\"transfer\":\"transfers_to_client\",\"delivery\":\"on_full_payment\"},{\"asset_type\":\"physical_tooling\",\"applicable\":false}]} -- both asset types answered (applicable false, the default, is a valid answer; production_files includes the tech pack and BOM), background_ip_used true needs background_ip_declaration, and a PRODUCTION link also needs production_location and subcontractors_declared. To hide it from the customer afterwards, update the quote with --ip-schedule-hidden."
2894
2894
  },
2895
2895
  {
2896
2896
  "name": "resample_request_id",
@@ -3287,6 +3287,18 @@
3287
3287
  "default": null,
3288
3288
  "help": "ip_schedule: who owns each asset the job creates (JSON object; pass `null` to clear). A supplied schedule must be COMPLETE — the API refuses a half-answered one, so a quote can never carry a schedule the factory could not have submitted itself."
3289
3289
  },
3290
+ {
3291
+ "name": "ip_schedule_hidden",
3292
+ "kind": "option",
3293
+ "opts": [
3294
+ "--ip-schedule-hidden"
3295
+ ],
3296
+ "type": "flag",
3297
+ "required": false,
3298
+ "multiple": false,
3299
+ "default": null,
3300
+ "help": "ip_schedule_hidden_from_customer: hide this quote's IP Schedule from the customer, or show it again. Visible unless set."
3301
+ },
3290
3302
  {
3291
3303
  "name": "available_sizes_json",
3292
3304
  "kind": "option",
@@ -4471,6 +4483,52 @@
4471
4483
  }
4472
4484
  ]
4473
4485
  },
4486
+ {
4487
+ "name": "designer create checkout",
4488
+ "persona": "designer",
4489
+ "verb": "create",
4490
+ "noun": "checkout",
4491
+ "access": "write",
4492
+ "api": "POST /designer/payments/basket/create-session",
4493
+ "external": null,
4494
+ "side_effects": [
4495
+ "records the brand's acceptance of the sampling/production terms",
4496
+ "creates a Stripe checkout session; nothing is charged until it is paid there"
4497
+ ],
4498
+ "approval": "ask",
4499
+ "reversible": null,
4500
+ "idempotent": null,
4501
+ "limit": null,
4502
+ "legacy": [],
4503
+ "help": "POST /designer/payments/basket/create-session — a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> (the `key` of each payable in `designer list payment`). Prints checkout_url.",
4504
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.create_checkout",
4505
+ "flags": [
4506
+ {
4507
+ "name": "key",
4508
+ "kind": "option",
4509
+ "opts": [
4510
+ "--key"
4511
+ ],
4512
+ "type": "text",
4513
+ "required": true,
4514
+ "multiple": true,
4515
+ "default": null,
4516
+ "help": "Charge key to pay (repeat)."
4517
+ },
4518
+ {
4519
+ "name": "accept_terms",
4520
+ "kind": "option",
4521
+ "opts": [
4522
+ "--accept-terms"
4523
+ ],
4524
+ "type": "flag",
4525
+ "required": false,
4526
+ "multiple": false,
4527
+ "default": false,
4528
+ "help": "The brand accepts the sampling/production terms for these charges."
4529
+ }
4530
+ ]
4531
+ },
4474
4532
  {
4475
4533
  "name": "designer create collection",
4476
4534
  "persona": "designer",
@@ -4711,6 +4769,88 @@
4711
4769
  }
4712
4770
  ]
4713
4771
  },
4772
+ {
4773
+ "name": "designer create quote-award",
4774
+ "persona": "designer",
4775
+ "verb": "create",
4776
+ "noun": "quote-award",
4777
+ "access": "write",
4778
+ "api": "POST /designer/quotes/group_pay_prepare",
4779
+ "external": null,
4780
+ "side_effects": [
4781
+ "creates (or reuses) a DRAFT sampling order on each chosen quote",
4782
+ "nothing is charged until that order is paid"
4783
+ ],
4784
+ "approval": "ask",
4785
+ "reversible": null,
4786
+ "idempotent": null,
4787
+ "limit": null,
4788
+ "legacy": [],
4789
+ "help": "POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat --option for the quote's optional extras. Prints the sampling_order_ids to pay with `designer create checkout --key sampling:<id>`.",
4790
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.create_quote_award",
4791
+ "flags": [
4792
+ {
4793
+ "name": "product_id",
4794
+ "kind": "argument",
4795
+ "opts": [
4796
+ "product_id"
4797
+ ],
4798
+ "type": "text",
4799
+ "required": true,
4800
+ "multiple": false,
4801
+ "default": null,
4802
+ "help": "Product id."
4803
+ },
4804
+ {
4805
+ "name": "quote",
4806
+ "kind": "option",
4807
+ "opts": [
4808
+ "--quote"
4809
+ ],
4810
+ "type": "text",
4811
+ "required": true,
4812
+ "multiple": false,
4813
+ "default": null,
4814
+ "help": "Supplier link id of the chosen quote."
4815
+ },
4816
+ {
4817
+ "name": "option",
4818
+ "kind": "option",
4819
+ "opts": [
4820
+ "--option"
4821
+ ],
4822
+ "type": "text",
4823
+ "required": false,
4824
+ "multiple": true,
4825
+ "default": [],
4826
+ "help": "Optional extra on the quote to include (repeat)."
4827
+ },
4828
+ {
4829
+ "name": "no_base_sampling",
4830
+ "kind": "option",
4831
+ "opts": [
4832
+ "--no-base-sampling"
4833
+ ],
4834
+ "type": "flag",
4835
+ "required": false,
4836
+ "multiple": false,
4837
+ "default": false,
4838
+ "help": "Leave the quote's base sample out."
4839
+ },
4840
+ {
4841
+ "name": "notes",
4842
+ "kind": "option",
4843
+ "opts": [
4844
+ "--notes"
4845
+ ],
4846
+ "type": "text",
4847
+ "required": false,
4848
+ "multiple": false,
4849
+ "default": null,
4850
+ "help": "Note for the supplier."
4851
+ }
4852
+ ]
4853
+ },
4714
4854
  {
4715
4855
  "name": "designer create rfq",
4716
4856
  "persona": "designer",
@@ -5061,6 +5201,49 @@
5061
5201
  }
5062
5202
  ]
5063
5203
  },
5204
+ {
5205
+ "name": "designer get production-price",
5206
+ "persona": "designer",
5207
+ "verb": "get",
5208
+ "noun": "production-price",
5209
+ "access": "read",
5210
+ "api": "GET /designer/production_orders/{production_order_id}/calculate_price",
5211
+ "external": null,
5212
+ "side_effects": [],
5213
+ "approval": "auto",
5214
+ "reversible": null,
5215
+ "idempotent": null,
5216
+ "limit": null,
5217
+ "legacy": [],
5218
+ "help": "GET /designer/production_orders/{id}/calculate_price — what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line.",
5219
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.get_production_price",
5220
+ "flags": [
5221
+ {
5222
+ "name": "production_order_id",
5223
+ "kind": "argument",
5224
+ "opts": [
5225
+ "production_order_id"
5226
+ ],
5227
+ "type": "text",
5228
+ "required": true,
5229
+ "multiple": false,
5230
+ "default": null,
5231
+ "help": "Production order id."
5232
+ },
5233
+ {
5234
+ "name": "phase",
5235
+ "kind": "option",
5236
+ "opts": [
5237
+ "--phase"
5238
+ ],
5239
+ "type": "text",
5240
+ "required": false,
5241
+ "multiple": false,
5242
+ "default": "deposit",
5243
+ "help": "deposit or final."
5244
+ }
5245
+ ]
5246
+ },
5064
5247
  {
5065
5248
  "name": "designer get quote-advice",
5066
5249
  "persona": "designer",
@@ -5599,6 +5782,97 @@
5599
5782
  }
5600
5783
  ]
5601
5784
  },
5785
+ {
5786
+ "name": "designer set production-cart",
5787
+ "persona": "designer",
5788
+ "verb": "set",
5789
+ "noun": "production-cart",
5790
+ "access": "write",
5791
+ "api": "PUT /designer/production_orders/{production_order_id}/cart",
5792
+ "external": null,
5793
+ "side_effects": [
5794
+ "replaces the run's quantities; nothing is ordered until it is locked"
5795
+ ],
5796
+ "approval": "auto",
5797
+ "reversible": null,
5798
+ "idempotent": null,
5799
+ "limit": null,
5800
+ "legacy": [],
5801
+ "help": "PUT /designer/production_orders/{id}/cart — the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repeats as 'SIZE,#RRGGBB=COUNT' (quote it: # starts a shell comment), with the sizes and colour hex codes from the order's available_sizes / available_colors (`designer list production-order --product <id>`). Refused once the run is locked.",
5802
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.set_production_cart",
5803
+ "flags": [
5804
+ {
5805
+ "name": "production_order_id",
5806
+ "kind": "argument",
5807
+ "opts": [
5808
+ "production_order_id"
5809
+ ],
5810
+ "type": "text",
5811
+ "required": true,
5812
+ "multiple": false,
5813
+ "default": null,
5814
+ "help": "Production order id."
5815
+ },
5816
+ {
5817
+ "name": "qty",
5818
+ "kind": "option",
5819
+ "opts": [
5820
+ "--qty"
5821
+ ],
5822
+ "type": "text",
5823
+ "required": true,
5824
+ "multiple": true,
5825
+ "default": null,
5826
+ "help": "SIZE,#RRGGBB=COUNT for one cell (repeat)."
5827
+ }
5828
+ ]
5829
+ },
5830
+ {
5831
+ "name": "designer set production-lock",
5832
+ "persona": "designer",
5833
+ "verb": "set",
5834
+ "noun": "production-lock",
5835
+ "access": "write",
5836
+ "api": "POST /designer/production_orders/{production_order_id}/configuration-lock",
5837
+ "external": null,
5838
+ "side_effects": [
5839
+ "places the order: the quantities are final and the deposit becomes payable",
5840
+ "can be unlocked again until the deposit is paid"
5841
+ ],
5842
+ "approval": "ask",
5843
+ "reversible": null,
5844
+ "idempotent": null,
5845
+ "limit": null,
5846
+ "legacy": [],
5847
+ "help": "POST /designer/production_orders/{id}/configuration-lock — place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: `designer create checkout --key production:<id>:deposit --accept-terms`.",
5848
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.set_production_lock",
5849
+ "flags": [
5850
+ {
5851
+ "name": "production_order_id",
5852
+ "kind": "argument",
5853
+ "opts": [
5854
+ "production_order_id"
5855
+ ],
5856
+ "type": "text",
5857
+ "required": true,
5858
+ "multiple": false,
5859
+ "default": null,
5860
+ "help": "Production order id."
5861
+ },
5862
+ {
5863
+ "name": "unlock",
5864
+ "kind": "option",
5865
+ "opts": [
5866
+ "--unlock"
5867
+ ],
5868
+ "type": "flag",
5869
+ "required": false,
5870
+ "multiple": false,
5871
+ "default": false,
5872
+ "help": "Reopen the quantities instead."
5873
+ }
5874
+ ]
5875
+ },
5602
5876
  {
5603
5877
  "name": "designer submit production-order",
5604
5878
  "persona": "designer",
@@ -5667,6 +5941,40 @@
5667
5941
  }
5668
5942
  ]
5669
5943
  },
5944
+ {
5945
+ "name": "designer submit tech-pack",
5946
+ "persona": "designer",
5947
+ "verb": "submit",
5948
+ "noun": "tech-pack",
5949
+ "access": "write",
5950
+ "api": "POST /designer/products/{product_id}/technical-specifications/confirm",
5951
+ "external": null,
5952
+ "side_effects": [
5953
+ "locks the tech pack as the version suppliers quote against",
5954
+ "opens the product for sourcing"
5955
+ ],
5956
+ "approval": "ask",
5957
+ "reversible": null,
5958
+ "idempotent": null,
5959
+ "limit": null,
5960
+ "legacy": [],
5961
+ "help": "POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack, the step before anyone is asked to quote.",
5962
+ "function": "makea_cli.commands.brand.cmd.register.<locals>.submit_tech_pack",
5963
+ "flags": [
5964
+ {
5965
+ "name": "product_id",
5966
+ "kind": "argument",
5967
+ "opts": [
5968
+ "product_id"
5969
+ ],
5970
+ "type": "text",
5971
+ "required": true,
5972
+ "multiple": false,
5973
+ "default": null,
5974
+ "help": "Product id."
5975
+ }
5976
+ ]
5977
+ },
5670
5978
  {
5671
5979
  "name": "designer update action",
5672
5980
  "persona": "designer",
@@ -0,0 +1,106 @@
1
+ """Token broker mode: the CLI trades its own argv for a token, and has no fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import socket
8
+ import tempfile
9
+ import threading
10
+ import unittest
11
+ from unittest import mock
12
+
13
+ from makea_cli import api_client, broker
14
+ from makea_cli.errors import AuthError
15
+
16
+
17
+ class _FakeBroker:
18
+ """A unix-socket broker that answers one canned reply per connection."""
19
+
20
+ def __init__(self, reply: dict):
21
+ self.reply = reply
22
+ self.seen: list[dict] = []
23
+ self._dir = tempfile.mkdtemp()
24
+ self.path = os.path.join(self._dir, "b.sock")
25
+ self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
26
+ self._sock.bind(self.path)
27
+ self._sock.listen(4)
28
+ self._thread = threading.Thread(target=self._serve, daemon=True)
29
+ self._thread.start()
30
+
31
+ def _serve(self):
32
+ while True:
33
+ try:
34
+ conn, _ = self._sock.accept()
35
+ except OSError:
36
+ return
37
+ with conn:
38
+ data = b""
39
+ while not data.endswith(b"\n"):
40
+ chunk = conn.recv(4096)
41
+ if not chunk:
42
+ break
43
+ data += chunk
44
+ self.seen.append(json.loads(data.decode() or "{}"))
45
+ conn.sendall(json.dumps(self.reply).encode() + b"\n")
46
+
47
+ def close(self):
48
+ self._sock.close()
49
+
50
+
51
+ class BrokerTests(unittest.TestCase):
52
+ def setUp(self):
53
+ broker._cached = None
54
+
55
+ def tearDown(self):
56
+ broker._cached = None
57
+
58
+ def test_argv_is_exchanged_for_a_token(self):
59
+ fake = _FakeBroker({"ok": True, "token": "delegation_abc", "persona": "designer"})
60
+ try:
61
+ with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
62
+ with mock.patch("sys.argv", ["makea-cli", "designer", "list", "product"]):
63
+ headers = api_client._headers()
64
+ finally:
65
+ fake.close()
66
+ self.assertEqual(headers["x-impersonation-token"], "delegation_abc")
67
+ self.assertNotIn("Authorization", headers)
68
+ self.assertEqual(fake.seen, [{"argv": ["designer", "list", "product"]}])
69
+
70
+ def test_one_invocation_spends_one_ticket(self):
71
+ fake = _FakeBroker({"ok": True, "token": "delegation_abc"})
72
+ try:
73
+ with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
74
+ api_client._headers()
75
+ api_client._headers()
76
+ finally:
77
+ fake.close()
78
+ self.assertEqual(len(fake.seen), 1)
79
+
80
+ def test_a_refusal_is_an_auth_error_not_a_login_fallback(self):
81
+ fake = _FakeBroker({"ok": False, "error": "no ticket for this command"})
82
+ try:
83
+ with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
84
+ with mock.patch.object(api_client, "ensure_fresh_id_token") as login:
85
+ with self.assertRaises(AuthError) as ctx:
86
+ api_client._headers()
87
+ login.assert_not_called()
88
+ finally:
89
+ fake.close()
90
+ self.assertIn("no ticket", ctx.exception.message)
91
+
92
+ def test_a_missing_broker_is_an_auth_error(self):
93
+ with mock.patch.dict(os.environ, {broker.BROKER_ENV: "/nonexistent/b.sock"}):
94
+ with self.assertRaises(AuthError):
95
+ api_client._headers()
96
+
97
+ def test_without_a_broker_the_login_is_used(self):
98
+ env = {k: v for k, v in os.environ.items() if k != broker.BROKER_ENV}
99
+ with mock.patch.dict(os.environ, env, clear=True):
100
+ with mock.patch.object(api_client, "ensure_fresh_id_token", return_value="jwt"):
101
+ headers = api_client._headers()
102
+ self.assertEqual(headers["Authorization"], "Bearer jwt")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
@@ -395,7 +395,16 @@ class OrderLifecycleTests(_PersonaCase):
395
395
 
396
396
 
397
397
  class NoMoneyPathTests(unittest.TestCase):
398
- """No CLI command may reach a payment or subscription endpoint."""
398
+ """No CLI command may move money; one may hand the brand a link to pay.
399
+
400
+ ``designer create checkout`` creates a Stripe checkout session and nothing
401
+ else: the charge happens on Stripe's page, entered by the brand. It is the
402
+ only write allowed near a payment endpoint, and it always asks first,
403
+ because it also records the brand's acceptance of the terms. Bank
404
+ transfers, subscriptions and every other payment write stay out.
405
+ """
406
+
407
+ CHECKOUT_LINK = "designer create checkout"
399
408
 
400
409
  def test_no_command_touches_money(self):
401
410
  from makea_cli import registry
@@ -406,6 +415,19 @@ class NoMoneyPathTests(unittest.TestCase):
406
415
  offenders = [
407
416
  (s.name, s.api)
408
417
  for s in registry.specs()
409
- if not s.external and any(f in s.api for f in forbidden) and s.access == "write"
418
+ if not s.external
419
+ and any(f in s.api for f in forbidden)
420
+ and s.access == "write"
421
+ and s.name != self.CHECKOUT_LINK
410
422
  ]
411
423
  self.assertEqual(offenders, [], "a write command reaches money movement")
424
+
425
+ def test_the_checkout_link_asks_and_only_creates_a_session(self):
426
+ from makea_cli import registry
427
+ from makea_cli.main import build_cli
428
+
429
+ build_cli()
430
+ spec = registry.spec_for("designer", "create", "checkout")
431
+ self.assertIsNotNone(spec)
432
+ self.assertEqual(spec.approval, "ask")
433
+ self.assertEqual(spec.api, "POST /designer/payments/basket/create-session")
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes