@localizeaso/cli 0.1.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +35 -0
- package/packages/asc-shared/dist/app-store-review.d.ts +610 -0
- package/packages/asc-shared/dist/app-store-review.d.ts.map +1 -0
- package/packages/asc-shared/dist/app-store-review.js +242 -0
- package/packages/asc-shared/dist/aso-keyword-map.d.ts +94 -0
- package/packages/asc-shared/dist/aso-keyword-map.d.ts.map +1 -0
- package/packages/asc-shared/dist/aso-keyword-map.js +292 -0
- package/packages/asc-shared/dist/constants.d.ts +15 -0
- package/packages/asc-shared/dist/constants.d.ts.map +1 -0
- package/packages/asc-shared/dist/constants.js +130 -0
- package/packages/asc-shared/dist/cross-localization.d.ts +29 -0
- package/packages/asc-shared/dist/cross-localization.d.ts.map +1 -0
- package/packages/asc-shared/dist/cross-localization.js +189 -0
- package/packages/asc-shared/dist/dedupe.d.ts +17 -0
- package/packages/asc-shared/dist/dedupe.d.ts.map +1 -0
- package/packages/asc-shared/dist/dedupe.js +104 -0
- package/packages/asc-shared/dist/design-tokens.d.ts +83 -0
- package/packages/asc-shared/dist/design-tokens.d.ts.map +1 -0
- package/packages/asc-shared/dist/design-tokens.js +73 -0
- package/packages/asc-shared/dist/index.d.ts +16 -0
- package/packages/asc-shared/dist/index.d.ts.map +1 -0
- package/packages/asc-shared/dist/index.js +16 -0
- package/packages/asc-shared/dist/keywords.d.ts +48 -0
- package/packages/asc-shared/dist/keywords.d.ts.map +1 -0
- package/packages/asc-shared/dist/keywords.js +376 -0
- package/packages/asc-shared/dist/limits.d.ts +11 -0
- package/packages/asc-shared/dist/limits.d.ts.map +1 -0
- package/packages/asc-shared/dist/limits.js +9 -0
- package/packages/asc-shared/dist/locales.d.ts +10 -0
- package/packages/asc-shared/dist/locales.d.ts.map +1 -0
- package/packages/asc-shared/dist/locales.js +314 -0
- package/packages/asc-shared/dist/monetization-boundary.d.ts +148 -0
- package/packages/asc-shared/dist/monetization-boundary.d.ts.map +1 -0
- package/packages/asc-shared/dist/monetization-boundary.js +365 -0
- package/packages/asc-shared/dist/post-approval-paths.d.ts +30 -0
- package/packages/asc-shared/dist/post-approval-paths.d.ts.map +1 -0
- package/packages/asc-shared/dist/post-approval-paths.js +25 -0
- package/packages/asc-shared/dist/review-gate-summary.d.ts +166 -0
- package/packages/asc-shared/dist/review-gate-summary.d.ts.map +1 -0
- package/packages/asc-shared/dist/review-gate-summary.js +354 -0
- package/packages/asc-shared/dist/reviewer-feedback.d.ts +19 -0
- package/packages/asc-shared/dist/reviewer-feedback.d.ts.map +1 -0
- package/packages/asc-shared/dist/reviewer-feedback.js +94 -0
- package/packages/asc-shared/dist/screenshot-review.d.ts +478 -0
- package/packages/asc-shared/dist/screenshot-review.d.ts.map +1 -0
- package/packages/asc-shared/dist/screenshot-review.js +17 -0
- package/packages/asc-shared/dist/supabase.types.d.ts +541 -0
- package/packages/asc-shared/dist/supabase.types.d.ts.map +1 -0
- package/packages/asc-shared/dist/supabase.types.js +5 -0
- package/packages/asc-shared/dist/validation.d.ts +42 -0
- package/packages/asc-shared/dist/validation.d.ts.map +1 -0
- package/packages/asc-shared/dist/validation.js +113 -0
- package/scripts/ensure-shared-build.mjs +76 -0
- package/scripts/export-astro-mcp-apps.mjs +841 -0
- package/scripts/localizeaso.mjs +2100 -0
- package/scripts/review-agent.mjs +9092 -0
- package/scripts/review-mcp.mjs +5931 -0
|
@@ -0,0 +1,2100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { chmodSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const repoRoot = join(__dirname, '..');
|
|
12
|
+
const reviewAgentScript = join(__dirname, 'review-agent.mjs');
|
|
13
|
+
const reviewMcpScript = join(__dirname, 'review-mcp.mjs');
|
|
14
|
+
const astroMcpExportScript = join(__dirname, 'export-astro-mcp-apps.mjs');
|
|
15
|
+
const ensureSharedBuildScript = join(__dirname, 'ensure-shared-build.mjs');
|
|
16
|
+
const DEFAULT_CLI_CONFIG_PATH = join(homedir(), '.localizeaso', 'config.json');
|
|
17
|
+
|
|
18
|
+
const HUMAN_ONLY_REVIEW_AGENT_COMMANDS = new Set([
|
|
19
|
+
'approve',
|
|
20
|
+
'save-decisions',
|
|
21
|
+
'status',
|
|
22
|
+
'apply-plan',
|
|
23
|
+
'field-approve',
|
|
24
|
+
'field-save-decisions',
|
|
25
|
+
'field-status',
|
|
26
|
+
'field-apply-plan',
|
|
27
|
+
'field-metadata-files',
|
|
28
|
+
'field-apply-drafts',
|
|
29
|
+
'field-apply-keywords',
|
|
30
|
+
'field-pricing-payload',
|
|
31
|
+
'field-submit-metadata',
|
|
32
|
+
'field-submit-pricing',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const FRIENDLY_HUMAN_ONLY_SUBCOMMANDS = new Set([
|
|
36
|
+
...HUMAN_ONLY_REVIEW_AGENT_COMMANDS,
|
|
37
|
+
'reject',
|
|
38
|
+
'rejected',
|
|
39
|
+
'apply',
|
|
40
|
+
'apply-drafts',
|
|
41
|
+
'apply-keywords',
|
|
42
|
+
'metadata-files',
|
|
43
|
+
'pricing-payload',
|
|
44
|
+
'submit-metadata',
|
|
45
|
+
'submit-pricing',
|
|
46
|
+
'mark-applied',
|
|
47
|
+
'mark-submitted',
|
|
48
|
+
'mark-rejected',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const FRIENDLY_HUMAN_ONLY_REVIEW_COMMANDS = {
|
|
52
|
+
screenshots: {
|
|
53
|
+
reject: 'status',
|
|
54
|
+
rejected: 'status',
|
|
55
|
+
apply: 'apply-plan',
|
|
56
|
+
'mark-applied': 'status',
|
|
57
|
+
'mark-submitted': 'status',
|
|
58
|
+
'mark-rejected': 'status',
|
|
59
|
+
},
|
|
60
|
+
fields: {
|
|
61
|
+
approve: 'field-approve',
|
|
62
|
+
'save-decisions': 'field-save-decisions',
|
|
63
|
+
status: 'field-status',
|
|
64
|
+
reject: 'field-status',
|
|
65
|
+
rejected: 'field-status',
|
|
66
|
+
apply: 'field-apply-plan',
|
|
67
|
+
'apply-plan': 'field-apply-plan',
|
|
68
|
+
'apply-drafts': 'field-apply-drafts',
|
|
69
|
+
'apply-keywords': 'field-apply-keywords',
|
|
70
|
+
'metadata-files': 'field-metadata-files',
|
|
71
|
+
'pricing-payload': 'field-pricing-payload',
|
|
72
|
+
'submit-metadata': 'field-submit-metadata',
|
|
73
|
+
'submit-pricing': 'field-submit-pricing',
|
|
74
|
+
'mark-applied': 'field-status',
|
|
75
|
+
'mark-submitted': 'field-status',
|
|
76
|
+
'mark-rejected': 'field-status',
|
|
77
|
+
},
|
|
78
|
+
metadata: {
|
|
79
|
+
approve: 'field-approve',
|
|
80
|
+
'save-decisions': 'field-save-decisions',
|
|
81
|
+
status: 'field-status',
|
|
82
|
+
reject: 'field-status',
|
|
83
|
+
rejected: 'field-status',
|
|
84
|
+
apply: 'field-apply-drafts',
|
|
85
|
+
'apply-plan': 'field-apply-plan',
|
|
86
|
+
'apply-drafts': 'field-apply-drafts',
|
|
87
|
+
'metadata-files': 'field-metadata-files',
|
|
88
|
+
'submit-metadata': 'field-submit-metadata',
|
|
89
|
+
'mark-applied': 'field-status',
|
|
90
|
+
'mark-submitted': 'field-status',
|
|
91
|
+
'mark-rejected': 'field-status',
|
|
92
|
+
},
|
|
93
|
+
keywords: {
|
|
94
|
+
approve: 'field-approve',
|
|
95
|
+
'save-decisions': 'field-save-decisions',
|
|
96
|
+
status: 'field-status',
|
|
97
|
+
reject: 'field-status',
|
|
98
|
+
rejected: 'field-status',
|
|
99
|
+
apply: 'field-apply-keywords',
|
|
100
|
+
'apply-plan': 'field-apply-plan',
|
|
101
|
+
'apply-keywords': 'field-apply-keywords',
|
|
102
|
+
'mark-applied': 'field-status',
|
|
103
|
+
'mark-submitted': 'field-status',
|
|
104
|
+
'mark-rejected': 'field-status',
|
|
105
|
+
},
|
|
106
|
+
pricing: {
|
|
107
|
+
approve: 'field-approve',
|
|
108
|
+
'save-decisions': 'field-save-decisions',
|
|
109
|
+
status: 'field-status',
|
|
110
|
+
reject: 'field-status',
|
|
111
|
+
rejected: 'field-status',
|
|
112
|
+
apply: 'field-apply-plan',
|
|
113
|
+
'apply-plan': 'field-apply-plan',
|
|
114
|
+
'pricing-payload': 'field-pricing-payload',
|
|
115
|
+
'submit-pricing': 'field-submit-pricing',
|
|
116
|
+
'mark-applied': 'field-status',
|
|
117
|
+
'mark-submitted': 'field-status',
|
|
118
|
+
'mark-rejected': 'field-status',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function printUsage() {
|
|
123
|
+
console.log(`LocalizeASO CLI
|
|
124
|
+
|
|
125
|
+
Friendly entry point for BYO Codex/AI review workflows. Agents may create setup,
|
|
126
|
+
keyword context, proposals, and review jobs; protected apply, approval, schedule,
|
|
127
|
+
status, upload, publish, and App Store submit actions remain human-only.
|
|
128
|
+
|
|
129
|
+
Usage:
|
|
130
|
+
localizeaso pricing parity --app-id APP_ID --file pricing-parity-plan.json
|
|
131
|
+
localizeaso pricing popup --app-id APP_ID --file pricing-parity-plan.json
|
|
132
|
+
localizeaso ppp popup --app-id APP_ID --file pricing-parity-plan.json
|
|
133
|
+
localizeaso price-parity --app-id APP_ID --file pricing-parity-plan.json
|
|
134
|
+
Create a pricing field-review job and export the human consent handoff.
|
|
135
|
+
Use pricing popup or add --open when you explicitly want a browser window.
|
|
136
|
+
Requires Agent Pass or hosted pass. Does not approve, export, schedule, or submit.
|
|
137
|
+
ppp, price-parity, and pricing-parity are product aliases for the same safe surface.
|
|
138
|
+
|
|
139
|
+
localizeaso pricing bundle JOB_ID --out field-bundle.json
|
|
140
|
+
localizeaso pricing brief JOB_ID --out pricing-brief.json
|
|
141
|
+
localizeaso pricing proposal-template JOB_ID --out field-proposal.json
|
|
142
|
+
localizeaso pricing jobs --app-id APP_ID
|
|
143
|
+
localizeaso pricing open-next --app-id APP_ID
|
|
144
|
+
localizeaso pricing open JOB_ID
|
|
145
|
+
localizeaso pricing popup JOB_ID
|
|
146
|
+
localizeaso pricing readiness JOB_ID --proposal-id PROPOSAL_ID
|
|
147
|
+
localizeaso pricing handoff-summary JOB_ID --out handoff-summary.json
|
|
148
|
+
localizeaso pricing submit JOB_ID --file field-proposal.json [--no-open]
|
|
149
|
+
localizeaso pricing refine JOB_ID --instructions "Reviewer feedback" --out field-refine-result.json
|
|
150
|
+
Safe aliases for existing pricing field-review jobs. They do not approve,
|
|
151
|
+
export/schedule prices, publish, or submit anything.
|
|
152
|
+
Pricing refine snapshots should carry current price, agent proposal, human
|
|
153
|
+
final price, not-applicable keyword mapping markers, pricing evidence,
|
|
154
|
+
territory context, schedule warnings, signal coverage, rationale, decisions,
|
|
155
|
+
and diffs for the next proposal pass.
|
|
156
|
+
|
|
157
|
+
localizeaso pricing manifest --app-id APP_ID --file pricing-parity-plan.json --out pricing-field-job.json
|
|
158
|
+
Free/local step. Convert a pricing parity plan into a field-review manifest only.
|
|
159
|
+
Does not create a review job, use hosted AI, touch App Store Connect, or open approval.
|
|
160
|
+
|
|
161
|
+
localizeaso boundary [--kind screenshots|field|metadata|keywords|pricing|workspace]
|
|
162
|
+
Print the free/local vs Agent Pass vs hosted pass boundary.
|
|
163
|
+
|
|
164
|
+
localizeaso doctor
|
|
165
|
+
localizeaso doctor --json
|
|
166
|
+
localizeaso dashboard doctor [--json]
|
|
167
|
+
Local read-only DX check for backend/dashboard URLs. Detects common local
|
|
168
|
+
dashboard ports and prints the recommended LOCALIZEASO_DASHBOARD value when
|
|
169
|
+
dashboard.test or a stale localhost port would break human review links.
|
|
170
|
+
Use --json when an agent, plugin, or backend handoff should consume the
|
|
171
|
+
recommended URLs without scraping text.
|
|
172
|
+
Also reports whether authenticated review links can be created from
|
|
173
|
+
LOCALIZEASO_TOKEN, so a local agent can distinguish a broken URL from a
|
|
174
|
+
dashboard sign-in/session problem.
|
|
175
|
+
Friendly review and MCP commands auto-inject the detected local dashboard
|
|
176
|
+
URL into their child process when dashboard.test or a stale local dashboard
|
|
177
|
+
URL would break human review links.
|
|
178
|
+
Auto/start/submit aliases return JSON/CLI handoff by default; add --open,
|
|
179
|
+
or use popup/open, when the human is ready to open a browser window.
|
|
180
|
+
|
|
181
|
+
localizeaso login --email you@example.com --password-stdin [--backend URL] [--dashboard URL] [--staging]
|
|
182
|
+
localizeaso whoami [--json]
|
|
183
|
+
localizeaso logout
|
|
184
|
+
Stores a local CLI session in ~/.localizeaso/config.json so agents do not
|
|
185
|
+
need LOCALIZEASO_TOKEN in every shell. LOCALIZEASO_TOKEN, LOCALIZEASO_BACKEND,
|
|
186
|
+
and LOCALIZEASO_DASHBOARD still override the local config for CI/automation.
|
|
187
|
+
|
|
188
|
+
localizeaso workspace jobs --app-id APP_ID
|
|
189
|
+
localizeaso workspace open-next --app-id APP_ID
|
|
190
|
+
localizeaso workspace boundary
|
|
191
|
+
localizeaso workspace runbook --app-id APP_ID [--astro-app APP_STORE_ID] [--json]
|
|
192
|
+
Inspect the combined Field + Screenshot human-review queue or open the next
|
|
193
|
+
human review screen, or print the workspace monetization boundary.
|
|
194
|
+
Navigation/boundary only; does not approve, apply, publish, schedule, mark
|
|
195
|
+
status, or submit.
|
|
196
|
+
runbook prints an agent-safe BYO workflow for local doctor, Astro keyword
|
|
197
|
+
export/import, metadata/keyword/screenshot/pricing review starts, proposal
|
|
198
|
+
handoff, and human review navigation. It does not run the steps.
|
|
199
|
+
|
|
200
|
+
localizeaso astro export [--app APP_STORE_ID] [--keyword-context-out keyword-context.json]
|
|
201
|
+
Read-only Astro MCP export for own tracked apps. Can write provider-neutral
|
|
202
|
+
keyword-context JSON for LocalizeASO review jobs. Does not approve, apply,
|
|
203
|
+
submit, or touch App Store Connect.
|
|
204
|
+
|
|
205
|
+
localizeaso astro keywords [--app APP_STORE_ID] [--out keyword-context.json]
|
|
206
|
+
localizeaso astro context [--app APP_STORE_ID] [--out keyword-context.json]
|
|
207
|
+
Friendly read-only Astro MCP keyword-context export for Codex/agent proposal
|
|
208
|
+
generation. Defaults to keyword-context.json and skips ranking history.
|
|
209
|
+
|
|
210
|
+
localizeaso keywords import-csv APP_ID --file optional-auto --astro-dir .
|
|
211
|
+
Persist Astro/CSV keyword rows into LocalizeASO ASO keyword inventory.
|
|
212
|
+
Agent-safe setup only; does not approve, apply, publish, schedule, or submit.
|
|
213
|
+
|
|
214
|
+
localizeaso keywords attach-field FIELD_JOB_ID --file keyword-context.json
|
|
215
|
+
localizeaso keywords attach-screenshot SCREENSHOT_JOB_ID --file keyword-context.json
|
|
216
|
+
localizeaso keywords attach FIELD_JOB_ID --file keyword-context.json
|
|
217
|
+
localizeaso keywords context FIELD_JOB_ID --file keyword-context.json
|
|
218
|
+
localizeaso keywords attach-field-csv FIELD_JOB_ID --file optional-auto --astro-dir .
|
|
219
|
+
localizeaso keywords attach-screenshot-csv SCREENSHOT_JOB_ID --file optional-auto --astro-dir .
|
|
220
|
+
localizeaso keywords attach-csv FIELD_JOB_ID --file optional-auto --astro-dir .
|
|
221
|
+
localizeaso keywords context-csv FIELD_JOB_ID --file optional-auto --astro-dir .
|
|
222
|
+
Attach provider-neutral JSON or CSV-derived keyword context before proposal generation.
|
|
223
|
+
localizeaso keywords start --file keywords-field-job.json --open
|
|
224
|
+
localizeaso keywords auto --file keywords-field-job.json
|
|
225
|
+
localizeaso keywords auto-import --file keywords-field-job.json
|
|
226
|
+
Recommended for BYO-agent setup: auto uses optional Astro CSV discovery,
|
|
227
|
+
existing keyword sync, and exports the human review handoff; auto-import also
|
|
228
|
+
persists discovered CSV rows into the LocalizeASO ASO keyword inventory.
|
|
229
|
+
localizeaso keywords bundle JOB_ID --out field-bundle.json
|
|
230
|
+
localizeaso keywords prompt JOB_ID --out agent-prompt.md
|
|
231
|
+
localizeaso keywords proposal-template JOB_ID --out field-proposal.json
|
|
232
|
+
localizeaso keywords jobs --app-id APP_ID
|
|
233
|
+
localizeaso keywords open-next --app-id APP_ID
|
|
234
|
+
localizeaso keywords open JOB_ID
|
|
235
|
+
localizeaso keywords popup JOB_ID
|
|
236
|
+
localizeaso keywords readiness JOB_ID --proposal-id PROPOSAL_ID
|
|
237
|
+
localizeaso keywords handoff-summary JOB_ID --out handoff-summary.json
|
|
238
|
+
localizeaso keywords aso-map JOB_ID --out aso-keyword-map.json
|
|
239
|
+
localizeaso keywords submit JOB_ID --file field-proposal.json [--no-open]
|
|
240
|
+
localizeaso keywords refine JOB_ID --instructions "Reviewer feedback" --out field-refine-result.json
|
|
241
|
+
Keyword-review aliases for the field-review surface. They do not approve,
|
|
242
|
+
apply keywords, publish, mark status, or submit anything.
|
|
243
|
+
|
|
244
|
+
localizeaso screenshots start --file screenshot-job.json --open
|
|
245
|
+
localizeaso screenshots auto --file screenshot-job.json
|
|
246
|
+
localizeaso screenshots auto-import --file screenshot-job.json
|
|
247
|
+
Recommended for BYO-agent setup: auto uses optional Astro CSV discovery and
|
|
248
|
+
exports the human review handoff; auto-import also persists discovered CSV rows
|
|
249
|
+
into the LocalizeASO ASO keyword inventory before fetching the bundle.
|
|
250
|
+
localizeaso screenshots bundle JOB_ID --out screenshot-bundle.json --handoff screenshot-handoff.json
|
|
251
|
+
localizeaso screenshots prompt JOB_ID --out agent-prompt.md
|
|
252
|
+
localizeaso screenshots proposal-template JOB_ID --out screenshot-proposal.json
|
|
253
|
+
localizeaso screenshots jobs [--app-id APP_ID] [--status proposal_ready]
|
|
254
|
+
localizeaso screenshots open-next [--app-id APP_ID]
|
|
255
|
+
localizeaso screenshots open JOB_ID
|
|
256
|
+
localizeaso screenshots popup JOB_ID
|
|
257
|
+
localizeaso screenshots readiness JOB_ID --proposal-id PROPOSAL_ID
|
|
258
|
+
localizeaso screenshots handoff-summary JOB_ID --out handoff-summary.json
|
|
259
|
+
localizeaso screenshots attach-keywords JOB_ID --file keyword-context.json
|
|
260
|
+
localizeaso screenshots attach JOB_ID --file keyword-context.json
|
|
261
|
+
localizeaso screenshots context JOB_ID --file keyword-context.json
|
|
262
|
+
localizeaso screenshots attach-keywords-csv JOB_ID --file optional-auto --astro-dir .
|
|
263
|
+
localizeaso screenshots attach-csv JOB_ID --file optional-auto --astro-dir .
|
|
264
|
+
localizeaso screenshots context-csv JOB_ID --file optional-auto --astro-dir .
|
|
265
|
+
localizeaso screenshots keyword-brief JOB_ID --out keyword-brief.json
|
|
266
|
+
localizeaso screenshots keyword-prompt JOB_ID --out keyword-agent-prompt.md
|
|
267
|
+
localizeaso screenshots keyword-automation JOB_ID --out keyword-automation.json
|
|
268
|
+
localizeaso screenshots submit JOB_ID --file screenshot-proposal.json [--no-open]
|
|
269
|
+
localizeaso screenshots submit-proposal JOB_ID --file screenshot-proposal.json [--no-open]
|
|
270
|
+
localizeaso screenshots refine JOB_ID --target-locales de-DE --context-snapshot-file copied-review-context.md --instructions "Reviewer feedback" --out screenshot-refine-result.json
|
|
271
|
+
localizeaso fields start --file field-job.json --open
|
|
272
|
+
localizeaso fields auto --file field-job.json
|
|
273
|
+
localizeaso fields auto-import --file field-job.json
|
|
274
|
+
Recommended for metadata/keyword BYO-agent setup: auto uses optional Astro
|
|
275
|
+
CSV discovery, existing keyword sync, and exports the human review handoff.
|
|
276
|
+
Pricing auto-start skips keyword flags and uses pricing brief instead.
|
|
277
|
+
localizeaso fields bundle JOB_ID --out field-bundle.json --handoff field-handoff.json
|
|
278
|
+
localizeaso fields prompt JOB_ID --out agent-prompt.md
|
|
279
|
+
localizeaso fields proposal-template JOB_ID --out field-proposal.json
|
|
280
|
+
localizeaso fields jobs [--app-id APP_ID] [--surface metadata|keywords|pricing] [--status proposal_ready]
|
|
281
|
+
localizeaso fields open-next [--app-id APP_ID] [--surface metadata|keywords|pricing]
|
|
282
|
+
localizeaso fields open JOB_ID
|
|
283
|
+
localizeaso fields popup JOB_ID
|
|
284
|
+
localizeaso fields readiness JOB_ID --proposal-id PROPOSAL_ID
|
|
285
|
+
localizeaso fields handoff-summary JOB_ID --out handoff-summary.json
|
|
286
|
+
localizeaso fields sync-keywords JOB_ID --out synced-keyword-context.json
|
|
287
|
+
localizeaso fields attach-keywords JOB_ID --file keyword-context.json
|
|
288
|
+
localizeaso fields attach JOB_ID --file keyword-context.json
|
|
289
|
+
localizeaso fields context JOB_ID --file keyword-context.json
|
|
290
|
+
localizeaso fields attach-keywords-csv JOB_ID --file optional-auto --astro-dir .
|
|
291
|
+
localizeaso fields attach-csv JOB_ID --file optional-auto --astro-dir .
|
|
292
|
+
localizeaso fields context-csv JOB_ID --file optional-auto --astro-dir .
|
|
293
|
+
Field keyword sync/context commands are metadata/keyword-only; pricing
|
|
294
|
+
field-review jobs reject keyword inputs and use pricing brief instead.
|
|
295
|
+
localizeaso fields keyword-brief JOB_ID --out keyword-brief.json
|
|
296
|
+
localizeaso fields keyword-prompt JOB_ID --out keyword-agent-prompt.md
|
|
297
|
+
localizeaso fields keyword-automation JOB_ID --out keyword-automation.json
|
|
298
|
+
localizeaso fields aso-map JOB_ID --out aso-keyword-map.json
|
|
299
|
+
localizeaso fields pricing-brief JOB_ID --out pricing-brief.json
|
|
300
|
+
localizeaso fields submit JOB_ID --file field-proposal.json [--no-open]
|
|
301
|
+
localizeaso fields submit-proposal JOB_ID --file field-proposal.json [--no-open]
|
|
302
|
+
localizeaso fields refine JOB_ID --target-locales de-DE --context-snapshot-file copied-field-review-context.md --instructions "Reviewer feedback" --out field-refine-result.json
|
|
303
|
+
localizeaso metadata start --file metadata-field-job.json --open
|
|
304
|
+
localizeaso metadata auto --file metadata-field-job.json
|
|
305
|
+
localizeaso metadata auto-import --file metadata-field-job.json
|
|
306
|
+
Recommended for BYO-agent setup: auto uses optional Astro CSV discovery,
|
|
307
|
+
existing keyword sync, and exports the human review handoff; auto-import also
|
|
308
|
+
persists discovered CSV rows into the LocalizeASO ASO keyword inventory.
|
|
309
|
+
localizeaso metadata bundle JOB_ID --out field-bundle.json
|
|
310
|
+
localizeaso metadata prompt JOB_ID --out agent-prompt.md
|
|
311
|
+
localizeaso metadata proposal-template JOB_ID --out field-proposal.json
|
|
312
|
+
localizeaso metadata jobs --app-id APP_ID
|
|
313
|
+
localizeaso metadata open-next --app-id APP_ID
|
|
314
|
+
localizeaso metadata open JOB_ID
|
|
315
|
+
localizeaso metadata popup JOB_ID
|
|
316
|
+
localizeaso metadata readiness JOB_ID --proposal-id PROPOSAL_ID
|
|
317
|
+
localizeaso metadata handoff-summary JOB_ID --out handoff-summary.json
|
|
318
|
+
localizeaso metadata sync-keywords JOB_ID --out synced-keyword-context.json
|
|
319
|
+
localizeaso metadata aso-map JOB_ID --out aso-keyword-map.json
|
|
320
|
+
localizeaso metadata attach-keywords JOB_ID --file keyword-context.json
|
|
321
|
+
localizeaso metadata attach JOB_ID --file keyword-context.json
|
|
322
|
+
localizeaso metadata context JOB_ID --file keyword-context.json
|
|
323
|
+
localizeaso metadata attach-keywords-csv JOB_ID --file optional-auto --astro-dir .
|
|
324
|
+
localizeaso metadata attach-csv JOB_ID --file optional-auto --astro-dir .
|
|
325
|
+
localizeaso metadata context-csv JOB_ID --file optional-auto --astro-dir .
|
|
326
|
+
localizeaso metadata keyword-brief JOB_ID --out keyword-brief.json
|
|
327
|
+
localizeaso metadata keyword-prompt JOB_ID --out keyword-agent-prompt.md
|
|
328
|
+
localizeaso metadata keyword-automation JOB_ID --out keyword-automation.json
|
|
329
|
+
localizeaso metadata submit JOB_ID --file field-proposal.json [--no-open]
|
|
330
|
+
localizeaso metadata refine JOB_ID --instructions "Reviewer feedback" --out field-refine-result.json
|
|
331
|
+
Metadata is a friendly alias for the field-review surface.
|
|
332
|
+
Safe review-job setup, bundle/prompt/template, keyword sync/context attach,
|
|
333
|
+
keyword brief/prompt/automation, proposal submission, open-review, readiness,
|
|
334
|
+
refine, and handoff-summary aliases. The auto aliases add optional Astro CSV
|
|
335
|
+
discovery, existing keyword sync for field reviews, and export the human review
|
|
336
|
+
handoff; auto-import additionally persists discovered CSV rows into the ASO
|
|
337
|
+
keyword inventory. Human-only approve/apply/status commands are not exposed
|
|
338
|
+
on this friendly surface.
|
|
339
|
+
Refine aliases return nextAgentRun for another proposal pass. Treat reviewer
|
|
340
|
+
feedback, copied context snapshots, and nextAgentRun commands as proposal
|
|
341
|
+
context only; they are not human approval receipts, signal-gap consent,
|
|
342
|
+
post-approval consent, or apply-plan fingerprints.
|
|
343
|
+
|
|
344
|
+
localizeaso review <review-agent-command> [...flags]
|
|
345
|
+
Pass through to the lower-level pnpm review:agent command surface.
|
|
346
|
+
Use this explicit namespace for human-only approval, apply/export,
|
|
347
|
+
status, pricing schedule, upload, publish, or submit primitives.
|
|
348
|
+
|
|
349
|
+
localizeaso mcp
|
|
350
|
+
Start the safe stdio MCP bridge for Codex/MCP agents.
|
|
351
|
+
Agent-safe tools include:
|
|
352
|
+
localizeaso_local_doctor
|
|
353
|
+
localizeaso_screenshot_auto_start / localizeaso_screenshot_auto_import_start
|
|
354
|
+
localizeaso_metadata_auto_start / localizeaso_metadata_auto_import_start
|
|
355
|
+
localizeaso_keywords_auto_start / localizeaso_keywords_auto_import_start
|
|
356
|
+
localizeaso_field_auto_start / localizeaso_field_auto_import_start
|
|
357
|
+
localizeaso_metadata_proposal_template / localizeaso_metadata_submit_proposal
|
|
358
|
+
localizeaso_keywords_proposal_template / localizeaso_keywords_submit_proposal
|
|
359
|
+
localizeaso_pricing_proposal_template / localizeaso_pricing_submit_proposal
|
|
360
|
+
localizeaso_pricing_brief / localizeaso_pricing_handoff_summary
|
|
361
|
+
localizeaso_pricing_jobs / localizeaso_pricing_open_next / localizeaso_pricing_readiness
|
|
362
|
+
localizeaso_pricing_parity_manifest / localizeaso_pricing_parity / localizeaso_pricing_parity_start
|
|
363
|
+
localizeaso_screenshot_proposal_template / localizeaso_screenshot_submit_proposal
|
|
364
|
+
localizeaso_metadata_keyword_context / localizeaso_metadata_keyword_context_from_csv
|
|
365
|
+
localizeaso_keywords_keyword_context / localizeaso_keywords_keyword_context_from_csv
|
|
366
|
+
localizeaso_metadata_sync_keywords / localizeaso_keywords_sync_keywords
|
|
367
|
+
localizeaso_review_jobs / localizeaso_review_open_next
|
|
368
|
+
localizeaso_workspace_runbook
|
|
369
|
+
localizeaso_astro_keywords / localizeaso_import_aso_keywords_from_csv
|
|
370
|
+
Auto-start MCP tools default to keywordsCsv="optional-auto" and astroDir=".",
|
|
371
|
+
then return the human review handoff for consent/review navigation. Pricing
|
|
372
|
+
auto-start deliberately avoids keyword/Astro inputs and uses pricing briefs.
|
|
373
|
+
The bridge intentionally does not expose approve, reject, apply, status,
|
|
374
|
+
publish, pricing schedule, screenshot upload, or App Store submit tools.
|
|
375
|
+
|
|
376
|
+
Environment:
|
|
377
|
+
LOCALIZEASO_TOKEN Optional override for the local CLI login token.
|
|
378
|
+
LOCALIZEASO_CONFIG Optional path for the local CLI login config.
|
|
379
|
+
LOCALIZEASO_BACKEND Defaults in review-agent to http://localhost:8787.
|
|
380
|
+
LOCALIZEASO_DASHBOARD Local review URL fallback. Use localizeaso doctor if
|
|
381
|
+
Expo/Vite starts the dashboard on another port.
|
|
382
|
+
EXPO_PUBLIC_DASHBOARD_URL also works as the shared local dashboard fallback.
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizedArgs(argv) {
|
|
387
|
+
const args = [...argv];
|
|
388
|
+
const command = args.shift();
|
|
389
|
+
|
|
390
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
391
|
+
return { kind: 'help', args: [] };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (command === 'mcp') {
|
|
395
|
+
return { kind: 'mcp', args };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (command === 'doctor' || command === 'diagnose' || command === 'health') {
|
|
399
|
+
return { kind: 'doctor', args };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (command === 'login' || command === 'logout' || command === 'whoami') {
|
|
403
|
+
return { kind: command, args };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (command === 'dashboard' || command === 'dash') {
|
|
407
|
+
const subcommand = args.shift();
|
|
408
|
+
if (!subcommand || subcommand === 'doctor' || subcommand === 'diagnose' || subcommand === 'health') {
|
|
409
|
+
return { kind: 'doctor', args };
|
|
410
|
+
}
|
|
411
|
+
return { kind: 'review-agent', args: [command, subcommand, ...args] };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (command === 'astro') {
|
|
415
|
+
return { kind: 'astro-export', args: mapAstroArgs(args) };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (command === 'review') {
|
|
419
|
+
return { kind: 'review-agent', args };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (command === 'workspace' || command === 'work' || command === 'queue') {
|
|
423
|
+
const workspaceMapped = mapWorkspaceArgs(args);
|
|
424
|
+
if (workspaceMapped.kind === 'workspace-runbook') {
|
|
425
|
+
return workspaceMapped;
|
|
426
|
+
}
|
|
427
|
+
return { kind: 'review-agent', args: workspaceMapped };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (command === 'keywords' || command === 'keyword') {
|
|
431
|
+
return mappedFriendlyNamespace('keywords', args, mapKeywordArgs);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (command === 'screenshots' || command === 'screenshot') {
|
|
435
|
+
return mappedFriendlyNamespace('screenshots', args, mapScreenshotArgs);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (command === 'fields' || command === 'field') {
|
|
439
|
+
return mappedFriendlyNamespace('fields', args, mapFieldArgs);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (command === 'metadata') {
|
|
443
|
+
return mappedFriendlyNamespace('metadata', args, (namespaceArgs) => mapFieldArgs(namespaceArgs, 'metadata'));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (command === 'boundary' || command === 'monetization-boundary') {
|
|
447
|
+
return { kind: 'review-agent', args: ['monetization-boundary', ...args] };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (command === 'pricing') {
|
|
451
|
+
return mappedFriendlyNamespace('pricing', args, mapPricingArgs);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (command === 'ppp' || command === 'price-parity' || command === 'pricing-parity') {
|
|
455
|
+
return mappedFriendlyNamespace('pricing', args, mapPricingProductArgs);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (HUMAN_ONLY_REVIEW_AGENT_COMMANDS.has(command)) {
|
|
459
|
+
return { kind: 'blocked-human-only', command, args };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { kind: 'review-agent', args: [command, ...args] };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function mappedFriendlyNamespace(namespace, args, mapper) {
|
|
466
|
+
const subcommand = args[0];
|
|
467
|
+
if (isFriendlyProtectedMutationIntent(namespace, subcommand)) {
|
|
468
|
+
return blockedFriendlyHumanOnly(namespace, subcommand, args.slice(1));
|
|
469
|
+
}
|
|
470
|
+
const mapped = mapper([...args]);
|
|
471
|
+
if (mapped?.kind === 'blocked-human-only') return mapped;
|
|
472
|
+
return { kind: 'review-agent', args: mapped };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function blockedFriendlyHumanOnly(namespace, subcommand, args) {
|
|
476
|
+
const reviewCommand = FRIENDLY_HUMAN_ONLY_REVIEW_COMMANDS[namespace]?.[subcommand] ?? subcommand;
|
|
477
|
+
return {
|
|
478
|
+
kind: 'blocked-human-only',
|
|
479
|
+
command: `${namespace} ${subcommand}`,
|
|
480
|
+
reviewCommand,
|
|
481
|
+
args,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isFriendlyHumanOnlySubcommand(subcommand) {
|
|
486
|
+
return FRIENDLY_HUMAN_ONLY_SUBCOMMANDS.has(subcommand);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function wantsJsonOutput(args = []) {
|
|
490
|
+
return args.includes('--json') || args.includes('-j');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function normalizedSubcommandTokens(subcommand) {
|
|
494
|
+
return String(subcommand ?? '')
|
|
495
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
496
|
+
.toLowerCase()
|
|
497
|
+
.split(/[-_\s]+/)
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isFriendlyProtectedMutationIntent(namespace, subcommand) {
|
|
502
|
+
if (isFriendlyHumanOnlySubcommand(subcommand)) return true;
|
|
503
|
+
|
|
504
|
+
const tokens = normalizedSubcommandTokens(subcommand);
|
|
505
|
+
if (!tokens.length) return false;
|
|
506
|
+
|
|
507
|
+
const tokenSet = new Set(tokens);
|
|
508
|
+
const hasAny = (values) => values.some((value) => tokenSet.has(value));
|
|
509
|
+
const hasPair = (actions, targets) => hasAny(actions) && hasAny(targets);
|
|
510
|
+
const writeActions = [
|
|
511
|
+
'delete',
|
|
512
|
+
'export',
|
|
513
|
+
'finalize',
|
|
514
|
+
'mutate',
|
|
515
|
+
'mutated',
|
|
516
|
+
'mutates',
|
|
517
|
+
'mutation',
|
|
518
|
+
'mutations',
|
|
519
|
+
'payload',
|
|
520
|
+
'publish',
|
|
521
|
+
'published',
|
|
522
|
+
'push',
|
|
523
|
+
'reorder',
|
|
524
|
+
'replace',
|
|
525
|
+
'schedule',
|
|
526
|
+
'submit',
|
|
527
|
+
'submitted',
|
|
528
|
+
'upload',
|
|
529
|
+
'uploaded',
|
|
530
|
+
];
|
|
531
|
+
const appStoreTargets = ['app', 'store', 'appstore', 'asc'];
|
|
532
|
+
const surfaceTargets = ['metadata', 'keyword', 'keywords', 'pricing', 'price', 'prices', 'screenshot', 'screenshots'];
|
|
533
|
+
|
|
534
|
+
if (hasPair(writeActions, appStoreTargets) || hasPair(writeActions, surfaceTargets)) return true;
|
|
535
|
+
|
|
536
|
+
if (namespace === 'metadata') {
|
|
537
|
+
return hasAny(['export', 'mutate', 'mutation', 'publish', 'push', 'replace', 'upload']);
|
|
538
|
+
}
|
|
539
|
+
if (namespace === 'keywords') {
|
|
540
|
+
return hasAny(['export', 'mutate', 'mutation', 'publish', 'push', 'replace', 'upload']);
|
|
541
|
+
}
|
|
542
|
+
if (namespace === 'pricing') {
|
|
543
|
+
return hasAny(['export', 'mutate', 'mutation', 'payload', 'publish', 'push', 'schedule', 'upload']);
|
|
544
|
+
}
|
|
545
|
+
if (namespace === 'screenshots') {
|
|
546
|
+
return hasAny(['delete', 'mutate', 'mutation', 'publish', 'push', 'reorder', 'replace', 'upload']);
|
|
547
|
+
}
|
|
548
|
+
if (namespace === 'fields') {
|
|
549
|
+
return hasAny(['export', 'mutate', 'mutation', 'payload', 'publish', 'push', 'schedule', 'upload']);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function hasFlag(args, flag) {
|
|
556
|
+
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function hasNoOpenFlag(args) {
|
|
560
|
+
return args.some((arg) => arg === '--no-open' || arg === '--open=false' || arg === '--open=0');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function flagValue(args, flag) {
|
|
564
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
565
|
+
const token = args[index];
|
|
566
|
+
if (token === flag) return args[index + 1];
|
|
567
|
+
if (token.startsWith(`${flag}=`)) return token.slice(flag.length + 1);
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function parseLocalFlagArgs(args = []) {
|
|
573
|
+
const flags = {};
|
|
574
|
+
const positional = [];
|
|
575
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
576
|
+
const token = args[index];
|
|
577
|
+
if (!token.startsWith('--')) {
|
|
578
|
+
positional.push(token);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const equalIndex = token.indexOf('=');
|
|
582
|
+
if (equalIndex !== -1) {
|
|
583
|
+
flags[token.slice(2, equalIndex)] = token.slice(equalIndex + 1);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const key = token.slice(2);
|
|
587
|
+
const next = args[index + 1];
|
|
588
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
589
|
+
flags[key] = next;
|
|
590
|
+
index += 1;
|
|
591
|
+
} else {
|
|
592
|
+
flags[key] = true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { flags, positional };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function withDefaultFlag(args, flag, value) {
|
|
599
|
+
if (hasFlag(args, flag)) return args;
|
|
600
|
+
return value === undefined ? [...args, flag] : [...args, flag, value];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function withDefaultOpenFlag(args) {
|
|
604
|
+
if (hasNoOpenFlag(args)) return args;
|
|
605
|
+
return withDefaultFlag(args, '--open');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function withProposalSubmitDefaults(args) {
|
|
609
|
+
return args;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function withAutoKeywordContextDefaults(args, { includeSync = false, includeImport = false } = {}) {
|
|
613
|
+
let result = [...args];
|
|
614
|
+
if (includeSync) result = withDefaultFlag(result, '--sync-keywords');
|
|
615
|
+
result = withDefaultFlag(result, '--keywords-csv', 'optional-auto');
|
|
616
|
+
result = withDefaultFlag(result, '--astro-dir', '.');
|
|
617
|
+
if (includeImport) result = withDefaultFlag(result, '--import-keywords');
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function withFieldAutoDefaults(args, defaultSurface, options) {
|
|
622
|
+
const surface = flagValue(args, '--surface') ?? defaultSurface;
|
|
623
|
+
if (surface === 'pricing') {
|
|
624
|
+
return [...args];
|
|
625
|
+
}
|
|
626
|
+
return withAutoKeywordContextDefaults(args, options);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function mapAstroKeywordContextArgs(args) {
|
|
630
|
+
const result = [];
|
|
631
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
632
|
+
const token = args[index];
|
|
633
|
+
if (token === '--out') {
|
|
634
|
+
result.push('--keyword-context-out');
|
|
635
|
+
if (args[index + 1] !== undefined) {
|
|
636
|
+
result.push(args[index + 1]);
|
|
637
|
+
index += 1;
|
|
638
|
+
}
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (token.startsWith('--out=')) {
|
|
642
|
+
result.push(`--keyword-context-out=${token.slice('--out='.length)}`);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
result.push(token);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let mapped = withDefaultFlag(result, '--keyword-context-out', 'keyword-context.json');
|
|
649
|
+
if (
|
|
650
|
+
!hasFlag(mapped, '--skip-ranking-history') &&
|
|
651
|
+
!hasFlag(mapped, '--history-period') &&
|
|
652
|
+
!hasFlag(mapped, '--max-ranking-history')
|
|
653
|
+
) {
|
|
654
|
+
mapped = withDefaultFlag(mapped, '--skip-ranking-history');
|
|
655
|
+
}
|
|
656
|
+
return mapped;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function mapAstroArgs(args) {
|
|
660
|
+
const subcommand = args.shift();
|
|
661
|
+
if (!subcommand || subcommand === 'export') {
|
|
662
|
+
return args;
|
|
663
|
+
}
|
|
664
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
665
|
+
return ['--help'];
|
|
666
|
+
}
|
|
667
|
+
if (
|
|
668
|
+
subcommand === 'keywords' ||
|
|
669
|
+
subcommand === 'keyword-context' ||
|
|
670
|
+
subcommand === 'context' ||
|
|
671
|
+
subcommand === 'context-export'
|
|
672
|
+
) {
|
|
673
|
+
return mapAstroKeywordContextArgs(args);
|
|
674
|
+
}
|
|
675
|
+
return [subcommand, ...args];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function mapWorkspaceArgs(args) {
|
|
679
|
+
const subcommand = args.shift();
|
|
680
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
681
|
+
return ['help'];
|
|
682
|
+
}
|
|
683
|
+
if (
|
|
684
|
+
subcommand === 'runbook' ||
|
|
685
|
+
subcommand === 'agent-runbook' ||
|
|
686
|
+
subcommand === 'prepare' ||
|
|
687
|
+
subcommand === 'plan'
|
|
688
|
+
) {
|
|
689
|
+
return { kind: 'workspace-runbook', args };
|
|
690
|
+
}
|
|
691
|
+
const mapping = {
|
|
692
|
+
jobs: 'review-jobs',
|
|
693
|
+
list: 'review-jobs',
|
|
694
|
+
queue: 'review-jobs',
|
|
695
|
+
'open-next': 'review-open-next',
|
|
696
|
+
next: 'review-open-next',
|
|
697
|
+
boundary: 'monetization-boundary',
|
|
698
|
+
'monetization-boundary': 'monetization-boundary',
|
|
699
|
+
};
|
|
700
|
+
const command = mapping[subcommand] || 'help';
|
|
701
|
+
return command === 'monetization-boundary'
|
|
702
|
+
? [command, '--kind', 'workspace', ...args]
|
|
703
|
+
: [command, ...args];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function withDefaultSurface(command, args, surface) {
|
|
707
|
+
if (
|
|
708
|
+
command !== 'field-start' &&
|
|
709
|
+
command !== 'field-create' &&
|
|
710
|
+
command !== 'field-jobs' &&
|
|
711
|
+
command !== 'field-open-next' &&
|
|
712
|
+
command !== 'field-sync-keywords' &&
|
|
713
|
+
command !== 'field-keyword-context' &&
|
|
714
|
+
command !== 'field-keyword-context-from-csv' &&
|
|
715
|
+
command !== 'field-keyword-brief' &&
|
|
716
|
+
command !== 'field-keyword-prompt' &&
|
|
717
|
+
command !== 'field-keyword-automation' &&
|
|
718
|
+
command !== 'field-aso-keyword-map'
|
|
719
|
+
) {
|
|
720
|
+
return [command, ...args];
|
|
721
|
+
}
|
|
722
|
+
if (hasFlag(args, '--surface')) {
|
|
723
|
+
return [command, ...args];
|
|
724
|
+
}
|
|
725
|
+
return [command, ...args, '--surface', surface];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function mapPricingArgs(args) {
|
|
729
|
+
const subcommand = args.shift();
|
|
730
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
731
|
+
return ['help'];
|
|
732
|
+
}
|
|
733
|
+
if (isFriendlyHumanOnlySubcommand(subcommand)) {
|
|
734
|
+
return blockedFriendlyHumanOnly('pricing', subcommand, args);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (
|
|
738
|
+
subcommand === 'parity' ||
|
|
739
|
+
subcommand === 'review'
|
|
740
|
+
) {
|
|
741
|
+
return ['pricing-parity', ...args];
|
|
742
|
+
}
|
|
743
|
+
if (subcommand === 'popup' || subcommand === 'review-popup' || subcommand === 'consent-screen') {
|
|
744
|
+
if (hasFlag(args, '--file') || hasFlag(args, '--app-id')) {
|
|
745
|
+
return ['pricing-parity', ...withDefaultOpenFlag(args)];
|
|
746
|
+
}
|
|
747
|
+
return ['field-open', ...args];
|
|
748
|
+
}
|
|
749
|
+
if (subcommand === 'manifest' || subcommand === 'parity-manifest') {
|
|
750
|
+
return ['pricing-parity-manifest', ...args];
|
|
751
|
+
}
|
|
752
|
+
if (subcommand === 'start' || subcommand === 'parity-start') {
|
|
753
|
+
return ['pricing-parity-start', ...args];
|
|
754
|
+
}
|
|
755
|
+
if (subcommand === 'boundary') {
|
|
756
|
+
return ['monetization-boundary', '--kind', 'pricing', ...args];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const mapping = {
|
|
760
|
+
bundle: 'field-bundle',
|
|
761
|
+
prompt: 'field-prompt',
|
|
762
|
+
'agent-prompt': 'field-prompt',
|
|
763
|
+
'proposal-template': 'field-proposal-template',
|
|
764
|
+
template: 'field-proposal-template',
|
|
765
|
+
'pricing-brief': 'field-pricing-brief',
|
|
766
|
+
brief: 'field-pricing-brief',
|
|
767
|
+
submit: 'field-submit-proposal',
|
|
768
|
+
'submit-proposal': 'field-submit-proposal',
|
|
769
|
+
refine: 'field-refine-request',
|
|
770
|
+
'refine-request': 'field-refine-request',
|
|
771
|
+
open: 'field-open',
|
|
772
|
+
readiness: 'field-readiness',
|
|
773
|
+
'handoff-summary': 'field-handoff-summary',
|
|
774
|
+
jobs: 'field-jobs',
|
|
775
|
+
list: 'field-jobs',
|
|
776
|
+
queue: 'field-jobs',
|
|
777
|
+
'open-next': 'field-open-next',
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const command = mapping[subcommand] || 'help';
|
|
781
|
+
const mappedArgs = command === 'field-submit-proposal' ? withProposalSubmitDefaults(args) : args;
|
|
782
|
+
return withDefaultSurface(command, mappedArgs, 'pricing');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function mapPricingProductArgs(args) {
|
|
786
|
+
const [subcommand] = args;
|
|
787
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
788
|
+
return mapPricingArgs(args);
|
|
789
|
+
}
|
|
790
|
+
if (subcommand.startsWith('-')) {
|
|
791
|
+
return mapPricingArgs(['parity', ...args]);
|
|
792
|
+
}
|
|
793
|
+
return mapPricingArgs(args);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function mapKeywordArgs(args) {
|
|
797
|
+
const subcommand = args.shift();
|
|
798
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
799
|
+
return ['help'];
|
|
800
|
+
}
|
|
801
|
+
if (isFriendlyHumanOnlySubcommand(subcommand)) {
|
|
802
|
+
return blockedFriendlyHumanOnly('keywords', subcommand, args);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (subcommand === 'auto' || subcommand === 'auto-start') {
|
|
806
|
+
return withDefaultSurface(
|
|
807
|
+
'field-start',
|
|
808
|
+
withFieldAutoDefaults(args, 'keywords', { includeSync: true }),
|
|
809
|
+
'keywords',
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (subcommand === 'auto-import' || subcommand === 'auto-start-import') {
|
|
813
|
+
return withDefaultSurface(
|
|
814
|
+
'field-start',
|
|
815
|
+
withFieldAutoDefaults(args, 'keywords', { includeSync: true, includeImport: true }),
|
|
816
|
+
'keywords',
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (subcommand === 'import-csv' || subcommand === 'import-astro-csv') {
|
|
820
|
+
return ['import-aso-keywords-from-csv', ...args];
|
|
821
|
+
}
|
|
822
|
+
if (subcommand === 'sync-field') {
|
|
823
|
+
return ['field-sync-keywords', ...args];
|
|
824
|
+
}
|
|
825
|
+
if (subcommand === 'context-from-csv' || subcommand === 'convert-csv') {
|
|
826
|
+
return ['keyword-context-from-csv', ...args];
|
|
827
|
+
}
|
|
828
|
+
if (subcommand === 'attach-screenshot') {
|
|
829
|
+
return ['keyword-context', ...args];
|
|
830
|
+
}
|
|
831
|
+
if (subcommand === 'attach-field') {
|
|
832
|
+
return ['field-keyword-context', ...args];
|
|
833
|
+
}
|
|
834
|
+
if (subcommand === 'attach' || subcommand === 'context') {
|
|
835
|
+
return withDefaultSurface('field-keyword-context', args, 'keywords');
|
|
836
|
+
}
|
|
837
|
+
if (subcommand === 'attach-screenshot-csv') {
|
|
838
|
+
return ['keyword-context-from-csv', ...args];
|
|
839
|
+
}
|
|
840
|
+
if (subcommand === 'attach-field-csv') {
|
|
841
|
+
return ['field-keyword-context-from-csv', ...args];
|
|
842
|
+
}
|
|
843
|
+
if (subcommand === 'attach-csv' || subcommand === 'context-csv') {
|
|
844
|
+
return withDefaultSurface('field-keyword-context-from-csv', args, 'keywords');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const mapping = {
|
|
848
|
+
start: 'field-start',
|
|
849
|
+
create: 'field-create',
|
|
850
|
+
jobs: 'field-jobs',
|
|
851
|
+
list: 'field-jobs',
|
|
852
|
+
queue: 'field-jobs',
|
|
853
|
+
'open-next': 'field-open-next',
|
|
854
|
+
bundle: 'field-bundle',
|
|
855
|
+
prompt: 'field-prompt',
|
|
856
|
+
'agent-prompt': 'field-prompt',
|
|
857
|
+
'proposal-template': 'field-proposal-template',
|
|
858
|
+
template: 'field-proposal-template',
|
|
859
|
+
'submit-proposal': 'field-submit-proposal',
|
|
860
|
+
submit: 'field-submit-proposal',
|
|
861
|
+
open: 'field-open',
|
|
862
|
+
popup: 'field-open',
|
|
863
|
+
'review-popup': 'field-open',
|
|
864
|
+
'consent-screen': 'field-open',
|
|
865
|
+
refine: 'field-refine-request',
|
|
866
|
+
'refine-request': 'field-refine-request',
|
|
867
|
+
readiness: 'field-readiness',
|
|
868
|
+
'handoff-summary': 'field-handoff-summary',
|
|
869
|
+
'sync-keywords': 'field-sync-keywords',
|
|
870
|
+
'sync-keyword-context': 'field-sync-keywords',
|
|
871
|
+
'keyword-context': 'field-keyword-context',
|
|
872
|
+
context: 'field-keyword-context',
|
|
873
|
+
attach: 'field-keyword-context',
|
|
874
|
+
'attach-keywords': 'field-keyword-context',
|
|
875
|
+
'attach-keyword-context': 'field-keyword-context',
|
|
876
|
+
'keyword-context-from-csv': 'field-keyword-context-from-csv',
|
|
877
|
+
'context-csv': 'field-keyword-context-from-csv',
|
|
878
|
+
'attach-csv': 'field-keyword-context-from-csv',
|
|
879
|
+
'attach-keywords-csv': 'field-keyword-context-from-csv',
|
|
880
|
+
'attach-keyword-csv': 'field-keyword-context-from-csv',
|
|
881
|
+
'aso-map': 'field-aso-keyword-map',
|
|
882
|
+
'aso-keyword-map': 'field-aso-keyword-map',
|
|
883
|
+
'keyword-map': 'field-aso-keyword-map',
|
|
884
|
+
'keyword-brief': 'field-keyword-brief',
|
|
885
|
+
'keyword-prompt': 'field-keyword-prompt',
|
|
886
|
+
'keyword-automation': 'field-keyword-automation',
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const command = mapping[subcommand] || 'help';
|
|
890
|
+
const mappedArgs = command === 'field-submit-proposal' ? withProposalSubmitDefaults(args) : args;
|
|
891
|
+
return withDefaultSurface(command, mappedArgs, 'keywords');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function mapScreenshotArgs(args) {
|
|
895
|
+
const subcommand = args.shift();
|
|
896
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
897
|
+
return ['help'];
|
|
898
|
+
}
|
|
899
|
+
if (isFriendlyHumanOnlySubcommand(subcommand)) {
|
|
900
|
+
return blockedFriendlyHumanOnly('screenshots', subcommand, args);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (subcommand === 'auto' || subcommand === 'auto-start') {
|
|
904
|
+
return ['start', ...withAutoKeywordContextDefaults(args)];
|
|
905
|
+
}
|
|
906
|
+
if (subcommand === 'auto-import' || subcommand === 'auto-start-import') {
|
|
907
|
+
return ['start', ...withAutoKeywordContextDefaults(args, { includeImport: true })];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const mapping = {
|
|
911
|
+
start: 'start',
|
|
912
|
+
create: 'create',
|
|
913
|
+
jobs: 'jobs',
|
|
914
|
+
list: 'jobs',
|
|
915
|
+
queue: 'jobs',
|
|
916
|
+
'open-next': 'open-next',
|
|
917
|
+
bundle: 'bundle',
|
|
918
|
+
prompt: 'prompt',
|
|
919
|
+
'agent-prompt': 'prompt',
|
|
920
|
+
'proposal-template': 'proposal-template',
|
|
921
|
+
template: 'proposal-template',
|
|
922
|
+
'submit-proposal': 'submit-proposal',
|
|
923
|
+
submit: 'submit-proposal',
|
|
924
|
+
open: 'open',
|
|
925
|
+
popup: 'open',
|
|
926
|
+
'review-popup': 'open',
|
|
927
|
+
'consent-screen': 'open',
|
|
928
|
+
refine: 'refine-request',
|
|
929
|
+
'refine-request': 'refine-request',
|
|
930
|
+
readiness: 'readiness',
|
|
931
|
+
'handoff-summary': 'handoff-summary',
|
|
932
|
+
'keyword-context': 'keyword-context',
|
|
933
|
+
context: 'keyword-context',
|
|
934
|
+
attach: 'keyword-context',
|
|
935
|
+
'attach-keywords': 'keyword-context',
|
|
936
|
+
'attach-keyword-context': 'keyword-context',
|
|
937
|
+
'keyword-context-from-csv': 'keyword-context-from-csv',
|
|
938
|
+
'context-csv': 'keyword-context-from-csv',
|
|
939
|
+
'attach-csv': 'keyword-context-from-csv',
|
|
940
|
+
'attach-keywords-csv': 'keyword-context-from-csv',
|
|
941
|
+
'attach-keyword-csv': 'keyword-context-from-csv',
|
|
942
|
+
'keyword-brief': 'keyword-brief',
|
|
943
|
+
'keyword-prompt': 'keyword-prompt',
|
|
944
|
+
'keyword-automation': 'keyword-automation',
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const command = mapping[subcommand] || 'help';
|
|
948
|
+
const mappedArgs = command === 'submit-proposal' ? withProposalSubmitDefaults(args) : args;
|
|
949
|
+
return [command, ...mappedArgs];
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function mapFieldArgs(args, defaultSurface) {
|
|
953
|
+
const subcommand = args.shift();
|
|
954
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
955
|
+
return ['help'];
|
|
956
|
+
}
|
|
957
|
+
if (isFriendlyHumanOnlySubcommand(subcommand)) {
|
|
958
|
+
return blockedFriendlyHumanOnly(defaultSurface ?? 'fields', subcommand, args);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (subcommand === 'auto' || subcommand === 'auto-start') {
|
|
962
|
+
const startArgs = withFieldAutoDefaults(args, defaultSurface, { includeSync: true });
|
|
963
|
+
return defaultSurface ? withDefaultSurface('field-start', startArgs, defaultSurface) : ['field-start', ...startArgs];
|
|
964
|
+
}
|
|
965
|
+
if (subcommand === 'auto-import' || subcommand === 'auto-start-import') {
|
|
966
|
+
const startArgs = withFieldAutoDefaults(args, defaultSurface, { includeSync: true, includeImport: true });
|
|
967
|
+
return defaultSurface ? withDefaultSurface('field-start', startArgs, defaultSurface) : ['field-start', ...startArgs];
|
|
968
|
+
}
|
|
969
|
+
if (subcommand === 'boundary') {
|
|
970
|
+
return ['monetization-boundary', '--kind', defaultSurface ?? 'field', ...args];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const mapping = {
|
|
974
|
+
start: 'field-start',
|
|
975
|
+
create: 'field-create',
|
|
976
|
+
jobs: 'field-jobs',
|
|
977
|
+
list: 'field-jobs',
|
|
978
|
+
queue: 'field-jobs',
|
|
979
|
+
'open-next': 'field-open-next',
|
|
980
|
+
bundle: 'field-bundle',
|
|
981
|
+
prompt: 'field-prompt',
|
|
982
|
+
'agent-prompt': 'field-prompt',
|
|
983
|
+
'proposal-template': 'field-proposal-template',
|
|
984
|
+
template: 'field-proposal-template',
|
|
985
|
+
'submit-proposal': 'field-submit-proposal',
|
|
986
|
+
submit: 'field-submit-proposal',
|
|
987
|
+
open: 'field-open',
|
|
988
|
+
popup: 'field-open',
|
|
989
|
+
'review-popup': 'field-open',
|
|
990
|
+
'consent-screen': 'field-open',
|
|
991
|
+
refine: 'field-refine-request',
|
|
992
|
+
'refine-request': 'field-refine-request',
|
|
993
|
+
readiness: 'field-readiness',
|
|
994
|
+
'handoff-summary': 'field-handoff-summary',
|
|
995
|
+
'sync-keywords': 'field-sync-keywords',
|
|
996
|
+
'sync-keyword-context': 'field-sync-keywords',
|
|
997
|
+
'keyword-context': 'field-keyword-context',
|
|
998
|
+
context: 'field-keyword-context',
|
|
999
|
+
attach: 'field-keyword-context',
|
|
1000
|
+
'attach-keywords': 'field-keyword-context',
|
|
1001
|
+
'attach-keyword-context': 'field-keyword-context',
|
|
1002
|
+
'keyword-context-from-csv': 'field-keyword-context-from-csv',
|
|
1003
|
+
'context-csv': 'field-keyword-context-from-csv',
|
|
1004
|
+
'attach-csv': 'field-keyword-context-from-csv',
|
|
1005
|
+
'attach-keywords-csv': 'field-keyword-context-from-csv',
|
|
1006
|
+
'attach-keyword-csv': 'field-keyword-context-from-csv',
|
|
1007
|
+
'aso-map': 'field-aso-keyword-map',
|
|
1008
|
+
'aso-keyword-map': 'field-aso-keyword-map',
|
|
1009
|
+
'keyword-map': 'field-aso-keyword-map',
|
|
1010
|
+
'keyword-brief': 'field-keyword-brief',
|
|
1011
|
+
'keyword-prompt': 'field-keyword-prompt',
|
|
1012
|
+
'keyword-automation': 'field-keyword-automation',
|
|
1013
|
+
'pricing-brief': 'field-pricing-brief',
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const command = mapping[subcommand] || 'help';
|
|
1017
|
+
const mappedArgs = command === 'field-submit-proposal' ? withProposalSubmitDefaults(args) : args;
|
|
1018
|
+
if (defaultSurface) {
|
|
1019
|
+
return withDefaultSurface(command, mappedArgs, defaultSurface);
|
|
1020
|
+
}
|
|
1021
|
+
return [command, ...mappedArgs];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function runNodeScript(script, args, options = {}) {
|
|
1025
|
+
const result = spawnSync(process.execPath, [script, ...args], {
|
|
1026
|
+
cwd: repoRoot,
|
|
1027
|
+
env: options.injectLocalDashboard ? envWithDetectedLocalDashboard(process.env) : process.env,
|
|
1028
|
+
stdio: 'inherit',
|
|
1029
|
+
});
|
|
1030
|
+
if (result.error) {
|
|
1031
|
+
console.error(result.error.message);
|
|
1032
|
+
return 1;
|
|
1033
|
+
}
|
|
1034
|
+
return result.status ?? 1;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function ensureSharedBuild() {
|
|
1038
|
+
if (process.env.LOCALIZEASO_SKIP_SHARED_BUILD === '1') return 0;
|
|
1039
|
+
return runNodeScript(ensureSharedBuildScript, []);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function cleanEnvUrl(value) {
|
|
1043
|
+
return typeof value === 'string' && value.trim() ? value.trim().replace(/\/+$/, '') : '';
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function cliConfigPath(env = process.env) {
|
|
1047
|
+
return typeof env.LOCALIZEASO_CONFIG === 'string' && env.LOCALIZEASO_CONFIG.trim()
|
|
1048
|
+
? env.LOCALIZEASO_CONFIG.trim()
|
|
1049
|
+
: DEFAULT_CLI_CONFIG_PATH;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function readCliConfig(env = process.env) {
|
|
1053
|
+
try {
|
|
1054
|
+
const raw = readFileSync(cliConfigPath(env), 'utf8');
|
|
1055
|
+
const parsed = JSON.parse(raw);
|
|
1056
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (error?.code === 'ENOENT') return {};
|
|
1059
|
+
return {};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function writeCliConfig(config, env = process.env) {
|
|
1064
|
+
const path = cliConfigPath(env);
|
|
1065
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1066
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
1067
|
+
try {
|
|
1068
|
+
chmodSync(path, 0o600);
|
|
1069
|
+
} catch {
|
|
1070
|
+
// Best effort on platforms/filesystems that support POSIX permissions.
|
|
1071
|
+
}
|
|
1072
|
+
return path;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function configuredToken(env = process.env) {
|
|
1076
|
+
const envToken = typeof env.LOCALIZEASO_TOKEN === 'string' ? env.LOCALIZEASO_TOKEN.trim() : '';
|
|
1077
|
+
if (envToken) return envToken;
|
|
1078
|
+
const config = readCliConfig(env);
|
|
1079
|
+
return typeof config.token === 'string' ? config.token.trim() : '';
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function stagingUrls() {
|
|
1083
|
+
return {
|
|
1084
|
+
backend: 'https://staging-api.localizeaso.com',
|
|
1085
|
+
dashboard: 'https://staging-dash.localizeaso.com',
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function productionUrls() {
|
|
1090
|
+
return {
|
|
1091
|
+
backend: 'https://api.localizeaso.com',
|
|
1092
|
+
dashboard: 'https://dash.localizeaso.com',
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function unique(values) {
|
|
1097
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function configuredDashboardUrl(env = process.env) {
|
|
1101
|
+
const config = readCliConfig(env);
|
|
1102
|
+
return cleanEnvUrl(
|
|
1103
|
+
env.LOCALIZEASO_DASHBOARD ||
|
|
1104
|
+
env.LOCALIZEASO_DASHBOARD_URL ||
|
|
1105
|
+
env.PUBLIC_DASHBOARD_URL ||
|
|
1106
|
+
env.EXPO_PUBLIC_DASHBOARD_URL ||
|
|
1107
|
+
config.dashboard ||
|
|
1108
|
+
'',
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function configuredBackendUrl(env = process.env) {
|
|
1113
|
+
const config = readCliConfig(env);
|
|
1114
|
+
return cleanEnvUrl(env.LOCALIZEASO_BACKEND || config.backend || 'http://localhost:8787');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function isDashboardTestUrl(value) {
|
|
1118
|
+
try {
|
|
1119
|
+
return new URL(value).hostname.toLowerCase() === 'dashboard.test';
|
|
1120
|
+
} catch {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function isLocalDashboardUrl(value) {
|
|
1126
|
+
try {
|
|
1127
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
1128
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
|
|
1129
|
+
} catch {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function localDashboardCandidates(env = process.env) {
|
|
1135
|
+
return unique([
|
|
1136
|
+
configuredDashboardUrl(env),
|
|
1137
|
+
'http://localhost:5174',
|
|
1138
|
+
'http://localhost:5173',
|
|
1139
|
+
'http://localhost:8081',
|
|
1140
|
+
'http://localhost:8084',
|
|
1141
|
+
'http://localhost:19006',
|
|
1142
|
+
]).filter((value) => value && !isDashboardTestUrl(value));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function probeUrl(url, timeoutMs) {
|
|
1146
|
+
const script = `
|
|
1147
|
+
const url = process.argv[1];
|
|
1148
|
+
const timeout = Number(process.argv[2] || 1200);
|
|
1149
|
+
const net = require('node:net');
|
|
1150
|
+
let parsed;
|
|
1151
|
+
try {
|
|
1152
|
+
parsed = new URL(url);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
process.stdout.write(JSON.stringify({ ok: false, error: 'invalid URL' }));
|
|
1155
|
+
process.exit(0);
|
|
1156
|
+
}
|
|
1157
|
+
const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
|
|
1158
|
+
const socket = net.connect({ host: parsed.hostname, port });
|
|
1159
|
+
const done = (payload) => {
|
|
1160
|
+
socket.removeAllListeners();
|
|
1161
|
+
socket.destroy();
|
|
1162
|
+
process.stdout.write(JSON.stringify(payload));
|
|
1163
|
+
};
|
|
1164
|
+
socket.setTimeout(timeout);
|
|
1165
|
+
socket.once('connect', () => done({ ok: true, status: 'listening' }));
|
|
1166
|
+
socket.once('timeout', () => done({ ok: false, error: 'timeout' }));
|
|
1167
|
+
socket.once('error', (error) => done({ ok: false, error: error && error.message ? error.message : String(error) }));
|
|
1168
|
+
`;
|
|
1169
|
+
const result = spawnSync(process.execPath, ['-e', script, url, String(timeoutMs)], {
|
|
1170
|
+
cwd: repoRoot,
|
|
1171
|
+
encoding: 'utf8',
|
|
1172
|
+
timeout: timeoutMs + 1000,
|
|
1173
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1174
|
+
});
|
|
1175
|
+
if (result.error) return { ok: false, error: result.error.message };
|
|
1176
|
+
try {
|
|
1177
|
+
return JSON.parse(result.stdout || '{}');
|
|
1178
|
+
} catch {
|
|
1179
|
+
return { ok: false, error: 'invalid probe response' };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export function detectedLocalDashboardUrl({
|
|
1184
|
+
env = process.env,
|
|
1185
|
+
probe = probeUrl,
|
|
1186
|
+
timeoutMs = Number(env.LOCALIZEASO_DASHBOARD_AUTO_TIMEOUT_MS || env.LOCALIZEASO_DOCTOR_TIMEOUT_MS || 250),
|
|
1187
|
+
} = {}) {
|
|
1188
|
+
if (env.LOCALIZEASO_DASHBOARD_AUTO_DETECT === '0') return '';
|
|
1189
|
+
if (env.NODE_ENV === 'production') return '';
|
|
1190
|
+
const configured = configuredDashboardUrl(env);
|
|
1191
|
+
if (configured && !isDashboardTestUrl(configured) && !isLocalDashboardUrl(configured)) return '';
|
|
1192
|
+
|
|
1193
|
+
for (const url of localDashboardCandidates(env)) {
|
|
1194
|
+
const result = probe(url, timeoutMs);
|
|
1195
|
+
if (result?.ok) return url === configured ? '' : url;
|
|
1196
|
+
}
|
|
1197
|
+
return '';
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
export function envWithDetectedLocalDashboard(env = process.env, probe = probeUrl) {
|
|
1201
|
+
const configured = configuredDashboardUrl(env);
|
|
1202
|
+
if (configured && !isDashboardTestUrl(configured) && !isLocalDashboardUrl(configured)) return env;
|
|
1203
|
+
|
|
1204
|
+
const detected = detectedLocalDashboardUrl({ env, probe });
|
|
1205
|
+
if (!detected) return env;
|
|
1206
|
+
return {
|
|
1207
|
+
...env,
|
|
1208
|
+
LOCALIZEASO_DASHBOARD: detected,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function parseDoctorOptions(args = []) {
|
|
1213
|
+
const json = args.includes('--json') || args.includes('-j');
|
|
1214
|
+
return { json };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function printAuthUsage(command = 'login') {
|
|
1218
|
+
if (command === 'whoami') {
|
|
1219
|
+
console.log('Usage: localizeaso whoami [--json]');
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (command === 'logout') {
|
|
1223
|
+
console.log('Usage: localizeaso logout [--json]');
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
console.log(`Usage:
|
|
1227
|
+
localizeaso login --email you@example.com --password-stdin [--backend URL] [--dashboard URL]
|
|
1228
|
+
localizeaso login --email you@example.com --password-stdin --staging
|
|
1229
|
+
localizeaso login --email you@example.com --password-stdin --prod
|
|
1230
|
+
|
|
1231
|
+
Options:
|
|
1232
|
+
--email EMAIL Account email.
|
|
1233
|
+
--password-stdin Read password from stdin.
|
|
1234
|
+
--password VALUE Local-only convenience; avoid in shared shells.
|
|
1235
|
+
--backend URL Backend API URL.
|
|
1236
|
+
--dashboard URL Dashboard URL for review links.
|
|
1237
|
+
--staging Use staging defaults.
|
|
1238
|
+
--prod Use production defaults.
|
|
1239
|
+
--json Print machine-readable result.
|
|
1240
|
+
`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function localAuthDiagnostics(env = process.env) {
|
|
1244
|
+
const tokenAvailable = configuredToken(env).length > 0;
|
|
1245
|
+
const authLinkEnabled = env.LOCALIZEASO_AUTH_REVIEW_LINK !== '0';
|
|
1246
|
+
const browserOpeningDisabled =
|
|
1247
|
+
env.LOCALIZEASO_DISABLE_OPEN === '1' || env.LOCALIZEASO_DISABLE_BROWSER_OPEN === '1';
|
|
1248
|
+
const canCreateAuthenticatedReviewLinks = Boolean(tokenAvailable && authLinkEnabled && !browserOpeningDisabled);
|
|
1249
|
+
const dashboardSessionMayBeRequired = !canCreateAuthenticatedReviewLinks;
|
|
1250
|
+
const guidance = canCreateAuthenticatedReviewLinks
|
|
1251
|
+
? 'Friendly open/submit commands can request short-lived authenticated dashboard continue links for review URLs.'
|
|
1252
|
+
: browserOpeningDisabled
|
|
1253
|
+
? 'Browser opening is disabled. Open review URLs manually while signed in, or rerun without LOCALIZEASO_DISABLE_OPEN and with LOCALIZEASO_TOKEN so the CLI can request a short-lived dashboard continue link.'
|
|
1254
|
+
: !tokenAvailable
|
|
1255
|
+
? 'LOCALIZEASO_TOKEN is not set. Raw review URLs may land on sign-in unless the browser already has a dashboard session.'
|
|
1256
|
+
: 'LOCALIZEASO_AUTH_REVIEW_LINK=0 disables short-lived dashboard continue links; raw review URLs may require an existing dashboard session.';
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
tokenAvailable,
|
|
1260
|
+
authLinkEnabled,
|
|
1261
|
+
browserOpeningDisabled,
|
|
1262
|
+
canCreateAuthenticatedReviewLinks,
|
|
1263
|
+
dashboardSessionMayBeRequired,
|
|
1264
|
+
guidance,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function shellQuote(value) {
|
|
1269
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function localDoctorHandoff({ backendUrl, recommendedDashboard, auth }) {
|
|
1273
|
+
const recommendedEnvironment = {
|
|
1274
|
+
LOCALIZEASO_BACKEND: backendUrl,
|
|
1275
|
+
...(recommendedDashboard ? { LOCALIZEASO_DASHBOARD: recommendedDashboard } : {}),
|
|
1276
|
+
LOCALIZEASO_DISABLE_OPEN: '1',
|
|
1277
|
+
};
|
|
1278
|
+
const shellExports = Object.entries(recommendedEnvironment).map(
|
|
1279
|
+
([key, value]) => `export ${key}=${shellQuote(value)}`,
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
kind: 'localizeaso_local_doctor_handoff',
|
|
1284
|
+
recommendedEnvironment,
|
|
1285
|
+
shellExports,
|
|
1286
|
+
reviewUrlPolicy: {
|
|
1287
|
+
browserOpenDefault: 'disabled_for_agents',
|
|
1288
|
+
dashboardTestFallback: recommendedDashboard
|
|
1289
|
+
? `dashboard.test review links should resolve locally as ${recommendedDashboard}/...`
|
|
1290
|
+
: 'dashboard.test review links need LOCALIZEASO_DASHBOARD pointing at a running local dashboard.',
|
|
1291
|
+
authenticatedReviewLinks: auth.canCreateAuthenticatedReviewLinks
|
|
1292
|
+
? 'available_with_LOCALIZEASO_TOKEN'
|
|
1293
|
+
: 'not_available_without_LOCALIZEASO_TOKEN_and_browser_opening',
|
|
1294
|
+
humanOpenInstruction:
|
|
1295
|
+
'Keep LOCALIZEASO_DISABLE_OPEN=1 for agent runs. A signed-in human can open the returned reviewUrl manually or rerun a specific open/popup command with --open.',
|
|
1296
|
+
},
|
|
1297
|
+
agentBoundary: {
|
|
1298
|
+
mayRun: ['doctor', 'bundle', 'prompt', 'proposal-template', 'keyword-context', 'submit-proposal', 'refine'],
|
|
1299
|
+
mustNotRun: ['approve', 'reject', 'apply', 'export', 'schedule-pricing', 'publish', 'submit', 'mark-status'],
|
|
1300
|
+
},
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function doctorReport() {
|
|
1305
|
+
const timeoutMs = Number(process.env.LOCALIZEASO_DOCTOR_TIMEOUT_MS || 1200);
|
|
1306
|
+
const backendUrl = configuredBackendUrl();
|
|
1307
|
+
const dashboardEnv = configuredDashboardUrl();
|
|
1308
|
+
const backend = probeUrl(backendUrl, timeoutMs);
|
|
1309
|
+
const dashboards = localDashboardCandidates().map((url) => ({
|
|
1310
|
+
url,
|
|
1311
|
+
result: probeUrl(url, timeoutMs),
|
|
1312
|
+
}));
|
|
1313
|
+
const recommendedDashboard = dashboards.find((entry) => entry.result.ok)?.url || '';
|
|
1314
|
+
const auth = localAuthDiagnostics();
|
|
1315
|
+
const ok = Boolean(recommendedDashboard && backend.ok);
|
|
1316
|
+
const handoff = localDoctorHandoff({ backendUrl, recommendedDashboard, auth });
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
kind: 'localizeaso_local_doctor',
|
|
1320
|
+
ok,
|
|
1321
|
+
timeoutMs,
|
|
1322
|
+
backend: {
|
|
1323
|
+
url: backendUrl,
|
|
1324
|
+
ok: Boolean(backend.ok),
|
|
1325
|
+
status: backend.status || null,
|
|
1326
|
+
error: backend.error || null,
|
|
1327
|
+
},
|
|
1328
|
+
dashboard: {
|
|
1329
|
+
configuredUrl: dashboardEnv || null,
|
|
1330
|
+
configuredIsDashboardTest: Boolean(dashboardEnv && isDashboardTestUrl(dashboardEnv)),
|
|
1331
|
+
recommendedUrl: recommendedDashboard || null,
|
|
1332
|
+
candidates: dashboards.map((entry) => ({
|
|
1333
|
+
url: entry.url,
|
|
1334
|
+
ok: Boolean(entry.result.ok),
|
|
1335
|
+
status: entry.result.status || null,
|
|
1336
|
+
error: entry.result.error || null,
|
|
1337
|
+
})),
|
|
1338
|
+
},
|
|
1339
|
+
auth,
|
|
1340
|
+
environment: {
|
|
1341
|
+
LOCALIZEASO_BACKEND: backendUrl,
|
|
1342
|
+
LOCALIZEASO_DASHBOARD: recommendedDashboard || null,
|
|
1343
|
+
LOCALIZEASO_TOKEN: auth.tokenAvailable ? 'set' : null,
|
|
1344
|
+
LOCALIZEASO_AUTH_REVIEW_LINK: auth.authLinkEnabled ? '1' : '0',
|
|
1345
|
+
},
|
|
1346
|
+
handoff,
|
|
1347
|
+
safety: {
|
|
1348
|
+
readOnly: true,
|
|
1349
|
+
createsReviewJobs: false,
|
|
1350
|
+
approves: false,
|
|
1351
|
+
rejects: false,
|
|
1352
|
+
applies: false,
|
|
1353
|
+
exports: false,
|
|
1354
|
+
schedulesPricing: false,
|
|
1355
|
+
publishes: false,
|
|
1356
|
+
submits: false,
|
|
1357
|
+
marksStatus: false,
|
|
1358
|
+
},
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function doctorCheck(args = []) {
|
|
1363
|
+
const options = parseDoctorOptions(args);
|
|
1364
|
+
const report = doctorReport();
|
|
1365
|
+
|
|
1366
|
+
if (options.json) {
|
|
1367
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1368
|
+
return report.ok ? 0 : 1;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
console.log('LocalizeASO local doctor');
|
|
1372
|
+
console.log('');
|
|
1373
|
+
console.log(
|
|
1374
|
+
`Backend: ${report.backend.url} ${
|
|
1375
|
+
report.backend.ok ? 'OK (port listening)' : `not reachable${report.backend.error ? ` (${report.backend.error})` : ''}`
|
|
1376
|
+
}`,
|
|
1377
|
+
);
|
|
1378
|
+
if (report.dashboard.configuredUrl) {
|
|
1379
|
+
console.log(
|
|
1380
|
+
`Configured dashboard: ${report.dashboard.configuredUrl}${
|
|
1381
|
+
report.dashboard.configuredIsDashboardTest ? ' (dashboard.test is not resolvable without local DNS)' : ''
|
|
1382
|
+
}`,
|
|
1383
|
+
);
|
|
1384
|
+
} else {
|
|
1385
|
+
console.log('Configured dashboard: not set');
|
|
1386
|
+
}
|
|
1387
|
+
console.log('');
|
|
1388
|
+
console.log('Dashboard candidates:');
|
|
1389
|
+
for (const entry of report.dashboard.candidates) {
|
|
1390
|
+
console.log(` ${entry.ok ? 'OK ' : '-- '} ${entry.url}${entry.ok ? ' (port listening)' : ''}`);
|
|
1391
|
+
}
|
|
1392
|
+
console.log('');
|
|
1393
|
+
console.log('Review auth:');
|
|
1394
|
+
console.log(` LOCALIZEASO_TOKEN: ${report.auth.tokenAvailable ? 'set' : 'not set'}`);
|
|
1395
|
+
console.log(` Authenticated review links: ${report.auth.canCreateAuthenticatedReviewLinks ? 'available' : 'not available'}`);
|
|
1396
|
+
console.log(` Browser opening: ${report.auth.browserOpeningDisabled ? 'disabled' : 'enabled'}`);
|
|
1397
|
+
console.log(` ${report.auth.guidance}`);
|
|
1398
|
+
console.log('');
|
|
1399
|
+
if (report.dashboard.recommendedUrl) {
|
|
1400
|
+
console.log(`Recommended: export LOCALIZEASO_DASHBOARD=${report.dashboard.recommendedUrl}`);
|
|
1401
|
+
console.log(`Review links from dashboard.test should resolve locally as ${report.dashboard.recommendedUrl}/...`);
|
|
1402
|
+
} else {
|
|
1403
|
+
console.log('No local dashboard responded. Start it with pnpm --filter asc-dashboard dev -- --port 5174');
|
|
1404
|
+
}
|
|
1405
|
+
console.log('');
|
|
1406
|
+
console.log('Agent environment:');
|
|
1407
|
+
for (const line of report.handoff.shellExports) {
|
|
1408
|
+
console.log(` ${line}`);
|
|
1409
|
+
}
|
|
1410
|
+
console.log(` ${report.handoff.reviewUrlPolicy.humanOpenInstruction}`);
|
|
1411
|
+
console.log('');
|
|
1412
|
+
console.log('Safety: doctor is read-only. It does not create review jobs, approve, apply, submit, or mark status.');
|
|
1413
|
+
return report.ok ? 0 : 1;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function profileUrlsFromFlags(flags = {}) {
|
|
1417
|
+
if (flags.staging === true || flags.profile === 'staging') return { profile: 'staging', ...stagingUrls() };
|
|
1418
|
+
if (
|
|
1419
|
+
flags.prod === true ||
|
|
1420
|
+
flags.production === true ||
|
|
1421
|
+
flags.profile === 'prod' ||
|
|
1422
|
+
flags.profile === 'production'
|
|
1423
|
+
) {
|
|
1424
|
+
return { profile: 'production', ...productionUrls() };
|
|
1425
|
+
}
|
|
1426
|
+
return { profile: 'custom', backend: configuredBackendUrl(), dashboard: configuredDashboardUrl() };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function passwordFromLoginFlags(flags) {
|
|
1430
|
+
if (typeof flags.password === 'string') return flags.password;
|
|
1431
|
+
if (flags['password-stdin'] === true) return readFileSync(0, 'utf8').trimEnd();
|
|
1432
|
+
return '';
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function authHeader(token) {
|
|
1436
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
async function readJsonResponse(response) {
|
|
1440
|
+
const text = await response.text();
|
|
1441
|
+
if (!text) return null;
|
|
1442
|
+
try {
|
|
1443
|
+
return JSON.parse(text);
|
|
1444
|
+
} catch {
|
|
1445
|
+
return { error: text };
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async function loginCommand(args = []) {
|
|
1450
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1451
|
+
printAuthUsage('login');
|
|
1452
|
+
return 0;
|
|
1453
|
+
}
|
|
1454
|
+
const { flags } = parseLocalFlagArgs(args);
|
|
1455
|
+
const profile = profileUrlsFromFlags(flags);
|
|
1456
|
+
const backend = cleanEnvUrl(flags.backend || flags.api || profile.backend);
|
|
1457
|
+
const dashboard = cleanEnvUrl(flags.dashboard || profile.dashboard);
|
|
1458
|
+
const email = typeof flags.email === 'string' ? flags.email.trim().toLowerCase() : '';
|
|
1459
|
+
const password = passwordFromLoginFlags(flags);
|
|
1460
|
+
const json = flags.json === true;
|
|
1461
|
+
|
|
1462
|
+
if (!backend) {
|
|
1463
|
+
console.error('Missing backend URL. Use --backend URL, --staging, or --prod.');
|
|
1464
|
+
return 2;
|
|
1465
|
+
}
|
|
1466
|
+
if (!email) {
|
|
1467
|
+
console.error('Missing email. Use --email you@example.com.');
|
|
1468
|
+
return 2;
|
|
1469
|
+
}
|
|
1470
|
+
if (!password) {
|
|
1471
|
+
console.error('Missing password. Pipe it with --password-stdin or pass --password for local-only tests.');
|
|
1472
|
+
return 2;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const response = await fetch(`${backend}/auth/cli-login`, {
|
|
1476
|
+
method: 'POST',
|
|
1477
|
+
headers: { 'content-type': 'application/json' },
|
|
1478
|
+
body: JSON.stringify({ email, password }),
|
|
1479
|
+
});
|
|
1480
|
+
const payload = await readJsonResponse(response);
|
|
1481
|
+
const token = typeof payload?.session?.access_token === 'string' ? payload.session.access_token.trim() : '';
|
|
1482
|
+
|
|
1483
|
+
if (!response.ok || !token) {
|
|
1484
|
+
console.error(payload?.error || `Login failed with HTTP ${response.status}.`);
|
|
1485
|
+
return 1;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const existing = readCliConfig();
|
|
1489
|
+
const config = {
|
|
1490
|
+
...existing,
|
|
1491
|
+
profile: profile.profile,
|
|
1492
|
+
backend,
|
|
1493
|
+
...(dashboard ? { dashboard } : {}),
|
|
1494
|
+
token,
|
|
1495
|
+
user: payload.user ?? null,
|
|
1496
|
+
session: {
|
|
1497
|
+
token_type: payload.session?.token_type || 'bearer',
|
|
1498
|
+
expires_at: payload.session?.expires_at ?? null,
|
|
1499
|
+
},
|
|
1500
|
+
updatedAt: new Date().toISOString(),
|
|
1501
|
+
};
|
|
1502
|
+
const path = writeCliConfig(config);
|
|
1503
|
+
|
|
1504
|
+
if (json) {
|
|
1505
|
+
console.log(
|
|
1506
|
+
JSON.stringify(
|
|
1507
|
+
{
|
|
1508
|
+
kind: 'localizeaso_cli_login',
|
|
1509
|
+
ok: true,
|
|
1510
|
+
configPath: path,
|
|
1511
|
+
profile: config.profile,
|
|
1512
|
+
backend,
|
|
1513
|
+
dashboard: dashboard || null,
|
|
1514
|
+
user: config.user,
|
|
1515
|
+
tokenStored: true,
|
|
1516
|
+
},
|
|
1517
|
+
null,
|
|
1518
|
+
2,
|
|
1519
|
+
),
|
|
1520
|
+
);
|
|
1521
|
+
} else {
|
|
1522
|
+
console.log(`Logged in${config.user?.email ? ` as ${config.user.email}` : ''}.`);
|
|
1523
|
+
console.log(`Saved CLI session to ${path}.`);
|
|
1524
|
+
}
|
|
1525
|
+
return 0;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
async function logoutCommand(args = []) {
|
|
1529
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1530
|
+
printAuthUsage('logout');
|
|
1531
|
+
return 0;
|
|
1532
|
+
}
|
|
1533
|
+
const { flags } = parseLocalFlagArgs(args);
|
|
1534
|
+
const json = flags.json === true;
|
|
1535
|
+
const config = readCliConfig();
|
|
1536
|
+
const token = configuredToken();
|
|
1537
|
+
const backend = configuredBackendUrl();
|
|
1538
|
+
|
|
1539
|
+
if (token && backend) {
|
|
1540
|
+
await fetch(`${backend}/auth/sign-out`, {
|
|
1541
|
+
method: 'POST',
|
|
1542
|
+
headers: authHeader(token),
|
|
1543
|
+
}).catch(() => null);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const nextConfig = { ...config };
|
|
1547
|
+
delete nextConfig.token;
|
|
1548
|
+
delete nextConfig.session;
|
|
1549
|
+
nextConfig.updatedAt = new Date().toISOString();
|
|
1550
|
+
const path = writeCliConfig(nextConfig);
|
|
1551
|
+
|
|
1552
|
+
if (json) {
|
|
1553
|
+
console.log(JSON.stringify({ kind: 'localizeaso_cli_logout', ok: true, configPath: path }, null, 2));
|
|
1554
|
+
} else {
|
|
1555
|
+
console.log(`Logged out. Updated ${path}.`);
|
|
1556
|
+
}
|
|
1557
|
+
return 0;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
async function whoamiCommand(args = []) {
|
|
1561
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1562
|
+
printAuthUsage('whoami');
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
const { flags } = parseLocalFlagArgs(args);
|
|
1566
|
+
const json = flags.json === true;
|
|
1567
|
+
const token = configuredToken();
|
|
1568
|
+
const backend = configuredBackendUrl();
|
|
1569
|
+
if (!token) {
|
|
1570
|
+
const payload = {
|
|
1571
|
+
kind: 'localizeaso_cli_whoami',
|
|
1572
|
+
ok: false,
|
|
1573
|
+
backend,
|
|
1574
|
+
authenticated: false,
|
|
1575
|
+
error: 'Not logged in. Run localizeaso login --email you@example.com --password-stdin.',
|
|
1576
|
+
};
|
|
1577
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
1578
|
+
else console.error(payload.error);
|
|
1579
|
+
return 1;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const response = await fetch(`${backend}/auth/session`, {
|
|
1583
|
+
headers: authHeader(token),
|
|
1584
|
+
});
|
|
1585
|
+
const payload = await readJsonResponse(response);
|
|
1586
|
+
const result = {
|
|
1587
|
+
kind: 'localizeaso_cli_whoami',
|
|
1588
|
+
ok: response.ok && Boolean(payload?.user),
|
|
1589
|
+
backend,
|
|
1590
|
+
dashboard: configuredDashboardUrl() || null,
|
|
1591
|
+
authenticated: Boolean(payload?.user),
|
|
1592
|
+
user: payload?.user ?? null,
|
|
1593
|
+
isPaid: payload?.isPaid ?? false,
|
|
1594
|
+
isTrial: payload?.isTrial ?? false,
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
if (json) {
|
|
1598
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1599
|
+
} else if (result.authenticated) {
|
|
1600
|
+
console.log(`Logged in${result.user?.email ? ` as ${result.user.email}` : ''}.`);
|
|
1601
|
+
console.log(`Backend: ${backend}`);
|
|
1602
|
+
} else {
|
|
1603
|
+
console.error(payload?.error || `Session check failed with HTTP ${response.status}.`);
|
|
1604
|
+
}
|
|
1605
|
+
return result.ok ? 0 : 1;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function workspaceRunbookCommandStep({ id, label, command, safety, note, monetization, requiresFile }) {
|
|
1609
|
+
return {
|
|
1610
|
+
id,
|
|
1611
|
+
label,
|
|
1612
|
+
command,
|
|
1613
|
+
safety,
|
|
1614
|
+
...(note ? { note } : {}),
|
|
1615
|
+
...(monetization ? { monetization } : {}),
|
|
1616
|
+
...(requiresFile ? { requiresFile } : {}),
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function workspaceMonetizationBoundary() {
|
|
1621
|
+
const script = `
|
|
1622
|
+
import { buildLocalizeAsoMonetizationBoundary } from './packages/asc-shared/dist/index.js';
|
|
1623
|
+
process.stdout.write(JSON.stringify(buildLocalizeAsoMonetizationBoundary('workspace')));
|
|
1624
|
+
`;
|
|
1625
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
|
|
1626
|
+
cwd: repoRoot,
|
|
1627
|
+
env: process.env,
|
|
1628
|
+
encoding: 'utf8',
|
|
1629
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1630
|
+
});
|
|
1631
|
+
if (result.error || result.status !== 0) return null;
|
|
1632
|
+
try {
|
|
1633
|
+
return JSON.parse(result.stdout);
|
|
1634
|
+
} catch {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function workspaceValueBoundary(boundary) {
|
|
1640
|
+
const ledger = boundary && typeof boundary === 'object' && !Array.isArray(boundary)
|
|
1641
|
+
? boundary.valueLedger
|
|
1642
|
+
: null;
|
|
1643
|
+
const byo = ledger && typeof ledger.byoAgentOneTime === 'object' && !Array.isArray(ledger.byoAgentOneTime)
|
|
1644
|
+
? ledger.byoAgentOneTime
|
|
1645
|
+
: {};
|
|
1646
|
+
const hosted = ledger && typeof ledger.hostedConvenience === 'object' && !Array.isArray(ledger.hostedConvenience)
|
|
1647
|
+
? ledger.hostedConvenience
|
|
1648
|
+
: {};
|
|
1649
|
+
const approval = ledger && typeof ledger.approvalGate === 'object' && !Array.isArray(ledger.approvalGate)
|
|
1650
|
+
? ledger.approvalGate
|
|
1651
|
+
: {};
|
|
1652
|
+
|
|
1653
|
+
return {
|
|
1654
|
+
byoAgentOneTime: {
|
|
1655
|
+
purchaseModel: typeof byo.purchaseModel === 'string' ? byo.purchaseModel : 'cheap_one_time_or_lifetime',
|
|
1656
|
+
aiCostOwner: typeof byo.aiCostOwner === 'string' ? byo.aiCostOwner : 'customer',
|
|
1657
|
+
includes: Array.isArray(byo.includes)
|
|
1658
|
+
? byo.includes
|
|
1659
|
+
: [
|
|
1660
|
+
'BYO Codex/AI proposal generation',
|
|
1661
|
+
'persistent backend review jobs',
|
|
1662
|
+
'review history and approval receipts',
|
|
1663
|
+
'Figma apply-plan comfort after approval',
|
|
1664
|
+
'paid app slot for persisted review work',
|
|
1665
|
+
],
|
|
1666
|
+
excludes: Array.isArray(byo.excludes)
|
|
1667
|
+
? byo.excludes
|
|
1668
|
+
: ['LocalizeASO-hosted AI spend', 'hosted App Store Connect upload/submit convenience'],
|
|
1669
|
+
},
|
|
1670
|
+
hostedConvenience: {
|
|
1671
|
+
purchaseModel: typeof hosted.purchaseModel === 'string'
|
|
1672
|
+
? hosted.purchaseModel
|
|
1673
|
+
: 'paid_hosted_credit_or_subscription',
|
|
1674
|
+
aiCostOwner: typeof hosted.aiCostOwner === 'string'
|
|
1675
|
+
? hosted.aiCostOwner
|
|
1676
|
+
: 'localizeaso_or_customer_for_submit_only',
|
|
1677
|
+
includes: Array.isArray(hosted.includes)
|
|
1678
|
+
? hosted.includes
|
|
1679
|
+
: [
|
|
1680
|
+
'hosted backend review history and reviewer feedback',
|
|
1681
|
+
'Figma comfort handoffs and apply plans after approval',
|
|
1682
|
+
'paid app slots for persisted review work',
|
|
1683
|
+
'hosted App Store Connect upload/submit convenience after approval',
|
|
1684
|
+
],
|
|
1685
|
+
excludes: Array.isArray(hosted.excludes)
|
|
1686
|
+
? hosted.excludes
|
|
1687
|
+
: ['using hosted AI budget for BYO-only Agent Pass runs', 'submit, schedule, publish, or apply before human approval'],
|
|
1688
|
+
},
|
|
1689
|
+
approvalGate: {
|
|
1690
|
+
appliesBefore: typeof approval.appliesBefore === 'string'
|
|
1691
|
+
? approval.appliesBefore
|
|
1692
|
+
: 'figma_apply_or_app_store_connect_submit',
|
|
1693
|
+
required: approval.required !== false,
|
|
1694
|
+
agentCanBypass: approval.agentCanBypass === true,
|
|
1695
|
+
notes: Array.isArray(approval.notes)
|
|
1696
|
+
? approval.notes
|
|
1697
|
+
: [
|
|
1698
|
+
'agents may generate proposals and refinements only',
|
|
1699
|
+
'humans must approve the exact apply plan before Figma apply or App Store Connect submit',
|
|
1700
|
+
],
|
|
1701
|
+
},
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function workspaceReviewRunbook(args = []) {
|
|
1706
|
+
const { flags } = parseLocalFlagArgs(args);
|
|
1707
|
+
const appId = typeof flags['app-id'] === 'string' && flags['app-id'].trim()
|
|
1708
|
+
? flags['app-id'].trim()
|
|
1709
|
+
: 'APP_ID';
|
|
1710
|
+
const astroApp = typeof flags['astro-app'] === 'string' && flags['astro-app'].trim()
|
|
1711
|
+
? flags['astro-app'].trim()
|
|
1712
|
+
: 'APP_STORE_ID';
|
|
1713
|
+
const metadataFile = typeof flags['metadata-file'] === 'string' && flags['metadata-file'].trim()
|
|
1714
|
+
? flags['metadata-file'].trim()
|
|
1715
|
+
: 'metadata-field-job.json';
|
|
1716
|
+
const keywordsFile = typeof flags['keywords-file'] === 'string' && flags['keywords-file'].trim()
|
|
1717
|
+
? flags['keywords-file'].trim()
|
|
1718
|
+
: 'keywords-field-job.json';
|
|
1719
|
+
const screenshotsFile = typeof flags['screenshots-file'] === 'string' && flags['screenshots-file'].trim()
|
|
1720
|
+
? flags['screenshots-file'].trim()
|
|
1721
|
+
: 'screenshot-job.json';
|
|
1722
|
+
const pricingFile = typeof flags['pricing-file'] === 'string' && flags['pricing-file'].trim()
|
|
1723
|
+
? flags['pricing-file'].trim()
|
|
1724
|
+
: 'pricing-parity-plan.json';
|
|
1725
|
+
const includePricing = flags.pricing !== false && flags['no-pricing'] !== true;
|
|
1726
|
+
const includeMetadata = flags.metadata !== false && flags['no-metadata'] !== true;
|
|
1727
|
+
const includeKeywords = flags.keywords !== false && flags['no-keywords'] !== true;
|
|
1728
|
+
const includeScreenshots = flags.screenshots !== false && flags['no-screenshots'] !== true;
|
|
1729
|
+
const envPrefix = 'LOCALIZEASO_DISABLE_OPEN=1';
|
|
1730
|
+
const quotedAppId = shellQuote(appId);
|
|
1731
|
+
const quotedAstroApp = shellQuote(astroApp);
|
|
1732
|
+
const monetizationBoundary = workspaceMonetizationBoundary();
|
|
1733
|
+
|
|
1734
|
+
const setupSteps = [
|
|
1735
|
+
workspaceRunbookCommandStep({
|
|
1736
|
+
id: 'local_doctor',
|
|
1737
|
+
label: 'Resolve local backend/dashboard URLs',
|
|
1738
|
+
command: `${envPrefix} pnpm localizeaso doctor --json`,
|
|
1739
|
+
safety: 'read_only',
|
|
1740
|
+
note: 'Use the handoff.recommendedEnvironment values before running agent setup commands. Do not open review URLs from the agent session.',
|
|
1741
|
+
}),
|
|
1742
|
+
workspaceRunbookCommandStep({
|
|
1743
|
+
id: 'workspace_boundary',
|
|
1744
|
+
label: 'Inspect free/local vs paid boundary',
|
|
1745
|
+
command: `${envPrefix} pnpm localizeaso workspace boundary`,
|
|
1746
|
+
safety: 'read_only',
|
|
1747
|
+
monetization: 'free_local_guidance',
|
|
1748
|
+
}),
|
|
1749
|
+
workspaceRunbookCommandStep({
|
|
1750
|
+
id: 'astro_keyword_context',
|
|
1751
|
+
label: 'Export provider-neutral Astro keyword context',
|
|
1752
|
+
command: `${envPrefix} pnpm localizeaso astro keywords --app ${quotedAstroApp} --out keyword-context.json`,
|
|
1753
|
+
safety: 'read_only',
|
|
1754
|
+
note: 'Uses the customer-owned Astro/MCP connection. It does not need App Store Connect credentials in LocalizeASO.',
|
|
1755
|
+
}),
|
|
1756
|
+
workspaceRunbookCommandStep({
|
|
1757
|
+
id: 'import_astro_csv_keywords',
|
|
1758
|
+
label: 'Optionally persist discovered Astro CSV keywords',
|
|
1759
|
+
command: `${envPrefix} pnpm localizeaso keywords import-csv ${quotedAppId} --file optional-auto --astro-dir .`,
|
|
1760
|
+
safety: 'agent_safe_keyword_inventory_setup',
|
|
1761
|
+
monetization: 'agent_pass_or_hosted_pass',
|
|
1762
|
+
note: 'Persists ASO keyword research rows only. It does not approve, apply, publish, schedule, or submit anything.',
|
|
1763
|
+
}),
|
|
1764
|
+
];
|
|
1765
|
+
|
|
1766
|
+
const reviewStartSteps = [];
|
|
1767
|
+
if (includeMetadata) {
|
|
1768
|
+
reviewStartSteps.push(workspaceRunbookCommandStep({
|
|
1769
|
+
id: 'metadata_auto_start',
|
|
1770
|
+
label: 'Start metadata review with keyword sync and optional Astro CSV',
|
|
1771
|
+
command: `${envPrefix} pnpm localizeaso metadata auto --file ${shellQuote(metadataFile)} --bundle-out metadata-bundle.json --handoff metadata-handoff.json`,
|
|
1772
|
+
safety: 'agent_safe_review_job_setup',
|
|
1773
|
+
monetization: 'agent_pass_or_hosted_pass',
|
|
1774
|
+
requiresFile: metadataFile,
|
|
1775
|
+
}));
|
|
1776
|
+
}
|
|
1777
|
+
if (includeKeywords) {
|
|
1778
|
+
reviewStartSteps.push(workspaceRunbookCommandStep({
|
|
1779
|
+
id: 'keywords_auto_start',
|
|
1780
|
+
label: 'Start keyword review with keyword sync and optional Astro CSV',
|
|
1781
|
+
command: `${envPrefix} pnpm localizeaso keywords auto --file ${shellQuote(keywordsFile)} --bundle-out keywords-bundle.json --handoff keywords-handoff.json`,
|
|
1782
|
+
safety: 'agent_safe_review_job_setup',
|
|
1783
|
+
monetization: 'agent_pass_or_hosted_pass',
|
|
1784
|
+
requiresFile: keywordsFile,
|
|
1785
|
+
}));
|
|
1786
|
+
}
|
|
1787
|
+
if (includeScreenshots) {
|
|
1788
|
+
reviewStartSteps.push(workspaceRunbookCommandStep({
|
|
1789
|
+
id: 'screenshots_auto_start',
|
|
1790
|
+
label: 'Start screenshot review with optional Astro CSV keyword context',
|
|
1791
|
+
command: `${envPrefix} pnpm localizeaso screenshots auto --file ${shellQuote(screenshotsFile)} --bundle-out screenshot-bundle.json --handoff screenshot-handoff.json`,
|
|
1792
|
+
safety: 'agent_safe_review_job_setup',
|
|
1793
|
+
monetization: 'agent_pass_or_hosted_pass',
|
|
1794
|
+
requiresFile: screenshotsFile,
|
|
1795
|
+
}));
|
|
1796
|
+
}
|
|
1797
|
+
if (includePricing) {
|
|
1798
|
+
reviewStartSteps.push(workspaceRunbookCommandStep({
|
|
1799
|
+
id: 'pricing_parity_start',
|
|
1800
|
+
label: 'Start pricing parity review from local PPP plan',
|
|
1801
|
+
command: `${envPrefix} pnpm localizeaso pricing parity --app-id ${quotedAppId} --file ${shellQuote(pricingFile)} --bundle-out pricing-field-bundle.json --handoff pricing-field-handoff.json`,
|
|
1802
|
+
safety: 'agent_safe_review_job_setup',
|
|
1803
|
+
monetization: 'agent_pass_or_hosted_pass',
|
|
1804
|
+
requiresFile: pricingFile,
|
|
1805
|
+
note: 'Pricing review uses pricing evidence, not keyword/Astro inputs. Scheduling remains human-only after approval.',
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const proposalSteps = [];
|
|
1810
|
+
if (includeMetadata) {
|
|
1811
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1812
|
+
id: 'metadata_proposal_template',
|
|
1813
|
+
label: 'Create/edit metadata proposal template after bundle fetch',
|
|
1814
|
+
command: `${envPrefix} pnpm localizeaso metadata proposal-template FIELD_JOB_ID --out field-proposal.json`,
|
|
1815
|
+
safety: 'agent_safe_local_file_template',
|
|
1816
|
+
}));
|
|
1817
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1818
|
+
id: 'metadata_submit_proposal',
|
|
1819
|
+
label: 'Submit reviewable metadata proposal',
|
|
1820
|
+
command: `${envPrefix} pnpm localizeaso metadata submit FIELD_JOB_ID --file field-proposal.json`,
|
|
1821
|
+
safety: 'agent_safe_proposal_submission',
|
|
1822
|
+
note: 'Returns the human review handoff without opening a browser by default.',
|
|
1823
|
+
}));
|
|
1824
|
+
}
|
|
1825
|
+
if (includeKeywords) {
|
|
1826
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1827
|
+
id: 'keywords_proposal_template',
|
|
1828
|
+
label: 'Create/edit keyword proposal template after bundle fetch',
|
|
1829
|
+
command: `${envPrefix} pnpm localizeaso keywords proposal-template KEYWORD_FIELD_JOB_ID --out keywords-proposal.json`,
|
|
1830
|
+
safety: 'agent_safe_local_file_template',
|
|
1831
|
+
}));
|
|
1832
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1833
|
+
id: 'keywords_submit_proposal',
|
|
1834
|
+
label: 'Submit reviewable keyword proposal',
|
|
1835
|
+
command: `${envPrefix} pnpm localizeaso keywords submit KEYWORD_FIELD_JOB_ID --file keywords-proposal.json`,
|
|
1836
|
+
safety: 'agent_safe_proposal_submission',
|
|
1837
|
+
note: 'Returns the human review handoff without opening a browser by default.',
|
|
1838
|
+
}));
|
|
1839
|
+
}
|
|
1840
|
+
if (includePricing) {
|
|
1841
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1842
|
+
id: 'pricing_proposal_template',
|
|
1843
|
+
label: 'Create/edit pricing proposal template after pricing brief or bundle fetch',
|
|
1844
|
+
command: `${envPrefix} pnpm localizeaso pricing proposal-template PRICING_FIELD_JOB_ID --out pricing-proposal.json`,
|
|
1845
|
+
safety: 'agent_safe_local_file_template',
|
|
1846
|
+
note: 'Pricing proposals use pricing evidence and territory context, not keyword/Astro inputs.',
|
|
1847
|
+
}));
|
|
1848
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1849
|
+
id: 'pricing_submit_proposal',
|
|
1850
|
+
label: 'Submit reviewable pricing proposal',
|
|
1851
|
+
command: `${envPrefix} pnpm localizeaso pricing submit PRICING_FIELD_JOB_ID --file pricing-proposal.json`,
|
|
1852
|
+
safety: 'agent_safe_proposal_submission',
|
|
1853
|
+
note: 'Returns the human review handoff without opening a browser by default; pricing export, scheduling, hosted submit, and status remain human-only after approval.',
|
|
1854
|
+
}));
|
|
1855
|
+
}
|
|
1856
|
+
if (includeScreenshots) {
|
|
1857
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1858
|
+
id: 'screenshots_proposal_template',
|
|
1859
|
+
label: 'Create/edit screenshot proposal template after bundle fetch',
|
|
1860
|
+
command: `${envPrefix} pnpm localizeaso screenshots proposal-template SCREENSHOT_JOB_ID --out screenshot-proposal.json`,
|
|
1861
|
+
safety: 'agent_safe_local_file_template',
|
|
1862
|
+
}));
|
|
1863
|
+
proposalSteps.push(workspaceRunbookCommandStep({
|
|
1864
|
+
id: 'screenshots_submit_proposal',
|
|
1865
|
+
label: 'Submit reviewable screenshot proposal',
|
|
1866
|
+
command: `${envPrefix} pnpm localizeaso screenshots submit SCREENSHOT_JOB_ID --file screenshot-proposal.json`,
|
|
1867
|
+
safety: 'agent_safe_proposal_submission',
|
|
1868
|
+
note: 'Returns the human review handoff without opening a browser by default.',
|
|
1869
|
+
}));
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const humanReviewSteps = [
|
|
1873
|
+
workspaceRunbookCommandStep({
|
|
1874
|
+
id: 'workspace_jobs',
|
|
1875
|
+
label: 'Inspect combined human review queue',
|
|
1876
|
+
command: `${envPrefix} pnpm localizeaso workspace jobs --app-id ${quotedAppId}`,
|
|
1877
|
+
safety: 'read_only_queue_inspection',
|
|
1878
|
+
}),
|
|
1879
|
+
workspaceRunbookCommandStep({
|
|
1880
|
+
id: 'workspace_open_next',
|
|
1881
|
+
label: 'Return next review handoff for a signed-in human',
|
|
1882
|
+
command: `${envPrefix} pnpm localizeaso workspace open-next --app-id ${quotedAppId}`,
|
|
1883
|
+
safety: 'human_review_navigation_only',
|
|
1884
|
+
note: 'Returns the next human review handoff without opening a browser. A signed-in human can open the returned reviewUrl manually or rerun with --open intentionally.',
|
|
1885
|
+
}),
|
|
1886
|
+
];
|
|
1887
|
+
|
|
1888
|
+
return {
|
|
1889
|
+
kind: 'localizeaso_workspace_agent_runbook',
|
|
1890
|
+
appId,
|
|
1891
|
+
astroApp,
|
|
1892
|
+
recommendedEnvironment: {
|
|
1893
|
+
LOCALIZEASO_BACKEND: configuredBackendUrl(),
|
|
1894
|
+
LOCALIZEASO_DISABLE_OPEN: '1',
|
|
1895
|
+
...(configuredDashboardUrl() ? { LOCALIZEASO_DASHBOARD: configuredDashboardUrl() } : {}),
|
|
1896
|
+
},
|
|
1897
|
+
workflow: {
|
|
1898
|
+
setup: setupSteps,
|
|
1899
|
+
reviewStarts: reviewStartSteps,
|
|
1900
|
+
proposals: proposalSteps,
|
|
1901
|
+
humanReview: humanReviewSteps,
|
|
1902
|
+
},
|
|
1903
|
+
safety: {
|
|
1904
|
+
runbookOnly: true,
|
|
1905
|
+
browserOpenDefault: 'disabled_for_agents',
|
|
1906
|
+
appStoreConnectCredentialsRequiredForAgentSetup: false,
|
|
1907
|
+
hostedAiRequiredForByoAgentSetup: false,
|
|
1908
|
+
approvalAllowed: false,
|
|
1909
|
+
applyAllowed: false,
|
|
1910
|
+
pricingScheduleAllowed: false,
|
|
1911
|
+
appStoreSubmitAllowed: false,
|
|
1912
|
+
statusUpdateAllowed: false,
|
|
1913
|
+
humanOnlyActions: [
|
|
1914
|
+
'approve',
|
|
1915
|
+
'reject',
|
|
1916
|
+
'apply',
|
|
1917
|
+
'export-approved-files',
|
|
1918
|
+
'schedule-pricing',
|
|
1919
|
+
'publish',
|
|
1920
|
+
'submit-to-app-store-connect',
|
|
1921
|
+
'mark-status',
|
|
1922
|
+
],
|
|
1923
|
+
},
|
|
1924
|
+
monetization: {
|
|
1925
|
+
byoAgent: 'Cheap/one-time Agent Pass can cover persistent review jobs, review history, Figma/field handoffs, and app slots while the customer pays for their own Codex/AI/Astro usage.',
|
|
1926
|
+
hosted: 'Hosted Submit Pass covers approved App Store Connect convenience without LocalizeASO-hosted AI; hosted AI proposal generation stays a separate hosted AI/full pass.',
|
|
1927
|
+
freeLocal: 'Local manifest creation, local Astro export, proposal templates, and safety runbooks can stay free/open as acquisition surface.',
|
|
1928
|
+
},
|
|
1929
|
+
valueBoundary: workspaceValueBoundary(monetizationBoundary),
|
|
1930
|
+
monetizationBoundary,
|
|
1931
|
+
reviewContract: {
|
|
1932
|
+
requiredSignalsPerTarget: ['current', 'proposed', 'final', 'assignedKeywords', 'unassignedKeywords', 'warnings', 'rationale', 'diff'],
|
|
1933
|
+
screenshotContext: 'Metadata and keyword reviews should inspect context.screenshotContext when available; pricing reviews use pricing evidence instead.',
|
|
1934
|
+
noBrowserAgentInstruction:
|
|
1935
|
+
'Keep LOCALIZEASO_DISABLE_OPEN=1. Hand review URLs/commands to a signed-in human instead of opening browser windows from the agent session.',
|
|
1936
|
+
},
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function printWorkspaceRunbookText(runbook) {
|
|
1941
|
+
console.log('LocalizeASO workspace agent runbook');
|
|
1942
|
+
console.log('');
|
|
1943
|
+
console.log(`App: ${runbook.appId}`);
|
|
1944
|
+
console.log(`Astro app: ${runbook.astroApp}`);
|
|
1945
|
+
console.log('');
|
|
1946
|
+
console.log('Agent environment:');
|
|
1947
|
+
for (const [key, value] of Object.entries(runbook.recommendedEnvironment)) {
|
|
1948
|
+
console.log(` export ${key}=${shellQuote(value)}`);
|
|
1949
|
+
}
|
|
1950
|
+
for (const [section, steps] of Object.entries(runbook.workflow)) {
|
|
1951
|
+
console.log('');
|
|
1952
|
+
console.log(`${section}:`);
|
|
1953
|
+
for (const step of steps) {
|
|
1954
|
+
console.log(` - ${step.label}`);
|
|
1955
|
+
console.log(` ${step.command}`);
|
|
1956
|
+
console.log(` safety: ${step.safety}`);
|
|
1957
|
+
if (step.note) console.log(` note: ${step.note}`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (runbook.valueBoundary) {
|
|
1961
|
+
const byoIncludes = Array.isArray(runbook.valueBoundary.byoAgentOneTime?.includes)
|
|
1962
|
+
? runbook.valueBoundary.byoAgentOneTime.includes.slice(0, 4).join(', ')
|
|
1963
|
+
: 'BYO proposal generation, review history, Figma handoffs, app slots';
|
|
1964
|
+
const hostedIncludes = Array.isArray(runbook.valueBoundary.hostedConvenience?.includes)
|
|
1965
|
+
? runbook.valueBoundary.hostedConvenience.includes.slice(0, 4).join(', ')
|
|
1966
|
+
: 'hosted review history, Figma comfort, app slots, submit convenience';
|
|
1967
|
+
const approvalRequired = runbook.valueBoundary.approvalGate?.required !== false ? 'required' : 'optional';
|
|
1968
|
+
const approvalBypass = runbook.valueBoundary.approvalGate?.agentCanBypass === true ? 'yes' : 'no';
|
|
1969
|
+
console.log('');
|
|
1970
|
+
console.log('Value boundary:');
|
|
1971
|
+
console.log(` BYO one-time: ${byoIncludes}`);
|
|
1972
|
+
console.log(` Hosted convenience: ${hostedIncludes}`);
|
|
1973
|
+
console.log(` Approval gate: ${approvalRequired}; agent bypass: ${approvalBypass}`);
|
|
1974
|
+
}
|
|
1975
|
+
console.log('');
|
|
1976
|
+
console.log('Safety: runbook only. It does not approve, apply, export approved files, schedule pricing, publish, submit, or mark status.');
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function workspaceRunbookCheck(args = []) {
|
|
1980
|
+
const { flags } = parseLocalFlagArgs(args);
|
|
1981
|
+
const runbook = workspaceReviewRunbook(args);
|
|
1982
|
+
const jsonOutput = flags.json === true || flags.json === 'true' || flags.j === true;
|
|
1983
|
+
const outPath = typeof flags.out === 'string' && flags.out.trim() ? flags.out.trim() : '';
|
|
1984
|
+
|
|
1985
|
+
if (outPath) {
|
|
1986
|
+
writeFileSync(outPath, JSON.stringify(runbook, null, 2), 'utf8');
|
|
1987
|
+
console.error(`Wrote ${outPath}`);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (jsonOutput || outPath) {
|
|
1991
|
+
console.log(JSON.stringify(runbook, null, 2));
|
|
1992
|
+
} else {
|
|
1993
|
+
printWorkspaceRunbookText(runbook);
|
|
1994
|
+
}
|
|
1995
|
+
return 0;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
export function mapLocalizeAsoCliArgs(argv) {
|
|
1999
|
+
return normalizedArgs(argv);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
async function main() {
|
|
2003
|
+
const mapped = normalizedArgs(process.argv.slice(2));
|
|
2004
|
+
if (mapped.kind === 'help') {
|
|
2005
|
+
printUsage();
|
|
2006
|
+
return 0;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (mapped.kind === 'blocked-human-only') {
|
|
2010
|
+
const reviewCommand = mapped.reviewCommand ?? mapped.command;
|
|
2011
|
+
if (wantsJsonOutput(mapped.args)) {
|
|
2012
|
+
console.log(JSON.stringify({
|
|
2013
|
+
kind: 'blocked-human-only',
|
|
2014
|
+
command: mapped.command,
|
|
2015
|
+
reviewCommand,
|
|
2016
|
+
blocked: true,
|
|
2017
|
+
agentSafe: false,
|
|
2018
|
+
humanOnly: true,
|
|
2019
|
+
error:
|
|
2020
|
+
`localizeaso ${mapped.command} is a lower-level human-only review-agent command.`,
|
|
2021
|
+
nextHumanAction:
|
|
2022
|
+
'Open the LocalizeASO review UI or run the lower-level review command only from a concrete human approval/post-approval action.',
|
|
2023
|
+
allowedFriendlyAlternatives: [
|
|
2024
|
+
'localizeaso workspace jobs --app-id APP_ID',
|
|
2025
|
+
'localizeaso workspace open-next --app-id APP_ID',
|
|
2026
|
+
'localizeaso metadata open JOB_ID',
|
|
2027
|
+
'localizeaso screenshots open JOB_ID',
|
|
2028
|
+
'localizeaso pricing open JOB_ID',
|
|
2029
|
+
],
|
|
2030
|
+
mcpSafety: {
|
|
2031
|
+
agentSafe: false,
|
|
2032
|
+
humanOnly: true,
|
|
2033
|
+
blocked: true,
|
|
2034
|
+
requestedCommand: mapped.command,
|
|
2035
|
+
protectedActionsRemainHumanOnly: true,
|
|
2036
|
+
approvalApplySubmitStatusAllowed: false,
|
|
2037
|
+
},
|
|
2038
|
+
}, null, 2));
|
|
2039
|
+
return 2;
|
|
2040
|
+
}
|
|
2041
|
+
console.error(
|
|
2042
|
+
[
|
|
2043
|
+
`localizeaso ${mapped.command} is a lower-level human-only review-agent command.`,
|
|
2044
|
+
`Use localizeaso review ${reviewCommand} ${mapped.args.join(' ')}`.trim(),
|
|
2045
|
+
'only from a concrete human approval/post-approval action, or use a friendly safe command such as localizeaso metadata open JOB_ID, localizeaso screenshots open JOB_ID, or localizeaso pricing open JOB_ID.',
|
|
2046
|
+
].join('\n'),
|
|
2047
|
+
);
|
|
2048
|
+
return 2;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (process.env.LOCALIZEASO_CLI_DRY_RUN === '1') {
|
|
2052
|
+
console.log(JSON.stringify(mapped, null, 2));
|
|
2053
|
+
return 0;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
if (mapped.kind === 'doctor') {
|
|
2057
|
+
return doctorCheck(mapped.args);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (mapped.kind === 'login') {
|
|
2061
|
+
return loginCommand(mapped.args);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (mapped.kind === 'logout') {
|
|
2065
|
+
return logoutCommand(mapped.args);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (mapped.kind === 'whoami') {
|
|
2069
|
+
return whoamiCommand(mapped.args);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (mapped.kind === 'workspace-runbook') {
|
|
2073
|
+
const buildExit = ensureSharedBuild();
|
|
2074
|
+
if (buildExit !== 0) return buildExit;
|
|
2075
|
+
return workspaceRunbookCheck(mapped.args);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
if (mapped.kind === 'mcp') {
|
|
2079
|
+
return runNodeScript(reviewMcpScript, mapped.args, { injectLocalDashboard: true });
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (mapped.kind === 'astro-export') {
|
|
2083
|
+
return runNodeScript(astroMcpExportScript, mapped.args);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const buildExit = ensureSharedBuild();
|
|
2087
|
+
if (buildExit !== 0) return buildExit;
|
|
2088
|
+
return runNodeScript(reviewAgentScript, mapped.args, { injectLocalDashboard: true });
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
2092
|
+
main()
|
|
2093
|
+
.then((code) => {
|
|
2094
|
+
process.exitCode = code;
|
|
2095
|
+
})
|
|
2096
|
+
.catch((error) => {
|
|
2097
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2098
|
+
process.exitCode = 1;
|
|
2099
|
+
});
|
|
2100
|
+
}
|