@kin-tio/cli 0.6.1 → 0.7.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.
@@ -124,6 +124,40 @@ function parseBoundedText(value, fallback, name, maxBytes) {
124
124
  }
125
125
  return parsed;
126
126
  }
127
+ function createIlinkEnrollmentConfig(environment = process.env, root, platform = process.platform) {
128
+ environment = copyEnvironment(environment);
129
+ const home = path.resolve(root || resolveInstanceRoot(environment));
130
+ const storageKey = String(environment.ILINK_STORAGE_KEY || '').trim();
131
+ if (storageKey && !/^[A-Za-z0-9_-]{43}$/u.test(storageKey)) {
132
+ throw new Error('ILINK_STORAGE_KEY must be a canonical 32-byte base64url value');
133
+ }
134
+ const state = resolveStateFiles(environment, home);
135
+ const storageKeyFile = path.resolve(home, environment.ILINK_STORAGE_KEY_FILE ||
136
+ path.join(path.dirname(state.databaseFile), 'ilink-storage.key'));
137
+ if (platform === 'win32') {
138
+ for (const [name, filePath] of [
139
+ ['KINTIO_DB_FILE', state.databaseFile],
140
+ ['Kintio state lock', state.lockFile],
141
+ ['ILINK_STORAGE_KEY_FILE', storageKeyFile],
142
+ ]) {
143
+ if (!isPathInside(home, filePath)) {
144
+ throw new Error(`${name} must stay inside KINTIO_HOME on Windows`);
145
+ }
146
+ }
147
+ }
148
+ return Object.freeze({
149
+ home,
150
+ state,
151
+ ilink: Object.freeze({
152
+ storageKey,
153
+ storageKeyFile,
154
+ baseUrl: environment.ILINK_BASE_URL || 'https://ilinkai.weixin.qq.com/',
155
+ apiTimeoutMs: parsePositiveInteger(environment.ILINK_API_TIMEOUT_MS, 15_000, 'ILINK_API_TIMEOUT_MS', 120_000),
156
+ longPollTimeoutMs: parsePositiveInteger(environment.ILINK_LONG_POLL_TIMEOUT_MS, 35_000, 'ILINK_LONG_POLL_TIMEOUT_MS', 120_000),
157
+ maxAccounts: parsePositiveInteger(environment.ILINK_MAX_ACCOUNTS, 20, 'ILINK_MAX_ACCOUNTS', 1_000),
158
+ }),
159
+ });
160
+ }
127
161
  export function createConfig(environment = process.env, root, platform = process.platform) {
128
162
  environment = copyEnvironment(environment);
129
163
  const instanceRoot = path.resolve(root || resolveInstanceRoot(environment));
@@ -150,22 +184,14 @@ export function createConfig(environment = process.env, root, platform = process
150
184
  }
151
185
  const ilinkEnabled = parseBoolean(environment.ILINK_ENABLED, false);
152
186
  const codexEnabled = parseBoolean(environment.CODEX_ENABLED, apiEnabled || ilinkEnabled);
153
- const ilinkStorageKey = String(environment.ILINK_STORAGE_KEY || '').trim();
154
- if (ilinkStorageKey && !/^[A-Za-z0-9_-]{43}$/u.test(ilinkStorageKey)) {
155
- throw new Error('ILINK_STORAGE_KEY must be a canonical 32-byte base64url value');
156
- }
157
- const { databaseFile, lockFile } = resolveStateFiles(environment, instanceRoot);
187
+ const enrollment = createIlinkEnrollmentConfig(environment, instanceRoot, platform);
188
+ const { databaseFile, lockFile } = enrollment.state;
158
189
  const codexWorkingDirectory = path.resolve(instanceRoot, environment.CODEX_WORKING_DIRECTORY ||
159
190
  'codex-workspace');
160
- const ilinkStorageKeyFile = path.resolve(instanceRoot, environment.ILINK_STORAGE_KEY_FILE ||
161
- path.join(path.dirname(databaseFile), 'ilink-storage.key'));
162
191
  const codexImageTempDirectory = path.resolve(instanceRoot, environment.CODEX_IMAGE_TMP_DIR ||
163
192
  'data/codex-input');
164
193
  if (platform === 'win32') {
165
194
  for (const [name, filePath] of [
166
- ['KINTIO_DB_FILE', databaseFile],
167
- ['Kintio state lock', lockFile],
168
- ['ILINK_STORAGE_KEY_FILE', ilinkStorageKeyFile],
169
195
  ['CODEX_IMAGE_TMP_DIR', codexImageTempDirectory],
170
196
  ]) {
171
197
  if (!isPathInside(instanceRoot, filePath)) {
@@ -201,12 +227,7 @@ export function createConfig(environment = process.env, root, platform = process
201
227
  }),
202
228
  ilink: Object.freeze({
203
229
  enabled: ilinkEnabled,
204
- storageKey: ilinkStorageKey,
205
- storageKeyFile: ilinkStorageKeyFile,
206
- baseUrl: environment.ILINK_BASE_URL || 'https://ilinkai.weixin.qq.com/',
207
- apiTimeoutMs: parsePositiveInteger(environment.ILINK_API_TIMEOUT_MS, 15_000, 'ILINK_API_TIMEOUT_MS', 120_000),
208
- longPollTimeoutMs: parsePositiveInteger(environment.ILINK_LONG_POLL_TIMEOUT_MS, 35_000, 'ILINK_LONG_POLL_TIMEOUT_MS', 120_000),
209
- maxAccounts: parsePositiveInteger(environment.ILINK_MAX_ACCOUNTS, 20, 'ILINK_MAX_ACCOUNTS', 1_000),
230
+ ...enrollment.ilink,
210
231
  }),
211
232
  state: Object.freeze({
212
233
  databaseFile,
@@ -222,6 +243,10 @@ export function createConfig(environment = process.env, root, platform = process
222
243
  });
223
244
  }
224
245
  export function loadConfig(options = {}) {
246
+ const loaded = loadConfigurationEnvironment(options);
247
+ return createConfig(loaded.environment, loaded.root);
248
+ }
249
+ function loadConfigurationEnvironment(options) {
225
250
  const environment = copyEnvironment(options.environment || process.env);
226
251
  const configuredEnvFile = options.envFile || environment.KINTIO_CONFIG_FILE;
227
252
  const defaultRoot = path.join(path.resolve(options.homeDirectory || os.homedir()), '.kintio');
@@ -233,5 +258,35 @@ export function loadConfig(options = {}) {
233
258
  const envFile = path.resolve(configuredEnvFile || path.join(initialRoot, '.env'));
234
259
  loadEnvironmentFile(envFile, environment);
235
260
  const root = path.resolve(options.root || environment.KINTIO_HOME || initialRoot);
236
- return createConfig(environment, root);
261
+ return { environment, root };
262
+ }
263
+ export function loadIlinkEnrollmentConfig(options = {}) {
264
+ const loaded = loadConfigurationEnvironment(options);
265
+ return createIlinkEnrollmentConfig(loaded.environment, loaded.root);
266
+ }
267
+ export function loadIlinkRuntimeConfig(options = {}) {
268
+ const { environment, root } = loadConfigurationEnvironment(options);
269
+ const enrollment = createIlinkEnrollmentConfig(environment, root);
270
+ const workingDirectory = path.resolve(root, environment.CODEX_WORKING_DIRECTORY || 'codex-workspace');
271
+ const imageTempDirectory = path.resolve(root, environment.CODEX_IMAGE_TMP_DIR || 'data/codex-input');
272
+ if (process.platform === 'win32' && !isPathInside(root, imageTempDirectory)) {
273
+ throw new Error('CODEX_IMAGE_TMP_DIR must stay inside KINTIO_HOME on Windows');
274
+ }
275
+ const shutdownTimeoutMs = parsePositiveInteger(environment.SHUTDOWN_TIMEOUT_MS, 10_000, 'SHUTDOWN_TIMEOUT_MS', MAX_SHUTDOWN_TIMEOUT_MS);
276
+ if (shutdownTimeoutMs < 1_000) {
277
+ throw new Error('SHUTDOWN_TIMEOUT_MS must be at least 1000');
278
+ }
279
+ return Object.freeze({
280
+ state: Object.freeze({
281
+ ...enrollment.state,
282
+ shutdownTimeoutMs,
283
+ }),
284
+ ilink: Object.freeze({ enabled: true, ...enrollment.ilink }),
285
+ codex: Object.freeze({
286
+ enabled: true,
287
+ imageTempDirectory,
288
+ workingDirectory,
289
+ generatedImageDirectory: path.join(workingDirectory, 'generated_images'),
290
+ }),
291
+ });
237
292
  }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import { openIlinkOperatorControl, } from './cli-login.js';
3
+ function choices(accounts, runtimeActive) {
4
+ return accounts.map((account, index) => ` ${index + 1}. ${JSON.stringify(account.providerAccountId)} ` +
5
+ `${account.accountKey} [` +
6
+ `${runtimeActive && account.runtimeEnabled ? 'running' : 'stopped'}]`).join('\n');
7
+ }
8
+ function selectAccount(accounts, selector, runtimeActive) {
9
+ if (accounts.length === 0) {
10
+ throw new Error('No iLink account is enrolled; run "kintio ilink login" first');
11
+ }
12
+ if (!selector) {
13
+ if (accounts.length === 1)
14
+ return accounts[0];
15
+ throw new Error(`Multiple iLink accounts are enrolled; use --account with one choice:\n` +
16
+ choices(accounts, runtimeActive));
17
+ }
18
+ const matches = accounts.filter((account) => account.accountKey === selector || account.providerAccountId === selector);
19
+ if (matches.length !== 1) {
20
+ throw new Error(`Unknown or ambiguous iLink account ${JSON.stringify(selector)}:\n` +
21
+ choices(accounts, runtimeActive));
22
+ }
23
+ return matches[0];
24
+ }
25
+ export async function runIlinkAccountCommand({ command, selector, confirmed = false, config, packageRoot, signal, stdout, openControl, }) {
26
+ if (!openControl && !fs.existsSync(config.state.databaseFile)) {
27
+ if (command === 'list') {
28
+ stdout('No iLink accounts enrolled.\n');
29
+ return { startForeground: false, runningCount: 0 };
30
+ }
31
+ throw new Error('No iLink account is enrolled; run "kintio ilink login" first');
32
+ }
33
+ const control = await (openControl?.() ||
34
+ openIlinkOperatorControl(config, packageRoot, signal));
35
+ try {
36
+ const accounts = await control.listAccounts();
37
+ const runtimeActive = control.mode === 'runtime';
38
+ if (command === 'list') {
39
+ stdout(accounts.length
40
+ ? `${choices(accounts, runtimeActive)}\n`
41
+ : 'No iLink accounts enrolled.\n');
42
+ return {
43
+ startForeground: false,
44
+ runningCount: accounts.filter((account) => account.runtimeEnabled).length,
45
+ };
46
+ }
47
+ const account = selectAccount(accounts, selector, runtimeActive);
48
+ if (command === 'delete' && !confirmed) {
49
+ throw new Error(`Deleting ${JSON.stringify(account.providerAccountId)} permanently removes the account, ` +
50
+ 'credentials, conversations, messages, media, send records, and audit records; ' +
51
+ 'repeat with --yes');
52
+ }
53
+ const result = command === 'delete'
54
+ ? await control.deleteAccount(account.accountKey)
55
+ : await control.setAccountRuntime(account.accountKey, command === 'start');
56
+ stdout(`${command === 'delete' ? 'Deleted' : command === 'start' ? 'Started' : 'Stopped'} ` +
57
+ `${JSON.stringify(account.providerAccountId)}.\n`);
58
+ return {
59
+ startForeground: command === 'start' && control.mode === 'standalone',
60
+ runningCount: result.runningCount,
61
+ };
62
+ }
63
+ finally {
64
+ await control.close();
65
+ }
66
+ }