polycast-cli 0.1.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 ADDED
@@ -0,0 +1,112 @@
1
+ # polycast-cli
2
+
3
+ The `polycast` CLI is the command-line client for Polycast — OTA
4
+ localization for iOS. It manages projects, languages, and publishing from
5
+ your terminal and CI. Your app's day-to-day string syncing is handled
6
+ automatically by the Xcode build plugin; the CLI is for setup, control,
7
+ and CI.
8
+
9
+ Requires Node 18+.
10
+
11
+ ```bash
12
+ npx polycast-cli init # or: npm install -g polycast-cli → `polycast` # or: npm install -g polycast
13
+ ```
14
+
15
+ ## Quickstart (60 seconds)
16
+
17
+ ```bash
18
+ npx polycast login # paste your org admin token
19
+ npx polycast-cli init # or: npm install -g polycast-cli → `polycast` # creates the project, writes polycast.json
20
+ npx polycast langs add ja pt-BR es # AI drafting starts immediately
21
+ npx polycast publish --wait # drafts → live bundles
22
+ ```
23
+
24
+ Add the two integration touches `init` printed, build once so the plugin
25
+ pushes your strings, and your app is live in four languages.
26
+
27
+ ## Authentication — two credentials, on purpose
28
+
29
+ | Credential | Prefix | Used by | Where it lives |
30
+ |---|---|---|---|
31
+ | Org admin token | `pcadm_` | login, init, langs, publish, status | OS keychain (macOS) / `~/.config/polycast/token` 0600 |
32
+ | Project key | `pc_live_` / `pc_test_` | push, pull, build plugin | `polycast.json` in your repo; safe for CI |
33
+
34
+ The project key can only submit source strings and read published
35
+ translations (which your app ships publicly anyway). The admin token can
36
+ enable languages, edit, and publish — it never goes in files or CI.
37
+ `POLYCAST_ADMIN_TOKEN` / `POLYCAST_PROJECT_KEY` env vars override for
38
+ headless use.
39
+
40
+ ## polycast.json
41
+
42
+ ```json
43
+ {
44
+ "endpoint": "https://api.polycast.dev",
45
+ "edgeEndpoint": "https://polycast-edge.mecurylab.workers.dev",
46
+ "projectKey": "pc_live_8f2k...",
47
+ "catalogPath": "App/Localizable.xcstrings"
48
+ }
49
+ ```
50
+
51
+ `endpoint` is the write path (ingest/admin — also what the build plugin
52
+ uses); `edgeEndpoint` is the public read path (`pull`, `doctor`). `init`
53
+ writes both and gitignores `.polycast/` (local push state, shared
54
+ byte-for-byte with the Swift build plugin — digests are identical, so the
55
+ CLI and plugin never re-push each other's work).
56
+
57
+ ## Commands
58
+
59
+ - `login` / `logout` — keychain-stored admin token
60
+ - `init` — detect catalog + Xcode project, create project + key, write
61
+ config. Idempotent: re-running verifies instead of recreating.
62
+ Flags: `--name`, `--catalog`, `--endpoint`, `--yes`
63
+ - `langs` — coverage table · `langs add ja ko th` / `langs add all`
64
+ (App Store locale set) / `langs remove th`. Adding shows the draft
65
+ count and confirms (auto-yes with `--yes` or `CI=true`).
66
+ - `push` — manual string push (what the plugin does on every build)
67
+ - `pull` — merge **published** translations into the catalog so binary
68
+ fallbacks stay fresh. `--locales ja,pt-BR` to limit; `--check` exits 1
69
+ if the catalog is stale (CI guard). Never touches the source language;
70
+ preserves Xcode's JSON formatting.
71
+ - `publish [locales...]` — promote drafts to live bundles. `--wait`
72
+ polls until drafting finishes first. Needs-review strings stay
73
+ unpublished and are called out.
74
+ - `status` — the project at a glance; exit 1 if jobs are dead-lettered
75
+ (CI health gate). `--json` for scripts.
76
+ - `doctor` — every local + remote check with a one-line fix each:
77
+ catalog parses · polycast.json valid · build plugin attached ·
78
+ .polycast gitignored · project key authenticates · admin token valid ·
79
+ edge reachable · ETag/304 round-trip.
80
+ - `gen` — type-safe Swift constants from the catalog
81
+ (`--output Sources/Generated/PolycastKeys.swift`); use with
82
+ `PText(L.paywall.title)`.
83
+
84
+ All commands: `--json` where output is data, no prompts when `CI=true`
85
+ or `--yes`. Exit codes: 0 success · 1 actionable failure · 2 usage error.
86
+
87
+ ## CI recipes
88
+
89
+ ```yaml
90
+ # bake fresh fallbacks into every release, before xcodebuild archive
91
+ - run: npx polycast pull
92
+ env:
93
+ POLYCAST_PROJECT_KEY: ${{ secrets.POLYCAST_PROJECT_KEY }}
94
+
95
+ # or fail the build if fallbacks are stale
96
+ - run: npx polycast pull --check
97
+ ```
98
+
99
+ Only the project key ever goes in CI secrets; if `polycast.json` is
100
+ committed, no secret is needed at all.
101
+
102
+ ## Troubleshooting
103
+
104
+ - **No strings after building** → `polycast doctor`; usual cause is the
105
+ plugin missing from the target or a sandboxed network push
106
+ (workaround: `polycast push`).
107
+ - **Translations not showing in the app** → drafts aren't live until
108
+ `polycast publish`; devices refresh on foreground.
109
+ - **A translation looks wrong** → edit via the admin API/dashboard grid
110
+ (becomes human-locked), publish. Live in minutes.
111
+ - **401** → admin commands need `polycast login`; push/pull need a valid
112
+ `polycast.json`. Doctor says which.
package/dist/api.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ export declare function requireAdminToken(): string;
2
+ export declare const admin: {
3
+ listProjects: (api: string, token: string) => Promise<{
4
+ projects: Array<{
5
+ id: string;
6
+ name: string;
7
+ slug: string;
8
+ }>;
9
+ }>;
10
+ resolveKey: (api: string, token: string, key: string) => Promise<{
11
+ projectId: string;
12
+ name: string;
13
+ slug: string;
14
+ }>;
15
+ createProject: (api: string, token: string, body: Record<string, unknown>) => Promise<{
16
+ id: string;
17
+ name: string;
18
+ slug: string;
19
+ }>;
20
+ createKey: (api: string, token: string, projectId: string, prefix: string) => Promise<{
21
+ id: string;
22
+ key: string;
23
+ }>;
24
+ putLocales: (api: string, token: string, projectId: string, localesBody: unknown) => Promise<{
25
+ draftJobsEnqueued: number;
26
+ }>;
27
+ status: (api: string, token: string, projectId: string) => Promise<ProjectStatus>;
28
+ getProject: (api: string, token: string, projectId: string) => Promise<{
29
+ id: string;
30
+ name: string;
31
+ locales: Array<{
32
+ localeId: string;
33
+ enabled: boolean;
34
+ nativeName: string;
35
+ }>;
36
+ }>;
37
+ publish: (api: string, token: string, projectId: string, locales?: string[]) => Promise<{
38
+ published: Array<{
39
+ locale: string;
40
+ bundleVersion: number;
41
+ etag: string;
42
+ }>;
43
+ }>;
44
+ };
45
+ export interface ProjectStatus {
46
+ project: {
47
+ id: string;
48
+ name: string;
49
+ sourceLocale: string;
50
+ };
51
+ totalStrings: number;
52
+ locales: Array<{
53
+ locale: string;
54
+ nativeName: string;
55
+ enabled: boolean;
56
+ published: number;
57
+ drafts: number;
58
+ needsReview: number;
59
+ coveragePct: number;
60
+ lastPublish: {
61
+ bundleVersion: number;
62
+ at: string;
63
+ } | null;
64
+ }>;
65
+ deadLetter: Array<{
66
+ name: string;
67
+ n: number;
68
+ }>;
69
+ eventCount: number;
70
+ }
71
+ export declare const project: {
72
+ pushStrings: (api: string, key: string, payload: unknown) => Promise<{
73
+ inserted: number;
74
+ updated: number;
75
+ unchanged: number;
76
+ draftJobsEnqueued: number;
77
+ }>;
78
+ };
79
+ export declare const edge: {
80
+ manifest: (edgeUrl: string, key: string) => Promise<{
81
+ schemaVersion: number;
82
+ locales: Array<{
83
+ id: string;
84
+ nativeName: string;
85
+ bundleVersion: number;
86
+ }>;
87
+ }>;
88
+ bundle: (edgeUrl: string, key: string, locale: string) => Promise<{
89
+ bundleVersion: number;
90
+ strings: Record<string, {
91
+ v?: string;
92
+ plural?: Record<string, string>;
93
+ }>;
94
+ }>;
95
+ /** Raw fetch for doctor's ETag round-trip check. */
96
+ raw: (edgeUrl: string, key: string, locale: string, headers?: Record<string, string>) => Promise<Response>;
97
+ };
package/dist/api.js ADDED
@@ -0,0 +1,59 @@
1
+ import { loadToken } from "./keychain.js";
2
+ import { CliError } from "./util.js";
3
+ async function request(url, init = {}) {
4
+ const headers = {
5
+ "content-type": "application/json",
6
+ ...init.headers,
7
+ };
8
+ if (init.auth)
9
+ headers["authorization"] = `Bearer ${init.auth}`;
10
+ let res;
11
+ try {
12
+ res = await fetch(url, { ...init, headers });
13
+ }
14
+ catch (err) {
15
+ throw new CliError(`cannot reach ${new URL(url).origin} — ${err.message}`);
16
+ }
17
+ const text = await res.text();
18
+ let body = null;
19
+ try {
20
+ body = text ? JSON.parse(text) : null;
21
+ }
22
+ catch { /* non-JSON error page */ }
23
+ if (!res.ok) {
24
+ const e = body?.error;
25
+ if (res.status === 401) {
26
+ throw new CliError(e?.message ?? "unauthorized — run `polycast login` (admin) or check polycast.json (project key)");
27
+ }
28
+ throw new CliError(`${init.method ?? "GET"} ${new URL(url).pathname} → ${res.status}: ${e?.message ?? text.slice(0, 200)}`);
29
+ }
30
+ return body;
31
+ }
32
+ export function requireAdminToken() {
33
+ const token = loadToken();
34
+ if (!token)
35
+ throw new CliError("not logged in — run `polycast login`");
36
+ return token;
37
+ }
38
+ // ---- Admin (org token) ----
39
+ export const admin = {
40
+ listProjects: (api, token) => request(`${api}/v1/admin/projects`, { auth: token }),
41
+ resolveKey: (api, token, key) => request(`${api}/v1/admin/projects/resolve-key`, { method: "POST", auth: token, body: JSON.stringify({ key }) }),
42
+ createProject: (api, token, body) => request(`${api}/v1/admin/projects`, { method: "POST", auth: token, body: JSON.stringify(body) }),
43
+ createKey: (api, token, projectId, prefix) => request(`${api}/v1/admin/projects/${projectId}/keys`, { method: "POST", auth: token, body: JSON.stringify({ prefix }) }),
44
+ putLocales: (api, token, projectId, localesBody) => request(`${api}/v1/admin/projects/${projectId}/locales`, { method: "PUT", auth: token, body: JSON.stringify(localesBody) }),
45
+ status: (api, token, projectId) => request(`${api}/v1/admin/projects/${projectId}/status`, { auth: token }),
46
+ getProject: (api, token, projectId) => request(`${api}/v1/admin/projects/${projectId}`, { auth: token }),
47
+ publish: (api, token, projectId, locales) => request(`${api}/v1/admin/projects/${projectId}/publish`, { method: "POST", auth: token, body: JSON.stringify(locales?.length ? { locales } : {}) }),
48
+ };
49
+ // ---- Project key surfaces ----
50
+ export const project = {
51
+ pushStrings: (api, key, payload) => request(`${api}/v1/projects/${key}/strings`, { method: "POST", auth: key, body: JSON.stringify(payload) }),
52
+ };
53
+ export const edge = {
54
+ manifest: (edgeUrl, key) => request(`${edgeUrl}/v1/projects/${key}/manifest`),
55
+ bundle: (edgeUrl, key, locale) => request(`${edgeUrl}/v1/projects/${key}/locales/${locale}`),
56
+ /** Raw fetch for doctor's ETag round-trip check. */
57
+ raw: (edgeUrl, key, locale, headers = {}) => fetch(`${edgeUrl}/v1/projects/${key}/locales/${locale}`, { headers }),
58
+ };
59
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ export interface Flags {
2
+ yes: boolean;
3
+ json: boolean;
4
+ wait: boolean;
5
+ check: boolean;
6
+ token: boolean;
7
+ name?: string;
8
+ catalog?: string;
9
+ endpoint?: string;
10
+ output?: string;
11
+ locales?: string;
12
+ org?: string;
13
+ }
14
+ export declare function login(flags: Flags): Promise<void>;
15
+ export declare function whoami(flags: Flags): Promise<void>;
16
+ export declare function logout(): Promise<void>;
17
+ export declare function init(flags: Flags): Promise<void>;
18
+ export declare function langs(args: string[], flags: Flags): Promise<void>;
19
+ export declare function push(flags: Flags): Promise<void>;
20
+ export declare function pull(flags: Flags): Promise<void>;
21
+ export declare function publish(args: string[], flags: Flags): Promise<void>;
22
+ export declare function status(flags: Flags): Promise<void>;
23
+ export declare function doctor(): Promise<void>;
24
+ /** Plant Polycast+Shadow.swift into each source module so plain Text —
25
+ * and any design-system component built on it — renders OTA. */
26
+ export declare function shadow(args: string[], _flags: Flags): Promise<void>;
27
+ export declare function gen(flags: Flags): Promise<void>;