@lionrapid/cli 0.3.0 → 0.5.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/CHANGELOG.md +128 -2
- package/dist/api-client.d.ts +79 -1
- package/dist/api-client.js +60 -1
- package/dist/cli.js +294 -45
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +47 -5
- package/dist/commands/init.d.ts +73 -2
- package/dist/commands/init.js +0 -0
- package/dist/commands/login.d.ts +50 -4
- package/dist/commands/login.js +70 -16
- package/dist/commands/pull.d.ts +15 -1
- package/dist/commands/pull.js +92 -4
- package/dist/commands/push.d.ts +62 -1
- package/dist/commands/push.js +198 -5
- package/dist/commands/status.d.ts +15 -0
- package/dist/commands/status.js +16 -5
- package/dist/commands/whoami.d.ts +50 -0
- package/dist/commands/whoami.js +66 -0
- package/dist/config.d.ts +100 -5
- package/dist/config.js +239 -22
- package/dist/history.d.ts +44 -0
- package/dist/history.js +65 -0
- package/dist/job-history.d.ts +11 -2
- package/dist/job-history.js +1 -13
- package/dist/lockfile.d.ts +90 -0
- package/dist/lockfile.js +110 -0
- package/dist/time.d.ts +27 -0
- package/dist/time.js +53 -0
- package/package.json +2 -2
package/dist/lockfile.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.lionrapid/lock.json` — the record of what the SERVER last said.
|
|
3
|
+
*
|
|
4
|
+
* Today `push` re-uploads every string every time: 653 on the first run, 653
|
|
5
|
+
* on the second. And there is no way to know a translator edited a target
|
|
6
|
+
* locally, so `pull` would silently overwrite it. Both are the same missing
|
|
7
|
+
* thing — no record of the last known server state — and one mechanism buys
|
|
8
|
+
* incremental push, conflict detection, and a CI action that does not
|
|
9
|
+
* re-translate the world on every run.
|
|
10
|
+
*
|
|
11
|
+
* COMMITTED to version control, like `package-lock.json`. Per-machine would
|
|
12
|
+
* mean CI and a laptop disagree about what changed, which is exactly the
|
|
13
|
+
* disagreement an automated pipeline cannot tolerate.
|
|
14
|
+
*
|
|
15
|
+
* Two fields, two owners, and the split matters:
|
|
16
|
+
* `sourceHash` what we last SENT for a source key (push writes it)
|
|
17
|
+
* `serverUpdatedAt` when the server last told us about a row (PULL ONLY)
|
|
18
|
+
* Push must never fabricate `serverUpdatedAt`, or the lock would claim
|
|
19
|
+
* knowledge of a server state it never observed.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { generateAutoKey } from '@lionrapid/core';
|
|
24
|
+
import { ConfigError } from './config.js';
|
|
25
|
+
/** Where the lock lives, relative to the project root. */
|
|
26
|
+
export const LOCKFILE_PATH = join('.lionrapid', 'lock.json');
|
|
27
|
+
/** The only shape this CLI writes. A higher number is a newer CLI's file. */
|
|
28
|
+
export const LOCK_VERSION = 1;
|
|
29
|
+
/** A lock that knows nothing. Distinct from "everything changed" — see `readLock`. */
|
|
30
|
+
export function emptyLock() {
|
|
31
|
+
return { version: LOCK_VERSION, entries: {} };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hash a source value.
|
|
35
|
+
*
|
|
36
|
+
* Reuses `generateAutoKey`, the md5-12 already shared by core, edge, WordPress
|
|
37
|
+
* TS and WordPress PHP and pinned by `parity-vectors.json`. A second hash
|
|
38
|
+
* would be a second thing to keep in lockstep for no gain. Inline markup is
|
|
39
|
+
* part of the value, so the indexed tags travel into the hash — a bold run
|
|
40
|
+
* added around a word is a real change to push.
|
|
41
|
+
*/
|
|
42
|
+
export function hashSource(value) {
|
|
43
|
+
const styles = Object.keys(value.styles ?? {}).length > 0 ? JSON.stringify(value.styles) : '';
|
|
44
|
+
return generateAutoKey(styles ? `${value.text}${styles}` : value.text);
|
|
45
|
+
}
|
|
46
|
+
/** The lock's key for a row: the coordinates a push actually addresses. */
|
|
47
|
+
export function lockKey(namespace, key) {
|
|
48
|
+
return `${namespace}:${key}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the lock, or an empty one when there is no file.
|
|
52
|
+
*
|
|
53
|
+
* A MISSING lock is "we know nothing" (a backfill), never "everything
|
|
54
|
+
* changed". A CORRUPT lock throws: silently rebuilding one would mark local
|
|
55
|
+
* edits as server-known and lose them on the very next pull, which is the
|
|
56
|
+
* failure this file exists to prevent.
|
|
57
|
+
*/
|
|
58
|
+
export function readLock(cwd, opts = {}) {
|
|
59
|
+
const path = join(cwd, LOCKFILE_PATH);
|
|
60
|
+
if (!existsSync(path))
|
|
61
|
+
return emptyLock();
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new ConfigError(`Could not parse ${LOCKFILE_PATH}: ${err instanceof Error ? err.message : String(err)}.\n` +
|
|
68
|
+
` Delete ${LOCKFILE_PATH} and run \`lionrapid pull\` to rebuild it from the server.`);
|
|
69
|
+
}
|
|
70
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
71
|
+
throw new ConfigError(`${LOCKFILE_PATH} is not a JSON object.\n` +
|
|
72
|
+
` Delete ${LOCKFILE_PATH} and run \`lionrapid pull\` to rebuild it.`);
|
|
73
|
+
}
|
|
74
|
+
const lock = parsed;
|
|
75
|
+
if (typeof lock.version !== 'number' || lock.version > LOCK_VERSION) {
|
|
76
|
+
throw new ConfigError(`${LOCKFILE_PATH} is version ${String(lock.version)}, but this CLI understands ${LOCK_VERSION}.\n` +
|
|
77
|
+
' Upgrade @lionrapid/cli, or delete the file and re-pull.');
|
|
78
|
+
}
|
|
79
|
+
if (opts.projectId && lock.projectId && lock.projectId !== opts.projectId) {
|
|
80
|
+
throw new ConfigError(`${LOCKFILE_PATH} belongs to project ${lock.projectId}, but this config is ${opts.projectId}.\n` +
|
|
81
|
+
' A lock from another project would compute a meaningless diff — delete it and re-pull.');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
version: lock.version,
|
|
85
|
+
...(lock.projectId ? { projectId: lock.projectId } : {}),
|
|
86
|
+
entries: lock.entries !== null && typeof lock.entries === 'object' && !Array.isArray(lock.entries)
|
|
87
|
+
? lock.entries
|
|
88
|
+
: {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write the lock, keys sorted.
|
|
93
|
+
*
|
|
94
|
+
* Sorted because this file is committed: a lock that reorders itself produces
|
|
95
|
+
* a diff on every run and stops being reviewable, which is most of the reason
|
|
96
|
+
* to commit it. Written via temp+rename so an interrupted run cannot leave a
|
|
97
|
+
* half-written lock that the next run would refuse to parse.
|
|
98
|
+
*/
|
|
99
|
+
export function writeLock(cwd, lock) {
|
|
100
|
+
const path = join(cwd, LOCKFILE_PATH);
|
|
101
|
+
const sorted = {};
|
|
102
|
+
for (const key of Object.keys(lock.entries).sort())
|
|
103
|
+
sorted[key] = lock.entries[key];
|
|
104
|
+
const body = `${JSON.stringify({ ...lock, entries: sorted }, null, 2)}\n`;
|
|
105
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
106
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
107
|
+
writeFileSync(tmp, body);
|
|
108
|
+
renameSync(tmp, path);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=lockfile.js.map
|
package/dist/time.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp handling for values that come off the wire.
|
|
3
|
+
*
|
|
4
|
+
* tm-server serialises with `datetime.isoformat()` and its clock is UTC, so a
|
|
5
|
+
* job's `completed_at` arrives as `2026-08-07T16:35:24.248993` — an ISO
|
|
6
|
+
* date-time with NO timezone designator. `Date.parse` reads that as LOCAL
|
|
7
|
+
* time, which on a UTC+3 machine rendered every server timestamp three hours
|
|
8
|
+
* early: `lionrapid status` showed a job finishing three hours before the push
|
|
9
|
+
* that created it.
|
|
10
|
+
*
|
|
11
|
+
* The rule is about the STRING, not about tm-server: no offset means UTC, an
|
|
12
|
+
* offset means exactly what it says. Appending `Z` unconditionally would fix
|
|
13
|
+
* today and break the day the server starts sending real offsets.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Epoch milliseconds for a server timestamp, or null if it is absent or
|
|
17
|
+
* unparseable. An offset-less date-time is read as UTC; anything already
|
|
18
|
+
* carrying a zone is passed through untouched.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseServerTime(value: string | undefined | null): number | null;
|
|
21
|
+
/**
|
|
22
|
+
* `2026-08-07 19:35:24` in the reader's own timezone. A value we cannot parse
|
|
23
|
+
* is echoed verbatim rather than rendered as "Invalid Date" — showing the raw
|
|
24
|
+
* string at least tells you what the server said.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatLocal(value: string | undefined | null): string;
|
|
27
|
+
//# sourceMappingURL=time.d.ts.map
|
package/dist/time.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp handling for values that come off the wire.
|
|
3
|
+
*
|
|
4
|
+
* tm-server serialises with `datetime.isoformat()` and its clock is UTC, so a
|
|
5
|
+
* job's `completed_at` arrives as `2026-08-07T16:35:24.248993` — an ISO
|
|
6
|
+
* date-time with NO timezone designator. `Date.parse` reads that as LOCAL
|
|
7
|
+
* time, which on a UTC+3 machine rendered every server timestamp three hours
|
|
8
|
+
* early: `lionrapid status` showed a job finishing three hours before the push
|
|
9
|
+
* that created it.
|
|
10
|
+
*
|
|
11
|
+
* The rule is about the STRING, not about tm-server: no offset means UTC, an
|
|
12
|
+
* offset means exactly what it says. Appending `Z` unconditionally would fix
|
|
13
|
+
* today and break the day the server starts sending real offsets.
|
|
14
|
+
*/
|
|
15
|
+
/** True when the time part already carries `Z` or a `±HH:MM` / `±HHMM` offset. */
|
|
16
|
+
function hasTimezone(value) {
|
|
17
|
+
const timePart = value.slice(value.indexOf('T') + 1);
|
|
18
|
+
return /[Zz]$/.test(timePart) || /[+-]\d{2}:?\d{2}$/.test(timePart);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Epoch milliseconds for a server timestamp, or null if it is absent or
|
|
22
|
+
* unparseable. An offset-less date-time is read as UTC; anything already
|
|
23
|
+
* carrying a zone is passed through untouched.
|
|
24
|
+
*/
|
|
25
|
+
export function parseServerTime(value) {
|
|
26
|
+
if (!value)
|
|
27
|
+
return null;
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return null;
|
|
31
|
+
// A date-only value has no time to misinterpret — JS already reads those as
|
|
32
|
+
// UTC, and appending `Z` would make it unparseable.
|
|
33
|
+
const naive = trimmed.includes('T') && !hasTimezone(trimmed);
|
|
34
|
+
const ms = Date.parse(naive ? `${trimmed}Z` : trimmed);
|
|
35
|
+
return Number.isNaN(ms) ? null : ms;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `2026-08-07 19:35:24` in the reader's own timezone. A value we cannot parse
|
|
39
|
+
* is echoed verbatim rather than rendered as "Invalid Date" — showing the raw
|
|
40
|
+
* string at least tells you what the server said.
|
|
41
|
+
*/
|
|
42
|
+
export function formatLocal(value) {
|
|
43
|
+
if (!value)
|
|
44
|
+
return '—';
|
|
45
|
+
const ms = parseServerTime(value);
|
|
46
|
+
if (ms === null)
|
|
47
|
+
return value;
|
|
48
|
+
const d = new Date(ms);
|
|
49
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
50
|
+
return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
51
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=time.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lionrapid/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "LionRapid CLI
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "LionRapid CLI — sync translation files (iOS, Android, Flutter, JSON, YAML, PO) with the LionRapid backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|