@01.software/cli 0.6.0 → 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.
package/dist/index.js CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  CollectionClient,
10
10
  ServerCommerceClient
11
11
  } from "@01.software/sdk";
12
- import pc from "picocolors";
13
12
 
14
13
  // src/lib/credentials.ts
15
14
  import {
@@ -93,7 +92,7 @@ function loadTenantList() {
93
92
  const data = JSON.parse(raw);
94
93
  if (!Array.isArray(data)) return null;
95
94
  const valid = data.filter(
96
- (t) => typeof t?.id === "string" && typeof t?.name === "string"
95
+ (t2) => typeof t2?.id === "string" && typeof t2?.name === "string"
97
96
  );
98
97
  return valid.length > 0 ? valid : null;
99
98
  } catch {
@@ -126,77 +125,209 @@ ${entry}
126
125
  }
127
126
  }
128
127
 
129
- // src/lib/client.ts
130
- function isValidBearerToken(secret) {
131
- return secret.startsWith("sk01_") || secret.startsWith("pat01_");
128
+ // src/lib/output.ts
129
+ import pc from "picocolors";
130
+
131
+ // src/lib/api-error.ts
132
+ var PERMISSION_CODES = [
133
+ "tenant_mismatch",
134
+ "account_suspended",
135
+ "feature_disabled",
136
+ "role_denied",
137
+ "credential_invalid",
138
+ "pat_tenant_unpinned",
139
+ "publishable_key_mismatch",
140
+ "scope_denied"
141
+ ];
142
+ var DEGRADED_CODES = [
143
+ "redis_unavailable",
144
+ "provider_unavailable",
145
+ "rate_limited"
146
+ ];
147
+ var NETWORK_CODES = [
148
+ "upstream_timeout",
149
+ "upstream_5xx",
150
+ "dns_failure"
151
+ ];
152
+ function isPermissionCode(code) {
153
+ return PERMISSION_CODES.includes(code);
132
154
  }
133
- var tenantHeaderInterceptorInstalled = false;
134
- function installTenantHeaderInterceptor(tenantId) {
135
- if (tenantHeaderInterceptorInstalled) return;
136
- tenantHeaderInterceptorInstalled = true;
137
- const originalFetch = globalThis.fetch.bind(globalThis);
138
- globalThis.fetch = async (input, init) => {
139
- const headers = new Headers(init?.headers);
140
- if (!headers.has("X-Tenant-Id")) headers.set("X-Tenant-Id", tenantId);
141
- return originalFetch(input, { ...init, headers });
142
- };
155
+ function isDegradedCode(code) {
156
+ return DEGRADED_CODES.includes(code);
143
157
  }
144
- function resolveClient(apiKeyFlag) {
145
- let publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY;
146
- let secretKey = apiKeyFlag ?? process.env.SOFTWARE_SECRET_KEY;
147
- let tenantId;
148
- if (!publishableKey || !secretKey) {
149
- const local = loadLocalCredentials();
150
- if (local) {
151
- publishableKey = publishableKey ?? local.publishableKey;
152
- secretKey = secretKey ?? local.secretKey;
153
- tenantId = local.tenantId;
158
+ function isNetworkCode(code) {
159
+ return NETWORK_CODES.includes(code);
160
+ }
161
+ function classifyError(err) {
162
+ if (err && typeof err === "object") {
163
+ const obj = err;
164
+ if (typeof obj.code === "string") {
165
+ const code = obj.code;
166
+ if (isPermissionCode(code)) {
167
+ return { type: "permission", code };
168
+ }
169
+ if (isDegradedCode(code)) {
170
+ const out = { type: "degraded", code };
171
+ if (typeof obj.retryAfter === "number") {
172
+ out.retryAfter = obj.retryAfter;
173
+ }
174
+ return out;
175
+ }
176
+ if (isNetworkCode(code)) {
177
+ return { type: "network", code };
178
+ }
154
179
  }
155
- }
156
- if (!publishableKey || !secretKey) {
157
- const stored = loadCredentials();
158
- if (stored) {
159
- publishableKey = publishableKey ?? stored.publishableKey;
160
- secretKey = secretKey ?? stored.secretKey;
161
- tenantId = tenantId ?? stored.tenantId;
180
+ if (obj.type === "validation" && typeof obj.code === "string") {
181
+ const out = { type: "validation", code: obj.code };
182
+ if (typeof obj.field === "string") out.field = obj.field;
183
+ if (obj.detail && typeof obj.detail === "object") {
184
+ out.detail = obj.detail;
185
+ }
186
+ return out;
187
+ }
188
+ const name = typeof obj.name === "string" ? obj.name : void 0;
189
+ const status = typeof obj.status === "number" ? obj.status : void 0;
190
+ if (name === "ConfigError" || status === 401) {
191
+ return { type: "permission", code: "credential_invalid" };
192
+ }
193
+ if (name === "NetworkError" || name === "TimeoutError" || status === 408 || status === 503) {
194
+ return { type: "network", code: "upstream_timeout" };
195
+ }
196
+ if (name === "GoneError" || status === 404) {
197
+ return {
198
+ type: "validation",
199
+ code: "not_found",
200
+ detail: typeof obj.message === "string" ? { message: obj.message } : void 0
201
+ };
202
+ }
203
+ if (name === "UsageLimitError" || status === 429) {
204
+ const out = { type: "degraded", code: "rate_limited" };
205
+ if (typeof obj.retryAfter === "number") out.retryAfter = obj.retryAfter;
206
+ return out;
207
+ }
208
+ if (name === "ValidationError" || status === 400 || status === 422) {
209
+ return {
210
+ type: "validation",
211
+ code: "invalid_argument",
212
+ detail: typeof obj.message === "string" ? { message: obj.message } : void 0
213
+ };
162
214
  }
163
215
  }
164
- if (!publishableKey || !secretKey) {
165
- console.error(pc.red("Authentication required."));
166
- console.error(
167
- pc.dim(
168
- 'Run "01 login" to authenticate via browser,\nor pass --api-key <token>,\nor set SOFTWARE_PUBLISHABLE_KEY and SOFTWARE_SECRET_KEY environment variables.'
169
- )
170
- );
171
- process.exit(2);
172
- }
173
- if (!isValidBearerToken(secretKey)) {
174
- console.error(
175
- pc.red(
176
- "Invalid API key format. Expected sk01_ or pat01_ token.\nLegacy hex credentials are no longer accepted \u2014 run `01 login` to re-authenticate."
177
- )
178
- );
179
- process.exit(2);
180
- }
181
- if (tenantId) installTenantHeaderInterceptor(tenantId);
182
- const serverOptions = { publishableKey, secretKey, tenantId };
183
216
  return {
184
- collections: new CollectionClient(publishableKey, secretKey),
185
- commerce: new ServerCommerceClient(serverOptions),
186
- publishableKey,
187
- secretKey
217
+ type: "validation",
218
+ code: "unknown",
219
+ detail: { message: err instanceof Error ? err.message : String(err) }
188
220
  };
189
221
  }
222
+ function adminErrorExitCode(err) {
223
+ if (err.code === "unknown") return 1;
224
+ switch (err.type) {
225
+ case "permission":
226
+ return 2;
227
+ case "validation":
228
+ return err.code === "not_found" ? 5 : 3;
229
+ case "network":
230
+ return 4;
231
+ case "degraded":
232
+ return err.code === "rate_limited" ? 6 : 4;
233
+ }
234
+ }
235
+
236
+ // src/lib/i18n.ts
237
+ var CLI_I18N_KO = {
238
+ // CLI-surface microcopy
239
+ AuthenticationRequired: "\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4",
240
+ RunLoginToAuthenticate: "`01 login` \uBA85\uB839\uC73C\uB85C \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D\uC744 \uC9C4\uD589\uD558\uAC70\uB098,",
241
+ PassApiKey: "`--api-key <token>` \uC635\uC158\uC744 \uC804\uB2EC\uD558\uAC70\uB098,",
242
+ SetEnvVars: "`SOFTWARE_PUBLISHABLE_KEY` / `SOFTWARE_SECRET_KEY` \uD658\uACBD \uBCC0\uC218\uB97C \uC124\uC815\uD558\uC138\uC694.",
243
+ InvalidApiKeyFormat: "API \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. `sk01_` \uB610\uB294 `pat01_` \uD1A0\uD070\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
244
+ LegacyHexCredentialsRejected: "\uB808\uAC70\uC2DC hex \uC790\uACA9 \uC99D\uBA85\uC740 \uB354 \uC774\uC0C1 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 `01 login`\uC73C\uB85C \uB2E4\uC2DC \uC778\uC99D\uD558\uC138\uC694.",
245
+ InvalidJsonObject: "`--{{label}}` \uC778\uC790\uB294 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
246
+ InvalidJsonArray: "`--{{label}}` \uC778\uC790\uB294 JSON \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.",
247
+ InvalidJsonValue: "`--{{label}}` \uC778\uC790\uC758 JSON \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {{value}}",
248
+ Empty: "(\uC5C6\uC74C)",
249
+ Total: "\uCD1D {{n}}\uAC1C",
250
+ PageOf: "{{page}} / {{total}} \uD398\uC774\uC9C0",
251
+ // AdminError code translations (mirrors apps/console admin.ts).
252
+ tenant_mismatch: "\uAD8C\uD55C\uC774 \uC5C6\uB294 \uD14C\uB10C\uD2B8",
253
+ account_suspended: "\uACC4\uC815\uC774 \uC815\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
254
+ feature_disabled: "\uC774 \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
255
+ role_denied: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4",
256
+ credential_invalid: "\uC790\uACA9 \uC99D\uBA85\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
257
+ pat_tenant_unpinned: "PAT \uD1A0\uD070\uC758 \uD14C\uB10C\uD2B8\uAC00 \uACE0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4",
258
+ publishable_key_mismatch: "\uACF5\uAC1C \uD0A4\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
259
+ scope_denied: "\uBC94\uC704 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
260
+ redis_unavailable: "\uCE90\uC2DC\uAC00 \uC77C\uC2DC\uC801\uC73C\uB85C \uC0AC\uC6A9 \uBD88\uAC00\uD569\uB2C8\uB2E4",
261
+ provider_unavailable: "\uC678\uBD80 \uACF5\uAE09\uC790\uAC00 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
262
+ rate_limited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4 \u2014 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694",
263
+ upstream_timeout: "\uC751\uB2F5 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
264
+ upstream_5xx: "\uC678\uBD80 \uC11C\uBE44\uC2A4 \uC624\uB958",
265
+ dns_failure: "\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0 \uC624\uB958",
266
+ not_found: "\uB9AC\uC18C\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
267
+ invalid_argument: "\uC778\uC790\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
268
+ unknown: "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"
269
+ };
270
+ var CLI_I18N_EN = {
271
+ AuthenticationRequired: "Authentication required",
272
+ RunLoginToAuthenticate: "Run `01 login` to authenticate via browser, or",
273
+ PassApiKey: "pass `--api-key <token>`, or",
274
+ SetEnvVars: "set `SOFTWARE_PUBLISHABLE_KEY` and `SOFTWARE_SECRET_KEY` environment variables.",
275
+ InvalidApiKeyFormat: "Invalid API key format. Expected `sk01_` or `pat01_` token.",
276
+ LegacyHexCredentialsRejected: "Legacy hex credentials are no longer accepted \u2014 run `01 login` to re-authenticate.",
277
+ InvalidJsonObject: "--{{label}} must be a JSON object.",
278
+ InvalidJsonArray: "--{{label}} must be a JSON array.",
279
+ InvalidJsonValue: "Invalid JSON for --{{label}}: {{value}}",
280
+ Empty: "(empty)",
281
+ Total: "{{n}} total",
282
+ PageOf: "page {{page}}/{{total}}",
283
+ tenant_mismatch: "Tenant access denied",
284
+ account_suspended: "Your account has been suspended",
285
+ feature_disabled: "This feature is disabled",
286
+ role_denied: "Insufficient role permissions",
287
+ credential_invalid: "Invalid credentials",
288
+ pat_tenant_unpinned: "PAT token tenant is not pinned",
289
+ publishable_key_mismatch: "Publishable key mismatch",
290
+ scope_denied: "Scope permission denied",
291
+ redis_unavailable: "Cache temporarily unavailable",
292
+ provider_unavailable: "Upstream provider unavailable",
293
+ rate_limited: "Too many requests \u2014 please try again shortly",
294
+ upstream_timeout: "Upstream request timed out",
295
+ upstream_5xx: "Upstream service error",
296
+ dns_failure: "Network connection error",
297
+ not_found: "Resource not found",
298
+ invalid_argument: "Invalid argument",
299
+ unknown: "Unknown error"
300
+ };
301
+ var activeLocale = null;
302
+ function detectLocale() {
303
+ if (activeLocale) return activeLocale;
304
+ const raw = process.env.LC_ALL ?? process.env.LANG ?? process.env.LANGUAGE ?? "";
305
+ return raw.toLowerCase().startsWith("ko") ? "ko" : "en";
306
+ }
307
+ function setLocale(locale) {
308
+ activeLocale = locale ?? null;
309
+ }
310
+ function t(key, vars) {
311
+ const table = detectLocale() === "ko" ? CLI_I18N_KO : CLI_I18N_EN;
312
+ let s = table[key];
313
+ if (vars) {
314
+ for (const [k, v] of Object.entries(vars)) {
315
+ const escaped = k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
316
+ s = s.replace(new RegExp(`\\{\\{${escaped}\\}\\}`, "g"), String(v));
317
+ }
318
+ }
319
+ return s;
320
+ }
190
321
 
191
322
  // src/lib/output.ts
192
- import pc2 from "picocolors";
323
+ var CLI_I18N_KEYS = Object.keys(CLI_I18N_KO);
193
324
  function printJson(data) {
194
325
  console.log(JSON.stringify(data, null, 2));
195
326
  }
196
327
  function printTable(data) {
197
328
  if (Array.isArray(data)) {
198
329
  if (data.length === 0) {
199
- console.log(pc2.dim("(empty)"));
330
+ console.log(pc.dim(t("Empty")));
200
331
  return;
201
332
  }
202
333
  const keys = Object.keys(data[0]);
@@ -224,7 +355,7 @@ function printTable(data) {
224
355
  const maxKey = Math.max(...entries.map(([k]) => k.length));
225
356
  for (const [key, value] of entries) {
226
357
  const display = typeof value === "object" ? JSON.stringify(value) : String(value);
227
- console.log(`${pc2.bold(key.padEnd(maxKey))} ${display}`);
358
+ console.log(`${pc.bold(key.padEnd(maxKey))} ${display}`);
228
359
  }
229
360
  } else {
230
361
  console.log(String(data));
@@ -250,9 +381,12 @@ function printResult(data, format) {
250
381
  const resp = data;
251
382
  printTable(resp.docs);
252
383
  console.log(
253
- pc2.dim(
384
+ pc.dim(
254
385
  `
255
- ${resp.totalDocs} total | page ${resp.page}/${resp.totalPages}`
386
+ ${t("Total", { n: resp.totalDocs })} | ${t("PageOf", {
387
+ page: resp.page,
388
+ total: resp.totalPages
389
+ })}`
256
390
  )
257
391
  );
258
392
  return;
@@ -263,35 +397,112 @@ ${resp.totalDocs} total | page ${resp.page}/${resp.totalPages}`
263
397
  }
264
398
  }
265
399
  function getExitCode(error) {
266
- if (!error || typeof error !== "object") return 1;
267
- const err = error;
268
- if (err.name === "ConfigError") return 2;
269
- if (err.name === "ValidationError") return 3;
270
- if (err.name === "NetworkError" || err.name === "TimeoutError") return 4;
271
- if (err.name === "GoneError") return 5;
272
- if (err.name === "UsageLimitError") return 6;
273
- const s = err.status;
274
- if (s === 401) return 2;
275
- if (s === 400 || s === 422) return 3;
276
- if (s === 408 || s === 503) return 4;
277
- if (s === 404) return 5;
278
- if (s === 429) return 6;
279
- return 1;
400
+ return adminErrorExitCode(classifyError(error));
280
401
  }
281
- function exitWithError(error) {
282
- printError(error);
283
- process.exit(getExitCode(error));
402
+ function resolveFormat(options) {
403
+ return options.format ?? process.env.OUTPUT_FORMAT ?? "json";
284
404
  }
285
- function printError(error) {
286
- if (error && typeof error === "object" && "message" in error) {
287
- const err = error;
288
- console.error(pc2.red(`Error: ${err.message}`));
289
- if (err.code) console.error(pc2.dim(`Code: ${err.code}`));
290
- if (err.status) console.error(pc2.dim(`Status: ${err.status}`));
291
- if (err.suggestion) console.error(pc2.yellow(err.suggestion));
405
+ function printError(error, options = {}) {
406
+ const adminError = classifyError(error);
407
+ const knownKey = CLI_I18N_KEYS.includes(adminError.code) ? adminError.code : null;
408
+ const localized = knownKey ? t(knownKey) : null;
409
+ const rawErr = error && typeof error === "object" ? error : null;
410
+ const rawMessage = rawErr && typeof rawErr.message === "string" ? rawErr.message : "";
411
+ const stringErr = typeof error === "string" ? error : "";
412
+ const detailMessage = rawErr && rawErr.detail && typeof rawErr.detail === "object" && typeof rawErr.detail.message === "string" ? String(rawErr.detail.message) : "";
413
+ const headline = rawMessage || stringErr || detailMessage || localized || adminError.code;
414
+ console.error(pc.red(`Error: ${headline}`));
415
+ if (rawErr && typeof rawErr.code === "string" && rawErr.code !== adminError.code) {
416
+ console.error(pc.dim(`Code: ${rawErr.code}`));
292
417
  } else {
293
- console.error(pc2.red(String(error)));
418
+ console.error(pc.dim(`Code: ${adminError.code}`));
294
419
  }
420
+ if (rawErr && typeof rawErr.status === "number") {
421
+ console.error(pc.dim(`Status: ${rawErr.status}`));
422
+ }
423
+ console.error(pc.dim(`Type: ${adminError.type}`));
424
+ if (adminError.type === "degraded" && typeof adminError.retryAfter === "number") {
425
+ console.error(pc.yellow(`Retry after: ${adminError.retryAfter}s`));
426
+ }
427
+ if (rawErr && typeof rawErr.suggestion === "string") {
428
+ console.error(pc.yellow(rawErr.suggestion));
429
+ }
430
+ if (resolveFormat(options) === "json") {
431
+ console.log(JSON.stringify(adminError));
432
+ }
433
+ }
434
+ function exitWithError(error, options = {}) {
435
+ printError(error, options);
436
+ process.exit(getExitCode(error));
437
+ }
438
+
439
+ // src/lib/client.ts
440
+ function isValidBearerToken(secret) {
441
+ return secret.startsWith("sk01_") || secret.startsWith("pat01_");
442
+ }
443
+ var tenantHeaderInterceptorInstalled = false;
444
+ function installTenantHeaderInterceptor(tenantId) {
445
+ if (tenantHeaderInterceptorInstalled) return;
446
+ tenantHeaderInterceptorInstalled = true;
447
+ const originalFetch = globalThis.fetch.bind(globalThis);
448
+ globalThis.fetch = async (input, init) => {
449
+ const headers = new Headers(init?.headers);
450
+ if (!headers.has("X-Tenant-Id")) headers.set("X-Tenant-Id", tenantId);
451
+ return originalFetch(input, { ...init, headers });
452
+ };
453
+ }
454
+ function resolveClient(apiKeyFlag) {
455
+ let publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY;
456
+ let secretKey = apiKeyFlag ?? process.env.SOFTWARE_SECRET_KEY;
457
+ let tenantId;
458
+ if (!publishableKey || !secretKey) {
459
+ const local = loadLocalCredentials();
460
+ if (local) {
461
+ publishableKey = publishableKey ?? local.publishableKey;
462
+ secretKey = secretKey ?? local.secretKey;
463
+ tenantId = local.tenantId;
464
+ }
465
+ }
466
+ if (!publishableKey || !secretKey) {
467
+ const stored = loadCredentials();
468
+ if (stored) {
469
+ publishableKey = publishableKey ?? stored.publishableKey;
470
+ secretKey = secretKey ?? stored.secretKey;
471
+ tenantId = tenantId ?? stored.tenantId;
472
+ }
473
+ }
474
+ if (!publishableKey || !secretKey) {
475
+ exitWithError({
476
+ type: "permission",
477
+ code: "credential_invalid",
478
+ detail: {
479
+ message: t("AuthenticationRequired"),
480
+ steps: [
481
+ t("RunLoginToAuthenticate"),
482
+ t("PassApiKey"),
483
+ t("SetEnvVars")
484
+ ]
485
+ }
486
+ });
487
+ }
488
+ if (!isValidBearerToken(secretKey)) {
489
+ exitWithError({
490
+ type: "permission",
491
+ code: "credential_invalid",
492
+ detail: {
493
+ message: t("InvalidApiKeyFormat"),
494
+ suggestion: t("LegacyHexCredentialsRejected")
495
+ }
496
+ });
497
+ }
498
+ if (tenantId) installTenantHeaderInterceptor(tenantId);
499
+ const serverOptions = { publishableKey, secretKey, tenantId };
500
+ return {
501
+ collections: new CollectionClient(publishableKey, secretKey),
502
+ commerce: new ServerCommerceClient(serverOptions),
503
+ publishableKey,
504
+ secretKey
505
+ };
295
506
  }
296
507
 
297
508
  // src/commands/crud.ts
@@ -300,32 +511,44 @@ import { basename } from "path";
300
511
  import { COLLECTIONS } from "@01.software/sdk";
301
512
 
302
513
  // src/lib/parse.ts
303
- import pc3 from "picocolors";
304
- function parseJson(value, label) {
514
+ function failArg(label, value, expectedKind) {
515
+ const message = expectedKind === "object" ? t("InvalidJsonObject", { label }) : expectedKind === "array" ? t("InvalidJsonArray", { label }) : t("InvalidJsonValue", { label, value });
516
+ exitWithError({
517
+ type: "validation",
518
+ code: "invalid_argument",
519
+ field: label,
520
+ // `message` is read by `printError` for the stderr headline so the
521
+ // user sees the label/value in the one-line summary; `detail` carries
522
+ // the structured fields for `--format json` consumers.
523
+ message,
524
+ detail: { message, value, field: label }
525
+ });
526
+ }
527
+ function parseJson(value, label, options = {}) {
528
+ let parsed;
305
529
  try {
306
- const parsed = JSON.parse(value);
307
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
308
- console.error(pc3.red(`--${label} must be a JSON object.`));
309
- process.exit(3);
310
- }
311
- return parsed;
530
+ parsed = JSON.parse(value);
312
531
  } catch {
313
- console.error(pc3.red(`Invalid JSON for --${label}: ${value}`));
314
- process.exit(3);
532
+ return failArg(label, value, "value");
533
+ }
534
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
535
+ return failArg(label, value, "object");
315
536
  }
537
+ void options;
538
+ return parsed;
316
539
  }
317
- function parseJsonArray(value, label) {
540
+ function parseJsonArray(value, label, options = {}) {
541
+ let parsed;
318
542
  try {
319
- const parsed = JSON.parse(value);
320
- if (!Array.isArray(parsed)) {
321
- console.error(pc3.red(`--${label} must be a JSON array.`));
322
- process.exit(3);
323
- }
324
- return parsed;
543
+ parsed = JSON.parse(value);
325
544
  } catch {
326
- console.error(pc3.red(`Invalid JSON for --${label}: ${value}`));
327
- process.exit(3);
545
+ return failArg(label, value, "value");
546
+ }
547
+ if (!Array.isArray(parsed)) {
548
+ return failArg(label, value, "array");
328
549
  }
550
+ void options;
551
+ return parsed;
329
552
  }
330
553
 
331
554
  // src/commands/crud.ts
@@ -854,7 +1077,7 @@ import { createServer } from "http";
854
1077
  import { execFile, exec } from "child_process";
855
1078
  import { platform } from "os";
856
1079
  import { URL } from "url";
857
- import pc4 from "picocolors";
1080
+ import pc2 from "picocolors";
858
1081
  var WEB_URL = process.env.SOFTWARE_WEB_URL || "https://01.software";
859
1082
  var TIMEOUT_MS = 3 * 60 * 1e3;
860
1083
  function escapeHtml(s) {
@@ -864,7 +1087,7 @@ function openBrowser(url) {
864
1087
  const os = platform();
865
1088
  const onError = () => {
866
1089
  console.log(
867
- pc4.yellow(
1090
+ pc2.yellow(
868
1091
  `Could not open browser automatically. Open this URL manually:
869
1092
  ${url}`
870
1093
  )
@@ -910,7 +1133,7 @@ async function exchangeCode(code) {
910
1133
  if (!res.ok) {
911
1134
  const body = await res.text().catch(() => "");
912
1135
  console.error(
913
- pc4.red(
1136
+ pc2.red(
914
1137
  `Exchange failed: HTTP ${res.status} from ${url}${body ? ` \u2014 ${body.slice(0, 200)}` : ""}`
915
1138
  )
916
1139
  );
@@ -918,7 +1141,7 @@ async function exchangeCode(code) {
918
1141
  }
919
1142
  const data = await res.json();
920
1143
  if (typeof data.publishableKey !== "string" || typeof data.secretKey !== "string" || typeof data.tenantName !== "string" || typeof data.tenantId !== "string") {
921
- console.error(pc4.red(`Exchange failed: malformed response from ${url}`));
1144
+ console.error(pc2.red(`Exchange failed: malformed response from ${url}`));
922
1145
  return null;
923
1146
  }
924
1147
  return {
@@ -927,12 +1150,12 @@ async function exchangeCode(code) {
927
1150
  tenantName: data.tenantName,
928
1151
  tenantId: data.tenantId,
929
1152
  tenants: Array.isArray(data.tenants) ? data.tenants.filter(
930
- (t) => typeof t?.id === "string" && typeof t?.name === "string"
1153
+ (t2) => typeof t2?.id === "string" && typeof t2?.name === "string"
931
1154
  ) : void 0
932
1155
  };
933
1156
  } catch (err) {
934
1157
  console.error(
935
- pc4.red(
1158
+ pc2.red(
936
1159
  `Exchange request to ${url} failed: ${err instanceof Error ? err.message : String(err)}`
937
1160
  )
938
1161
  );
@@ -954,7 +1177,7 @@ function startAuthServer(options) {
954
1177
  const error = url.searchParams.get("error");
955
1178
  if (error) {
956
1179
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(ERROR_HTML(error));
957
- console.error(pc4.red(`Login failed: ${error}`));
1180
+ console.error(pc2.red(`Login failed: ${error}`));
958
1181
  cleanup(2);
959
1182
  return;
960
1183
  }
@@ -967,14 +1190,14 @@ function startAuthServer(options) {
967
1190
  }
968
1191
  if (receivedState !== options.state) {
969
1192
  res.writeHead(403, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(ERROR_HTML('State mismatch. Start over with "01 login".'));
970
- console.error(pc4.red("Login failed: state mismatch."));
1193
+ console.error(pc2.red("Login failed: state mismatch."));
971
1194
  cleanup(2);
972
1195
  return;
973
1196
  }
974
1197
  exchangeCode(code).then((creds) => {
975
1198
  if (!creds) {
976
1199
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(ERROR_HTML('Invalid or expired code. Start over with "01 login".'));
977
- console.error(pc4.red("Login failed: code exchange failed."));
1200
+ console.error(pc2.red("Login failed: code exchange failed."));
978
1201
  cleanup(2);
979
1202
  return;
980
1203
  }
@@ -987,9 +1210,9 @@ function startAuthServer(options) {
987
1210
  if (creds.tenants && creds.tenants.length > 0) {
988
1211
  saveTenantList(creds.tenants);
989
1212
  }
990
- console.log(pc4.green(`
1213
+ console.log(pc2.green(`
991
1214
  Logged in successfully!`));
992
- console.log(pc4.dim(`Tenant: ${creds.tenantName}`));
1215
+ console.log(pc2.dim(`Tenant: ${creds.tenantName}`));
993
1216
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(SUCCESS_HTML);
994
1217
  cleanup(0);
995
1218
  });
@@ -1011,7 +1234,7 @@ Logged in successfully!`));
1011
1234
  }
1012
1235
  timeout = setTimeout(() => {
1013
1236
  console.error(
1014
- pc4.red("\nLogin timed out (3 minutes). Please try again.")
1237
+ pc2.red("\nLogin timed out (3 minutes). Please try again.")
1015
1238
  );
1016
1239
  cleanup(4);
1017
1240
  }, TIMEOUT_MS);
@@ -1030,18 +1253,18 @@ function registerAuthCommands(program2) {
1030
1253
  state,
1031
1254
  saveFn: (creds) => {
1032
1255
  saveCredentials(creds);
1033
- console.log(pc4.dim(`Credentials saved to ${getCredentialsPath()}`));
1256
+ console.log(pc2.dim(`Credentials saved to ${getCredentialsPath()}`));
1034
1257
  }
1035
1258
  });
1036
1259
  const params = new URLSearchParams({ port: String(port), state });
1037
1260
  const loginUrl = `${WEB_URL}/cli-auth?${params.toString()}`;
1038
- console.log(pc4.dim("Opening browser for login..."));
1039
- console.log(pc4.dim(`If the browser does not open, visit:
1261
+ console.log(pc2.dim("Opening browser for login..."));
1262
+ console.log(pc2.dim(`If the browser does not open, visit:
1040
1263
  ${loginUrl}`));
1041
1264
  openBrowser(loginUrl);
1042
1265
  } catch (err) {
1043
1266
  console.error(
1044
- pc4.red(
1267
+ pc2.red(
1045
1268
  `Server error: ${err instanceof Error ? err.message : String(err)}`
1046
1269
  )
1047
1270
  );
@@ -1051,9 +1274,9 @@ ${loginUrl}`));
1051
1274
  program2.command("logout").description("Remove stored credentials").action(() => {
1052
1275
  const deleted = deleteCredentials();
1053
1276
  if (deleted) {
1054
- console.log(pc4.green("Logged out. Credentials removed."));
1277
+ console.log(pc2.green("Logged out. Credentials removed."));
1055
1278
  } else {
1056
- console.log(pc4.dim("No stored credentials found."));
1279
+ console.log(pc2.dim("No stored credentials found."));
1057
1280
  }
1058
1281
  });
1059
1282
  program2.command("whoami").description("Show current authentication status").action(() => {
@@ -1062,23 +1285,23 @@ ${loginUrl}`));
1062
1285
  const creds = localCreds || globalCreds;
1063
1286
  const isLocal = !!localCreds;
1064
1287
  if (!creds) {
1065
- console.log(pc4.dim('Not logged in. Run "01 login" to authenticate.'));
1288
+ console.log(pc2.dim('Not logged in. Run "01 login" to authenticate.'));
1066
1289
  return;
1067
1290
  }
1068
1291
  const masked = creds.publishableKey.length > 8 ? creds.publishableKey.slice(0, 4) + "..." + creds.publishableKey.slice(-4) : "****";
1069
- const scope = isLocal ? pc4.cyan(" (local)") : "";
1070
- console.log(`Tenant: ${pc4.bold(creds.tenantName)}${scope}`);
1071
- console.log(`Publishable Key: ${pc4.dim(masked)}`);
1072
- console.log(`Stored at: ${pc4.dim(creds.storedAt)}`);
1292
+ const scope = isLocal ? pc2.cyan(" (local)") : "";
1293
+ console.log(`Tenant: ${pc2.bold(creds.tenantName)}${scope}`);
1294
+ console.log(`Publishable Key: ${pc2.dim(masked)}`);
1295
+ console.log(`Stored at: ${pc2.dim(creds.storedAt)}`);
1073
1296
  console.log(
1074
- `File: ${pc4.dim(isLocal ? getLocalCredentialsPath() : getCredentialsPath())}`
1297
+ `File: ${pc2.dim(isLocal ? getLocalCredentialsPath() : getCredentialsPath())}`
1075
1298
  );
1076
1299
  });
1077
1300
  const tenant = program2.command("tenant").description("Manage tenant switching");
1078
1301
  tenant.command("list").description("Show cached tenant list").action(() => {
1079
1302
  const tenants = loadTenantList();
1080
1303
  if (!tenants || tenants.length === 0) {
1081
- console.log(pc4.dim('No cached tenants. Run "01 login" first.'));
1304
+ console.log(pc2.dim('No cached tenants. Run "01 login" first.'));
1082
1305
  return;
1083
1306
  }
1084
1307
  const localCreds = loadLocalCredentials();
@@ -1086,30 +1309,30 @@ ${loginUrl}`));
1086
1309
  const activeCreds = localCreds || globalCreds;
1087
1310
  const activeId = activeCreds?.tenantId;
1088
1311
  const activeName = activeCreds?.tenantName;
1089
- console.log(pc4.bold("Cached tenants:\n"));
1090
- for (const t of tenants) {
1091
- const active = (activeId ? t.id === activeId : t.name === activeName) ? pc4.green(" *") : "";
1092
- console.log(` ${t.name}${active}`);
1312
+ console.log(pc2.bold("Cached tenants:\n"));
1313
+ for (const t2 of tenants) {
1314
+ const active = (activeId ? t2.id === activeId : t2.name === activeName) ? pc2.green(" *") : "";
1315
+ console.log(` ${t2.name}${active}`);
1093
1316
  }
1094
1317
  console.log();
1095
1318
  if (activeName) {
1096
1319
  const scope = localCreds ? "(local)" : "(global)";
1097
- console.log(pc4.dim(`* active ${scope}`));
1320
+ console.log(pc2.dim(`* active ${scope}`));
1098
1321
  }
1099
1322
  });
1100
1323
  tenant.command("use <name>").description("Switch to a different tenant via browser").option("--local", "Save credentials locally in the current project").action(async (name, opts) => {
1101
1324
  const tenants = loadTenantList();
1102
1325
  if (!tenants || tenants.length === 0) {
1103
- console.error(pc4.red('No cached tenants. Run "01 login" first.'));
1326
+ console.error(pc2.red('No cached tenants. Run "01 login" first.'));
1104
1327
  process.exit(2);
1105
1328
  }
1106
1329
  const match = tenants.find(
1107
- (t) => t.name.toLowerCase() === name.toLowerCase()
1330
+ (t2) => t2.name.toLowerCase() === name.toLowerCase()
1108
1331
  );
1109
1332
  if (!match) {
1110
- console.error(pc4.red(`Tenant "${name}" not found in cache.`));
1333
+ console.error(pc2.red(`Tenant "${name}" not found in cache.`));
1111
1334
  console.error(
1112
- pc4.dim(`Available: ${tenants.map((t) => t.name).join(", ")}`)
1335
+ pc2.dim(`Available: ${tenants.map((t2) => t2.name).join(", ")}`)
1113
1336
  );
1114
1337
  process.exit(3);
1115
1338
  }
@@ -1122,12 +1345,12 @@ ${loginUrl}`));
1122
1345
  if (isLocal) {
1123
1346
  saveLocalCredentials(creds);
1124
1347
  console.log(
1125
- pc4.dim(`Credentials saved to ${getLocalCredentialsPath()}`)
1348
+ pc2.dim(`Credentials saved to ${getLocalCredentialsPath()}`)
1126
1349
  );
1127
1350
  } else {
1128
1351
  saveCredentials(creds);
1129
1352
  console.log(
1130
- pc4.dim(`Credentials saved to ${getCredentialsPath()}`)
1353
+ pc2.dim(`Credentials saved to ${getCredentialsPath()}`)
1131
1354
  );
1132
1355
  }
1133
1356
  }
@@ -1138,13 +1361,13 @@ ${loginUrl}`));
1138
1361
  tenantId: match.id
1139
1362
  });
1140
1363
  const loginUrl = `${WEB_URL}/cli-auth?${params.toString()}`;
1141
- console.log(pc4.dim(`Switching to tenant "${match.name}"...`));
1142
- console.log(pc4.dim(`If the browser does not open, visit:
1364
+ console.log(pc2.dim(`Switching to tenant "${match.name}"...`));
1365
+ console.log(pc2.dim(`If the browser does not open, visit:
1143
1366
  ${loginUrl}`));
1144
1367
  openBrowser(loginUrl);
1145
1368
  } catch (err) {
1146
1369
  console.error(
1147
- pc4.red(
1370
+ pc2.red(
1148
1371
  `Server error: ${err instanceof Error ? err.message : String(err)}`
1149
1372
  )
1150
1373
  );
@@ -1243,12 +1466,17 @@ function registerMcpCommands(program2) {
1243
1466
  // src/index.ts
1244
1467
  var require2 = createRequire(import.meta.url);
1245
1468
  var { version } = require2("../package.json");
1246
- process.on("unhandledRejection", (err) => {
1247
- exitWithError(err);
1248
- });
1249
1469
  var program = new Command();
1250
- program.name("01").description("CLI for the 01.software platform").version(version).option("--api-key <key>", "API key (sk01_... or pat01_... token)").option("--format <format>", "Output format: json, table, or ndjson");
1470
+ program.name("01").description("CLI for the 01.software platform").version(version).option("--api-key <key>", "API key (sk01_... or pat01_... token)").option("--format <format>", "Output format: json, table, or ndjson").option("--lang <locale>", "Force locale (ko or en)");
1251
1471
  var getFormat = () => program.opts().format ?? process.env.OUTPUT_FORMAT ?? "json";
1472
+ program.hook("preAction", () => {
1473
+ const lang = program.opts().lang;
1474
+ if (lang === "ko" || lang === "en") setLocale(lang);
1475
+ process.env.OUTPUT_FORMAT = getFormat();
1476
+ });
1477
+ process.on("unhandledRejection", (err) => {
1478
+ exitWithError(err, { format: getFormat() });
1479
+ });
1252
1480
  var getClient = () => resolveClient(program.opts().apiKey);
1253
1481
  registerCrudCommands(program, getClient, getFormat);
1254
1482
  registerOrderCommands(program, getClient, getFormat);