@ait-co/console-cli 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +194 -0
- package/README.md +96 -79
- package/dist/cli.mjs +604 -62
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -3
package/dist/cli.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import { unzipSync } from "fflate";
|
|
|
8
8
|
import { checkbox, confirm, editor, input, password, select } from "@inquirer/prompts";
|
|
9
9
|
import { imageSize } from "image-size";
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
11
|
-
import { constants, createReadStream } from "node:fs";
|
|
11
|
+
import { constants, createReadStream, existsSync } from "node:fs";
|
|
12
|
+
import { createInterface } from "node:readline/promises";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
14
|
import { createHash } from "node:crypto";
|
|
14
15
|
//#region src/api/http.ts
|
|
@@ -2448,7 +2449,7 @@ function optionalPathArray(input, key, configDir) {
|
|
|
2448
2449
|
});
|
|
2449
2450
|
}
|
|
2450
2451
|
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
2451
|
-
function isValidEmail(v) {
|
|
2452
|
+
function isValidEmail$1(v) {
|
|
2452
2453
|
return EMAIL_REGEX.test(v.toLowerCase());
|
|
2453
2454
|
}
|
|
2454
2455
|
const TITLE_KO_REGEX = /^[가-힣A-Za-z0-9 :·?]+$/;
|
|
@@ -2510,7 +2511,7 @@ function validateManifest(raw, configDir) {
|
|
|
2510
2511
|
}
|
|
2511
2512
|
const appName = requireString(raw, "appName");
|
|
2512
2513
|
const csEmail = requireString(raw, "csEmail");
|
|
2513
|
-
if (!isValidEmail(csEmail)) throw new ManifestError("invalid-config", `csEmail is not a valid email address (got ${csEmail})`, "csEmail");
|
|
2514
|
+
if (!isValidEmail$1(csEmail)) throw new ManifestError("invalid-config", `csEmail is not a valid email address (got ${csEmail})`, "csEmail");
|
|
2514
2515
|
const subtitle = requireString(raw, "subtitle");
|
|
2515
2516
|
if (subtitle.length > MANIFEST_LIMITS.subtitleMaxChars) throw new ManifestError("invalid-config", `subtitle must be ${MANIFEST_LIMITS.subtitleMaxChars} characters or fewer (got ${subtitle.length})`, "subtitle");
|
|
2516
2517
|
const description = requireString(raw, "description");
|
|
@@ -2792,7 +2793,7 @@ function validateAppName(raw) {
|
|
|
2792
2793
|
return true;
|
|
2793
2794
|
}
|
|
2794
2795
|
function validateEmail(raw) {
|
|
2795
|
-
if (!isValidEmail(raw)) return "not a valid email address";
|
|
2796
|
+
if (!isValidEmail$1(raw)) return "not a valid email address";
|
|
2796
2797
|
return true;
|
|
2797
2798
|
}
|
|
2798
2799
|
function validateSubtitle(raw) {
|
|
@@ -3601,7 +3602,7 @@ function serviceStatusFor(entry) {
|
|
|
3601
3602
|
}
|
|
3602
3603
|
const POLL_MIN_INTERVAL_SEC = 30;
|
|
3603
3604
|
const POLL_MAX_INTERVAL_SEC = 3600;
|
|
3604
|
-
const statusCommand$
|
|
3605
|
+
const statusCommand$2 = defineCommand({
|
|
3605
3606
|
meta: {
|
|
3606
3607
|
name: "status",
|
|
3607
3608
|
description: "Show the derived review state of a mini-app (under-review / rejected / approved)."
|
|
@@ -5702,7 +5703,7 @@ const appCommand = defineCommand({
|
|
|
5702
5703
|
}),
|
|
5703
5704
|
ls: lsCommand$4,
|
|
5704
5705
|
show: showCommand$2,
|
|
5705
|
-
status: statusCommand$
|
|
5706
|
+
status: statusCommand$2,
|
|
5706
5707
|
ratings: ratingsCommand,
|
|
5707
5708
|
reports: reportsCommand,
|
|
5708
5709
|
bundles: bundlesCommand,
|
|
@@ -8695,6 +8696,46 @@ async function fetchWorkspaceMembers(workspaceId, cookies, opts = {}) {
|
|
|
8695
8696
|
if (!Array.isArray(raw)) throw new Error(`Unexpected members shape for workspace=${workspaceId}: not an array`);
|
|
8696
8697
|
return raw.map((entry, index) => normalizeMember(entry, workspaceId, index));
|
|
8697
8698
|
}
|
|
8699
|
+
/**
|
|
8700
|
+
* Invite a user by email to the workspace.
|
|
8701
|
+
*
|
|
8702
|
+
* Maps to `POST /workspaces/:wid/invites/send/by-email`. Payload shape is
|
|
8703
|
+
* inferred from static bundle analysis (PR #118); `role` is optional —
|
|
8704
|
+
* omit to use the server default.
|
|
8705
|
+
*
|
|
8706
|
+
* ⚠️ Inferred endpoint: method/path confirmed, payload/response/errorCodes
|
|
8707
|
+
* not live-captured. See docs/api/members.md "Invite 관련 endpoint".
|
|
8708
|
+
*/
|
|
8709
|
+
async function inviteMember(workspaceId, email, role, cookies, opts = {}) {
|
|
8710
|
+
const url = `${BASE$1}/workspaces/${workspaceId}/invites/send/by-email`;
|
|
8711
|
+
const body = { email };
|
|
8712
|
+
if (role !== void 0) body.role = role;
|
|
8713
|
+
return { raw: await requestConsoleApi({
|
|
8714
|
+
method: "POST",
|
|
8715
|
+
url,
|
|
8716
|
+
cookies,
|
|
8717
|
+
body,
|
|
8718
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
8719
|
+
}) };
|
|
8720
|
+
}
|
|
8721
|
+
/**
|
|
8722
|
+
* Remove a member from the workspace by their `bizUserNo`.
|
|
8723
|
+
*
|
|
8724
|
+
* Maps to `DELETE /workspaces/:wid/members/:memberBizUserNo`. The path
|
|
8725
|
+
* param name `memberBizUserNo` is confirmed from bundle analysis (PR #118).
|
|
8726
|
+
* Response body shape is uncaptured; we treat any SUCCESS as success.
|
|
8727
|
+
*
|
|
8728
|
+
* ⚠️ Inferred endpoint: method/path confirmed, response/errorCodes not
|
|
8729
|
+
* live-captured. See docs/api/members.md "DELETE …/members/<memberBizUserNo>".
|
|
8730
|
+
*/
|
|
8731
|
+
async function removeMember(workspaceId, memberBizUserNo, cookies, opts = {}) {
|
|
8732
|
+
await requestConsoleApi({
|
|
8733
|
+
method: "DELETE",
|
|
8734
|
+
url: `${BASE$1}/workspaces/${workspaceId}/members/${memberBizUserNo}`,
|
|
8735
|
+
cookies,
|
|
8736
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
8737
|
+
});
|
|
8738
|
+
}
|
|
8698
8739
|
function normalizeMember(raw, workspaceId, index) {
|
|
8699
8740
|
if (raw === null || typeof raw !== "object") throw new Error(`Unexpected member entry at index ${index} for workspace=${workspaceId}: not an object`);
|
|
8700
8741
|
const rec = raw;
|
|
@@ -8719,60 +8760,185 @@ function normalizeMember(raw, workspaceId, index) {
|
|
|
8719
8760
|
isAdult: Boolean(rec.isAdult)
|
|
8720
8761
|
};
|
|
8721
8762
|
}
|
|
8763
|
+
//#endregion
|
|
8764
|
+
//#region src/commands/members.ts
|
|
8765
|
+
const lsCommand$2 = defineCommand({
|
|
8766
|
+
meta: {
|
|
8767
|
+
name: "ls",
|
|
8768
|
+
description: "List members of the selected workspace."
|
|
8769
|
+
},
|
|
8770
|
+
args: {
|
|
8771
|
+
workspace: {
|
|
8772
|
+
type: "string",
|
|
8773
|
+
description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
|
|
8774
|
+
},
|
|
8775
|
+
json: {
|
|
8776
|
+
type: "boolean",
|
|
8777
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
8778
|
+
default: false
|
|
8779
|
+
}
|
|
8780
|
+
},
|
|
8781
|
+
async run({ args }) {
|
|
8782
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
8783
|
+
if (!ctx) return;
|
|
8784
|
+
const { session, workspaceId } = ctx;
|
|
8785
|
+
printContextHeader(ctx, { json: args.json });
|
|
8786
|
+
try {
|
|
8787
|
+
const members = await fetchWorkspaceMembers(workspaceId, session.cookies);
|
|
8788
|
+
if (args.json) {
|
|
8789
|
+
emitJson({
|
|
8790
|
+
ok: true,
|
|
8791
|
+
workspaceId,
|
|
8792
|
+
members: members.map((m) => ({
|
|
8793
|
+
bizUserNo: m.bizUserNo,
|
|
8794
|
+
name: m.name,
|
|
8795
|
+
email: m.email,
|
|
8796
|
+
status: m.status,
|
|
8797
|
+
role: m.role,
|
|
8798
|
+
isOwnerDelegationRequested: m.isOwnerDelegationRequested
|
|
8799
|
+
}))
|
|
8800
|
+
});
|
|
8801
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
8802
|
+
}
|
|
8803
|
+
if (members.length === 0) {
|
|
8804
|
+
process.stdout.write(`No members in workspace ${workspaceId}.\n`);
|
|
8805
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
8806
|
+
}
|
|
8807
|
+
for (const m of members) process.stdout.write(`${m.bizUserNo}\t${m.name}\t${m.email}\t${m.role}\t${m.status}\n`);
|
|
8808
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
8809
|
+
} catch (err) {
|
|
8810
|
+
return emitFailureFromError(args.json, err);
|
|
8811
|
+
}
|
|
8812
|
+
}
|
|
8813
|
+
});
|
|
8814
|
+
function isValidEmail(email) {
|
|
8815
|
+
const at = email.indexOf("@");
|
|
8816
|
+
if (at <= 0) return false;
|
|
8817
|
+
const domain = email.slice(at + 1);
|
|
8818
|
+
return domain.length > 0 && domain.includes(".");
|
|
8819
|
+
}
|
|
8722
8820
|
const membersCommand = defineCommand({
|
|
8723
8821
|
meta: {
|
|
8724
8822
|
name: "members",
|
|
8725
|
-
description: "Inspect workspace members."
|
|
8823
|
+
description: "Inspect and manage workspace members."
|
|
8726
8824
|
},
|
|
8727
|
-
subCommands: {
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
workspace: {
|
|
8734
|
-
type: "string",
|
|
8735
|
-
description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
|
|
8825
|
+
subCommands: {
|
|
8826
|
+
ls: lsCommand$2,
|
|
8827
|
+
invite: defineCommand({
|
|
8828
|
+
meta: {
|
|
8829
|
+
name: "invite",
|
|
8830
|
+
description: "Invite a user to the workspace by email."
|
|
8736
8831
|
},
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8832
|
+
args: {
|
|
8833
|
+
email: {
|
|
8834
|
+
type: "positional",
|
|
8835
|
+
required: true,
|
|
8836
|
+
description: "Email address of the user to invite."
|
|
8837
|
+
},
|
|
8838
|
+
role: {
|
|
8839
|
+
type: "string",
|
|
8840
|
+
description: "Role to assign (default: server default). Example: MEMBER."
|
|
8841
|
+
},
|
|
8842
|
+
workspace: {
|
|
8843
|
+
type: "string",
|
|
8844
|
+
description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
|
|
8845
|
+
},
|
|
8846
|
+
json: {
|
|
8847
|
+
type: "boolean",
|
|
8848
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
8849
|
+
default: false
|
|
8850
|
+
}
|
|
8851
|
+
},
|
|
8852
|
+
async run({ args }) {
|
|
8853
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
8854
|
+
if (!ctx) return;
|
|
8855
|
+
const { session, workspaceId } = ctx;
|
|
8856
|
+
printContextHeader(ctx, { json: args.json });
|
|
8857
|
+
const email = String(args.email).trim();
|
|
8858
|
+
if (!isValidEmail(email)) {
|
|
8859
|
+
const message = `<email> must be a valid email address (got ${JSON.stringify(email)})`;
|
|
8860
|
+
if (args.json) emitJson({
|
|
8861
|
+
ok: false,
|
|
8862
|
+
reason: "invalid-email",
|
|
8863
|
+
message
|
|
8762
8864
|
});
|
|
8865
|
+
else process.stderr.write(`${message}\n`);
|
|
8866
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
8867
|
+
}
|
|
8868
|
+
const role = args.role ? String(args.role).trim() : void 0;
|
|
8869
|
+
try {
|
|
8870
|
+
await inviteMember(workspaceId, email, role, session.cookies);
|
|
8871
|
+
if (args.json) {
|
|
8872
|
+
emitJson({
|
|
8873
|
+
ok: true,
|
|
8874
|
+
workspaceId,
|
|
8875
|
+
email
|
|
8876
|
+
});
|
|
8877
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
8878
|
+
}
|
|
8879
|
+
process.stdout.write(`Invited ${email} to workspace ${workspaceId}.\n`);
|
|
8763
8880
|
return exitAfterFlush(ExitCode.Ok);
|
|
8881
|
+
} catch (err) {
|
|
8882
|
+
return emitFailureFromError(args.json, err);
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
}),
|
|
8886
|
+
remove: defineCommand({
|
|
8887
|
+
meta: {
|
|
8888
|
+
name: "remove",
|
|
8889
|
+
description: "Remove a member from the workspace by their bizUserNo."
|
|
8890
|
+
},
|
|
8891
|
+
args: {
|
|
8892
|
+
bizUserNo: {
|
|
8893
|
+
type: "positional",
|
|
8894
|
+
required: true,
|
|
8895
|
+
description: "bizUserNo of the member to remove (from `aitcc members ls`)."
|
|
8896
|
+
},
|
|
8897
|
+
workspace: {
|
|
8898
|
+
type: "string",
|
|
8899
|
+
description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
|
|
8900
|
+
},
|
|
8901
|
+
json: {
|
|
8902
|
+
type: "boolean",
|
|
8903
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
8904
|
+
default: false
|
|
8764
8905
|
}
|
|
8765
|
-
|
|
8766
|
-
|
|
8906
|
+
},
|
|
8907
|
+
async run({ args }) {
|
|
8908
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
8909
|
+
if (!ctx) return;
|
|
8910
|
+
const { session, workspaceId } = ctx;
|
|
8911
|
+
printContextHeader(ctx, { json: args.json });
|
|
8912
|
+
const rawId = String(args.bizUserNo);
|
|
8913
|
+
const parsed = parsePositiveInt$1(rawId);
|
|
8914
|
+
if (parsed === null) {
|
|
8915
|
+
const message = `<bizUserNo> must be a positive integer (got ${JSON.stringify(rawId)})`;
|
|
8916
|
+
if (args.json) emitJson({
|
|
8917
|
+
ok: false,
|
|
8918
|
+
reason: "invalid-id",
|
|
8919
|
+
message
|
|
8920
|
+
});
|
|
8921
|
+
else process.stderr.write(`${message}\n`);
|
|
8922
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
8923
|
+
}
|
|
8924
|
+
try {
|
|
8925
|
+
await removeMember(workspaceId, parsed, session.cookies);
|
|
8926
|
+
if (args.json) {
|
|
8927
|
+
emitJson({
|
|
8928
|
+
ok: true,
|
|
8929
|
+
workspaceId,
|
|
8930
|
+
bizUserNo: parsed
|
|
8931
|
+
});
|
|
8932
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
8933
|
+
}
|
|
8934
|
+
process.stdout.write(`Removed member ${parsed} from workspace ${workspaceId}.\n`);
|
|
8767
8935
|
return exitAfterFlush(ExitCode.Ok);
|
|
8936
|
+
} catch (err) {
|
|
8937
|
+
return emitFailureFromError(args.json, err);
|
|
8768
8938
|
}
|
|
8769
|
-
for (const m of members) process.stdout.write(`${m.bizUserNo}\t${m.name}\t${m.email}\t${m.role}\t${m.status}\n`);
|
|
8770
|
-
return exitAfterFlush(ExitCode.Ok);
|
|
8771
|
-
} catch (err) {
|
|
8772
|
-
return emitFailureFromError(args.json, err);
|
|
8773
8939
|
}
|
|
8774
|
-
}
|
|
8775
|
-
}
|
|
8940
|
+
})
|
|
8941
|
+
}
|
|
8776
8942
|
});
|
|
8777
8943
|
const BASE = "https://api-public.toss.im/api-public/v3/ipd-thor/api/v1";
|
|
8778
8944
|
async function fetchNotices(params, cookies, opts = {}) {
|
|
@@ -9026,6 +9192,393 @@ const noticesCommand = defineCommand({
|
|
|
9026
9192
|
}
|
|
9027
9193
|
});
|
|
9028
9194
|
//#endregion
|
|
9195
|
+
//#region src/version.ts
|
|
9196
|
+
function resolveVersion() {
|
|
9197
|
+
try {
|
|
9198
|
+
const injected = globalThis.AITCC_VERSION;
|
|
9199
|
+
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9200
|
+
} catch {}
|
|
9201
|
+
try {
|
|
9202
|
+
return "0.1.29";
|
|
9203
|
+
} catch {}
|
|
9204
|
+
return "0.0.0-dev";
|
|
9205
|
+
}
|
|
9206
|
+
const VERSION = resolveVersion();
|
|
9207
|
+
//#endregion
|
|
9208
|
+
//#region src/telemetry/state.ts
|
|
9209
|
+
/**
|
|
9210
|
+
* Telemetry consent state + anon_id I/O for console-cli.
|
|
9211
|
+
*
|
|
9212
|
+
* Storage: ~/.config/aitcc/telemetry.json (0600, XDG-aware via configDir())
|
|
9213
|
+
* Consistent with devtools' localStorage schema names where applicable.
|
|
9214
|
+
*/
|
|
9215
|
+
/** Current policy version. Bump whenever the privacy policy changes. */
|
|
9216
|
+
const CURRENT_POLICY_VERSION = "2026-05-12";
|
|
9217
|
+
function telemetryFilePath() {
|
|
9218
|
+
return join(configDir(), "telemetry.json");
|
|
9219
|
+
}
|
|
9220
|
+
async function readStateFile() {
|
|
9221
|
+
let raw;
|
|
9222
|
+
try {
|
|
9223
|
+
raw = await readFile(telemetryFilePath(), "utf8");
|
|
9224
|
+
} catch {
|
|
9225
|
+
return null;
|
|
9226
|
+
}
|
|
9227
|
+
let parsed;
|
|
9228
|
+
try {
|
|
9229
|
+
parsed = JSON.parse(raw);
|
|
9230
|
+
} catch {
|
|
9231
|
+
return null;
|
|
9232
|
+
}
|
|
9233
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
9234
|
+
const obj = parsed;
|
|
9235
|
+
if (obj.schemaVersion !== 1) return null;
|
|
9236
|
+
if (obj.consent !== "granted" && obj.consent !== "denied" && obj.consent !== "undecided") return null;
|
|
9237
|
+
if (typeof obj.policyVersion !== "string") return null;
|
|
9238
|
+
return {
|
|
9239
|
+
schemaVersion: 1,
|
|
9240
|
+
consent: obj.consent,
|
|
9241
|
+
policyVersion: obj.policyVersion,
|
|
9242
|
+
...typeof obj.anonId === "string" ? { anonId: obj.anonId } : {}
|
|
9243
|
+
};
|
|
9244
|
+
}
|
|
9245
|
+
async function writeStateFile(state) {
|
|
9246
|
+
const path = telemetryFilePath();
|
|
9247
|
+
await mkdir(dirname(path), {
|
|
9248
|
+
recursive: true,
|
|
9249
|
+
mode: 448
|
|
9250
|
+
});
|
|
9251
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
9252
|
+
try {
|
|
9253
|
+
await writeFile(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
9254
|
+
await rename(tmp, path);
|
|
9255
|
+
} catch (err) {
|
|
9256
|
+
await unlink(tmp).catch(() => {});
|
|
9257
|
+
throw err;
|
|
9258
|
+
}
|
|
9259
|
+
}
|
|
9260
|
+
/** Read the raw consent from disk. Returns 'undecided' if no file. */
|
|
9261
|
+
async function readConsentState() {
|
|
9262
|
+
const s = await readStateFile();
|
|
9263
|
+
if (!s) return "undecided";
|
|
9264
|
+
return s.consent;
|
|
9265
|
+
}
|
|
9266
|
+
/**
|
|
9267
|
+
* Resolve effective consent with policy-version bump rule:
|
|
9268
|
+
* - Previously 'granted' but on an old policy version → revert to 'undecided'
|
|
9269
|
+
* - Previously 'denied' on any version → stay 'denied'
|
|
9270
|
+
*/
|
|
9271
|
+
async function resolveEffectiveConsent() {
|
|
9272
|
+
const s = await readStateFile();
|
|
9273
|
+
if (!s) return "undecided";
|
|
9274
|
+
if (s.consent === "granted") {
|
|
9275
|
+
if (s.policyVersion !== "2026-05-12") return "undecided";
|
|
9276
|
+
return "granted";
|
|
9277
|
+
}
|
|
9278
|
+
return s.consent;
|
|
9279
|
+
}
|
|
9280
|
+
/**
|
|
9281
|
+
* Returns the stored anon_id, or generates + persists a new UUID v4.
|
|
9282
|
+
* Once generated it is never overwritten except after a successful deleteMyData call.
|
|
9283
|
+
*/
|
|
9284
|
+
async function getOrCreateAnonId() {
|
|
9285
|
+
const s = await readStateFile();
|
|
9286
|
+
if (s?.anonId) return s.anonId;
|
|
9287
|
+
const id = crypto.randomUUID();
|
|
9288
|
+
await writeStateFile({
|
|
9289
|
+
...s ?? {
|
|
9290
|
+
schemaVersion: 1,
|
|
9291
|
+
consent: "undecided",
|
|
9292
|
+
policyVersion: "2026-05-12"
|
|
9293
|
+
},
|
|
9294
|
+
anonId: id
|
|
9295
|
+
});
|
|
9296
|
+
return id;
|
|
9297
|
+
}
|
|
9298
|
+
async function acceptConsent() {
|
|
9299
|
+
await writeStateFile({
|
|
9300
|
+
schemaVersion: 1,
|
|
9301
|
+
consent: "granted",
|
|
9302
|
+
policyVersion: CURRENT_POLICY_VERSION,
|
|
9303
|
+
anonId: (await readStateFile())?.anonId ?? crypto.randomUUID()
|
|
9304
|
+
});
|
|
9305
|
+
}
|
|
9306
|
+
async function denyConsent() {
|
|
9307
|
+
const s = await readStateFile();
|
|
9308
|
+
await writeStateFile({
|
|
9309
|
+
schemaVersion: 1,
|
|
9310
|
+
consent: "denied",
|
|
9311
|
+
policyVersion: CURRENT_POLICY_VERSION,
|
|
9312
|
+
...s?.anonId ? { anonId: s.anonId } : {}
|
|
9313
|
+
});
|
|
9314
|
+
}
|
|
9315
|
+
/**
|
|
9316
|
+
* Delete data: send DELETE /e?anon_id=... to the server (if we have an id),
|
|
9317
|
+
* then rotate local anon_id so subsequent events are unlinkable.
|
|
9318
|
+
*/
|
|
9319
|
+
async function deleteMyData(endpoint) {
|
|
9320
|
+
const s = await readStateFile();
|
|
9321
|
+
if (!s?.anonId) return false;
|
|
9322
|
+
try {
|
|
9323
|
+
if (!(await fetch(`${endpoint}/e?anon_id=${encodeURIComponent(s.anonId)}`, { method: "DELETE" })).ok) return false;
|
|
9324
|
+
await writeStateFile({
|
|
9325
|
+
...s,
|
|
9326
|
+
anonId: crypto.randomUUID()
|
|
9327
|
+
});
|
|
9328
|
+
return true;
|
|
9329
|
+
} catch {
|
|
9330
|
+
return false;
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
//#endregion
|
|
9334
|
+
//#region src/telemetry/send.ts
|
|
9335
|
+
/**
|
|
9336
|
+
* Telemetry send — fire-and-forget with one retry.
|
|
9337
|
+
*
|
|
9338
|
+
* Rules:
|
|
9339
|
+
* 1. If consent ≠ "granted" — drop silently.
|
|
9340
|
+
* 2. POST event as JSON with 5 s timeout.
|
|
9341
|
+
* 3. On network error or non-2xx: retry ONCE after 2 s. On second failure: drop.
|
|
9342
|
+
* 4. Meta is capped at 256 bytes (JSON-serialized); oversized meta is dropped.
|
|
9343
|
+
* 5. All calls are non-blocking — caller never awaits send().
|
|
9344
|
+
*/
|
|
9345
|
+
/** Meta size cap per server contract (JSON bytes). */
|
|
9346
|
+
const META_BYTE_CAP = 256;
|
|
9347
|
+
function sanitizeMeta(meta) {
|
|
9348
|
+
if (meta === void 0) return void 0;
|
|
9349
|
+
const serialized = JSON.stringify(meta);
|
|
9350
|
+
if (new TextEncoder().encode(serialized).byteLength > META_BYTE_CAP) return void 0;
|
|
9351
|
+
return meta;
|
|
9352
|
+
}
|
|
9353
|
+
async function doFetch(endpoint, payload) {
|
|
9354
|
+
const controller = new AbortController();
|
|
9355
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
9356
|
+
try {
|
|
9357
|
+
return (await fetch(`${endpoint}/e`, {
|
|
9358
|
+
method: "POST",
|
|
9359
|
+
headers: { "Content-Type": "application/json" },
|
|
9360
|
+
body: JSON.stringify(payload),
|
|
9361
|
+
signal: controller.signal
|
|
9362
|
+
})).ok;
|
|
9363
|
+
} catch {
|
|
9364
|
+
return false;
|
|
9365
|
+
} finally {
|
|
9366
|
+
clearTimeout(timeoutId);
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
function delay(ms) {
|
|
9370
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9371
|
+
}
|
|
9372
|
+
/** Retry delay in ms — injectable for tests. */
|
|
9373
|
+
let RETRY_DELAY_MS = 2e3;
|
|
9374
|
+
/**
|
|
9375
|
+
* Send a telemetry event. Drops silently if consent is not 'granted'.
|
|
9376
|
+
* Returns a Promise but callers should NOT await it — fire-and-forget only.
|
|
9377
|
+
*/
|
|
9378
|
+
async function send(endpoint, event, version, meta) {
|
|
9379
|
+
if (await readConsentState() !== "granted") return;
|
|
9380
|
+
const sanitized = sanitizeMeta(meta);
|
|
9381
|
+
const payload = {
|
|
9382
|
+
source: "console-cli",
|
|
9383
|
+
event,
|
|
9384
|
+
anon_id: await getOrCreateAnonId(),
|
|
9385
|
+
version,
|
|
9386
|
+
ts: Date.now(),
|
|
9387
|
+
...sanitized !== void 0 ? { meta: sanitized } : {}
|
|
9388
|
+
};
|
|
9389
|
+
if (await doFetch(endpoint, payload)) return;
|
|
9390
|
+
await delay(RETRY_DELAY_MS);
|
|
9391
|
+
await doFetch(endpoint, payload);
|
|
9392
|
+
}
|
|
9393
|
+
//#endregion
|
|
9394
|
+
//#region src/telemetry/index.ts
|
|
9395
|
+
/**
|
|
9396
|
+
* Telemetry client — internal to @ait-co/console-cli.
|
|
9397
|
+
*
|
|
9398
|
+
* Usage: import { trackInvocation, trackInstall } from './telemetry/index.js'
|
|
9399
|
+
*
|
|
9400
|
+
* Events are fire-and-forget (non-blocking). Consent is opt-in only.
|
|
9401
|
+
* First invocation on a TTY prompts the user; non-TTY (CI) defaults to deny.
|
|
9402
|
+
*
|
|
9403
|
+
* Endpoint override for staging: AITCC_TELEMETRY_ENV=staging
|
|
9404
|
+
* (or automatically when VERSION contains '-dev').
|
|
9405
|
+
*/
|
|
9406
|
+
function resolveEndpoint() {
|
|
9407
|
+
if (process.env.AITCC_TELEMETRY_ENV === "staging") return "https://t-staging.aitc.dev";
|
|
9408
|
+
if (VERSION.includes("-dev")) return "https://t-staging.aitc.dev";
|
|
9409
|
+
return "https://t.aitc.dev";
|
|
9410
|
+
}
|
|
9411
|
+
const TELEMETRY_ENDPOINT = resolveEndpoint();
|
|
9412
|
+
/** Returns true if this is the very first run (telemetry.json does not exist). */
|
|
9413
|
+
function isFirstRun() {
|
|
9414
|
+
return !existsSync(telemetryFilePath());
|
|
9415
|
+
}
|
|
9416
|
+
/**
|
|
9417
|
+
* Prompt for consent on TTY. Defaults to deny on any non-TTY or error.
|
|
9418
|
+
* Called once per install (no file yet) when stdin/stdout are both TTYs.
|
|
9419
|
+
*/
|
|
9420
|
+
async function promptConsent() {
|
|
9421
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
9422
|
+
await denyConsent();
|
|
9423
|
+
return;
|
|
9424
|
+
}
|
|
9425
|
+
const rl = createInterface({
|
|
9426
|
+
input: process.stdin,
|
|
9427
|
+
output: process.stdout
|
|
9428
|
+
});
|
|
9429
|
+
try {
|
|
9430
|
+
process.stderr.write([
|
|
9431
|
+
"",
|
|
9432
|
+
"aitcc 사용 개선을 위해 익명 사용 통계를 수집해도 될까요?",
|
|
9433
|
+
" · 개인 식별 정보 없음 · 랜덤 익명 ID만 사용 · 언제든 off 가능",
|
|
9434
|
+
" 자세한 내용: https://docs.aitc.dev/privacy",
|
|
9435
|
+
""
|
|
9436
|
+
].join("\n"));
|
|
9437
|
+
if ((await rl.question("보내도 될까요? [y/N] ")).trim().toLowerCase() === "y") await acceptConsent();
|
|
9438
|
+
else await denyConsent();
|
|
9439
|
+
} catch {
|
|
9440
|
+
await denyConsent();
|
|
9441
|
+
} finally {
|
|
9442
|
+
rl.close();
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
/** True only on the first invocation after a fresh install. */
|
|
9446
|
+
async function isNewInstall() {
|
|
9447
|
+
const markerPath = `${telemetryFilePath()}.install`;
|
|
9448
|
+
if (existsSync(markerPath)) return false;
|
|
9449
|
+
try {
|
|
9450
|
+
await writeFile(markerPath, "1", { mode: 384 });
|
|
9451
|
+
return true;
|
|
9452
|
+
} catch {
|
|
9453
|
+
return false;
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9456
|
+
/**
|
|
9457
|
+
* Called at CLI entry point with the resolved top-level command name.
|
|
9458
|
+
* Handles first-run consent prompt, install detection, and event send.
|
|
9459
|
+
* Fire-and-forget: do NOT await this.
|
|
9460
|
+
*/
|
|
9461
|
+
async function trackInvocation(command) {
|
|
9462
|
+
try {
|
|
9463
|
+
if (isFirstRun()) await promptConsent();
|
|
9464
|
+
if (await resolveEffectiveConsent() !== "granted") return;
|
|
9465
|
+
if (await isNewInstall()) send(TELEMETRY_ENDPOINT, "cli_install", VERSION, {
|
|
9466
|
+
platform: process.platform,
|
|
9467
|
+
arch: process.arch
|
|
9468
|
+
});
|
|
9469
|
+
send(TELEMETRY_ENDPOINT, "cli_invoked", VERSION, { command });
|
|
9470
|
+
} catch {}
|
|
9471
|
+
}
|
|
9472
|
+
const telemetryCommand = defineCommand({
|
|
9473
|
+
meta: {
|
|
9474
|
+
name: "telemetry",
|
|
9475
|
+
description: "Manage anonymous usage telemetry (opt-in)."
|
|
9476
|
+
},
|
|
9477
|
+
subCommands: {
|
|
9478
|
+
status: defineCommand({
|
|
9479
|
+
meta: {
|
|
9480
|
+
name: "status",
|
|
9481
|
+
description: "Show current telemetry consent state and anon_id."
|
|
9482
|
+
},
|
|
9483
|
+
args: { json: {
|
|
9484
|
+
type: "boolean",
|
|
9485
|
+
description: "Emit machine-readable JSON.",
|
|
9486
|
+
default: false
|
|
9487
|
+
} },
|
|
9488
|
+
async run({ args }) {
|
|
9489
|
+
const consent = await resolveEffectiveConsent();
|
|
9490
|
+
const anonId = consent === "granted" ? await getOrCreateAnonId() : null;
|
|
9491
|
+
const filePath = telemetryFilePath();
|
|
9492
|
+
if (args.json) {
|
|
9493
|
+
process.stdout.write(`${JSON.stringify({
|
|
9494
|
+
ok: true,
|
|
9495
|
+
consent,
|
|
9496
|
+
policyVersion: CURRENT_POLICY_VERSION,
|
|
9497
|
+
endpoint: TELEMETRY_ENDPOINT,
|
|
9498
|
+
...anonId ? { anonId } : {},
|
|
9499
|
+
filePath
|
|
9500
|
+
})}\n`);
|
|
9501
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
9502
|
+
}
|
|
9503
|
+
process.stdout.write(`Telemetry: ${consent}\n`);
|
|
9504
|
+
process.stdout.write(`Policy version: ${CURRENT_POLICY_VERSION}\n`);
|
|
9505
|
+
process.stdout.write(`Endpoint: ${TELEMETRY_ENDPOINT}\n`);
|
|
9506
|
+
if (anonId) process.stdout.write(`Anon ID: ${anonId}\n`);
|
|
9507
|
+
process.stdout.write(`State file: ${filePath}\n`);
|
|
9508
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
9509
|
+
}
|
|
9510
|
+
}),
|
|
9511
|
+
enable: defineCommand({
|
|
9512
|
+
meta: {
|
|
9513
|
+
name: "enable",
|
|
9514
|
+
description: "Enable anonymous usage telemetry (opt-in)."
|
|
9515
|
+
},
|
|
9516
|
+
args: { json: {
|
|
9517
|
+
type: "boolean",
|
|
9518
|
+
description: "Emit machine-readable JSON.",
|
|
9519
|
+
default: false
|
|
9520
|
+
} },
|
|
9521
|
+
async run({ args }) {
|
|
9522
|
+
await acceptConsent();
|
|
9523
|
+
const anonId = await getOrCreateAnonId();
|
|
9524
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
9525
|
+
ok: true,
|
|
9526
|
+
consent: "granted",
|
|
9527
|
+
anonId
|
|
9528
|
+
})}\n`);
|
|
9529
|
+
else {
|
|
9530
|
+
process.stdout.write("Telemetry enabled. Thank you!\n");
|
|
9531
|
+
process.stdout.write(`Anon ID: ${anonId}\n`);
|
|
9532
|
+
}
|
|
9533
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
9534
|
+
}
|
|
9535
|
+
}),
|
|
9536
|
+
disable: defineCommand({
|
|
9537
|
+
meta: {
|
|
9538
|
+
name: "disable",
|
|
9539
|
+
description: "Disable anonymous usage telemetry."
|
|
9540
|
+
},
|
|
9541
|
+
args: { json: {
|
|
9542
|
+
type: "boolean",
|
|
9543
|
+
description: "Emit machine-readable JSON.",
|
|
9544
|
+
default: false
|
|
9545
|
+
} },
|
|
9546
|
+
async run({ args }) {
|
|
9547
|
+
await denyConsent();
|
|
9548
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
9549
|
+
ok: true,
|
|
9550
|
+
consent: "denied"
|
|
9551
|
+
})}\n`);
|
|
9552
|
+
else process.stdout.write("Telemetry disabled.\n");
|
|
9553
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
9554
|
+
}
|
|
9555
|
+
}),
|
|
9556
|
+
delete: defineCommand({
|
|
9557
|
+
meta: {
|
|
9558
|
+
name: "delete",
|
|
9559
|
+
description: "Delete your telemetry data from the server and rotate the local anon_id."
|
|
9560
|
+
},
|
|
9561
|
+
args: { json: {
|
|
9562
|
+
type: "boolean",
|
|
9563
|
+
description: "Emit machine-readable JSON.",
|
|
9564
|
+
default: false
|
|
9565
|
+
} },
|
|
9566
|
+
async run({ args }) {
|
|
9567
|
+
const beforeConsent = await readConsentState();
|
|
9568
|
+
const ok = await deleteMyData(TELEMETRY_ENDPOINT);
|
|
9569
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
9570
|
+
ok,
|
|
9571
|
+
...ok ? {} : { reason: beforeConsent === "undecided" ? "no-data" : "server-error" }
|
|
9572
|
+
})}\n`);
|
|
9573
|
+
else if (ok) process.stdout.write("Deletion request sent. Your data has been removed and a new anon ID assigned.\n");
|
|
9574
|
+
else if (beforeConsent === "undecided") process.stdout.write("No telemetry data to delete (telemetry was never enabled).\n");
|
|
9575
|
+
else process.stderr.write("Deletion request failed. Please try again or contact the maintainers.\n");
|
|
9576
|
+
return exitAfterFlush(ok || beforeConsent === "undecided" ? ExitCode.Ok : ExitCode.NetworkError);
|
|
9577
|
+
}
|
|
9578
|
+
})
|
|
9579
|
+
}
|
|
9580
|
+
});
|
|
9581
|
+
//#endregion
|
|
9029
9582
|
//#region src/github.ts
|
|
9030
9583
|
const REPO_OWNER = "apps-in-toss-community";
|
|
9031
9584
|
const REPO_NAME = "console-cli";
|
|
@@ -9159,19 +9712,6 @@ function sha256OfFile(path) {
|
|
|
9159
9712
|
});
|
|
9160
9713
|
}
|
|
9161
9714
|
//#endregion
|
|
9162
|
-
//#region src/version.ts
|
|
9163
|
-
function resolveVersion() {
|
|
9164
|
-
try {
|
|
9165
|
-
const injected = globalThis.AITCC_VERSION;
|
|
9166
|
-
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9167
|
-
} catch {}
|
|
9168
|
-
try {
|
|
9169
|
-
return "0.1.27";
|
|
9170
|
-
} catch {}
|
|
9171
|
-
return "0.0.0-dev";
|
|
9172
|
-
}
|
|
9173
|
-
const VERSION = resolveVersion();
|
|
9174
|
-
//#endregion
|
|
9175
9715
|
//#region src/commands/upgrade.ts
|
|
9176
9716
|
const execFileP = promisify(execFile);
|
|
9177
9717
|
function isStandaloneBinary() {
|
|
@@ -10319,10 +10859,12 @@ const main = defineCommand({
|
|
|
10319
10859
|
keys: keysCommand,
|
|
10320
10860
|
notices: noticesCommand,
|
|
10321
10861
|
me: meCommand,
|
|
10862
|
+
telemetry: telemetryCommand,
|
|
10322
10863
|
completion: completionCommand
|
|
10323
10864
|
}
|
|
10324
10865
|
});
|
|
10325
10866
|
cleanupStaleUpgradeArtifacts().catch(() => {});
|
|
10867
|
+
trackInvocation(process.argv.slice(2).find((a) => !a.startsWith("-")) ?? "(none)");
|
|
10326
10868
|
runMain(main);
|
|
10327
10869
|
//#endregion
|
|
10328
10870
|
export {};
|