@ai-digital-factory/lmt-cli 0.1.5 → 1.0.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 +175 -51
- package/dist/chunk-4N3XUBM4.js +1483 -0
- package/dist/chunk-7J2U4G5L.js +1394 -0
- package/dist/chunk-AQIOT6YD.js +2799 -0
- package/dist/chunk-DUF5HVSQ.js +58 -0
- package/dist/controller-app-UTLE4YVF.js +1382 -0
- package/dist/credential-app-QEFV6LZM.js +6 -0
- package/dist/index.js +8 -3536
- package/dist/menu-actions-HV2AY4KJ.js +67 -0
- package/dist/src-MZYWVU46.js +380 -0
- package/dist/web-5HW6PMSA.js +9 -0
- package/package.json +2 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installChromium,
|
|
3
|
+
listLiveOwners,
|
|
4
|
+
requestSession,
|
|
5
|
+
socketPathFor,
|
|
6
|
+
startSession
|
|
7
|
+
} from "./chunk-AQIOT6YD.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveCliTarget
|
|
10
|
+
} from "./chunk-4N3XUBM4.js";
|
|
11
|
+
|
|
12
|
+
// src/web/menu-actions.ts
|
|
13
|
+
import { lstat } from "node:fs/promises";
|
|
14
|
+
import { Writable } from "node:stream";
|
|
15
|
+
async function socketExists(sessionId) {
|
|
16
|
+
try {
|
|
17
|
+
return (await lstat(socketPathFor(sessionId))).isSocket();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function createWebMenuActions(options) {
|
|
23
|
+
const { environment, timeoutMs } = options;
|
|
24
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
25
|
+
const urls = /* @__PURE__ */ new Map();
|
|
26
|
+
const ended = /* @__PURE__ */ new Set();
|
|
27
|
+
const observe = (result) => {
|
|
28
|
+
if (result.sessionId && result.url) urls.set(result.sessionId, result.url);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
async start(input) {
|
|
33
|
+
const { result } = await startSession({ environment, ...input, timeoutMs, ...options.ownerScript ? { ownerScript: options.ownerScript } : {} });
|
|
34
|
+
if (input.fresh && result.sessionId && result.outcome !== "failure") fresh.set(result.sessionId, input.mode);
|
|
35
|
+
return observe(result);
|
|
36
|
+
},
|
|
37
|
+
async request(sessionId, action, fullPage) {
|
|
38
|
+
const result = await requestSession({ ...action, sessionId, timeoutMs, ...fullPage ? { fullPage } : {} });
|
|
39
|
+
if (action.action === "stop" || action.action === "close" || result.error?.code === "session_not_found") {
|
|
40
|
+
fresh.delete(sessionId);
|
|
41
|
+
urls.delete(sessionId);
|
|
42
|
+
if (result.outcome === "success" || result.error?.code === "session_not_found") ended.add(sessionId);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
return observe(result);
|
|
46
|
+
},
|
|
47
|
+
async sessions() {
|
|
48
|
+
const { origin } = resolveCliTarget({ LMT_ENV: environment });
|
|
49
|
+
const registered = (await listLiveOwners()).filter((owner) => owner.environment === environment && owner.origin === origin).map((owner) => ({ sessionId: owner.sessionId, mode: owner.mode, fresh: false }));
|
|
50
|
+
const candidates = [...registered, ...[...fresh].map(([sessionId, mode]) => ({ sessionId, mode, fresh: true }))].filter((session) => !ended.has(session.sessionId));
|
|
51
|
+
const live = await Promise.all(candidates.map(async (session) => await socketExists(session.sessionId) ? session : void 0));
|
|
52
|
+
return live.filter((session) => session !== void 0).map((session) => ({ ...session, url: urls.get(session.sessionId) ?? null }));
|
|
53
|
+
},
|
|
54
|
+
async installBrowser() {
|
|
55
|
+
let output = "";
|
|
56
|
+
const sink = new Writable({ write(chunk, _encoding, done) {
|
|
57
|
+
output = (output + chunk.toString()).slice(-4e3);
|
|
58
|
+
done();
|
|
59
|
+
} });
|
|
60
|
+
const code = await installChromium([], sink);
|
|
61
|
+
return { code, output: output.trim().split("\n").slice(-5).join("\n") };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
createWebMenuActions
|
|
67
|
+
};
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createOperatorSessionActions,
|
|
3
|
+
downloadCandleExport,
|
|
4
|
+
hasArgumentOtp,
|
|
5
|
+
runHeadlessCli,
|
|
6
|
+
runOperatorSessionValidation,
|
|
7
|
+
runPairCommand,
|
|
8
|
+
runZerodhaAction,
|
|
9
|
+
runZerodhaValidation
|
|
10
|
+
} from "./chunk-7J2U4G5L.js";
|
|
11
|
+
import {
|
|
12
|
+
CliProfileStore,
|
|
13
|
+
assertClientProfileName,
|
|
14
|
+
ownerOf,
|
|
15
|
+
profileNameOf,
|
|
16
|
+
reportSessionStatus,
|
|
17
|
+
resolveCliTarget
|
|
18
|
+
} from "./chunk-4N3XUBM4.js";
|
|
19
|
+
|
|
20
|
+
// src/controller-actions.ts
|
|
21
|
+
function createControllerActions(options) {
|
|
22
|
+
const { target, owner, store, timeoutMs } = options;
|
|
23
|
+
const fetchOption = options.fetchImpl ? { fetchImpl: options.fetchImpl } : {};
|
|
24
|
+
const smsOptions = {
|
|
25
|
+
...options.readLoginCredentials ? { readLoginCredentials: options.readLoginCredentials } : {},
|
|
26
|
+
...options.secretReader ? { secretReader: options.secretReader } : {}
|
|
27
|
+
};
|
|
28
|
+
const profile = profileNameOf(owner);
|
|
29
|
+
const callerProfile = profile === void 0 ? {} : { callerProfile: profile };
|
|
30
|
+
const operatorActions = (signal) => createOperatorSessionActions({
|
|
31
|
+
target,
|
|
32
|
+
profile: owner,
|
|
33
|
+
store,
|
|
34
|
+
timeoutMs,
|
|
35
|
+
...signal ? { signal } : {},
|
|
36
|
+
...fetchOption,
|
|
37
|
+
...smsOptions
|
|
38
|
+
});
|
|
39
|
+
const authenticated = async (action) => {
|
|
40
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
41
|
+
const actions = operatorActions(signal);
|
|
42
|
+
const api = await actions.getApi();
|
|
43
|
+
await actions.status();
|
|
44
|
+
return action(api, signal);
|
|
45
|
+
};
|
|
46
|
+
const zerodha = async (action, attemptId, otp) => {
|
|
47
|
+
const api = await operatorActions().getApi();
|
|
48
|
+
return runZerodhaAction(api, action, target.operatorId, AbortSignal.timeout(timeoutMs), attemptId, otp);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
async status() {
|
|
52
|
+
const { report } = await reportSessionStatus({
|
|
53
|
+
target,
|
|
54
|
+
owner,
|
|
55
|
+
store,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
58
|
+
...fetchOption
|
|
59
|
+
});
|
|
60
|
+
return report;
|
|
61
|
+
},
|
|
62
|
+
async unlockOperator() {
|
|
63
|
+
await operatorActions().loginFromSms();
|
|
64
|
+
},
|
|
65
|
+
startZerodhaLogin: () => zerodha("login"),
|
|
66
|
+
submitZerodhaOtp: (attemptId, otp) => zerodha("otp", attemptId, otp),
|
|
67
|
+
logout: () => operatorActions().logout(),
|
|
68
|
+
readSettings: () => authenticated((api) => api.getSettings()),
|
|
69
|
+
saveSettings: (patch) => authenticated((api) => api.saveSettings(patch)),
|
|
70
|
+
readToken: () => authenticated((api) => api.getTokenMetadata()),
|
|
71
|
+
replaceToken: (request) => authenticated((api) => api.replaceToken(request)),
|
|
72
|
+
exportCandles: (request, output) => authenticated(async (api, signal) => {
|
|
73
|
+
const candleExport = await api.exportCandles(request);
|
|
74
|
+
const download = output === void 0 ? void 0 : await downloadCandleExport(candleExport.url, output, signal, options.fetchImpl);
|
|
75
|
+
return { candleExport, ...download ? { download } : {} };
|
|
76
|
+
}),
|
|
77
|
+
pair: (action) => authenticated((api) => runPairCommand({ kind: "pair", environment: target.environment, ...action }, api)),
|
|
78
|
+
async zerodha(action, attemptId) {
|
|
79
|
+
await operatorActions().status();
|
|
80
|
+
return zerodha(action, attemptId);
|
|
81
|
+
},
|
|
82
|
+
validateOperatorSession: () => runOperatorSessionValidation({ target, store, ...callerProfile, ...fetchOption, ...smsOptions }),
|
|
83
|
+
validateZerodhaSession: (attemptId, otp) => runZerodhaValidation({
|
|
84
|
+
target,
|
|
85
|
+
store,
|
|
86
|
+
...callerProfile,
|
|
87
|
+
...fetchOption,
|
|
88
|
+
...attemptId ? { attemptId } : {},
|
|
89
|
+
...otp === void 0 ? {} : { readOtp: async () => otp }
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/preferences.ts
|
|
95
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
96
|
+
import path from "node:path";
|
|
97
|
+
var PreferenceStore = class {
|
|
98
|
+
directory;
|
|
99
|
+
file;
|
|
100
|
+
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd()) {
|
|
101
|
+
this.directory = path.join(homeDirectory, ".lmt");
|
|
102
|
+
this.file = path.join(this.directory, "preferences.json");
|
|
103
|
+
}
|
|
104
|
+
/** The last confirmed environment, or undefined when none is stored or the file is unreadable. */
|
|
105
|
+
async readEnvironment() {
|
|
106
|
+
try {
|
|
107
|
+
const value = JSON.parse(await readFile(this.file, "utf8")).environment;
|
|
108
|
+
return value === "local" || value === "dev" ? value : void 0;
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async saveEnvironment(environment) {
|
|
114
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
115
|
+
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
116
|
+
await writeFile(temporary, `${JSON.stringify({ environment })}
|
|
117
|
+
`, { mode: 384 });
|
|
118
|
+
await chmod(temporary, 384);
|
|
119
|
+
await rename(temporary, this.file);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/cli-env.ts
|
|
124
|
+
function ensureFetchIgnoresEnvProxy() {
|
|
125
|
+
process.env.NODE_USE_ENV_PROXY = "0";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/help.ts
|
|
129
|
+
function wantsCliHelp(argv) {
|
|
130
|
+
return argv.includes("--help") || argv.includes("-h") || argv[0] === "help";
|
|
131
|
+
}
|
|
132
|
+
var HEADLESS_COMMAND_USAGES = [
|
|
133
|
+
"login --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
134
|
+
"logout --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
135
|
+
"candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] [--output PATH] --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
136
|
+
"pair list --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
137
|
+
"pair assign --ce ROW --pe ROW --lots N --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
138
|
+
"pair unpair PAIR_ID --confirm --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
139
|
+
"pair report PAIR_ID [--publish | --output PATH] --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
140
|
+
"settings read --env local|dev [--profile NAME]",
|
|
141
|
+
"settings save --stdin --env local|dev [--profile NAME]",
|
|
142
|
+
"token read --env local|dev [--profile NAME]",
|
|
143
|
+
"token replace --stdin --env local|dev [--profile NAME]",
|
|
144
|
+
"status --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
145
|
+
"zerodha login --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
146
|
+
"zerodha observe --attempt ID --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
147
|
+
"zerodha otp --attempt ID --otp-stdin --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
148
|
+
"zerodha status --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
149
|
+
"validate operator-session --env local|dev [--profile NAME] [--timeout DURATION]",
|
|
150
|
+
"validate zerodha-session --env local|dev [--profile NAME] [--attempt ID [--otp-stdin]] [--timeout DURATION]",
|
|
151
|
+
"credential provision --env local|dev --credential-stdin"
|
|
152
|
+
];
|
|
153
|
+
function formatCliHelp() {
|
|
154
|
+
return `Live Market Terminal CLI
|
|
155
|
+
|
|
156
|
+
Usage (lmt is an alias of live-market-terminal):
|
|
157
|
+
live-market-terminal [--env local|dev] [--device mobile|desktop]
|
|
158
|
+
live-market-terminal interactive [--env local|dev] [--device mobile|desktop] [--profile NAME]
|
|
159
|
+
live-market-terminal credential provision [--env local|dev]
|
|
160
|
+
${HEADLESS_COMMAND_USAGES.map((usage) => ` live-market-terminal ${usage}`).join("\n")}
|
|
161
|
+
|
|
162
|
+
Headless options (--env, --profile, --timeout, --attempt, --otp-stdin, --credential-stdin) may appear before or after the command.
|
|
163
|
+
|
|
164
|
+
From the monorepo root:
|
|
165
|
+
pnpm cli [--env local|dev] # interactive; without --env choose on the first screen
|
|
166
|
+
pnpm -s cli:headless status --env local # build step runs quietly (-s)
|
|
167
|
+
|
|
168
|
+
Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
|
|
169
|
+
Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
|
|
170
|
+
|
|
171
|
+
Price Cross pair commands operate Paper pairs on the owning Broker through Gate; they never save Settings or place Orders.
|
|
172
|
+
pair list returns current and past pairs (JSON pairs.active / pairs.past). Broker refusals keep their reason codes
|
|
173
|
+
(invalid_lots, same_type, already_paired, order_in_flight, flag_disabled, ...) with exit 1.
|
|
174
|
+
pair unpair requires --confirm and returns exit details (unpair.exit) when a paper position closed; a repeat reports already_unpaired.
|
|
175
|
+
pair report downloads the lifetime workbook (default: server filename in the current directory, never overwritten);
|
|
176
|
+
--publish returns a new immutable Hosted URL instead.
|
|
177
|
+
A pair assign, unpair or publish timeout after submission is uncertain (exit 1); read pair list before resubmitting.
|
|
178
|
+
The CLI never repeats a submission.
|
|
179
|
+
|
|
180
|
+
Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
|
|
181
|
+
marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
|
|
182
|
+
Token replace reads {accessToken?, expected, continueReplacement?, verifyCurrent?} from protected stdin.
|
|
183
|
+
Use verifyCurrent: true with expected from token read to validate the saved token without supplying accessToken.
|
|
184
|
+
Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
|
|
185
|
+
|
|
186
|
+
Targets: local and dev only. Production is not permitted.
|
|
187
|
+
|
|
188
|
+
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
|
|
189
|
+
Obtain the OTP from Kite after login returns awaiting_otp; expired attempts require a new login.
|
|
190
|
+
OTP arguments (--otp CODE and --otp=CODE) are rejected. Pipe protected input; headless mode never prompts.
|
|
191
|
+
|
|
192
|
+
Validate Zerodha Session starts a new attempt; resume the returned ID with --attempt ID.
|
|
193
|
+
Supply --otp-stdin only when submitting a human OTP to that exact attempt.
|
|
194
|
+
Validation retains the caller profile session and can replace Broker's active attempt or refresh its shared token.
|
|
195
|
+
Interactive mode (a terminal is required) chooses the environment, then Log in establishes the Controller
|
|
196
|
+
Session: the Operator is unlocked from SMS and only the Zerodha OTP is typed. Status and Log out are in the menu.
|
|
197
|
+
Headless commands write one JSON object per line to stdout; diagnostics go to stderr.
|
|
198
|
+
|
|
199
|
+
Controller Session: without --profile, every command uses the one Operator Session the Controller holds
|
|
200
|
+
per environment, shared by every local Origin and worktree. login, zerodha login and zerodha otp establish it;
|
|
201
|
+
logout ends it at Gate. --profile NAME uses an isolated CLI Client Profile instead and never touches it.
|
|
202
|
+
status reports smsCredential, operatorSession and zerodha without creating or replacing a session;
|
|
203
|
+
a session Gate rejects is cleared locally and reported expired.
|
|
204
|
+
|
|
205
|
+
Exit codes (headless):
|
|
206
|
+
0 success or validation pass
|
|
207
|
+
1 action failure (including Gate or SMS unreachable)
|
|
208
|
+
2 invalid invocation or target
|
|
209
|
+
3 Operator Login required (--profile only)
|
|
210
|
+
4 blocked: Controller Session unusable (no valid Operator Session, Zerodha unauthenticated
|
|
211
|
+
or no SMS credential; nextStep says what to run), validation blocked, or Zerodha awaiting OTP
|
|
212
|
+
|
|
213
|
+
Options:
|
|
214
|
+
--env local|dev Target environment (interactive: skips the environment screen; headless: required)
|
|
215
|
+
--device mobile|desktop Interactive only: remembered for the Web menu and shown in the header
|
|
216
|
+
--profile NAME CLI Client Profile, an isolated session (default: the Controller Session)
|
|
217
|
+
--timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
|
|
218
|
+
--ce ROW / --pe ROW Market Watch rows for pair assign (CE leg, PE leg)
|
|
219
|
+
--lots N Pair Lots for pair assign; the Broker enforces 1 through its configured maximum
|
|
220
|
+
--confirm Required by pair unpair
|
|
221
|
+
--publish pair report: publish an immutable snapshot and return its URL
|
|
222
|
+
--output PATH pair report: write the downloaded workbook to PATH (overwrites)
|
|
223
|
+
--output PATH candle-export: save workbook locally; refuses to overwrite an existing file
|
|
224
|
+
--date YYYY-MM-DD Single IST date for candle-export; omitted: the exchange's latest completed trading day
|
|
225
|
+
--attempt ID Attempt ID returned by zerodha login (required for observe and otp)
|
|
226
|
+
--otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
|
|
227
|
+
--credential-stdin Read SMS credential from piped stdin (headless provision; without it a terminal shows a masked prompt)
|
|
228
|
+
--help, -h Show this help
|
|
229
|
+
|
|
230
|
+
Storage: ~/.lmt/secrets.pem (one SMS credential shared by every command, including lmt web, the Controller Session
|
|
231
|
+
per environment, and per-profile Gate cookies)
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/index.ts
|
|
236
|
+
var CliInvocationError = class extends Error {
|
|
237
|
+
};
|
|
238
|
+
var terminal = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
239
|
+
function normalizeArgv(argv) {
|
|
240
|
+
return argv[0] === "--" ? argv.slice(1) : argv;
|
|
241
|
+
}
|
|
242
|
+
var INTERACTIVE_OPTIONS = /* @__PURE__ */ new Set(["--env", "--device"]);
|
|
243
|
+
function hasOnlyInteractiveOptions(argv) {
|
|
244
|
+
if (argv.length % 2 !== 0) return false;
|
|
245
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
246
|
+
const value = argv[index + 1];
|
|
247
|
+
if (!INTERACTIVE_OPTIONS.has(argv[index]) || value.startsWith("--")) return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
function resolveCliInvocation(argv, inTerminal = terminal()) {
|
|
252
|
+
if (argv[0] === "interactive") return { kind: "interactive", argv };
|
|
253
|
+
if (hasOnlyInteractiveOptions(argv)) return { kind: "interactive", argv: ["interactive", ...argv] };
|
|
254
|
+
if (inTerminal && argv[0] === "credential" && argv[1] === "provision" && !argv.includes("--credential-stdin")) {
|
|
255
|
+
return { kind: "interactive", argv: ["interactive", ...argv] };
|
|
256
|
+
}
|
|
257
|
+
return { kind: "headless", argv };
|
|
258
|
+
}
|
|
259
|
+
async function bootstrapCli(dependencies = {}) {
|
|
260
|
+
if (dependencies.runApplication) {
|
|
261
|
+
await dependencies.runApplication();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const argv = normalizeArgv(process.argv.slice(2));
|
|
265
|
+
if (hasArgumentOtp(argv)) {
|
|
266
|
+
process.exitCode = await runHeadlessCli(argv);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (argv[0] === "web") {
|
|
270
|
+
const { runWebCli } = await import("./web-5HW6PMSA.js");
|
|
271
|
+
await runWebCli(argv.slice(1));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (wantsCliHelp(argv)) {
|
|
275
|
+
process.stdout.write(formatCliHelp());
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const invocation = resolveCliInvocation(argv);
|
|
279
|
+
if (invocation.kind === "headless") {
|
|
280
|
+
process.exitCode = await runHeadlessCli(invocation.argv);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!terminal()) {
|
|
284
|
+
process.stderr.write(`Interactive mode requires a terminal. Use a headless command instead.
|
|
285
|
+
|
|
286
|
+
${formatCliHelp()}`);
|
|
287
|
+
process.exitCode = 2;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
let parsed;
|
|
291
|
+
let environments;
|
|
292
|
+
try {
|
|
293
|
+
parsed = parseInteractiveArguments(invocation.argv);
|
|
294
|
+
if (parsed.environment) resolveCliTarget({ LMT_ENV: parsed.environment });
|
|
295
|
+
environments = ["local", "dev"].flatMap((environment) => {
|
|
296
|
+
try {
|
|
297
|
+
return [{ environment, origin: resolveCliTarget({ LMT_ENV: environment }).origin }];
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
throw new CliInvocationError(error instanceof Error ? error.message : "Invalid interactive CLI configuration.");
|
|
304
|
+
}
|
|
305
|
+
if (environments.length === 0) throw new CliInvocationError("Neither local nor dev could be resolved from lmt.config.json.");
|
|
306
|
+
const store = new CliProfileStore();
|
|
307
|
+
const [{ createElement }, { render }] = await Promise.all([import("react"), import("ink")]);
|
|
308
|
+
if (parsed.credentialProvision) {
|
|
309
|
+
const { CredentialProvisionApp } = await import("./credential-app-QEFV6LZM.js");
|
|
310
|
+
const app2 = render(createElement(CredentialProvisionApp, { store }));
|
|
311
|
+
await app2.waitUntilExit();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const [{ ControllerApp }, { createWebMenuActions }] = await Promise.all([import("./controller-app-UTLE4YVF.js"), import("./menu-actions-HV2AY4KJ.js")]);
|
|
315
|
+
const owner = ownerOf(parsed.profile);
|
|
316
|
+
const app = render(createElement(ControllerApp, {
|
|
317
|
+
environments,
|
|
318
|
+
...parsed.environment ? { initialEnvironment: parsed.environment } : {},
|
|
319
|
+
preferences: new PreferenceStore(),
|
|
320
|
+
store,
|
|
321
|
+
actionsFor: (environment) => createControllerActions({
|
|
322
|
+
target: resolveCliTarget({ LMT_ENV: environment }),
|
|
323
|
+
owner,
|
|
324
|
+
store,
|
|
325
|
+
timeoutMs: 3e4
|
|
326
|
+
}),
|
|
327
|
+
...parsed.profile ? { profile: parsed.profile } : {},
|
|
328
|
+
...parsed.device ? { device: parsed.device } : {},
|
|
329
|
+
// lmt web browsers use the Controller Session, so a --profile Controller has no Web submenu.
|
|
330
|
+
...parsed.profile ? {} : {
|
|
331
|
+
webActionsFor: (environment) => createWebMenuActions({ environment, timeoutMs: 3e4 })
|
|
332
|
+
}
|
|
333
|
+
}));
|
|
334
|
+
await app.waitUntilExit();
|
|
335
|
+
}
|
|
336
|
+
function parseInteractiveArguments(argv) {
|
|
337
|
+
const credentialProvision = argv[1] === "credential" && argv[2] === "provision";
|
|
338
|
+
const allowed = credentialProvision ? ["--env", "--profile"] : ["--env", "--profile", "--device"];
|
|
339
|
+
const values = /* @__PURE__ */ new Map();
|
|
340
|
+
for (let index = credentialProvision ? 3 : 1; index < argv.length; index += 2) {
|
|
341
|
+
const flag = argv[index];
|
|
342
|
+
const value = argv[index + 1];
|
|
343
|
+
if (!allowed.includes(flag) || value === void 0 || value.startsWith("--")) {
|
|
344
|
+
throw new Error(`Invalid interactive argument: ${flag}`);
|
|
345
|
+
}
|
|
346
|
+
if (values.has(flag)) throw new Error(`Argument ${flag} may be specified only once.`);
|
|
347
|
+
values.set(flag, value);
|
|
348
|
+
}
|
|
349
|
+
const environment = values.get("--env");
|
|
350
|
+
if (environment !== void 0 && environment !== "local" && environment !== "dev") {
|
|
351
|
+
throw new Error("--env must be local or dev; prod is not permitted.");
|
|
352
|
+
}
|
|
353
|
+
const device = values.get("--device");
|
|
354
|
+
if (device !== void 0 && device !== "mobile" && device !== "desktop") throw new Error("--device must be mobile or desktop.");
|
|
355
|
+
const profile = values.get("--profile");
|
|
356
|
+
if (profile !== void 0) assertClientProfileName(profile);
|
|
357
|
+
if (credentialProvision) return { ...environment ? { environment } : {}, credentialProvision };
|
|
358
|
+
return {
|
|
359
|
+
...environment ? { environment } : {},
|
|
360
|
+
...profile ? { profile } : {},
|
|
361
|
+
...device ? { device } : {},
|
|
362
|
+
credentialProvision
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function runCliMain() {
|
|
366
|
+
ensureFetchIgnoresEnvProxy();
|
|
367
|
+
bootstrapCli().catch((error) => {
|
|
368
|
+
const message = error instanceof Error ? error.message : "CLI failed to start.";
|
|
369
|
+
process.stderr.write(`${message}
|
|
370
|
+
`);
|
|
371
|
+
process.exitCode = error instanceof CliInvocationError ? 2 : 1;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
export {
|
|
375
|
+
bootstrapCli,
|
|
376
|
+
normalizeArgv,
|
|
377
|
+
parseInteractiveArguments,
|
|
378
|
+
resolveCliInvocation,
|
|
379
|
+
runCliMain
|
|
380
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-digital-factory/lmt-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"live-market-terminal": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ink": "7.1.1",
|
|
30
|
+
"playwright": "^1.55.0",
|
|
30
31
|
"react": "19.2.8"
|
|
31
32
|
}
|
|
32
33
|
}
|