@01.software/cli 0.6.0 → 0.7.1

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,107 @@ ${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}`));
419
+ }
420
+ if (rawErr && typeof rawErr.status === "number") {
421
+ console.error(pc.dim(`Status: ${rawErr.status}`));
294
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
+ function resolveClient(apiKeyFlag) {
444
+ let publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY;
445
+ let secretKey = apiKeyFlag ?? process.env.SOFTWARE_SECRET_KEY;
446
+ let tenantId;
447
+ if (!publishableKey || !secretKey) {
448
+ const local = loadLocalCredentials();
449
+ if (local) {
450
+ publishableKey = publishableKey ?? local.publishableKey;
451
+ secretKey = secretKey ?? local.secretKey;
452
+ tenantId = tenantId ?? local.tenantId;
453
+ }
454
+ }
455
+ if (!publishableKey || !secretKey) {
456
+ const stored = loadCredentials();
457
+ if (stored) {
458
+ publishableKey = publishableKey ?? stored.publishableKey;
459
+ secretKey = secretKey ?? stored.secretKey;
460
+ tenantId = tenantId ?? stored.tenantId;
461
+ }
462
+ }
463
+ if (!publishableKey || !secretKey) {
464
+ exitWithError({
465
+ type: "permission",
466
+ code: "credential_invalid",
467
+ detail: {
468
+ message: t("AuthenticationRequired"),
469
+ steps: [
470
+ t("RunLoginToAuthenticate"),
471
+ t("PassApiKey"),
472
+ t("SetEnvVars")
473
+ ]
474
+ }
475
+ });
476
+ }
477
+ if (!isValidBearerToken(secretKey)) {
478
+ exitWithError({
479
+ type: "permission",
480
+ code: "credential_invalid",
481
+ detail: {
482
+ message: t("InvalidApiKeyFormat"),
483
+ suggestion: t("LegacyHexCredentialsRejected")
484
+ }
485
+ });
486
+ }
487
+ const serverOptions = { publishableKey, secretKey, tenantId };
488
+ return {
489
+ collections: new CollectionClient(
490
+ publishableKey,
491
+ secretKey,
492
+ void 0,
493
+ void 0,
494
+ void 0,
495
+ tenantId
496
+ ),
497
+ commerce: new ServerCommerceClient(serverOptions),
498
+ publishableKey,
499
+ secretKey
500
+ };
295
501
  }
296
502
 
297
503
  // src/commands/crud.ts
@@ -300,32 +506,44 @@ import { basename } from "path";
300
506
  import { COLLECTIONS } from "@01.software/sdk";
301
507
 
302
508
  // src/lib/parse.ts
303
- import pc3 from "picocolors";
304
- function parseJson(value, label) {
509
+ function failArg(label, value, expectedKind) {
510
+ const message = expectedKind === "object" ? t("InvalidJsonObject", { label }) : expectedKind === "array" ? t("InvalidJsonArray", { label }) : t("InvalidJsonValue", { label, value });
511
+ exitWithError({
512
+ type: "validation",
513
+ code: "invalid_argument",
514
+ field: label,
515
+ // `message` is read by `printError` for the stderr headline so the
516
+ // user sees the label/value in the one-line summary; `detail` carries
517
+ // the structured fields for `--format json` consumers.
518
+ message,
519
+ detail: { message, value, field: label }
520
+ });
521
+ }
522
+ function parseJson(value, label, options = {}) {
523
+ let parsed;
305
524
  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;
525
+ parsed = JSON.parse(value);
312
526
  } catch {
313
- console.error(pc3.red(`Invalid JSON for --${label}: ${value}`));
314
- process.exit(3);
527
+ return failArg(label, value, "value");
315
528
  }
529
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
530
+ return failArg(label, value, "object");
531
+ }
532
+ void options;
533
+ return parsed;
316
534
  }
317
- function parseJsonArray(value, label) {
535
+ function parseJsonArray(value, label, options = {}) {
536
+ let parsed;
318
537
  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;
538
+ parsed = JSON.parse(value);
325
539
  } catch {
326
- console.error(pc3.red(`Invalid JSON for --${label}: ${value}`));
327
- process.exit(3);
540
+ return failArg(label, value, "value");
541
+ }
542
+ if (!Array.isArray(parsed)) {
543
+ return failArg(label, value, "array");
328
544
  }
545
+ void options;
546
+ return parsed;
329
547
  }
330
548
 
331
549
  // src/commands/crud.ts
@@ -854,7 +1072,7 @@ import { createServer } from "http";
854
1072
  import { execFile, exec } from "child_process";
855
1073
  import { platform } from "os";
856
1074
  import { URL } from "url";
857
- import pc4 from "picocolors";
1075
+ import pc2 from "picocolors";
858
1076
  var WEB_URL = process.env.SOFTWARE_WEB_URL || "https://01.software";
859
1077
  var TIMEOUT_MS = 3 * 60 * 1e3;
860
1078
  function escapeHtml(s) {
@@ -864,7 +1082,7 @@ function openBrowser(url) {
864
1082
  const os = platform();
865
1083
  const onError = () => {
866
1084
  console.log(
867
- pc4.yellow(
1085
+ pc2.yellow(
868
1086
  `Could not open browser automatically. Open this URL manually:
869
1087
  ${url}`
870
1088
  )
@@ -910,7 +1128,7 @@ async function exchangeCode(code) {
910
1128
  if (!res.ok) {
911
1129
  const body = await res.text().catch(() => "");
912
1130
  console.error(
913
- pc4.red(
1131
+ pc2.red(
914
1132
  `Exchange failed: HTTP ${res.status} from ${url}${body ? ` \u2014 ${body.slice(0, 200)}` : ""}`
915
1133
  )
916
1134
  );
@@ -918,7 +1136,7 @@ async function exchangeCode(code) {
918
1136
  }
919
1137
  const data = await res.json();
920
1138
  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}`));
1139
+ console.error(pc2.red(`Exchange failed: malformed response from ${url}`));
922
1140
  return null;
923
1141
  }
924
1142
  return {
@@ -927,12 +1145,12 @@ async function exchangeCode(code) {
927
1145
  tenantName: data.tenantName,
928
1146
  tenantId: data.tenantId,
929
1147
  tenants: Array.isArray(data.tenants) ? data.tenants.filter(
930
- (t) => typeof t?.id === "string" && typeof t?.name === "string"
1148
+ (t2) => typeof t2?.id === "string" && typeof t2?.name === "string"
931
1149
  ) : void 0
932
1150
  };
933
1151
  } catch (err) {
934
1152
  console.error(
935
- pc4.red(
1153
+ pc2.red(
936
1154
  `Exchange request to ${url} failed: ${err instanceof Error ? err.message : String(err)}`
937
1155
  )
938
1156
  );
@@ -954,7 +1172,7 @@ function startAuthServer(options) {
954
1172
  const error = url.searchParams.get("error");
955
1173
  if (error) {
956
1174
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(ERROR_HTML(error));
957
- console.error(pc4.red(`Login failed: ${error}`));
1175
+ console.error(pc2.red(`Login failed: ${error}`));
958
1176
  cleanup(2);
959
1177
  return;
960
1178
  }
@@ -967,14 +1185,14 @@ function startAuthServer(options) {
967
1185
  }
968
1186
  if (receivedState !== options.state) {
969
1187
  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."));
1188
+ console.error(pc2.red("Login failed: state mismatch."));
971
1189
  cleanup(2);
972
1190
  return;
973
1191
  }
974
1192
  exchangeCode(code).then((creds) => {
975
1193
  if (!creds) {
976
1194
  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."));
1195
+ console.error(pc2.red("Login failed: code exchange failed."));
978
1196
  cleanup(2);
979
1197
  return;
980
1198
  }
@@ -987,9 +1205,9 @@ function startAuthServer(options) {
987
1205
  if (creds.tenants && creds.tenants.length > 0) {
988
1206
  saveTenantList(creds.tenants);
989
1207
  }
990
- console.log(pc4.green(`
1208
+ console.log(pc2.green(`
991
1209
  Logged in successfully!`));
992
- console.log(pc4.dim(`Tenant: ${creds.tenantName}`));
1210
+ console.log(pc2.dim(`Tenant: ${creds.tenantName}`));
993
1211
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", Connection: "close" }).end(SUCCESS_HTML);
994
1212
  cleanup(0);
995
1213
  });
@@ -1011,7 +1229,7 @@ Logged in successfully!`));
1011
1229
  }
1012
1230
  timeout = setTimeout(() => {
1013
1231
  console.error(
1014
- pc4.red("\nLogin timed out (3 minutes). Please try again.")
1232
+ pc2.red("\nLogin timed out (3 minutes). Please try again.")
1015
1233
  );
1016
1234
  cleanup(4);
1017
1235
  }, TIMEOUT_MS);
@@ -1030,18 +1248,18 @@ function registerAuthCommands(program2) {
1030
1248
  state,
1031
1249
  saveFn: (creds) => {
1032
1250
  saveCredentials(creds);
1033
- console.log(pc4.dim(`Credentials saved to ${getCredentialsPath()}`));
1251
+ console.log(pc2.dim(`Credentials saved to ${getCredentialsPath()}`));
1034
1252
  }
1035
1253
  });
1036
1254
  const params = new URLSearchParams({ port: String(port), state });
1037
1255
  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:
1256
+ console.log(pc2.dim("Opening browser for login..."));
1257
+ console.log(pc2.dim(`If the browser does not open, visit:
1040
1258
  ${loginUrl}`));
1041
1259
  openBrowser(loginUrl);
1042
1260
  } catch (err) {
1043
1261
  console.error(
1044
- pc4.red(
1262
+ pc2.red(
1045
1263
  `Server error: ${err instanceof Error ? err.message : String(err)}`
1046
1264
  )
1047
1265
  );
@@ -1051,9 +1269,9 @@ ${loginUrl}`));
1051
1269
  program2.command("logout").description("Remove stored credentials").action(() => {
1052
1270
  const deleted = deleteCredentials();
1053
1271
  if (deleted) {
1054
- console.log(pc4.green("Logged out. Credentials removed."));
1272
+ console.log(pc2.green("Logged out. Credentials removed."));
1055
1273
  } else {
1056
- console.log(pc4.dim("No stored credentials found."));
1274
+ console.log(pc2.dim("No stored credentials found."));
1057
1275
  }
1058
1276
  });
1059
1277
  program2.command("whoami").description("Show current authentication status").action(() => {
@@ -1062,23 +1280,23 @@ ${loginUrl}`));
1062
1280
  const creds = localCreds || globalCreds;
1063
1281
  const isLocal = !!localCreds;
1064
1282
  if (!creds) {
1065
- console.log(pc4.dim('Not logged in. Run "01 login" to authenticate.'));
1283
+ console.log(pc2.dim('Not logged in. Run "01 login" to authenticate.'));
1066
1284
  return;
1067
1285
  }
1068
1286
  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)}`);
1287
+ const scope = isLocal ? pc2.cyan(" (local)") : "";
1288
+ console.log(`Tenant: ${pc2.bold(creds.tenantName)}${scope}`);
1289
+ console.log(`Publishable Key: ${pc2.dim(masked)}`);
1290
+ console.log(`Stored at: ${pc2.dim(creds.storedAt)}`);
1073
1291
  console.log(
1074
- `File: ${pc4.dim(isLocal ? getLocalCredentialsPath() : getCredentialsPath())}`
1292
+ `File: ${pc2.dim(isLocal ? getLocalCredentialsPath() : getCredentialsPath())}`
1075
1293
  );
1076
1294
  });
1077
1295
  const tenant = program2.command("tenant").description("Manage tenant switching");
1078
1296
  tenant.command("list").description("Show cached tenant list").action(() => {
1079
1297
  const tenants = loadTenantList();
1080
1298
  if (!tenants || tenants.length === 0) {
1081
- console.log(pc4.dim('No cached tenants. Run "01 login" first.'));
1299
+ console.log(pc2.dim('No cached tenants. Run "01 login" first.'));
1082
1300
  return;
1083
1301
  }
1084
1302
  const localCreds = loadLocalCredentials();
@@ -1086,30 +1304,30 @@ ${loginUrl}`));
1086
1304
  const activeCreds = localCreds || globalCreds;
1087
1305
  const activeId = activeCreds?.tenantId;
1088
1306
  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}`);
1307
+ console.log(pc2.bold("Cached tenants:\n"));
1308
+ for (const t2 of tenants) {
1309
+ const active = (activeId ? t2.id === activeId : t2.name === activeName) ? pc2.green(" *") : "";
1310
+ console.log(` ${t2.name}${active}`);
1093
1311
  }
1094
1312
  console.log();
1095
1313
  if (activeName) {
1096
1314
  const scope = localCreds ? "(local)" : "(global)";
1097
- console.log(pc4.dim(`* active ${scope}`));
1315
+ console.log(pc2.dim(`* active ${scope}`));
1098
1316
  }
1099
1317
  });
1100
1318
  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
1319
  const tenants = loadTenantList();
1102
1320
  if (!tenants || tenants.length === 0) {
1103
- console.error(pc4.red('No cached tenants. Run "01 login" first.'));
1321
+ console.error(pc2.red('No cached tenants. Run "01 login" first.'));
1104
1322
  process.exit(2);
1105
1323
  }
1106
1324
  const match = tenants.find(
1107
- (t) => t.name.toLowerCase() === name.toLowerCase()
1325
+ (t2) => t2.name.toLowerCase() === name.toLowerCase()
1108
1326
  );
1109
1327
  if (!match) {
1110
- console.error(pc4.red(`Tenant "${name}" not found in cache.`));
1328
+ console.error(pc2.red(`Tenant "${name}" not found in cache.`));
1111
1329
  console.error(
1112
- pc4.dim(`Available: ${tenants.map((t) => t.name).join(", ")}`)
1330
+ pc2.dim(`Available: ${tenants.map((t2) => t2.name).join(", ")}`)
1113
1331
  );
1114
1332
  process.exit(3);
1115
1333
  }
@@ -1122,12 +1340,12 @@ ${loginUrl}`));
1122
1340
  if (isLocal) {
1123
1341
  saveLocalCredentials(creds);
1124
1342
  console.log(
1125
- pc4.dim(`Credentials saved to ${getLocalCredentialsPath()}`)
1343
+ pc2.dim(`Credentials saved to ${getLocalCredentialsPath()}`)
1126
1344
  );
1127
1345
  } else {
1128
1346
  saveCredentials(creds);
1129
1347
  console.log(
1130
- pc4.dim(`Credentials saved to ${getCredentialsPath()}`)
1348
+ pc2.dim(`Credentials saved to ${getCredentialsPath()}`)
1131
1349
  );
1132
1350
  }
1133
1351
  }
@@ -1138,13 +1356,13 @@ ${loginUrl}`));
1138
1356
  tenantId: match.id
1139
1357
  });
1140
1358
  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:
1359
+ console.log(pc2.dim(`Switching to tenant "${match.name}"...`));
1360
+ console.log(pc2.dim(`If the browser does not open, visit:
1143
1361
  ${loginUrl}`));
1144
1362
  openBrowser(loginUrl);
1145
1363
  } catch (err) {
1146
1364
  console.error(
1147
- pc4.red(
1365
+ pc2.red(
1148
1366
  `Server error: ${err instanceof Error ? err.message : String(err)}`
1149
1367
  )
1150
1368
  );
@@ -1205,13 +1423,28 @@ import { existsSync as existsSync2 } from "fs";
1205
1423
  import { spawn } from "child_process";
1206
1424
  import { fileURLToPath } from "url";
1207
1425
  var __dirname = dirname(fileURLToPath(import.meta.url));
1208
- function findStdioEntry() {
1209
- for (const depth of ["../../../..", "../../.."]) {
1210
- const candidate = resolve(__dirname, depth, "apps/mcp/.xmcp/stdio.js");
1211
- if (existsSync2(candidate)) return candidate;
1426
+ function findStdioEntry(baseDir = __dirname) {
1427
+ const packaged = resolve(baseDir, "mcp/stdio.js");
1428
+ if (existsSync2(packaged)) return packaged;
1429
+ const roots = ["../../../..", "../../..", "../.."];
1430
+ const entries = ["apps/mcp/dist/stdio.js", "apps/mcp/.xmcp/stdio.js"];
1431
+ for (const entry of entries) {
1432
+ for (const root of roots) {
1433
+ const candidate = resolve(baseDir, root, entry);
1434
+ if (existsSync2(candidate)) return candidate;
1435
+ }
1212
1436
  }
1213
1437
  return null;
1214
1438
  }
1439
+ function createMcpEnv(baseEnv, client) {
1440
+ const env = {
1441
+ ...baseEnv,
1442
+ SOFTWARE_PUBLISHABLE_KEY: client.publishableKey,
1443
+ SOFTWARE_SECRET_KEY: client.secretKey
1444
+ };
1445
+ delete env.SOFTWARE_TENANT_ID;
1446
+ return env;
1447
+ }
1215
1448
  function registerMcpCommands(program2) {
1216
1449
  program2.command("mcp").description("Start MCP server over stdio").action(() => {
1217
1450
  const client = resolveClient(program2.opts().apiKey);
@@ -1224,11 +1457,7 @@ function registerMcpCommands(program2) {
1224
1457
  );
1225
1458
  }
1226
1459
  const child = spawn(process.execPath, [stdioEntry], {
1227
- env: {
1228
- ...process.env,
1229
- SOFTWARE_PUBLISHABLE_KEY: client.publishableKey,
1230
- SOFTWARE_SECRET_KEY: client.secretKey
1231
- },
1460
+ env: createMcpEnv(process.env, client),
1232
1461
  stdio: ["inherit", "inherit", "inherit"]
1233
1462
  });
1234
1463
  child.on("error", (err) => {
@@ -1243,12 +1472,17 @@ function registerMcpCommands(program2) {
1243
1472
  // src/index.ts
1244
1473
  var require2 = createRequire(import.meta.url);
1245
1474
  var { version } = require2("../package.json");
1246
- process.on("unhandledRejection", (err) => {
1247
- exitWithError(err);
1248
- });
1249
1475
  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");
1476
+ 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
1477
  var getFormat = () => program.opts().format ?? process.env.OUTPUT_FORMAT ?? "json";
1478
+ program.hook("preAction", () => {
1479
+ const lang = program.opts().lang;
1480
+ if (lang === "ko" || lang === "en") setLocale(lang);
1481
+ process.env.OUTPUT_FORMAT = getFormat();
1482
+ });
1483
+ process.on("unhandledRejection", (err) => {
1484
+ exitWithError(err, { format: getFormat() });
1485
+ });
1252
1486
  var getClient = () => resolveClient(program.opts().apiKey);
1253
1487
  registerCrudCommands(program, getClient, getFormat);
1254
1488
  registerOrderCommands(program, getClient, getFormat);