@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 +375 -147
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
(
|
|
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/
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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(
|
|
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(`${
|
|
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
|
-
|
|
384
|
+
pc.dim(
|
|
254
385
|
`
|
|
255
|
-
${resp.totalDocs}
|
|
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
|
-
|
|
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
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1213
|
+
console.log(pc2.green(`
|
|
991
1214
|
Logged in successfully!`));
|
|
992
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1039
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
1277
|
+
console.log(pc2.green("Logged out. Credentials removed."));
|
|
1055
1278
|
} else {
|
|
1056
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
1070
|
-
console.log(`Tenant: ${
|
|
1071
|
-
console.log(`Publishable Key: ${
|
|
1072
|
-
console.log(`Stored at: ${
|
|
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: ${
|
|
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(
|
|
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(
|
|
1090
|
-
for (const
|
|
1091
|
-
const active = (activeId ?
|
|
1092
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
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
|
-
(
|
|
1330
|
+
(t2) => t2.name.toLowerCase() === name.toLowerCase()
|
|
1108
1331
|
);
|
|
1109
1332
|
if (!match) {
|
|
1110
|
-
console.error(
|
|
1333
|
+
console.error(pc2.red(`Tenant "${name}" not found in cache.`));
|
|
1111
1334
|
console.error(
|
|
1112
|
-
|
|
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
|
-
|
|
1348
|
+
pc2.dim(`Credentials saved to ${getLocalCredentialsPath()}`)
|
|
1126
1349
|
);
|
|
1127
1350
|
} else {
|
|
1128
1351
|
saveCredentials(creds);
|
|
1129
1352
|
console.log(
|
|
1130
|
-
|
|
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(
|
|
1142
|
-
console.log(
|
|
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
|
-
|
|
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);
|