tutticode 0.1.1 → 0.2.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/README.md +76 -8
- package/README.zh-CN.md +63 -6
- package/dist/skills/tutticode/SKILL.md +12 -2
- package/dist/skills/tutticode/references/agent-loop.md +28 -1
- package/dist/skills/tutticode/references/devices/sampler.md +1 -1
- package/dist/skills/tutticode/references/devices.md +1 -1
- package/dist/skills/tutticode/references/for-agents.md +6 -3
- package/dist/skills/tutticode-sound-for-media/SKILL.md +9 -2
- package/dist/tutticode.cjs +3072 -353
- package/package.json +1 -1
package/dist/tutticode.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* tutticode v0.
|
|
2
|
+
/* tutticode v0.2.0 | https://tuttidaw.com */
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -2310,7 +2310,7 @@ var require_websocket = __commonJS({
|
|
|
2310
2310
|
var http = require("http");
|
|
2311
2311
|
var net = require("net");
|
|
2312
2312
|
var tls = require("tls");
|
|
2313
|
-
var { randomBytes:
|
|
2313
|
+
var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
|
|
2314
2314
|
var { Duplex, Readable } = require("stream");
|
|
2315
2315
|
var { URL: URL2 } = require("url");
|
|
2316
2316
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2865,8 +2865,8 @@ var require_websocket = __commonJS({
|
|
|
2865
2865
|
}
|
|
2866
2866
|
}
|
|
2867
2867
|
const defaultPort = isSecure ? 443 : 80;
|
|
2868
|
-
const key =
|
|
2869
|
-
const
|
|
2868
|
+
const key = randomBytes4(16).toString("base64");
|
|
2869
|
+
const request2 = isSecure ? https.request : http.request;
|
|
2870
2870
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2871
2871
|
let perMessageDeflate;
|
|
2872
2872
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -2944,12 +2944,12 @@ var require_websocket = __commonJS({
|
|
|
2944
2944
|
if (opts.auth && !options.headers.authorization) {
|
|
2945
2945
|
options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
|
|
2946
2946
|
}
|
|
2947
|
-
req = websocket._req =
|
|
2947
|
+
req = websocket._req = request2(opts);
|
|
2948
2948
|
if (websocket._redirects) {
|
|
2949
2949
|
websocket.emit("redirect", websocket.url, req);
|
|
2950
2950
|
}
|
|
2951
2951
|
} else {
|
|
2952
|
-
req = websocket._req =
|
|
2952
|
+
req = websocket._req = request2(opts);
|
|
2953
2953
|
}
|
|
2954
2954
|
if (opts.timeout) {
|
|
2955
2955
|
req.on("timeout", () => {
|
|
@@ -2998,7 +2998,7 @@ var require_websocket = __commonJS({
|
|
|
2998
2998
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2999
2999
|
return;
|
|
3000
3000
|
}
|
|
3001
|
-
const digest =
|
|
3001
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3002
3002
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
3003
3003
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
3004
3004
|
return;
|
|
@@ -3387,7 +3387,7 @@ var require_websocket_server = __commonJS({
|
|
|
3387
3387
|
var EventEmitter4 = require("events");
|
|
3388
3388
|
var http = require("http");
|
|
3389
3389
|
var { Duplex } = require("stream");
|
|
3390
|
-
var { createHash:
|
|
3390
|
+
var { createHash: createHash3 } = require("crypto");
|
|
3391
3391
|
var extension2 = require_extension();
|
|
3392
3392
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3393
3393
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3702,7 +3702,7 @@ var require_websocket_server = __commonJS({
|
|
|
3702
3702
|
}
|
|
3703
3703
|
if (this._state > RUNNING)
|
|
3704
3704
|
return abortHandshake(socket, 503);
|
|
3705
|
-
const digest =
|
|
3705
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3706
3706
|
const headers = [
|
|
3707
3707
|
"HTTP/1.1 101 Switching Protocols",
|
|
3708
3708
|
"Upgrade: websocket",
|
|
@@ -3791,12 +3791,703 @@ var require_websocket_server = __commonJS({
|
|
|
3791
3791
|
});
|
|
3792
3792
|
|
|
3793
3793
|
// src/bin.ts
|
|
3794
|
-
var
|
|
3794
|
+
var import_node_path12 = require("node:path");
|
|
3795
3795
|
|
|
3796
3796
|
// src/cli.ts
|
|
3797
|
-
var
|
|
3797
|
+
var import_node_fs11 = require("node:fs");
|
|
3798
3798
|
var import_node_readline2 = require("node:readline");
|
|
3799
3799
|
|
|
3800
|
+
// ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
|
|
3801
|
+
var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
|
|
3802
|
+
QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
|
|
3803
|
+
QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
|
|
3804
|
+
QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
|
|
3805
|
+
QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
|
|
3806
|
+
QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
|
|
3807
|
+
QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
|
|
3808
|
+
return QrCodeDataType2;
|
|
3809
|
+
})(QrCodeDataType || {});
|
|
3810
|
+
var LOW = [0, 1];
|
|
3811
|
+
var MEDIUM = [1, 0];
|
|
3812
|
+
var QUARTILE = [2, 3];
|
|
3813
|
+
var HIGH = [3, 2];
|
|
3814
|
+
var EccMap = {
|
|
3815
|
+
L: LOW,
|
|
3816
|
+
M: MEDIUM,
|
|
3817
|
+
Q: QUARTILE,
|
|
3818
|
+
H: HIGH
|
|
3819
|
+
};
|
|
3820
|
+
var NUMERIC_REGEX = /^\d*$/;
|
|
3821
|
+
var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
|
|
3822
|
+
var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
3823
|
+
var MIN_VERSION = 1;
|
|
3824
|
+
var MAX_VERSION = 40;
|
|
3825
|
+
var PENALTY_N1 = 3;
|
|
3826
|
+
var PENALTY_N2 = 3;
|
|
3827
|
+
var PENALTY_N3 = 40;
|
|
3828
|
+
var PENALTY_N4 = 10;
|
|
3829
|
+
var ECC_CODEWORDS_PER_BLOCK = [
|
|
3830
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
3831
|
+
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
|
3832
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
3833
|
+
// Low
|
|
3834
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
3835
|
+
// Medium
|
|
3836
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
3837
|
+
// Quartile
|
|
3838
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
|
|
3839
|
+
// High
|
|
3840
|
+
];
|
|
3841
|
+
var NUM_ERROR_CORRECTION_BLOCKS = [
|
|
3842
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
3843
|
+
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
|
3844
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
3845
|
+
// Low
|
|
3846
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
3847
|
+
// Medium
|
|
3848
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
3849
|
+
// Quartile
|
|
3850
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
|
|
3851
|
+
// High
|
|
3852
|
+
];
|
|
3853
|
+
var QrCode = class {
|
|
3854
|
+
/* -- Constructor (low level) and fields -- */
|
|
3855
|
+
// Creates a new QR Code with the given version number,
|
|
3856
|
+
// error correction level, data codeword bytes, and mask number.
|
|
3857
|
+
// This is a low-level API that most users should not use directly.
|
|
3858
|
+
// A mid-level API is the encodeSegments() function.
|
|
3859
|
+
constructor(version, ecc, dataCodewords, msk) {
|
|
3860
|
+
this.version = version;
|
|
3861
|
+
this.ecc = ecc;
|
|
3862
|
+
if (version < MIN_VERSION || version > MAX_VERSION)
|
|
3863
|
+
throw new RangeError("Version value out of range");
|
|
3864
|
+
if (msk < -1 || msk > 7)
|
|
3865
|
+
throw new RangeError("Mask value out of range");
|
|
3866
|
+
this.size = version * 4 + 17;
|
|
3867
|
+
const row = Array.from({ length: this.size }).fill(false);
|
|
3868
|
+
for (let i = 0; i < this.size; i++) {
|
|
3869
|
+
this.modules.push(row.slice());
|
|
3870
|
+
this.types.push(row.map(() => 0));
|
|
3871
|
+
}
|
|
3872
|
+
this.drawFunctionPatterns();
|
|
3873
|
+
const allCodewords = this.addEccAndInterleave(dataCodewords);
|
|
3874
|
+
this.drawCodewords(allCodewords);
|
|
3875
|
+
if (msk === -1) {
|
|
3876
|
+
let minPenalty = 1e9;
|
|
3877
|
+
for (let i = 0; i < 8; i++) {
|
|
3878
|
+
this.applyMask(i);
|
|
3879
|
+
this.drawFormatBits(i);
|
|
3880
|
+
const penalty = this.getPenaltyScore();
|
|
3881
|
+
if (penalty < minPenalty) {
|
|
3882
|
+
msk = i;
|
|
3883
|
+
minPenalty = penalty;
|
|
3884
|
+
}
|
|
3885
|
+
this.applyMask(i);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
this.mask = msk;
|
|
3889
|
+
this.applyMask(msk);
|
|
3890
|
+
this.drawFormatBits(msk);
|
|
3891
|
+
}
|
|
3892
|
+
/* -- Fields -- */
|
|
3893
|
+
// The width and height of this QR Code, measured in modules, between
|
|
3894
|
+
// 21 and 177 (inclusive). This is equal to version * 4 + 17.
|
|
3895
|
+
size;
|
|
3896
|
+
// The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
|
|
3897
|
+
// Even if a QR Code is created with automatic masking requested (mask = -1),
|
|
3898
|
+
// the resulting object still has a mask value between 0 and 7.
|
|
3899
|
+
mask;
|
|
3900
|
+
// The modules of this QR Code (false = light, true = dark).
|
|
3901
|
+
// Immutable after constructor finishes. Accessed through getModule().
|
|
3902
|
+
modules = [];
|
|
3903
|
+
types = [];
|
|
3904
|
+
/* -- Accessor methods -- */
|
|
3905
|
+
// Returns the color of the module (pixel) at the given coordinates, which is false
|
|
3906
|
+
// for light or true for dark. The top left corner has the coordinates (x=0, y=0).
|
|
3907
|
+
// If the given coordinates are out of bounds, then false (light) is returned.
|
|
3908
|
+
getModule(x, y) {
|
|
3909
|
+
return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
|
|
3910
|
+
}
|
|
3911
|
+
/* -- Private helper methods for constructor: Drawing function modules -- */
|
|
3912
|
+
// Reads this object's version field, and draws and marks all function modules.
|
|
3913
|
+
drawFunctionPatterns() {
|
|
3914
|
+
for (let i = 0; i < this.size; i++) {
|
|
3915
|
+
this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
|
|
3916
|
+
this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
|
|
3917
|
+
}
|
|
3918
|
+
this.drawFinderPattern(3, 3);
|
|
3919
|
+
this.drawFinderPattern(this.size - 4, 3);
|
|
3920
|
+
this.drawFinderPattern(3, this.size - 4);
|
|
3921
|
+
const alignPatPos = this.getAlignmentPatternPositions();
|
|
3922
|
+
const numAlign = alignPatPos.length;
|
|
3923
|
+
for (let i = 0; i < numAlign; i++) {
|
|
3924
|
+
for (let j = 0; j < numAlign; j++) {
|
|
3925
|
+
if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
|
|
3926
|
+
this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
this.drawFormatBits(0);
|
|
3930
|
+
this.drawVersion();
|
|
3931
|
+
}
|
|
3932
|
+
// Draws two copies of the format bits (with its own error correction code)
|
|
3933
|
+
// based on the given mask and this object's error correction level field.
|
|
3934
|
+
drawFormatBits(mask) {
|
|
3935
|
+
const data = this.ecc[1] << 3 | mask;
|
|
3936
|
+
let rem = data;
|
|
3937
|
+
for (let i = 0; i < 10; i++)
|
|
3938
|
+
rem = rem << 1 ^ (rem >>> 9) * 1335;
|
|
3939
|
+
const bits = (data << 10 | rem) ^ 21522;
|
|
3940
|
+
for (let i = 0; i <= 5; i++)
|
|
3941
|
+
this.setFunctionModule(8, i, getBit(bits, i));
|
|
3942
|
+
this.setFunctionModule(8, 7, getBit(bits, 6));
|
|
3943
|
+
this.setFunctionModule(8, 8, getBit(bits, 7));
|
|
3944
|
+
this.setFunctionModule(7, 8, getBit(bits, 8));
|
|
3945
|
+
for (let i = 9; i < 15; i++)
|
|
3946
|
+
this.setFunctionModule(14 - i, 8, getBit(bits, i));
|
|
3947
|
+
for (let i = 0; i < 8; i++)
|
|
3948
|
+
this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
|
|
3949
|
+
for (let i = 8; i < 15; i++)
|
|
3950
|
+
this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
|
|
3951
|
+
this.setFunctionModule(8, this.size - 8, true);
|
|
3952
|
+
}
|
|
3953
|
+
// Draws two copies of the version bits (with its own error correction code),
|
|
3954
|
+
// based on this object's version field, iff 7 <= version <= 40.
|
|
3955
|
+
drawVersion() {
|
|
3956
|
+
if (this.version < 7)
|
|
3957
|
+
return;
|
|
3958
|
+
let rem = this.version;
|
|
3959
|
+
for (let i = 0; i < 12; i++)
|
|
3960
|
+
rem = rem << 1 ^ (rem >>> 11) * 7973;
|
|
3961
|
+
const bits = this.version << 12 | rem;
|
|
3962
|
+
for (let i = 0; i < 18; i++) {
|
|
3963
|
+
const color = getBit(bits, i);
|
|
3964
|
+
const a = this.size - 11 + i % 3;
|
|
3965
|
+
const b = Math.floor(i / 3);
|
|
3966
|
+
this.setFunctionModule(a, b, color);
|
|
3967
|
+
this.setFunctionModule(b, a, color);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
// Draws a 9*9 finder pattern including the border separator,
|
|
3971
|
+
// with the center module at (x, y). Modules can be out of bounds.
|
|
3972
|
+
drawFinderPattern(x, y) {
|
|
3973
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
3974
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
3975
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
3976
|
+
const xx = x + dx;
|
|
3977
|
+
const yy = y + dy;
|
|
3978
|
+
if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
|
|
3979
|
+
this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
// Draws a 5*5 alignment pattern, with the center module
|
|
3984
|
+
// at (x, y). All modules must be in bounds.
|
|
3985
|
+
drawAlignmentPattern(x, y) {
|
|
3986
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
3987
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
3988
|
+
this.setFunctionModule(
|
|
3989
|
+
x + dx,
|
|
3990
|
+
y + dy,
|
|
3991
|
+
Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
|
|
3992
|
+
QrCodeDataType.Alignment
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
// Sets the color of a module and marks it as a function module.
|
|
3998
|
+
// Only used by the constructor. Coordinates must be in bounds.
|
|
3999
|
+
setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
|
|
4000
|
+
this.modules[y][x] = isDark;
|
|
4001
|
+
this.types[y][x] = type;
|
|
4002
|
+
}
|
|
4003
|
+
/* -- Private helper methods for constructor: Codewords and masking -- */
|
|
4004
|
+
// Returns a new byte string representing the given data with the appropriate error correction
|
|
4005
|
+
// codewords appended to it, based on this object's version and error correction level.
|
|
4006
|
+
addEccAndInterleave(data) {
|
|
4007
|
+
const ver = this.version;
|
|
4008
|
+
const ecl = this.ecc;
|
|
4009
|
+
if (data.length !== getNumDataCodewords(ver, ecl))
|
|
4010
|
+
throw new RangeError("Invalid argument");
|
|
4011
|
+
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
4012
|
+
const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
|
|
4013
|
+
const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
|
|
4014
|
+
const numShortBlocks = numBlocks - rawCodewords % numBlocks;
|
|
4015
|
+
const shortBlockLen = Math.floor(rawCodewords / numBlocks);
|
|
4016
|
+
const blocks = [];
|
|
4017
|
+
const rsDiv = reedSolomonComputeDivisor(blockEccLen);
|
|
4018
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
4019
|
+
const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
|
|
4020
|
+
k += dat.length;
|
|
4021
|
+
const ecc = reedSolomonComputeRemainder(dat, rsDiv);
|
|
4022
|
+
if (i < numShortBlocks)
|
|
4023
|
+
dat.push(0);
|
|
4024
|
+
blocks.push(dat.concat(ecc));
|
|
4025
|
+
}
|
|
4026
|
+
const result = [];
|
|
4027
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
4028
|
+
blocks.forEach((block, j) => {
|
|
4029
|
+
if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
|
|
4030
|
+
result.push(block[i]);
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
return result;
|
|
4034
|
+
}
|
|
4035
|
+
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
|
|
4036
|
+
// data area of this QR Code. Function modules need to be marked off before this is called.
|
|
4037
|
+
drawCodewords(data) {
|
|
4038
|
+
if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
|
|
4039
|
+
throw new RangeError("Invalid argument");
|
|
4040
|
+
let i = 0;
|
|
4041
|
+
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
4042
|
+
if (right === 6)
|
|
4043
|
+
right = 5;
|
|
4044
|
+
for (let vert = 0; vert < this.size; vert++) {
|
|
4045
|
+
for (let j = 0; j < 2; j++) {
|
|
4046
|
+
const x = right - j;
|
|
4047
|
+
const upward = (right + 1 & 2) === 0;
|
|
4048
|
+
const y = upward ? this.size - 1 - vert : vert;
|
|
4049
|
+
if (!this.types[y][x] && i < data.length * 8) {
|
|
4050
|
+
this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
|
|
4051
|
+
i++;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
// XORs the codeword modules in this QR Code with the given mask pattern.
|
|
4058
|
+
// The function modules must be marked and the codeword bits must be drawn
|
|
4059
|
+
// before masking. Due to the arithmetic of XOR, calling applyMask() with
|
|
4060
|
+
// the same mask value a second time will undo the mask. A final well-formed
|
|
4061
|
+
// QR Code needs exactly one (not zero, two, etc.) mask applied.
|
|
4062
|
+
applyMask(mask) {
|
|
4063
|
+
if (mask < 0 || mask > 7)
|
|
4064
|
+
throw new RangeError("Mask value out of range");
|
|
4065
|
+
for (let y = 0; y < this.size; y++) {
|
|
4066
|
+
for (let x = 0; x < this.size; x++) {
|
|
4067
|
+
let invert;
|
|
4068
|
+
switch (mask) {
|
|
4069
|
+
case 0:
|
|
4070
|
+
invert = (x + y) % 2 === 0;
|
|
4071
|
+
break;
|
|
4072
|
+
case 1:
|
|
4073
|
+
invert = y % 2 === 0;
|
|
4074
|
+
break;
|
|
4075
|
+
case 2:
|
|
4076
|
+
invert = x % 3 === 0;
|
|
4077
|
+
break;
|
|
4078
|
+
case 3:
|
|
4079
|
+
invert = (x + y) % 3 === 0;
|
|
4080
|
+
break;
|
|
4081
|
+
case 4:
|
|
4082
|
+
invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
|
|
4083
|
+
break;
|
|
4084
|
+
case 5:
|
|
4085
|
+
invert = x * y % 2 + x * y % 3 === 0;
|
|
4086
|
+
break;
|
|
4087
|
+
case 6:
|
|
4088
|
+
invert = (x * y % 2 + x * y % 3) % 2 === 0;
|
|
4089
|
+
break;
|
|
4090
|
+
case 7:
|
|
4091
|
+
invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
|
|
4092
|
+
break;
|
|
4093
|
+
default:
|
|
4094
|
+
throw new Error("Unreachable");
|
|
4095
|
+
}
|
|
4096
|
+
if (!this.types[y][x] && invert)
|
|
4097
|
+
this.modules[y][x] = !this.modules[y][x];
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
// Calculates and returns the penalty score based on state of this QR Code's current modules.
|
|
4102
|
+
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
|
|
4103
|
+
getPenaltyScore() {
|
|
4104
|
+
let result = 0;
|
|
4105
|
+
for (let y = 0; y < this.size; y++) {
|
|
4106
|
+
let runColor = false;
|
|
4107
|
+
let runX = 0;
|
|
4108
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
4109
|
+
for (let x = 0; x < this.size; x++) {
|
|
4110
|
+
if (this.modules[y][x] === runColor) {
|
|
4111
|
+
runX++;
|
|
4112
|
+
if (runX === 5)
|
|
4113
|
+
result += PENALTY_N1;
|
|
4114
|
+
else if (runX > 5)
|
|
4115
|
+
result++;
|
|
4116
|
+
} else {
|
|
4117
|
+
this.finderPenaltyAddHistory(runX, runHistory);
|
|
4118
|
+
if (!runColor)
|
|
4119
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
4120
|
+
runColor = this.modules[y][x];
|
|
4121
|
+
runX = 1;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
|
|
4125
|
+
}
|
|
4126
|
+
for (let x = 0; x < this.size; x++) {
|
|
4127
|
+
let runColor = false;
|
|
4128
|
+
let runY = 0;
|
|
4129
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
4130
|
+
for (let y = 0; y < this.size; y++) {
|
|
4131
|
+
if (this.modules[y][x] === runColor) {
|
|
4132
|
+
runY++;
|
|
4133
|
+
if (runY === 5)
|
|
4134
|
+
result += PENALTY_N1;
|
|
4135
|
+
else if (runY > 5)
|
|
4136
|
+
result++;
|
|
4137
|
+
} else {
|
|
4138
|
+
this.finderPenaltyAddHistory(runY, runHistory);
|
|
4139
|
+
if (!runColor)
|
|
4140
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
4141
|
+
runColor = this.modules[y][x];
|
|
4142
|
+
runY = 1;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
|
|
4146
|
+
}
|
|
4147
|
+
for (let y = 0; y < this.size - 1; y++) {
|
|
4148
|
+
for (let x = 0; x < this.size - 1; x++) {
|
|
4149
|
+
const color = this.modules[y][x];
|
|
4150
|
+
if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
|
|
4151
|
+
result += PENALTY_N2;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
let dark = 0;
|
|
4156
|
+
for (const row of this.modules)
|
|
4157
|
+
dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
|
|
4158
|
+
const total = this.size * this.size;
|
|
4159
|
+
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
|
4160
|
+
result += k * PENALTY_N4;
|
|
4161
|
+
return result;
|
|
4162
|
+
}
|
|
4163
|
+
/* -- Private helper functions -- */
|
|
4164
|
+
// Returns an ascending list of positions of alignment patterns for this version number.
|
|
4165
|
+
// Each position is in the range [0,177), and are used on both the x and y axes.
|
|
4166
|
+
// This could be implemented as lookup table of 40 variable-length lists of integers.
|
|
4167
|
+
getAlignmentPatternPositions() {
|
|
4168
|
+
if (this.version === 1) {
|
|
4169
|
+
return [];
|
|
4170
|
+
} else {
|
|
4171
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
4172
|
+
const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
|
|
4173
|
+
const result = [6];
|
|
4174
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
|
|
4175
|
+
result.splice(1, 0, pos);
|
|
4176
|
+
return result;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
// Can only be called immediately after a light run is added, and
|
|
4180
|
+
// returns either 0, 1, or 2. A helper function for getPenaltyScore().
|
|
4181
|
+
finderPenaltyCountPatterns(runHistory) {
|
|
4182
|
+
const n = runHistory[1];
|
|
4183
|
+
const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
|
|
4184
|
+
return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
|
|
4185
|
+
}
|
|
4186
|
+
// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
|
|
4187
|
+
finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
|
|
4188
|
+
if (currentRunColor) {
|
|
4189
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
4190
|
+
currentRunLength = 0;
|
|
4191
|
+
}
|
|
4192
|
+
currentRunLength += this.size;
|
|
4193
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
4194
|
+
return this.finderPenaltyCountPatterns(runHistory);
|
|
4195
|
+
}
|
|
4196
|
+
// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
|
|
4197
|
+
finderPenaltyAddHistory(currentRunLength, runHistory) {
|
|
4198
|
+
if (runHistory[0] === 0)
|
|
4199
|
+
currentRunLength += this.size;
|
|
4200
|
+
runHistory.pop();
|
|
4201
|
+
runHistory.unshift(currentRunLength);
|
|
4202
|
+
}
|
|
4203
|
+
};
|
|
4204
|
+
function appendBits(val, len, bb) {
|
|
4205
|
+
if (len < 0 || len > 31 || val >>> len !== 0)
|
|
4206
|
+
throw new RangeError("Value out of range");
|
|
4207
|
+
for (let i = len - 1; i >= 0; i--)
|
|
4208
|
+
bb.push(val >>> i & 1);
|
|
4209
|
+
}
|
|
4210
|
+
function getBit(x, i) {
|
|
4211
|
+
return (x >>> i & 1) !== 0;
|
|
4212
|
+
}
|
|
4213
|
+
var QrSegment = class {
|
|
4214
|
+
// Creates a new QR Code segment with the given attributes and data.
|
|
4215
|
+
// The character count (numChars) must agree with the mode and the bit buffer length,
|
|
4216
|
+
// but the constraint isn't checked. The given bit buffer is cloned and stored.
|
|
4217
|
+
constructor(mode, numChars, bitData) {
|
|
4218
|
+
this.mode = mode;
|
|
4219
|
+
this.numChars = numChars;
|
|
4220
|
+
this.bitData = bitData;
|
|
4221
|
+
if (numChars < 0)
|
|
4222
|
+
throw new RangeError("Invalid argument");
|
|
4223
|
+
this.bitData = bitData.slice();
|
|
4224
|
+
}
|
|
4225
|
+
/* -- Methods -- */
|
|
4226
|
+
// Returns a new copy of the data bits of this segment.
|
|
4227
|
+
getData() {
|
|
4228
|
+
return this.bitData.slice();
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
var MODE_NUMERIC = [1, 10, 12, 14];
|
|
4232
|
+
var MODE_ALPHANUMERIC = [2, 9, 11, 13];
|
|
4233
|
+
var MODE_BYTE = [4, 8, 16, 16];
|
|
4234
|
+
function numCharCountBits(mode, ver) {
|
|
4235
|
+
return mode[Math.floor((ver + 7) / 17) + 1];
|
|
4236
|
+
}
|
|
4237
|
+
function makeBytes(data) {
|
|
4238
|
+
const bb = [];
|
|
4239
|
+
for (const b of data)
|
|
4240
|
+
appendBits(b, 8, bb);
|
|
4241
|
+
return new QrSegment(MODE_BYTE, data.length, bb);
|
|
4242
|
+
}
|
|
4243
|
+
function makeNumeric(digits) {
|
|
4244
|
+
if (!isNumeric(digits))
|
|
4245
|
+
throw new RangeError("String contains non-numeric characters");
|
|
4246
|
+
const bb = [];
|
|
4247
|
+
for (let i = 0; i < digits.length; ) {
|
|
4248
|
+
const n = Math.min(digits.length - i, 3);
|
|
4249
|
+
appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
|
|
4250
|
+
i += n;
|
|
4251
|
+
}
|
|
4252
|
+
return new QrSegment(MODE_NUMERIC, digits.length, bb);
|
|
4253
|
+
}
|
|
4254
|
+
function makeAlphanumeric(text) {
|
|
4255
|
+
if (!isAlphanumeric(text))
|
|
4256
|
+
throw new RangeError("String contains unencodable characters in alphanumeric mode");
|
|
4257
|
+
const bb = [];
|
|
4258
|
+
let i;
|
|
4259
|
+
for (i = 0; i + 2 <= text.length; i += 2) {
|
|
4260
|
+
let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
|
|
4261
|
+
temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
|
|
4262
|
+
appendBits(temp, 11, bb);
|
|
4263
|
+
}
|
|
4264
|
+
if (i < text.length)
|
|
4265
|
+
appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
|
|
4266
|
+
return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
|
|
4267
|
+
}
|
|
4268
|
+
function makeSegments(text) {
|
|
4269
|
+
if (text === "")
|
|
4270
|
+
return [];
|
|
4271
|
+
else if (isNumeric(text))
|
|
4272
|
+
return [makeNumeric(text)];
|
|
4273
|
+
else if (isAlphanumeric(text))
|
|
4274
|
+
return [makeAlphanumeric(text)];
|
|
4275
|
+
else
|
|
4276
|
+
return [makeBytes(toUtf8ByteArray(text))];
|
|
4277
|
+
}
|
|
4278
|
+
function isNumeric(text) {
|
|
4279
|
+
return NUMERIC_REGEX.test(text);
|
|
4280
|
+
}
|
|
4281
|
+
function isAlphanumeric(text) {
|
|
4282
|
+
return ALPHANUMERIC_REGEX.test(text);
|
|
4283
|
+
}
|
|
4284
|
+
function getTotalBits(segs, version) {
|
|
4285
|
+
let result = 0;
|
|
4286
|
+
for (const seg of segs) {
|
|
4287
|
+
const ccbits = numCharCountBits(seg.mode, version);
|
|
4288
|
+
if (seg.numChars >= 1 << ccbits)
|
|
4289
|
+
return Number.POSITIVE_INFINITY;
|
|
4290
|
+
result += 4 + ccbits + seg.bitData.length;
|
|
4291
|
+
}
|
|
4292
|
+
return result;
|
|
4293
|
+
}
|
|
4294
|
+
function toUtf8ByteArray(str) {
|
|
4295
|
+
str = encodeURI(str);
|
|
4296
|
+
const result = [];
|
|
4297
|
+
for (let i = 0; i < str.length; i++) {
|
|
4298
|
+
if (str.charAt(i) !== "%") {
|
|
4299
|
+
result.push(str.charCodeAt(i));
|
|
4300
|
+
} else {
|
|
4301
|
+
result.push(Number.parseInt(str.substring(i + 1, i + 3), 16));
|
|
4302
|
+
i += 2;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
return result;
|
|
4306
|
+
}
|
|
4307
|
+
function getNumRawDataModules(ver) {
|
|
4308
|
+
if (ver < MIN_VERSION || ver > MAX_VERSION)
|
|
4309
|
+
throw new RangeError("Version number out of range");
|
|
4310
|
+
let result = (16 * ver + 128) * ver + 64;
|
|
4311
|
+
if (ver >= 2) {
|
|
4312
|
+
const numAlign = Math.floor(ver / 7) + 2;
|
|
4313
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
4314
|
+
if (ver >= 7)
|
|
4315
|
+
result -= 36;
|
|
4316
|
+
}
|
|
4317
|
+
return result;
|
|
4318
|
+
}
|
|
4319
|
+
function getNumDataCodewords(ver, ecl) {
|
|
4320
|
+
return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
4321
|
+
}
|
|
4322
|
+
function reedSolomonComputeDivisor(degree) {
|
|
4323
|
+
if (degree < 1 || degree > 255)
|
|
4324
|
+
throw new RangeError("Degree out of range");
|
|
4325
|
+
const result = [];
|
|
4326
|
+
for (let i = 0; i < degree - 1; i++)
|
|
4327
|
+
result.push(0);
|
|
4328
|
+
result.push(1);
|
|
4329
|
+
let root = 1;
|
|
4330
|
+
for (let i = 0; i < degree; i++) {
|
|
4331
|
+
for (let j = 0; j < result.length; j++) {
|
|
4332
|
+
result[j] = reedSolomonMultiply(result[j], root);
|
|
4333
|
+
if (j + 1 < result.length)
|
|
4334
|
+
result[j] ^= result[j + 1];
|
|
4335
|
+
}
|
|
4336
|
+
root = reedSolomonMultiply(root, 2);
|
|
4337
|
+
}
|
|
4338
|
+
return result;
|
|
4339
|
+
}
|
|
4340
|
+
function reedSolomonComputeRemainder(data, divisor) {
|
|
4341
|
+
const result = divisor.map((_) => 0);
|
|
4342
|
+
for (const b of data) {
|
|
4343
|
+
const factor = b ^ result.shift();
|
|
4344
|
+
result.push(0);
|
|
4345
|
+
divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
|
|
4346
|
+
}
|
|
4347
|
+
return result;
|
|
4348
|
+
}
|
|
4349
|
+
function reedSolomonMultiply(x, y) {
|
|
4350
|
+
if (x >>> 8 !== 0 || y >>> 8 !== 0)
|
|
4351
|
+
throw new RangeError("Byte out of range");
|
|
4352
|
+
let z = 0;
|
|
4353
|
+
for (let i = 7; i >= 0; i--) {
|
|
4354
|
+
z = z << 1 ^ (z >>> 7) * 285;
|
|
4355
|
+
z ^= (y >>> i & 1) * x;
|
|
4356
|
+
}
|
|
4357
|
+
return z;
|
|
4358
|
+
}
|
|
4359
|
+
function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
|
|
4360
|
+
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
|
|
4361
|
+
throw new RangeError("Invalid value");
|
|
4362
|
+
}
|
|
4363
|
+
let version;
|
|
4364
|
+
let dataUsedBits;
|
|
4365
|
+
for (version = minVersion; ; version++) {
|
|
4366
|
+
const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
|
|
4367
|
+
const usedBits = getTotalBits(segs, version);
|
|
4368
|
+
if (usedBits <= dataCapacityBits2) {
|
|
4369
|
+
dataUsedBits = usedBits;
|
|
4370
|
+
break;
|
|
4371
|
+
}
|
|
4372
|
+
if (version >= maxVersion)
|
|
4373
|
+
throw new RangeError("Data too long");
|
|
4374
|
+
}
|
|
4375
|
+
for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
|
|
4376
|
+
if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
|
|
4377
|
+
ecl = newEcl;
|
|
4378
|
+
}
|
|
4379
|
+
const bb = [];
|
|
4380
|
+
for (const seg of segs) {
|
|
4381
|
+
appendBits(seg.mode[0], 4, bb);
|
|
4382
|
+
appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
|
|
4383
|
+
for (const b of seg.getData())
|
|
4384
|
+
bb.push(b);
|
|
4385
|
+
}
|
|
4386
|
+
const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
|
|
4387
|
+
appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
|
|
4388
|
+
appendBits(0, (8 - bb.length % 8) % 8, bb);
|
|
4389
|
+
for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
|
|
4390
|
+
appendBits(padByte, 8, bb);
|
|
4391
|
+
const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
|
|
4392
|
+
bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
|
|
4393
|
+
return new QrCode(version, ecl, dataCodewords, mask);
|
|
4394
|
+
}
|
|
4395
|
+
function encode(data, options) {
|
|
4396
|
+
const {
|
|
4397
|
+
ecc = "L",
|
|
4398
|
+
boostEcc = false,
|
|
4399
|
+
minVersion = 1,
|
|
4400
|
+
maxVersion = 40,
|
|
4401
|
+
maskPattern = -1,
|
|
4402
|
+
border = 1
|
|
4403
|
+
} = options || {};
|
|
4404
|
+
const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
|
|
4405
|
+
if (!segment)
|
|
4406
|
+
throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
|
|
4407
|
+
const qr = encodeSegments(
|
|
4408
|
+
segment,
|
|
4409
|
+
EccMap[ecc],
|
|
4410
|
+
minVersion,
|
|
4411
|
+
maxVersion,
|
|
4412
|
+
maskPattern,
|
|
4413
|
+
boostEcc
|
|
4414
|
+
);
|
|
4415
|
+
const result = addBorder({
|
|
4416
|
+
version: qr.version,
|
|
4417
|
+
maskPattern: qr.mask,
|
|
4418
|
+
size: qr.size,
|
|
4419
|
+
data: qr.modules,
|
|
4420
|
+
types: qr.types
|
|
4421
|
+
}, border);
|
|
4422
|
+
if (options?.invert)
|
|
4423
|
+
result.data = result.data.map((row) => row.map((mod) => !mod));
|
|
4424
|
+
options?.onEncoded?.(result);
|
|
4425
|
+
return result;
|
|
4426
|
+
}
|
|
4427
|
+
function addBorder(input, border = 1) {
|
|
4428
|
+
if (!border)
|
|
4429
|
+
return input;
|
|
4430
|
+
const { size } = input;
|
|
4431
|
+
const newSize = size + border * 2;
|
|
4432
|
+
input.size = newSize;
|
|
4433
|
+
input.data.forEach((row) => {
|
|
4434
|
+
for (let i = 0; i < border; i++) {
|
|
4435
|
+
row.unshift(false);
|
|
4436
|
+
row.push(false);
|
|
4437
|
+
}
|
|
4438
|
+
});
|
|
4439
|
+
for (let i = 0; i < border; i++) {
|
|
4440
|
+
input.data.unshift(Array.from({ length: newSize }, (_) => false));
|
|
4441
|
+
input.data.push(Array.from({ length: newSize }, (_) => false));
|
|
4442
|
+
}
|
|
4443
|
+
const b = QrCodeDataType.Border;
|
|
4444
|
+
input.types.forEach((row) => {
|
|
4445
|
+
for (let i = 0; i < border; i++) {
|
|
4446
|
+
row.unshift(b);
|
|
4447
|
+
row.push(b);
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
for (let i = 0; i < border; i++) {
|
|
4451
|
+
input.types.unshift(Array.from({ length: newSize }, (_) => b));
|
|
4452
|
+
input.types.push(Array.from({ length: newSize }, (_) => b));
|
|
4453
|
+
}
|
|
4454
|
+
return input;
|
|
4455
|
+
}
|
|
4456
|
+
function getDataAt(data, x, y, defaults = false) {
|
|
4457
|
+
if (x < 0 || y < 0 || x >= data.length || y >= data.length)
|
|
4458
|
+
return defaults;
|
|
4459
|
+
return data[y][x];
|
|
4460
|
+
}
|
|
4461
|
+
function renderUnicodeCompact(data, options = {}) {
|
|
4462
|
+
const palette = {
|
|
4463
|
+
WHITE_ALL: "\u2588",
|
|
4464
|
+
WHITE_BLACK: "\u2580",
|
|
4465
|
+
BLACK_WHITE: "\u2584",
|
|
4466
|
+
BLACK_ALL: " "
|
|
4467
|
+
};
|
|
4468
|
+
const result = encode(data, options);
|
|
4469
|
+
const WHITE = false;
|
|
4470
|
+
const BLACK = true;
|
|
4471
|
+
const at = (x, y) => getDataAt(result.data, x, y, true);
|
|
4472
|
+
const lines = [];
|
|
4473
|
+
let line = "";
|
|
4474
|
+
for (let row = 0; row < result.size; row += 2) {
|
|
4475
|
+
for (let col = 0; col < result.size; col++) {
|
|
4476
|
+
if (at(col, row) === WHITE && at(col, row + 1) === WHITE)
|
|
4477
|
+
line += palette.WHITE_ALL;
|
|
4478
|
+
else if (at(col, row) === WHITE && at(col, row + 1) === BLACK)
|
|
4479
|
+
line += palette.WHITE_BLACK;
|
|
4480
|
+
else if (at(col, row) === BLACK && at(col, row + 1) === WHITE)
|
|
4481
|
+
line += palette.BLACK_WHITE;
|
|
4482
|
+
else
|
|
4483
|
+
line += palette.BLACK_ALL;
|
|
4484
|
+
}
|
|
4485
|
+
lines.push(line);
|
|
4486
|
+
line = "";
|
|
4487
|
+
}
|
|
4488
|
+
return lines.join("\n");
|
|
4489
|
+
}
|
|
4490
|
+
|
|
3800
4491
|
// ../tutticode-protocol/src/messages.ts
|
|
3801
4492
|
var BRIDGE_PROTOCOL = 1;
|
|
3802
4493
|
var BRIDGE_PATH = "/bridge";
|
|
@@ -3884,6 +4575,42 @@ function isFrameHeader(value) {
|
|
|
3884
4575
|
return typeof v.id === "string" && typeof v.part === "string" && Number.isInteger(v.seq) && v.seq >= 0 && typeof v.last === "boolean";
|
|
3885
4576
|
}
|
|
3886
4577
|
|
|
4578
|
+
// ../tutticode-protocol/src/auth.ts
|
|
4579
|
+
var TUTTICODE_SCOPE = "tutticode";
|
|
4580
|
+
var TUTTICODE_RESOURCE_PATH = "/api/tutticode";
|
|
4581
|
+
function tutticodeResourceUrl(origin) {
|
|
4582
|
+
return `${origin}${TUTTICODE_RESOURCE_PATH}`;
|
|
4583
|
+
}
|
|
4584
|
+
var TUTTICODE_CLIENT_ID = "tutticode-cli";
|
|
4585
|
+
var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
4586
|
+
var TUTTICODE_OAUTH_PATHS = {
|
|
4587
|
+
token: "/api/oauth/token",
|
|
4588
|
+
revoke: "/api/oauth/revoke",
|
|
4589
|
+
deviceAuthorization: "/api/oauth/device_authorization",
|
|
4590
|
+
/** Browser pages (lane B). */
|
|
4591
|
+
login: "/code/login",
|
|
4592
|
+
activate: "/code/activate",
|
|
4593
|
+
/** The terms of use page (the site has no `/terms`; it redirects here). */
|
|
4594
|
+
terms: "/page/terms-of-use"
|
|
4595
|
+
};
|
|
4596
|
+
function tutticodeTermsUrl(origin) {
|
|
4597
|
+
return `${origin}${TUTTICODE_OAUTH_PATHS.terms}`;
|
|
4598
|
+
}
|
|
4599
|
+
var TUTTICODE_LOGIN_HINT_COMMAND = "tutticode login --device --no-wait";
|
|
4600
|
+
var DEFAULT_TUTTICODE_LIMITS = {
|
|
4601
|
+
enforce: false,
|
|
4602
|
+
anonMaxSeconds: 60,
|
|
4603
|
+
anonWatermarkAfterSeconds: 30,
|
|
4604
|
+
watermarkGapSeconds: 3,
|
|
4605
|
+
signedInMaxSeconds: 300,
|
|
4606
|
+
signedInDailyRenders: 1e3,
|
|
4607
|
+
minCliVersion: "0.2.0"
|
|
4608
|
+
};
|
|
4609
|
+
var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,32})?$/;
|
|
4610
|
+
function isCliVersion(v) {
|
|
4611
|
+
return typeof v === "string" && VERSION.test(v);
|
|
4612
|
+
}
|
|
4613
|
+
|
|
3887
4614
|
// ../tutticode-protocol/src/validate.ts
|
|
3888
4615
|
var BRIDGE_MAX_CODE_CHARS = 2e5;
|
|
3889
4616
|
var FORMATS = ["wav", "flac", "mp3"];
|
|
@@ -3932,11 +4659,28 @@ function validRender(m) {
|
|
|
3932
4659
|
return "lufs must be -40..-5";
|
|
3933
4660
|
if (!optional(m.truePeak, (v) => finite(v) && v >= -12 && v <= 0))
|
|
3934
4661
|
return "truePeak must be -12..0";
|
|
3935
|
-
for (const key of ["stems", "analyze", "limit"])
|
|
4662
|
+
for (const key of ["stems", "analyze", "limit", "brandTag"])
|
|
3936
4663
|
if (!optional(m[key], (v) => typeof v === "boolean"))
|
|
3937
4664
|
return `${key} must be a boolean`;
|
|
3938
4665
|
return void 0;
|
|
3939
4666
|
}
|
|
4667
|
+
var MAX_TOKEN_CHARS = 4096;
|
|
4668
|
+
var TOKEN = /^[A-Za-z0-9._~+/=-]+$/;
|
|
4669
|
+
var INSTALL_ID = /^[A-Za-z0-9_-]{16,64}$/;
|
|
4670
|
+
function validAuth(m) {
|
|
4671
|
+
if (!optional(
|
|
4672
|
+
m.accessToken,
|
|
4673
|
+
(v) => typeof v === "string" && v.length > 0 && v.length <= MAX_TOKEN_CHARS && TOKEN.test(v)
|
|
4674
|
+
))
|
|
4675
|
+
return "auth.accessToken must be a token string";
|
|
4676
|
+
if (typeof m.installId !== "string" || !INSTALL_ID.test(m.installId))
|
|
4677
|
+
return "auth.installId must be 16..64 characters of [A-Za-z0-9_-]";
|
|
4678
|
+
if (!isCliVersion(m.cliVersion))
|
|
4679
|
+
return "auth.cliVersion must be major.minor.patch";
|
|
4680
|
+
if (m.client !== "cli" && m.client !== "mcp")
|
|
4681
|
+
return "auth.client must be cli or mcp";
|
|
4682
|
+
return void 0;
|
|
4683
|
+
}
|
|
3940
4684
|
function validateCommand(raw) {
|
|
3941
4685
|
if (!isObject(raw))
|
|
3942
4686
|
return { ok: false, reason: "not an object" };
|
|
@@ -3955,6 +4699,19 @@ function validateCommand(raw) {
|
|
|
3955
4699
|
return validCode(raw.code) ? { ok: true, value: raw } : { ok: false, reason: "open needs code" };
|
|
3956
4700
|
case "cancel":
|
|
3957
4701
|
return isId(raw.id) ? { ok: true, value: raw } : { ok: false, reason: "cancel needs an id" };
|
|
4702
|
+
case "auth": {
|
|
4703
|
+
const reason = validAuth(raw);
|
|
4704
|
+
if (reason)
|
|
4705
|
+
return { ok: false, reason };
|
|
4706
|
+
const auth = {
|
|
4707
|
+
type: "auth",
|
|
4708
|
+
...raw.accessToken !== void 0 ? { accessToken: raw.accessToken } : {},
|
|
4709
|
+
installId: raw.installId,
|
|
4710
|
+
cliVersion: raw.cliVersion,
|
|
4711
|
+
client: raw.client
|
|
4712
|
+
};
|
|
4713
|
+
return { ok: true, value: auth };
|
|
4714
|
+
}
|
|
3958
4715
|
default:
|
|
3959
4716
|
return { ok: false, reason: `unknown command type ${String(raw.type)}` };
|
|
3960
4717
|
}
|
|
@@ -3971,6 +4728,8 @@ function validateMessage(raw) {
|
|
|
3971
4728
|
if (raw.type === "error") {
|
|
3972
4729
|
if (typeof raw.code !== "string" || typeof raw.message !== "string")
|
|
3973
4730
|
return { ok: false, reason: "error needs code and message" };
|
|
4731
|
+
if (!optional(raw.limit, validLimit))
|
|
4732
|
+
return { ok: false, reason: "error.limit must be {name, value, signedInValue}" };
|
|
3974
4733
|
return { ok: true, value: raw };
|
|
3975
4734
|
}
|
|
3976
4735
|
if (!isId(raw.id))
|
|
@@ -3983,6 +4742,9 @@ function validateMessage(raw) {
|
|
|
3983
4742
|
return { ok: false, reason: "rendered needs meta" };
|
|
3984
4743
|
return { ok: true, value: raw };
|
|
3985
4744
|
}
|
|
4745
|
+
function validLimit(v) {
|
|
4746
|
+
return isObject(v) && v.name === "length" && inRange(v.value, 0, MAX_RENDER_SECONDS) && inRange(v.signedInValue, 0, MAX_RENDER_SECONDS);
|
|
4747
|
+
}
|
|
3986
4748
|
function bridgePageUrl(origin, locale, port, token) {
|
|
3987
4749
|
return `${origin}/${locale}/code/bridge#p=${port}&t=${encodeURIComponent(token)}`;
|
|
3988
4750
|
}
|
|
@@ -4070,6 +4832,12 @@ function validateRenderMeta(raw, expect) {
|
|
|
4070
4832
|
return no("meta.analysis must be an object");
|
|
4071
4833
|
if (typeof raw.engineVersion !== "string" || raw.engineVersion.length > MAX_VERSION_CHARS)
|
|
4072
4834
|
return no("meta.engineVersion malformed");
|
|
4835
|
+
let watermark;
|
|
4836
|
+
if (raw.watermark !== void 0) {
|
|
4837
|
+
watermark = validWatermark(raw.watermark, raw.seconds);
|
|
4838
|
+
if (!watermark)
|
|
4839
|
+
return no("meta.watermark must be {reason, at: ascending seconds within the render, removeWith}");
|
|
4840
|
+
}
|
|
4073
4841
|
if (typeof raw.editUrl !== "string" || raw.editUrl.length > MAX_EDIT_URL_CHARS || !expect.origins.includes(urlOrigin(raw.editUrl) ?? ""))
|
|
4074
4842
|
return no("meta.editUrl must be a link on the TuttiDAW site");
|
|
4075
4843
|
return {
|
|
@@ -4092,10 +4860,28 @@ function validateRenderMeta(raw, expect) {
|
|
|
4092
4860
|
...raw.analysis !== void 0 ? { analysis: raw.analysis } : {},
|
|
4093
4861
|
check: raw.check,
|
|
4094
4862
|
editUrl: raw.editUrl,
|
|
4095
|
-
engineVersion: raw.engineVersion
|
|
4863
|
+
engineVersion: raw.engineVersion,
|
|
4864
|
+
...watermark ? { watermark } : {}
|
|
4096
4865
|
}
|
|
4097
4866
|
};
|
|
4098
4867
|
}
|
|
4868
|
+
var MAX_WATERMARKS = 200;
|
|
4869
|
+
function validWatermark(v, seconds) {
|
|
4870
|
+
if (!isObject(v) || v.reason !== "anonymous")
|
|
4871
|
+
return void 0;
|
|
4872
|
+
if (typeof v.removeWith !== "string" || !v.removeWith || v.removeWith.length > 64)
|
|
4873
|
+
return void 0;
|
|
4874
|
+
if (!Array.isArray(v.at) || v.at.length === 0 || v.at.length > MAX_WATERMARKS)
|
|
4875
|
+
return void 0;
|
|
4876
|
+
const at = v.at;
|
|
4877
|
+
for (let i = 0; i < at.length; i++) {
|
|
4878
|
+
if (!inRange(at[i], 0, seconds))
|
|
4879
|
+
return void 0;
|
|
4880
|
+
if (i > 0 && at[i] <= at[i - 1])
|
|
4881
|
+
return void 0;
|
|
4882
|
+
}
|
|
4883
|
+
return { reason: "anonymous", at: [...at], removeWith: v.removeWith };
|
|
4884
|
+
}
|
|
4099
4885
|
function urlOrigin(url) {
|
|
4100
4886
|
try {
|
|
4101
4887
|
const parsed = new URL(url);
|
|
@@ -4105,22 +4891,677 @@ function urlOrigin(url) {
|
|
|
4105
4891
|
}
|
|
4106
4892
|
}
|
|
4107
4893
|
|
|
4894
|
+
// src/credentials.ts
|
|
4895
|
+
var import_node_child_process = require("node:child_process");
|
|
4896
|
+
var import_node_crypto = require("node:crypto");
|
|
4897
|
+
var import_node_fs2 = require("node:fs");
|
|
4898
|
+
var import_node_os2 = require("node:os");
|
|
4899
|
+
var import_node_path2 = require("node:path");
|
|
4900
|
+
|
|
4901
|
+
// src/env.ts
|
|
4902
|
+
var import_node_fs = require("node:fs");
|
|
4903
|
+
var import_node_os = require("node:os");
|
|
4904
|
+
var import_node_path = require("node:path");
|
|
4905
|
+
var VERSION2 = true ? "0.2.0" : readPackageVersion();
|
|
4906
|
+
function envBrowserArgs() {
|
|
4907
|
+
const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
|
|
4908
|
+
return args?.length ? args : void 0;
|
|
4909
|
+
}
|
|
4910
|
+
function stateDir() {
|
|
4911
|
+
const dir = process.env.TUTTICODE_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".tutticode");
|
|
4912
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
4913
|
+
return dir;
|
|
4914
|
+
}
|
|
4915
|
+
var assetsDir;
|
|
4916
|
+
function skillsDir() {
|
|
4917
|
+
return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
|
|
4918
|
+
(0, import_node_path.join)(__dirname, "..", "..", "..", "public", "skills");
|
|
4919
|
+
}
|
|
4920
|
+
function setSkillsDir(dir) {
|
|
4921
|
+
assetsDir = dir;
|
|
4922
|
+
}
|
|
4923
|
+
var LOG_ROLL_BYTES = 2 * 1024 * 1024;
|
|
4924
|
+
function createLogger(name, verbose) {
|
|
4925
|
+
let file2;
|
|
4926
|
+
try {
|
|
4927
|
+
const dir = (0, import_node_path.join)(stateDir(), "logs");
|
|
4928
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
4929
|
+
file2 = (0, import_node_path.join)(dir, `${name}.log`);
|
|
4930
|
+
if (((0, import_node_fs.statSync)(file2, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
|
|
4931
|
+
(0, import_node_fs.renameSync)(file2, `${file2}.1`);
|
|
4932
|
+
} catch {
|
|
4933
|
+
file2 = void 0;
|
|
4934
|
+
}
|
|
4935
|
+
return (level2, message) => {
|
|
4936
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
|
|
4937
|
+
if (verbose || level2 === "error")
|
|
4938
|
+
process.stderr.write(`tutticode: ${message}
|
|
4939
|
+
`);
|
|
4940
|
+
if (!file2)
|
|
4941
|
+
return;
|
|
4942
|
+
try {
|
|
4943
|
+
(0, import_node_fs.appendFileSync)(file2, `${line}
|
|
4944
|
+
`);
|
|
4945
|
+
} catch {
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
// src/credentials.ts
|
|
4951
|
+
var CREDENTIALS_FILE = "credentials";
|
|
4952
|
+
function credentialsPath() {
|
|
4953
|
+
return (0, import_node_path2.join)(stateDir(), CREDENTIALS_FILE);
|
|
4954
|
+
}
|
|
4955
|
+
var EMPTY = { version: 1, accounts: {} };
|
|
4956
|
+
function readCredentials(path = credentialsPath()) {
|
|
4957
|
+
let raw;
|
|
4958
|
+
try {
|
|
4959
|
+
if ((0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
4960
|
+
return EMPTY;
|
|
4961
|
+
raw = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
4962
|
+
} catch {
|
|
4963
|
+
return EMPTY;
|
|
4964
|
+
}
|
|
4965
|
+
if (!isObject2(raw) || raw.version !== 1 || !isObject2(raw.accounts))
|
|
4966
|
+
return EMPTY;
|
|
4967
|
+
const accounts = {};
|
|
4968
|
+
for (const [origin, entry] of Object.entries(raw.accounts))
|
|
4969
|
+
if (validAccount(entry))
|
|
4970
|
+
accounts[origin] = entry;
|
|
4971
|
+
return { version: 1, accounts };
|
|
4972
|
+
}
|
|
4973
|
+
function readAccount(origin) {
|
|
4974
|
+
return readCredentials().accounts[origin];
|
|
4975
|
+
}
|
|
4976
|
+
function validAccount(v) {
|
|
4977
|
+
return isObject2(v) && v.kind === "login" && typeof v.accessToken === "string" && typeof v.refreshToken === "string" && typeof v.accessTokenExpiresAt === "string" && typeof v.clientId === "string" && typeof v.scope === "string";
|
|
4978
|
+
}
|
|
4979
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4980
|
+
function writeCredentials(file2) {
|
|
4981
|
+
const path = credentialsPath();
|
|
4982
|
+
if (!Object.keys(file2.accounts).length) {
|
|
4983
|
+
(0, import_node_fs2.rmSync)(path, { force: true });
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
writePrivateFile(path, `${JSON.stringify(file2, null, 2)}
|
|
4987
|
+
`);
|
|
4988
|
+
}
|
|
4989
|
+
function saveAccount(origin, account) {
|
|
4990
|
+
return withCredentialsLock(() => {
|
|
4991
|
+
const accounts = { ...readCredentials().accounts };
|
|
4992
|
+
if (account)
|
|
4993
|
+
accounts[origin] = account;
|
|
4994
|
+
else
|
|
4995
|
+
delete accounts[origin];
|
|
4996
|
+
writeCredentials({ version: 1, accounts });
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
var PrivateDirError = class extends Error {
|
|
5000
|
+
};
|
|
5001
|
+
var privateDirs = /* @__PURE__ */ new Map();
|
|
5002
|
+
var fileId = (st) => `${st.dev}:${st.ino}`;
|
|
5003
|
+
function refusal(dir, why) {
|
|
5004
|
+
return new PrivateDirError(
|
|
5005
|
+
`${dir} ${why}, so tutticode will not keep a sign-in there. Point TUTTICODE_HOME at a folder you own that only you can use (or unset it for ~/.tutticode).`
|
|
5006
|
+
);
|
|
5007
|
+
}
|
|
5008
|
+
function ensurePrivateDir(dir = stateDir()) {
|
|
5009
|
+
const known = privateDirs.get(dir);
|
|
5010
|
+
const now = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
|
|
5011
|
+
if (known && now && fileId(now) === known)
|
|
5012
|
+
return dir;
|
|
5013
|
+
privateDirs.set(dir, checkPrivateDir(dir, true));
|
|
5014
|
+
return dir;
|
|
5015
|
+
}
|
|
5016
|
+
function checkPrivateDir(dir, fix) {
|
|
5017
|
+
const st = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
|
|
5018
|
+
if (!st)
|
|
5019
|
+
throw refusal(dir, "does not exist");
|
|
5020
|
+
if (st.isSymbolicLink())
|
|
5021
|
+
throw refusal(dir, "is a symbolic link or a junction");
|
|
5022
|
+
if (!st.isDirectory())
|
|
5023
|
+
throw refusal(dir, "is not a folder");
|
|
5024
|
+
const id = process.platform === "win32" ? checkWindowsDir(dir, fix) : checkPosixDir(dir, fix);
|
|
5025
|
+
if (id !== fileId(st))
|
|
5026
|
+
throw refusal(dir, "was replaced while it was checked");
|
|
5027
|
+
return id;
|
|
5028
|
+
}
|
|
5029
|
+
function checkPosixDir(dir, fix) {
|
|
5030
|
+
let fd;
|
|
5031
|
+
try {
|
|
5032
|
+
fd = (0, import_node_fs2.openSync)(
|
|
5033
|
+
dir,
|
|
5034
|
+
import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_DIRECTORY | import_node_fs2.constants.O_NOFOLLOW
|
|
5035
|
+
);
|
|
5036
|
+
} catch {
|
|
5037
|
+
throw refusal(dir, "was replaced while it was checked");
|
|
5038
|
+
}
|
|
5039
|
+
try {
|
|
5040
|
+
let st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
|
|
5041
|
+
if (process.getuid && st.uid !== BigInt(process.getuid()))
|
|
5042
|
+
throw refusal(dir, "belongs to another user");
|
|
5043
|
+
if (fix && st.mode & BigInt(63)) {
|
|
5044
|
+
try {
|
|
5045
|
+
(0, import_node_fs2.fchmodSync)(fd, 448);
|
|
5046
|
+
} catch {
|
|
5047
|
+
throw refusal(dir, "cannot be made private (chmod 0700 failed)");
|
|
5048
|
+
}
|
|
5049
|
+
st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
|
|
5050
|
+
}
|
|
5051
|
+
if (st.mode & BigInt(63))
|
|
5052
|
+
throw refusal(dir, "is open to other users");
|
|
5053
|
+
return fileId(st);
|
|
5054
|
+
} finally {
|
|
5055
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
var WINDOWS_OWNERS = /* @__PURE__ */ new Set(["SY", "BA", "S-1-5-18", "S-1-5-32-544"]);
|
|
5059
|
+
function checkWindowsDir(dir, fix) {
|
|
5060
|
+
const sid = currentUserSid();
|
|
5061
|
+
const inspect = () => {
|
|
5062
|
+
const sec = windowsSecurity(dir);
|
|
5063
|
+
if (!sid || !sec)
|
|
5064
|
+
throw refusal(dir, "has an owner or access list tutticode cannot check");
|
|
5065
|
+
if (sec.reparse)
|
|
5066
|
+
throw refusal(dir, "is a symbolic link or a junction");
|
|
5067
|
+
if (sec.owner !== sid && !WINDOWS_OWNERS.has(sec.owner))
|
|
5068
|
+
throw refusal(dir, `belongs to another user (${sec.owner})`);
|
|
5069
|
+
return { access: daclAccess(sec.sddl, sid), protected: sec.protected };
|
|
5070
|
+
};
|
|
5071
|
+
let state = inspect();
|
|
5072
|
+
if (fix && (!state.access.ok || !state.protected)) {
|
|
5073
|
+
if (!restrictDirAcl(dir))
|
|
5074
|
+
throw refusal(dir, "could not be limited to your user (icacls failed)");
|
|
5075
|
+
state = inspect();
|
|
5076
|
+
}
|
|
5077
|
+
if (!state.access.ok)
|
|
5078
|
+
throw refusal(dir, `is ${state.access.detail}`);
|
|
5079
|
+
return fileId((0, import_node_fs2.lstatSync)(dir, { bigint: true }));
|
|
5080
|
+
}
|
|
5081
|
+
function writePrivateFile(path, text) {
|
|
5082
|
+
const dir = (0, import_node_path2.dirname)(path);
|
|
5083
|
+
ensurePrivateDir(dir);
|
|
5084
|
+
const checked = privateDirs.get(dir);
|
|
5085
|
+
const target = (0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false });
|
|
5086
|
+
if (target && !target.isFile())
|
|
5087
|
+
throw new PrivateDirError(
|
|
5088
|
+
`${path} is not a plain file (a link, a junction or a folder), so tutticode will not write the sign-in through it. Remove it and try again.`
|
|
5089
|
+
);
|
|
5090
|
+
const changed = () => new PrivateDirError(
|
|
5091
|
+
`${dir} changed while the sign-in was being written (replaced, or its owner or access list changed); nothing was written.`
|
|
5092
|
+
);
|
|
5093
|
+
const tmp = `${path}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
|
|
5094
|
+
const tmpId = () => {
|
|
5095
|
+
const st = (0, import_node_fs2.lstatSync)(tmp, { bigint: true, throwIfNoEntry: false });
|
|
5096
|
+
return st && fileId(st);
|
|
5097
|
+
};
|
|
5098
|
+
const fd = (0, import_node_fs2.openSync)(
|
|
5099
|
+
tmp,
|
|
5100
|
+
import_node_fs2.constants.O_WRONLY | import_node_fs2.constants.O_CREAT | import_node_fs2.constants.O_EXCL | (import_node_fs2.constants.O_NOFOLLOW ?? 0),
|
|
5101
|
+
384
|
|
5102
|
+
);
|
|
5103
|
+
let open2 = true;
|
|
5104
|
+
try {
|
|
5105
|
+
const written = fileId((0, import_node_fs2.fstatSync)(fd, { bigint: true }));
|
|
5106
|
+
if (tmpId() !== written)
|
|
5107
|
+
throw changed();
|
|
5108
|
+
(0, import_node_fs2.writeFileSync)(fd, text);
|
|
5109
|
+
(0, import_node_fs2.fsyncSync)(fd);
|
|
5110
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5111
|
+
open2 = false;
|
|
5112
|
+
if (checkPrivateDir(dir, false) !== checked || tmpId() !== written)
|
|
5113
|
+
throw changed();
|
|
5114
|
+
if (process.platform === "win32" && !checkFileAccess(tmp)?.ok)
|
|
5115
|
+
throw new PrivateDirError(
|
|
5116
|
+
`Could not confirm that ${tmp} is private to your user; nothing was written.`
|
|
5117
|
+
);
|
|
5118
|
+
renameWithRetry(tmp, path);
|
|
5119
|
+
} catch (e) {
|
|
5120
|
+
if (open2)
|
|
5121
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5122
|
+
(0, import_node_fs2.rmSync)(tmp, { force: true });
|
|
5123
|
+
throw e;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function renameWithRetry(from, to) {
|
|
5127
|
+
for (let attempt = 0; ; attempt++) {
|
|
5128
|
+
try {
|
|
5129
|
+
(0, import_node_fs2.renameSync)(from, to);
|
|
5130
|
+
return;
|
|
5131
|
+
} catch (e) {
|
|
5132
|
+
const code = e.code;
|
|
5133
|
+
if (attempt >= 20 || code !== "EPERM" && code !== "EBUSY")
|
|
5134
|
+
throw e;
|
|
5135
|
+
sleepSync(25);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
function sleepSync(ms) {
|
|
5140
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5141
|
+
}
|
|
5142
|
+
var system32 = (exe) => (0, import_node_path2.join)(process.env.SystemRoot ?? "C:\\Windows", "System32", exe);
|
|
5143
|
+
var userSid;
|
|
5144
|
+
function currentUserSid() {
|
|
5145
|
+
if (userSid !== void 0)
|
|
5146
|
+
return userSid;
|
|
5147
|
+
const out = (0, import_node_child_process.spawnSync)(
|
|
5148
|
+
system32("whoami.exe"),
|
|
5149
|
+
["/user", "/fo", "csv", "/nh"],
|
|
5150
|
+
{
|
|
5151
|
+
encoding: "utf8",
|
|
5152
|
+
windowsHide: true
|
|
5153
|
+
}
|
|
5154
|
+
).stdout;
|
|
5155
|
+
userSid = /"(S-1-[0-9-]+)"/.exec(out ?? "")?.[1] ?? null;
|
|
5156
|
+
return userSid;
|
|
5157
|
+
}
|
|
5158
|
+
function icaclsUser() {
|
|
5159
|
+
const sid = currentUserSid();
|
|
5160
|
+
return sid ? `*${sid}` : process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}` : process.env.USERNAME ?? "";
|
|
5161
|
+
}
|
|
5162
|
+
var icacls = (args) => (0, import_node_child_process.spawnSync)(system32("icacls.exe"), args, {
|
|
5163
|
+
encoding: "utf8",
|
|
5164
|
+
windowsHide: true
|
|
5165
|
+
}).status === 0;
|
|
5166
|
+
function restrictDirAcl(dir) {
|
|
5167
|
+
const user = icaclsUser();
|
|
5168
|
+
return !!user && icacls([
|
|
5169
|
+
dir,
|
|
5170
|
+
"/inheritance:r",
|
|
5171
|
+
"/grant:r",
|
|
5172
|
+
`${user}:(OI)(CI)F`,
|
|
5173
|
+
"*S-1-5-18:(OI)(CI)F",
|
|
5174
|
+
"*S-1-5-32-544:(OI)(CI)F"
|
|
5175
|
+
]);
|
|
5176
|
+
}
|
|
5177
|
+
var PS_SECURITY = "$p = $env:TUTTICODE_ACL_PATH; $a = [int][IO.File]::GetAttributes($p); $s = (New-Object System.Security.AccessControl.DirectorySecurity($p, 'Owner,Access')).GetSecurityDescriptorSddlForm('Owner,Access'); [Console]::Out.Write([string]$a + [char]10 + $s)";
|
|
5178
|
+
function windowsSecurity(path) {
|
|
5179
|
+
const out = (0, import_node_child_process.spawnSync)(
|
|
5180
|
+
system32("WindowsPowerShell\\v1.0\\powershell.exe"),
|
|
5181
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", PS_SECURITY],
|
|
5182
|
+
{
|
|
5183
|
+
encoding: "utf8",
|
|
5184
|
+
windowsHide: true,
|
|
5185
|
+
env: { ...process.env, TUTTICODE_ACL_PATH: path }
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
5188
|
+
if (out.status !== 0)
|
|
5189
|
+
return void 0;
|
|
5190
|
+
const m = /^(\d+)\r?\n(O:(S-1-[0-9-]+|[A-Z]{2})D:[^\r\n]*)/.exec(
|
|
5191
|
+
out.stdout ?? ""
|
|
5192
|
+
);
|
|
5193
|
+
if (!m)
|
|
5194
|
+
return void 0;
|
|
5195
|
+
return {
|
|
5196
|
+
owner: m[3],
|
|
5197
|
+
sddl: m[2],
|
|
5198
|
+
protected: /D:[A-Z]*P/.test(m[2]),
|
|
5199
|
+
// FILE_ATTRIBUTE_REPARSE_POINT
|
|
5200
|
+
reparse: (Number(m[1]) & 1024) !== 0
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
var ALLOWED_SIDS = /* @__PURE__ */ new Set(["SY", "BA", "OW", "S-1-5-18", "S-1-5-32-544"]);
|
|
5204
|
+
function sddlAllowTrustees(sddl) {
|
|
5205
|
+
const trustees = [];
|
|
5206
|
+
for (const ace of sddl.matchAll(/\(([^)]*)\)/g)) {
|
|
5207
|
+
const fields = ace[1].split(";");
|
|
5208
|
+
if (fields[0] === "A" && fields[5])
|
|
5209
|
+
trustees.push(fields[5]);
|
|
5210
|
+
}
|
|
5211
|
+
return trustees;
|
|
5212
|
+
}
|
|
5213
|
+
function checkFileAccess(path) {
|
|
5214
|
+
const st = (0, import_node_fs2.lstatSync)(path);
|
|
5215
|
+
if (st.isSymbolicLink())
|
|
5216
|
+
return { ok: false, detail: "a symbolic link or a junction" };
|
|
5217
|
+
if (process.platform !== "win32") {
|
|
5218
|
+
const mode = st.mode & 511;
|
|
5219
|
+
return mode & 63 ? { ok: false, detail: `mode ${mode.toString(8).padStart(4, "0")}` } : { ok: true, detail: "mode 0600" };
|
|
5220
|
+
}
|
|
5221
|
+
const sid = currentUserSid();
|
|
5222
|
+
if (!sid)
|
|
5223
|
+
return void 0;
|
|
5224
|
+
const dump = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `tutticode-acl-${process.pid}.tmp`);
|
|
5225
|
+
try {
|
|
5226
|
+
const saved = (0, import_node_child_process.spawnSync)(system32("icacls.exe"), [path, "/save", dump], {
|
|
5227
|
+
windowsHide: true
|
|
5228
|
+
});
|
|
5229
|
+
if (saved.status !== 0)
|
|
5230
|
+
return void 0;
|
|
5231
|
+
return daclAccess((0, import_node_fs2.readFileSync)(dump).toString("utf16le"), sid);
|
|
5232
|
+
} finally {
|
|
5233
|
+
(0, import_node_fs2.rmSync)(dump, { force: true });
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
function daclAccess(sddl, sid) {
|
|
5237
|
+
const others = sddlAllowTrustees(sddl).filter(
|
|
5238
|
+
(t) => t !== sid && !ALLOWED_SIDS.has(t)
|
|
5239
|
+
);
|
|
5240
|
+
return others.length ? { ok: false, detail: `also readable by ${others.join(", ")}` } : { ok: true, detail: "only you can read it" };
|
|
5241
|
+
}
|
|
5242
|
+
function fixFileAccess(path) {
|
|
5243
|
+
if (process.platform !== "win32") {
|
|
5244
|
+
(0, import_node_fs2.chmodSync)(path, 384);
|
|
5245
|
+
return true;
|
|
5246
|
+
}
|
|
5247
|
+
writePrivateFile(path, (0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
5248
|
+
return checkFileAccess(path)?.ok ?? false;
|
|
5249
|
+
}
|
|
5250
|
+
var LOCK_STALE_MS = 3e4;
|
|
5251
|
+
async function withCredentialsLock(fn, options = {}) {
|
|
5252
|
+
const lock = (0, import_node_path2.join)(stateDir(), `${CREDENTIALS_FILE}.lock`);
|
|
5253
|
+
const staleMs = options.staleMs ?? LOCK_STALE_MS;
|
|
5254
|
+
for (; ; ) {
|
|
5255
|
+
if (takeLock(lock, staleMs))
|
|
5256
|
+
break;
|
|
5257
|
+
await new Promise((r2) => setTimeout(r2, options.pollMs ?? 100));
|
|
5258
|
+
}
|
|
5259
|
+
try {
|
|
5260
|
+
return await fn();
|
|
5261
|
+
} finally {
|
|
5262
|
+
(0, import_node_fs2.rmSync)(lock, { force: true });
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
function takeLock(lock, staleMs) {
|
|
5266
|
+
const mtime = (0, import_node_fs2.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs;
|
|
5267
|
+
if (mtime !== void 0 && Date.now() - mtime > staleMs)
|
|
5268
|
+
(0, import_node_fs2.rmSync)(lock, { force: true });
|
|
5269
|
+
try {
|
|
5270
|
+
const fd = (0, import_node_fs2.openSync)(lock, "wx", 384);
|
|
5271
|
+
(0, import_node_fs2.writeSync)(fd, String(process.pid));
|
|
5272
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5273
|
+
return true;
|
|
5274
|
+
} catch {
|
|
5275
|
+
return false;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
var INSTALL_ID2 = /^[A-Za-z0-9_-]{16,64}$/;
|
|
5279
|
+
function installId() {
|
|
5280
|
+
const path = (0, import_node_path2.join)(stateDir(), "install-id");
|
|
5281
|
+
try {
|
|
5282
|
+
const id2 = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
|
|
5283
|
+
if (INSTALL_ID2.test(id2))
|
|
5284
|
+
return id2;
|
|
5285
|
+
} catch {
|
|
5286
|
+
}
|
|
5287
|
+
const id = (0, import_node_crypto.randomBytes)(16).toString("base64url");
|
|
5288
|
+
try {
|
|
5289
|
+
(0, import_node_fs2.writeFileSync)(path, `${id}
|
|
5290
|
+
`, { mode: 384, flag: "wx" });
|
|
5291
|
+
} catch (e) {
|
|
5292
|
+
if (e.code === "EEXIST")
|
|
5293
|
+
try {
|
|
5294
|
+
const theirs = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
|
|
5295
|
+
if (INSTALL_ID2.test(theirs))
|
|
5296
|
+
return theirs;
|
|
5297
|
+
(0, import_node_fs2.writeFileSync)(path, `${id}
|
|
5298
|
+
`, { mode: 384 });
|
|
5299
|
+
} catch {
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
return id;
|
|
5303
|
+
}
|
|
5304
|
+
function deviceName() {
|
|
5305
|
+
const os = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : process.platform === "linux" ? "Linux" : process.platform;
|
|
5306
|
+
return `${(0, import_node_os2.hostname)()} (${os})`.slice(0, 100);
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
// src/account.ts
|
|
5310
|
+
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
5311
|
+
var ACCOUNT_PATH = "/studio/account";
|
|
5312
|
+
var SIGN_IN_EXPIRED = "Your TuttiCode sign-in expired after a year: run `tutticode login`.";
|
|
5313
|
+
var OAuthError = class extends Error {
|
|
5314
|
+
constructor(error, message, status, body = {}) {
|
|
5315
|
+
super(message);
|
|
5316
|
+
this.error = error;
|
|
5317
|
+
this.status = status;
|
|
5318
|
+
this.body = body;
|
|
5319
|
+
}
|
|
5320
|
+
/** `TUTTICODE_TOKEN_EXPIRED` etc. when the server sent one. */
|
|
5321
|
+
get code() {
|
|
5322
|
+
return typeof this.body.code === "string" ? this.body.code : void 0;
|
|
5323
|
+
}
|
|
5324
|
+
};
|
|
5325
|
+
var ApiUnreachable = class extends Error {
|
|
5326
|
+
};
|
|
5327
|
+
async function request(origin, path, init, deps) {
|
|
5328
|
+
try {
|
|
5329
|
+
return await (deps.fetch ?? fetch)(`${origin}${path}`, {
|
|
5330
|
+
...init,
|
|
5331
|
+
redirect: "error",
|
|
5332
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
|
|
5333
|
+
});
|
|
5334
|
+
} catch (e) {
|
|
5335
|
+
throw new ApiUnreachable(
|
|
5336
|
+
`${origin} did not answer (${e.message}).`
|
|
5337
|
+
);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
async function jsonBody(response) {
|
|
5341
|
+
try {
|
|
5342
|
+
const body = await response.json();
|
|
5343
|
+
return typeof body === "object" && body !== null ? body : {};
|
|
5344
|
+
} catch {
|
|
5345
|
+
return {};
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
async function postOAuth(origin, path, params, deps = {}) {
|
|
5349
|
+
const response = await request(
|
|
5350
|
+
origin,
|
|
5351
|
+
path,
|
|
5352
|
+
{
|
|
5353
|
+
method: "POST",
|
|
5354
|
+
headers: {
|
|
5355
|
+
"Content-Type": "application/json",
|
|
5356
|
+
Accept: "application/json"
|
|
5357
|
+
},
|
|
5358
|
+
body: JSON.stringify({ client_id: TUTTICODE_CLIENT_ID, ...params })
|
|
5359
|
+
},
|
|
5360
|
+
deps
|
|
5361
|
+
);
|
|
5362
|
+
const body = await jsonBody(response);
|
|
5363
|
+
if (response.ok)
|
|
5364
|
+
return body;
|
|
5365
|
+
if (response.status >= 500)
|
|
5366
|
+
throw new ApiUnreachable(`${origin} answered ${response.status}.`);
|
|
5367
|
+
const error = typeof body.error === "string" ? body.error : "invalid_request";
|
|
5368
|
+
if (response.status === 429 && error !== "slow_down")
|
|
5369
|
+
throw new ApiUnreachable(`${origin} is rate limiting (429).`);
|
|
5370
|
+
throw new OAuthError(
|
|
5371
|
+
error,
|
|
5372
|
+
typeof body.error_description === "string" ? body.error_description : error,
|
|
5373
|
+
response.status,
|
|
5374
|
+
body
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
function tokenResponse(body) {
|
|
5378
|
+
if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number")
|
|
5379
|
+
throw new ApiUnreachable("The token endpoint sent no tokens.");
|
|
5380
|
+
return body;
|
|
5381
|
+
}
|
|
5382
|
+
function storedAccount(tokens, now, base = {}) {
|
|
5383
|
+
return {
|
|
5384
|
+
kind: "login",
|
|
5385
|
+
clientId: TUTTICODE_CLIENT_ID,
|
|
5386
|
+
...base,
|
|
5387
|
+
accessToken: tokens.access_token,
|
|
5388
|
+
accessTokenExpiresAt: new Date(
|
|
5389
|
+
now + tokens.expires_in * 1e3
|
|
5390
|
+
).toISOString(),
|
|
5391
|
+
refreshToken: tokens.refresh_token,
|
|
5392
|
+
// A refresh never extends the year; keep the first end we learnt.
|
|
5393
|
+
grantExpiresAt: base.grantExpiresAt ?? (tokens.refresh_token_expires_in ? new Date(now + tokens.refresh_token_expires_in * 1e3).toISOString() : void 0),
|
|
5394
|
+
scope: tokens.scope || TUTTICODE_SCOPE,
|
|
5395
|
+
createdAt: base.createdAt ?? new Date(now).toISOString()
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
var TOKEN2 = /^[A-Za-z0-9._~+/=-]{1,4096}$/;
|
|
5399
|
+
function envToken() {
|
|
5400
|
+
const token = process.env.TUTTICODE_TOKEN?.trim();
|
|
5401
|
+
return token && TOKEN2.test(token) ? token : void 0;
|
|
5402
|
+
}
|
|
5403
|
+
var expiresWithin = (iso, ms, now) => !iso || !(Date.parse(iso) - now > ms);
|
|
5404
|
+
async function resolveToken(origin, deps = {}, options = {}) {
|
|
5405
|
+
const pat = options.envToken === false ? void 0 : envToken();
|
|
5406
|
+
if (pat)
|
|
5407
|
+
return { kind: "pat", accessToken: pat };
|
|
5408
|
+
const now = deps.now ?? Date.now;
|
|
5409
|
+
const stored = readAccount(origin);
|
|
5410
|
+
if (!stored)
|
|
5411
|
+
return { kind: "anonymous" };
|
|
5412
|
+
if (stored.grantExpiresAt && Date.parse(stored.grantExpiresAt) <= now())
|
|
5413
|
+
return { kind: "anonymous", ended: SIGN_IN_EXPIRED };
|
|
5414
|
+
if (!expiresWithin(stored.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
|
|
5415
|
+
return { kind: "login", accessToken: stored.accessToken };
|
|
5416
|
+
return withCredentialsLock(async () => {
|
|
5417
|
+
const current = readAccount(origin);
|
|
5418
|
+
if (!current)
|
|
5419
|
+
return { kind: "anonymous" };
|
|
5420
|
+
if (!expiresWithin(current.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
|
|
5421
|
+
return { kind: "login", accessToken: current.accessToken };
|
|
5422
|
+
try {
|
|
5423
|
+
const tokens = tokenResponse(
|
|
5424
|
+
await postOAuth(
|
|
5425
|
+
origin,
|
|
5426
|
+
TUTTICODE_OAUTH_PATHS.token,
|
|
5427
|
+
{ grant_type: "refresh_token", refresh_token: current.refreshToken },
|
|
5428
|
+
deps
|
|
5429
|
+
)
|
|
5430
|
+
);
|
|
5431
|
+
writeCredentials({
|
|
5432
|
+
version: 1,
|
|
5433
|
+
accounts: {
|
|
5434
|
+
...readCredentials().accounts,
|
|
5435
|
+
[origin]: storedAccount(tokens, now(), current)
|
|
5436
|
+
}
|
|
5437
|
+
});
|
|
5438
|
+
return { kind: "login", accessToken: tokens.access_token };
|
|
5439
|
+
} catch (e) {
|
|
5440
|
+
if (e instanceof OAuthError)
|
|
5441
|
+
return {
|
|
5442
|
+
kind: "anonymous",
|
|
5443
|
+
ended: e.code === "TUTTICODE_TOKEN_EXPIRED" ? SIGN_IN_EXPIRED : "Your TuttiCode sign-in is no longer valid (signed out on the account page, or replaced): run `tutticode login`."
|
|
5444
|
+
};
|
|
5445
|
+
return { kind: "login", accessToken: current.accessToken };
|
|
5446
|
+
}
|
|
5447
|
+
});
|
|
5448
|
+
}
|
|
5449
|
+
function bridgeAuth(token, client) {
|
|
5450
|
+
return {
|
|
5451
|
+
...token.accessToken ? { accessToken: token.accessToken } : {},
|
|
5452
|
+
installId: installId(),
|
|
5453
|
+
cliVersion: VERSION2,
|
|
5454
|
+
client
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
async function accountState(origin, deps = {}, options = {}) {
|
|
5458
|
+
const token = await resolveToken(origin, deps, options);
|
|
5459
|
+
if (!token.accessToken || token.kind === "anonymous")
|
|
5460
|
+
return token.ended ? {
|
|
5461
|
+
state: "rejected",
|
|
5462
|
+
tokenKind: "login",
|
|
5463
|
+
code: "TUTTICODE_TOKEN_EXPIRED",
|
|
5464
|
+
message: token.ended
|
|
5465
|
+
} : { state: "anonymous" };
|
|
5466
|
+
const tokenKind = token.kind;
|
|
5467
|
+
let response;
|
|
5468
|
+
try {
|
|
5469
|
+
response = await request(
|
|
5470
|
+
origin,
|
|
5471
|
+
"/api/tutticode/me",
|
|
5472
|
+
{
|
|
5473
|
+
headers: {
|
|
5474
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
5475
|
+
Accept: "application/json"
|
|
5476
|
+
}
|
|
5477
|
+
},
|
|
5478
|
+
deps
|
|
5479
|
+
);
|
|
5480
|
+
} catch (e) {
|
|
5481
|
+
return { state: "unreachable", tokenKind, message: e.message };
|
|
5482
|
+
}
|
|
5483
|
+
const body = await jsonBody(response);
|
|
5484
|
+
if (response.ok && isMe(body))
|
|
5485
|
+
return { state: "signed-in", tokenKind, me: body };
|
|
5486
|
+
if (response.status === 401 || response.status === 403)
|
|
5487
|
+
return {
|
|
5488
|
+
state: "rejected",
|
|
5489
|
+
tokenKind,
|
|
5490
|
+
...typeof body.code === "string" ? { code: body.code } : {},
|
|
5491
|
+
message: typeof body.message === "string" ? body.message : `The server refused the ${tokenKind === "pat" ? "TUTTICODE_TOKEN" : "sign-in"} (${response.status}).`
|
|
5492
|
+
};
|
|
5493
|
+
return {
|
|
5494
|
+
state: "unreachable",
|
|
5495
|
+
tokenKind,
|
|
5496
|
+
message: `${origin}/api/tutticode/me answered ${response.status}.`
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
function isMe(v) {
|
|
5500
|
+
const user = v.user;
|
|
5501
|
+
return typeof user === "object" && user !== null && typeof user.email === "string" && typeof v.plan === "string" && typeof v.limits === "object" && v.limits !== null;
|
|
5502
|
+
}
|
|
5503
|
+
function accountLimits(state) {
|
|
5504
|
+
if (state.state === "signed-in")
|
|
5505
|
+
return {
|
|
5506
|
+
maxSeconds: state.me.limits.maxSeconds,
|
|
5507
|
+
watermarkAfterSeconds: null
|
|
5508
|
+
};
|
|
5509
|
+
return {
|
|
5510
|
+
maxSeconds: DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds,
|
|
5511
|
+
watermarkAfterSeconds: DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
var LOGIN_HINT = {
|
|
5515
|
+
command: TUTTICODE_LOGIN_HINT_COMMAND,
|
|
5516
|
+
note: "A free account lifts the limits. Run the command, ask the user to open the link it prints and approve the code; keep working meanwhile."
|
|
5517
|
+
};
|
|
5518
|
+
function signedInAs(me) {
|
|
5519
|
+
return `${me.user.email} (TuttiCode, ${me.plan.toLowerCase()})`;
|
|
5520
|
+
}
|
|
5521
|
+
async function revokeToken(origin, token, deps = {}) {
|
|
5522
|
+
try {
|
|
5523
|
+
const response = await request(
|
|
5524
|
+
origin,
|
|
5525
|
+
TUTTICODE_OAUTH_PATHS.revoke,
|
|
5526
|
+
{
|
|
5527
|
+
method: "POST",
|
|
5528
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5529
|
+
body: new URLSearchParams({
|
|
5530
|
+
token,
|
|
5531
|
+
client_id: TUTTICODE_CLIENT_ID
|
|
5532
|
+
}).toString()
|
|
5533
|
+
},
|
|
5534
|
+
deps
|
|
5535
|
+
);
|
|
5536
|
+
return response.status < 500;
|
|
5537
|
+
} catch {
|
|
5538
|
+
return false;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
|
|
4108
5542
|
// src/args.ts
|
|
4109
5543
|
var import_node_util = require("node:util");
|
|
4110
5544
|
|
|
4111
5545
|
// src/output.ts
|
|
4112
|
-
var
|
|
4113
|
-
var
|
|
5546
|
+
var import_node_fs3 = require("node:fs");
|
|
5547
|
+
var import_node_path3 = require("node:path");
|
|
4114
5548
|
var EXIT_OK = 0;
|
|
4115
5549
|
var EXIT_CODE_ERRORS = 1;
|
|
4116
5550
|
var EXIT_BRIDGE = 2;
|
|
4117
5551
|
var EXIT_USAGE = 3;
|
|
5552
|
+
var EXIT_ANONYMOUS = 4;
|
|
5553
|
+
var EXIT_LIMIT = 5;
|
|
5554
|
+
var EXIT_UPGRADE = 6;
|
|
4118
5555
|
var CODE_ERRORS = /* @__PURE__ */ new Set(["compile", "too-long", "clip"]);
|
|
4119
5556
|
function exitCodeFor(outcome) {
|
|
4120
5557
|
switch (outcome.kind) {
|
|
4121
5558
|
case "checked":
|
|
4122
5559
|
return outcome.report.ok ? EXIT_OK : EXIT_CODE_ERRORS;
|
|
4123
5560
|
case "error":
|
|
5561
|
+
if (outcome.code === "limit")
|
|
5562
|
+
return EXIT_LIMIT;
|
|
5563
|
+
if (outcome.code === "upgrade")
|
|
5564
|
+
return EXIT_UPGRADE;
|
|
4124
5565
|
return CODE_ERRORS.has(outcome.code) ? EXIT_CODE_ERRORS : EXIT_BRIDGE;
|
|
4125
5566
|
default:
|
|
4126
5567
|
return EXIT_OK;
|
|
@@ -4135,29 +5576,29 @@ function safeFileStem(label) {
|
|
|
4135
5576
|
return RESERVED_NAMES.test(cleaned) ? `${cleaned}_` : cleaned;
|
|
4136
5577
|
}
|
|
4137
5578
|
function formatFromPath(path) {
|
|
4138
|
-
const ext = (0,
|
|
5579
|
+
const ext = (0, import_node_path3.extname)(path).slice(1).toLowerCase();
|
|
4139
5580
|
return ext === "wav" || ext === "flac" || ext === "mp3" ? ext : void 0;
|
|
4140
5581
|
}
|
|
4141
5582
|
var FORMATS2 = ["wav", "flac", "mp3"];
|
|
4142
5583
|
function writeFileNoFollow(path, bytes) {
|
|
4143
5584
|
try {
|
|
4144
|
-
if ((0,
|
|
5585
|
+
if ((0, import_node_fs3.lstatSync)(path).isSymbolicLink())
|
|
4145
5586
|
throw new Error(`Refusing to write through the symbolic link ${path}.`);
|
|
4146
5587
|
} catch (e) {
|
|
4147
5588
|
if (e.code !== "ENOENT")
|
|
4148
5589
|
throw e;
|
|
4149
5590
|
}
|
|
4150
|
-
const fd = (0,
|
|
5591
|
+
const fd = (0, import_node_fs3.openSync)(
|
|
4151
5592
|
path,
|
|
4152
|
-
|
|
5593
|
+
import_node_fs3.constants.O_WRONLY | import_node_fs3.constants.O_CREAT | import_node_fs3.constants.O_TRUNC | // POSIX only (undefined on Windows): fail rather than follow a link
|
|
4153
5594
|
// swapped in after the check above.
|
|
4154
|
-
(
|
|
5595
|
+
(import_node_fs3.constants.O_NOFOLLOW ?? 0)
|
|
4155
5596
|
);
|
|
4156
5597
|
try {
|
|
4157
5598
|
for (let at = 0; at < bytes.length; )
|
|
4158
|
-
at += (0,
|
|
5599
|
+
at += (0, import_node_fs3.writeSync)(fd, bytes, at, bytes.length - at);
|
|
4159
5600
|
} finally {
|
|
4160
|
-
(0,
|
|
5601
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
4161
5602
|
}
|
|
4162
5603
|
}
|
|
4163
5604
|
function writeRender(meta, parts, out, stemsDir) {
|
|
@@ -4168,30 +5609,30 @@ function writeRender(meta, parts, out, stemsDir) {
|
|
|
4168
5609
|
const mix = parts.get(MIX_PART);
|
|
4169
5610
|
if (!mix)
|
|
4170
5611
|
throw new Error("The render has no mix.");
|
|
4171
|
-
const mixPath = (0,
|
|
4172
|
-
(0,
|
|
5612
|
+
const mixPath = (0, import_node_path3.resolve)(out);
|
|
5613
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(mixPath), { recursive: true });
|
|
4173
5614
|
writeFileNoFollow(mixPath, mix);
|
|
4174
5615
|
const stems = {};
|
|
4175
5616
|
if (meta.stems.length) {
|
|
4176
|
-
const requested = (0,
|
|
4177
|
-
stemsDir ?? mixPath.slice(0, mixPath.length - (0,
|
|
5617
|
+
const requested = (0, import_node_path3.resolve)(
|
|
5618
|
+
stemsDir ?? mixPath.slice(0, mixPath.length - (0, import_node_path3.extname)(mixPath).length) + ".stems"
|
|
4178
5619
|
);
|
|
4179
|
-
(0,
|
|
4180
|
-
const dir = (0,
|
|
5620
|
+
(0, import_node_fs3.mkdirSync)(requested, { recursive: true });
|
|
5621
|
+
const dir = (0, import_node_fs3.realpathSync)(requested);
|
|
4181
5622
|
const used = new Set(
|
|
4182
|
-
[requested, dir].includes((0,
|
|
5623
|
+
[requested, dir].includes((0, import_node_path3.dirname)(mixPath)) ? [(0, import_node_path3.basename)(mixPath).toLowerCase()] : []
|
|
4183
5624
|
);
|
|
4184
5625
|
for (const label of meta.stems) {
|
|
4185
5626
|
const bytes = parts.get(stemPart(label));
|
|
4186
5627
|
if (!bytes)
|
|
4187
5628
|
continue;
|
|
4188
5629
|
const base = safeFileStem(label);
|
|
4189
|
-
let
|
|
4190
|
-
for (let n = 2; used.has(
|
|
4191
|
-
|
|
4192
|
-
used.add(
|
|
4193
|
-
const path = (0,
|
|
4194
|
-
if ((0,
|
|
5630
|
+
let file2 = `${base}.${meta.format}`;
|
|
5631
|
+
for (let n = 2; used.has(file2.toLowerCase()); n++)
|
|
5632
|
+
file2 = `${base}-${n}.${meta.format}`;
|
|
5633
|
+
used.add(file2.toLowerCase());
|
|
5634
|
+
const path = (0, import_node_path3.resolve)(dir, file2);
|
|
5635
|
+
if ((0, import_node_path3.dirname)(path) !== dir || (0, import_node_path3.basename)(path) !== file2)
|
|
4195
5636
|
throw new Error(`Refusing to write a stem outside ${dir}.`);
|
|
4196
5637
|
writeFileNoFollow(path, bytes);
|
|
4197
5638
|
stems[label] = path;
|
|
@@ -4200,11 +5641,11 @@ function writeRender(meta, parts, out, stemsDir) {
|
|
|
4200
5641
|
return { mix: mixPath, stems };
|
|
4201
5642
|
}
|
|
4202
5643
|
var fixed = (n, digits = 1) => Number.isFinite(n) ? n.toFixed(digits) : String(n);
|
|
4203
|
-
function formatDiagnostics(report,
|
|
5644
|
+
function formatDiagnostics(report, file2) {
|
|
4204
5645
|
const lines = [];
|
|
4205
5646
|
for (const d of report.diagnostics) {
|
|
4206
5647
|
lines.push(
|
|
4207
|
-
`${
|
|
5648
|
+
`${file2}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
|
|
4208
5649
|
);
|
|
4209
5650
|
if (d.snippet)
|
|
4210
5651
|
lines.push(` ${d.snippet}`);
|
|
@@ -4231,92 +5672,46 @@ function formatRenderSummary(meta, files) {
|
|
|
4231
5672
|
`Wrote ${files.mix} (${fixed(meta.seconds)} s, ${meta.bars} bars at ${fixed(
|
|
4232
5673
|
meta.bpm,
|
|
4233
5674
|
0
|
|
4234
|
-
)} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
|
|
5675
|
+
)} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
|
|
5676
|
+
meta.truePeak
|
|
5677
|
+
)} dBTP${gain}${limited})`
|
|
4235
5678
|
];
|
|
4236
5679
|
for (const warning of meta.warnings ?? [])
|
|
4237
5680
|
lines.push(`Note: ${warning}`);
|
|
4238
5681
|
const stems = Object.values(files.stems);
|
|
4239
5682
|
if (stems.length)
|
|
4240
|
-
lines.push(`Stems: ${stems.length} files in ${(0,
|
|
5683
|
+
lines.push(`Stems: ${stems.length} files in ${(0, import_node_path3.dirname)(stems[0])}`);
|
|
4241
5684
|
lines.push(`Edit in TuttiDAW: ${meta.editUrl}`);
|
|
4242
5685
|
return lines;
|
|
4243
5686
|
}
|
|
5687
|
+
var WATERMARK_LINE = "Voice tag 'register at TuttiDAW' mixed in throughout this render (renders over 30 s without sign-in); the stats include it. Remove it: tutticode login (free)";
|
|
5688
|
+
|
|
5689
|
+
// src/server.ts
|
|
5690
|
+
var import_node_crypto2 = require("node:crypto");
|
|
5691
|
+
var import_node_http = require("node:http");
|
|
5692
|
+
|
|
5693
|
+
// ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
5694
|
+
var import_stream = __toESM(require_stream(), 1);
|
|
5695
|
+
var import_extension = __toESM(require_extension(), 1);
|
|
5696
|
+
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
5697
|
+
var import_receiver = __toESM(require_receiver(), 1);
|
|
5698
|
+
var import_sender = __toESM(require_sender(), 1);
|
|
5699
|
+
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
5700
|
+
var import_websocket = __toESM(require_websocket(), 1);
|
|
5701
|
+
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
5702
|
+
var wrapper_default = import_websocket.default;
|
|
4244
5703
|
|
|
4245
5704
|
// src/server.ts
|
|
4246
|
-
var import_node_crypto = require("node:crypto");
|
|
4247
|
-
var import_node_http = require("node:http");
|
|
4248
|
-
|
|
4249
|
-
// ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
4250
|
-
var import_stream = __toESM(require_stream(), 1);
|
|
4251
|
-
var import_extension = __toESM(require_extension(), 1);
|
|
4252
|
-
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
4253
|
-
var import_receiver = __toESM(require_receiver(), 1);
|
|
4254
|
-
var import_sender = __toESM(require_sender(), 1);
|
|
4255
|
-
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
4256
|
-
var import_websocket = __toESM(require_websocket(), 1);
|
|
4257
|
-
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
4258
|
-
var wrapper_default = import_websocket.default;
|
|
4259
|
-
|
|
4260
|
-
// src/env.ts
|
|
4261
|
-
var import_node_fs2 = require("node:fs");
|
|
4262
|
-
var import_node_os = require("node:os");
|
|
4263
|
-
var import_node_path2 = require("node:path");
|
|
4264
|
-
var VERSION = true ? "0.1.1" : readPackageVersion();
|
|
4265
|
-
function envBrowserArgs() {
|
|
4266
|
-
const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
|
|
4267
|
-
return args?.length ? args : void 0;
|
|
4268
|
-
}
|
|
4269
|
-
function stateDir() {
|
|
4270
|
-
const dir = process.env.TUTTICODE_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".tutticode");
|
|
4271
|
-
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
4272
|
-
return dir;
|
|
4273
|
-
}
|
|
4274
|
-
var assetsDir;
|
|
4275
|
-
function skillsDir() {
|
|
4276
|
-
return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
|
|
4277
|
-
(0, import_node_path2.join)(__dirname, "..", "..", "..", "public", "skills");
|
|
4278
|
-
}
|
|
4279
|
-
function setSkillsDir(dir) {
|
|
4280
|
-
assetsDir = dir;
|
|
4281
|
-
}
|
|
4282
|
-
var LOG_ROLL_BYTES = 2 * 1024 * 1024;
|
|
4283
|
-
function createLogger(name, verbose) {
|
|
4284
|
-
let file;
|
|
4285
|
-
try {
|
|
4286
|
-
const dir = (0, import_node_path2.join)(stateDir(), "logs");
|
|
4287
|
-
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
4288
|
-
file = (0, import_node_path2.join)(dir, `${name}.log`);
|
|
4289
|
-
if (((0, import_node_fs2.statSync)(file, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
|
|
4290
|
-
(0, import_node_fs2.renameSync)(file, `${file}.1`);
|
|
4291
|
-
} catch {
|
|
4292
|
-
file = void 0;
|
|
4293
|
-
}
|
|
4294
|
-
return (level2, message) => {
|
|
4295
|
-
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
|
|
4296
|
-
if (verbose || level2 === "error")
|
|
4297
|
-
process.stderr.write(`tutticode: ${message}
|
|
4298
|
-
`);
|
|
4299
|
-
if (!file)
|
|
4300
|
-
return;
|
|
4301
|
-
try {
|
|
4302
|
-
(0, import_node_fs2.appendFileSync)(file, `${line}
|
|
4303
|
-
`);
|
|
4304
|
-
} catch {
|
|
4305
|
-
}
|
|
4306
|
-
};
|
|
4307
|
-
}
|
|
4308
|
-
|
|
4309
|
-
// src/server.ts
|
|
4310
5705
|
var CONTROL_PATH = "/control";
|
|
4311
5706
|
function newToken() {
|
|
4312
|
-
return (0,
|
|
5707
|
+
return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
|
|
4313
5708
|
}
|
|
4314
5709
|
function tokensEqual(expected, presented) {
|
|
4315
5710
|
if (typeof presented !== "string")
|
|
4316
5711
|
return false;
|
|
4317
5712
|
const a = Buffer.from(expected);
|
|
4318
5713
|
const b = Buffer.from(presented);
|
|
4319
|
-
return a.length === b.length && (0,
|
|
5714
|
+
return a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
4320
5715
|
}
|
|
4321
5716
|
function normalizeOrigin(value) {
|
|
4322
5717
|
try {
|
|
@@ -4408,7 +5803,7 @@ var BridgeServer = class {
|
|
|
4408
5803
|
return res.end(
|
|
4409
5804
|
JSON.stringify({
|
|
4410
5805
|
name: "tutticode",
|
|
4411
|
-
version:
|
|
5806
|
+
version: VERSION2,
|
|
4412
5807
|
protocol: BRIDGE_PROTOCOL
|
|
4413
5808
|
})
|
|
4414
5809
|
);
|
|
@@ -4472,6 +5867,21 @@ function pathOf(req) {
|
|
|
4472
5867
|
// src/args.ts
|
|
4473
5868
|
var UsageError = class extends Error {
|
|
4474
5869
|
};
|
|
5870
|
+
var accountOptions = {
|
|
5871
|
+
origin: { type: "string" },
|
|
5872
|
+
locale: { type: "string" },
|
|
5873
|
+
verbose: { type: "boolean", short: "v" }
|
|
5874
|
+
};
|
|
5875
|
+
function accountFlags(values) {
|
|
5876
|
+
const origin = values.origin;
|
|
5877
|
+
if (origin !== void 0 && !normalizeOrigin(origin))
|
|
5878
|
+
throw new UsageError(`--origin must be an http(s) origin (got ${origin}).`);
|
|
5879
|
+
return {
|
|
5880
|
+
origin,
|
|
5881
|
+
locale: values.locale,
|
|
5882
|
+
verbose: !!values.verbose
|
|
5883
|
+
};
|
|
5884
|
+
}
|
|
4475
5885
|
var bridgeOptions = {
|
|
4476
5886
|
origin: { type: "string" },
|
|
4477
5887
|
"dev-origin": { type: "string", multiple: true },
|
|
@@ -4598,7 +6008,8 @@ function parseCli(argv) {
|
|
|
4598
6008
|
case "doctor": {
|
|
4599
6009
|
const { values, positionals } = parse(rest, {
|
|
4600
6010
|
...bridgeOptions,
|
|
4601
|
-
json: { type: "boolean" }
|
|
6011
|
+
json: { type: "boolean" },
|
|
6012
|
+
fix: { type: "boolean" }
|
|
4602
6013
|
});
|
|
4603
6014
|
if (values.help)
|
|
4604
6015
|
return { name: "help", topic: "doctor" };
|
|
@@ -4607,9 +6018,56 @@ function parseCli(argv) {
|
|
|
4607
6018
|
return {
|
|
4608
6019
|
name: "doctor",
|
|
4609
6020
|
json: !!values.json,
|
|
6021
|
+
fix: !!values.fix,
|
|
4610
6022
|
bridge: bridgeFlags(values)
|
|
4611
6023
|
};
|
|
4612
6024
|
}
|
|
6025
|
+
case "login": {
|
|
6026
|
+
const { values, positionals } = parse(rest, {
|
|
6027
|
+
...accountOptions,
|
|
6028
|
+
device: { type: "boolean" },
|
|
6029
|
+
"no-wait": { type: "boolean" },
|
|
6030
|
+
json: { type: "boolean" }
|
|
6031
|
+
});
|
|
6032
|
+
if (values.help)
|
|
6033
|
+
return { name: "help", topic: "login" };
|
|
6034
|
+
if (positionals.length)
|
|
6035
|
+
throw new UsageError("login takes no arguments.");
|
|
6036
|
+
const noWait = !!values["no-wait"];
|
|
6037
|
+
if (values.json && !noWait)
|
|
6038
|
+
throw new UsageError("--json goes with --device --no-wait.");
|
|
6039
|
+
return {
|
|
6040
|
+
name: "login",
|
|
6041
|
+
// Only the device grant can finish without this process.
|
|
6042
|
+
device: !!values.device || noWait,
|
|
6043
|
+
noWait,
|
|
6044
|
+
json: !!values.json,
|
|
6045
|
+
account: accountFlags(values)
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
case "logout": {
|
|
6049
|
+
const { values, positionals } = parse(rest, accountOptions);
|
|
6050
|
+
if (values.help)
|
|
6051
|
+
return { name: "help", topic: "logout" };
|
|
6052
|
+
if (positionals.length)
|
|
6053
|
+
throw new UsageError("logout takes no arguments.");
|
|
6054
|
+
return { name: "logout", account: accountFlags(values) };
|
|
6055
|
+
}
|
|
6056
|
+
case "whoami": {
|
|
6057
|
+
const { values, positionals } = parse(rest, {
|
|
6058
|
+
...accountOptions,
|
|
6059
|
+
json: { type: "boolean" }
|
|
6060
|
+
});
|
|
6061
|
+
if (values.help)
|
|
6062
|
+
return { name: "help", topic: "whoami" };
|
|
6063
|
+
if (positionals.length)
|
|
6064
|
+
throw new UsageError("whoami takes no arguments.");
|
|
6065
|
+
return {
|
|
6066
|
+
name: "whoami",
|
|
6067
|
+
json: !!values.json,
|
|
6068
|
+
account: accountFlags(values)
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
4613
6071
|
case "setup": {
|
|
4614
6072
|
const { values, positionals } = parse(rest, {
|
|
4615
6073
|
yes: { type: "boolean", short: "y" },
|
|
@@ -4654,6 +6112,8 @@ function parseCli(argv) {
|
|
|
4654
6112
|
}
|
|
4655
6113
|
case "__daemon":
|
|
4656
6114
|
return { name: "daemon", config: rest[0] ?? "" };
|
|
6115
|
+
case "__login-poll":
|
|
6116
|
+
return { name: "login-poll", file: rest[0] ?? "" };
|
|
4657
6117
|
default:
|
|
4658
6118
|
throw new UsageError(`Unknown command ${name}. Run tutticode --help.`);
|
|
4659
6119
|
}
|
|
@@ -4677,11 +6137,12 @@ function parseRender(rest) {
|
|
|
4677
6137
|
"true-peak": { type: "string" },
|
|
4678
6138
|
limit: { type: "boolean" },
|
|
4679
6139
|
"no-analyze": { type: "boolean" },
|
|
6140
|
+
"no-brand-tag": { type: "boolean" },
|
|
4680
6141
|
json: { type: "boolean" }
|
|
4681
6142
|
});
|
|
4682
6143
|
if (values.help)
|
|
4683
6144
|
return { name: "help", topic: "render" };
|
|
4684
|
-
const
|
|
6145
|
+
const file2 = oneFile(positionals, "render");
|
|
4685
6146
|
const out = values.out;
|
|
4686
6147
|
if (!out)
|
|
4687
6148
|
throw new UsageError(
|
|
@@ -4731,11 +6192,12 @@ function parseRender(rest) {
|
|
|
4731
6192
|
...optional2("lufs", number(values, "lufs", -40, -5)),
|
|
4732
6193
|
...optional2("truePeak", number(values, "true-peak", -12, 0)),
|
|
4733
6194
|
...values.limit ? { limit: true } : {},
|
|
4734
|
-
...values["no-analyze"] ? { analyze: false } : {}
|
|
6195
|
+
...values["no-analyze"] ? { analyze: false } : {},
|
|
6196
|
+
...values["no-brand-tag"] ? { brandTag: false } : {}
|
|
4735
6197
|
};
|
|
4736
6198
|
return {
|
|
4737
6199
|
name: "render",
|
|
4738
|
-
file,
|
|
6200
|
+
file: file2,
|
|
4739
6201
|
out,
|
|
4740
6202
|
stemsDir: values.stems,
|
|
4741
6203
|
json: !!values.json,
|
|
@@ -4755,10 +6217,19 @@ Usage:
|
|
|
4755
6217
|
tutticode open <file|-> open the program in TuttiCode
|
|
4756
6218
|
tutticode mcp MCP stdio server (code_check, code_render, ...)
|
|
4757
6219
|
tutticode doctor is everything in place?
|
|
6220
|
+
tutticode login sign in (free): longer renders, no voice tag
|
|
6221
|
+
tutticode logout sign out on this computer
|
|
6222
|
+
tutticode whoami who renders, with which limits
|
|
4758
6223
|
tutticode setup optional browser policy + skills
|
|
4759
6224
|
tutticode skills install install the agent skills
|
|
4760
6225
|
|
|
4761
|
-
|
|
6226
|
+
Once TuttiDAW switches its limits on (being rolled out), renders without
|
|
6227
|
+
sign-in are limited to 60 s, and renders over 30 s carry a spoken "register
|
|
6228
|
+
at TuttiDAW" tag. TUTTICODE_TOKEN (an access token from the
|
|
6229
|
+
account page) signs in CI.
|
|
6230
|
+
|
|
6231
|
+
Exit codes: 0 ok, 1 errors in the code, 2 bridge/browser problem, 3 usage,
|
|
6232
|
+
4 not signed in (whoami), 5 over a limit without sign-in, 6 upgrade tutticode.
|
|
4762
6233
|
Run tutticode <command> --help for its flags.`,
|
|
4763
6234
|
check: `tutticode check <file|-> [--bpm N] [--json] [bridge flags]
|
|
4764
6235
|
|
|
@@ -4782,6 +6253,8 @@ program has errors.`,
|
|
|
4782
6253
|
--limit true-peak limiter instead of failing
|
|
4783
6254
|
(clip) or lowering; reported
|
|
4784
6255
|
--no-analyze skip the per-voice analysis
|
|
6256
|
+
--no-brand-tag no "Made with TuttiCode" file metadata
|
|
6257
|
+
(signed in only; TUTTICODE_BRAND_TAG=0)
|
|
4785
6258
|
--json print the RenderMeta + written files
|
|
4786
6259
|
--timeout S give up after S seconds (default 600)`,
|
|
4787
6260
|
open: `tutticode open <file|-> [bridge flags]
|
|
@@ -4791,11 +6264,34 @@ Opens the program in a visible TuttiCode tab (tuttidaw.com/code). With
|
|
|
4791
6264
|
mcp: `tutticode mcp [bridge flags]
|
|
4792
6265
|
|
|
4793
6266
|
MCP stdio server: code_check, code_render, code_compare, code_open,
|
|
4794
|
-
code_reference; resources = the TuttiCode skill references.
|
|
6267
|
+
code_reference, code_account; resources = the TuttiCode skill references.
|
|
6268
|
+
Signs the page in like the CLI (tutticode login, or TUTTICODE_TOKEN).
|
|
4795
6269
|
claude mcp add tutticode -- tutticode mcp`,
|
|
4796
|
-
doctor: `tutticode doctor [--json] [bridge flags]
|
|
6270
|
+
doctor: `tutticode doctor [--json] [--fix] [bridge flags]
|
|
6271
|
+
|
|
6272
|
+
Checks the browser, the page and the connection, prints the engine version
|
|
6273
|
+
and who renders (signed in / anonymous, the credentials file's permissions).
|
|
6274
|
+
--fix makes the credentials file private again.`,
|
|
6275
|
+
login: `tutticode login [--device [--no-wait] [--json]] [--origin <url>] [--locale <l>]
|
|
6276
|
+
|
|
6277
|
+
Signs in with a free TuttiDAW account: renders up to 5 minutes, no voice tag.
|
|
6278
|
+
Opens your browser; approve there and come back. The sign-in can only render
|
|
6279
|
+
with TuttiCode (it cannot open your projects, account or billing) and lasts a
|
|
6280
|
+
year.
|
|
6281
|
+
--device no browser here (SSH, a server): prints a code to approve at
|
|
6282
|
+
tuttidaw.com/code/activate on any device
|
|
6283
|
+
--no-wait with --device: print the code and return at once; tutticode
|
|
6284
|
+
finishes the sign-in in the background once it is approved.
|
|
6285
|
+
For agents: tutticode login --device --no-wait --json
|
|
6286
|
+
Needs a terminal unless --no-wait.`,
|
|
6287
|
+
logout: `tutticode logout [--origin <url>]
|
|
6288
|
+
|
|
6289
|
+
Signs this computer out (revokes its sign-in) and deletes the stored
|
|
6290
|
+
credentials. Other devices and access tokens: the account page.`,
|
|
6291
|
+
whoami: `tutticode whoami [--json] [--origin <url>]
|
|
4797
6292
|
|
|
4798
|
-
|
|
6293
|
+
Who renders: the masked email, plan, limits and today's usage. Exit 0 signed
|
|
6294
|
+
in, 4 not signed in, 2 when the sign-in was rejected.`,
|
|
4799
6295
|
setup: `tutticode setup [--yes] [--undo] [--no-skills]
|
|
4800
6296
|
|
|
4801
6297
|
Optionally lets tuttidaw.com reach tutticode in your everyday Chrome / Edge
|
|
@@ -4817,10 +6313,10 @@ agent found under your home directory).`,
|
|
|
4817
6313
|
};
|
|
4818
6314
|
|
|
4819
6315
|
// src/browser.ts
|
|
4820
|
-
var
|
|
4821
|
-
var
|
|
4822
|
-
var
|
|
4823
|
-
var
|
|
6316
|
+
var import_node_child_process2 = require("node:child_process");
|
|
6317
|
+
var import_node_fs4 = require("node:fs");
|
|
6318
|
+
var import_node_os3 = require("node:os");
|
|
6319
|
+
var import_node_path4 = require("node:path");
|
|
4824
6320
|
|
|
4825
6321
|
// src/cdp.ts
|
|
4826
6322
|
var import_node_events = require("node:events");
|
|
@@ -4925,8 +6421,8 @@ var CdpClient = class extends import_node_events.EventEmitter {
|
|
|
4925
6421
|
var hostPlatform = () => ({
|
|
4926
6422
|
platform: process.platform,
|
|
4927
6423
|
env: process.env,
|
|
4928
|
-
home: (0,
|
|
4929
|
-
exists:
|
|
6424
|
+
home: (0, import_node_os3.homedir)(),
|
|
6425
|
+
exists: import_node_fs4.existsSync
|
|
4930
6426
|
});
|
|
4931
6427
|
function browserCandidates(p = hostPlatform()) {
|
|
4932
6428
|
if (p.platform === "win32") {
|
|
@@ -4938,20 +6434,20 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4938
6434
|
return [
|
|
4939
6435
|
...roots.map((r2) => ({
|
|
4940
6436
|
kind: "chrome",
|
|
4941
|
-
path: (0,
|
|
6437
|
+
path: (0, import_node_path4.join)(r2, "Google", "Chrome", "Application", "chrome.exe")
|
|
4942
6438
|
})),
|
|
4943
6439
|
...roots.map((r2) => ({
|
|
4944
6440
|
kind: "edge",
|
|
4945
|
-
path: (0,
|
|
6441
|
+
path: (0, import_node_path4.join)(r2, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
4946
6442
|
}))
|
|
4947
6443
|
];
|
|
4948
6444
|
}
|
|
4949
6445
|
if (p.platform === "darwin") {
|
|
4950
|
-
const apps = ["/Applications", (0,
|
|
6446
|
+
const apps = ["/Applications", (0, import_node_path4.join)(p.home, "Applications")];
|
|
4951
6447
|
return [
|
|
4952
6448
|
...apps.map((a) => ({
|
|
4953
6449
|
kind: "chrome",
|
|
4954
|
-
path: (0,
|
|
6450
|
+
path: (0, import_node_path4.join)(
|
|
4955
6451
|
a,
|
|
4956
6452
|
"Google Chrome.app",
|
|
4957
6453
|
"Contents",
|
|
@@ -4961,7 +6457,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4961
6457
|
})),
|
|
4962
6458
|
...apps.map((a) => ({
|
|
4963
6459
|
kind: "edge",
|
|
4964
|
-
path: (0,
|
|
6460
|
+
path: (0, import_node_path4.join)(
|
|
4965
6461
|
a,
|
|
4966
6462
|
"Microsoft Edge.app",
|
|
4967
6463
|
"Contents",
|
|
@@ -4971,7 +6467,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4971
6467
|
})),
|
|
4972
6468
|
...apps.map((a) => ({
|
|
4973
6469
|
kind: "chromium",
|
|
4974
|
-
path: (0,
|
|
6470
|
+
path: (0, import_node_path4.join)(a, "Chromium.app", "Contents", "MacOS", "Chromium")
|
|
4975
6471
|
}))
|
|
4976
6472
|
];
|
|
4977
6473
|
}
|
|
@@ -4985,7 +6481,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4985
6481
|
["chromium", "chromium-browser"]
|
|
4986
6482
|
];
|
|
4987
6483
|
return names.flatMap(
|
|
4988
|
-
([kind, name]) => dirs.map((d) => ({ kind, path: (0,
|
|
6484
|
+
([kind, name]) => dirs.map((d) => ({ kind, path: (0, import_node_path4.join)(d, name) }))
|
|
4989
6485
|
);
|
|
4990
6486
|
}
|
|
4991
6487
|
function findBrowser(wanted, p = hostPlatform()) {
|
|
@@ -5004,7 +6500,7 @@ function findBrowser(wanted, p = hostPlatform()) {
|
|
|
5004
6500
|
}
|
|
5005
6501
|
function openExternal(url) {
|
|
5006
6502
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
5007
|
-
const child = (0,
|
|
6503
|
+
const child = (0, import_node_child_process2.spawn)(command, args, {
|
|
5008
6504
|
detached: true,
|
|
5009
6505
|
stdio: "ignore",
|
|
5010
6506
|
windowsHide: true
|
|
@@ -5106,7 +6602,7 @@ var HeadlessBrowser = class {
|
|
|
5106
6602
|
}
|
|
5107
6603
|
}
|
|
5108
6604
|
if (this.tempProfile) {
|
|
5109
|
-
(0,
|
|
6605
|
+
(0, import_node_fs4.rmSync)(this.tempProfile, { recursive: true, force: true });
|
|
5110
6606
|
this.tempProfile = void 0;
|
|
5111
6607
|
}
|
|
5112
6608
|
}
|
|
@@ -5114,8 +6610,8 @@ var HeadlessBrowser = class {
|
|
|
5114
6610
|
await this.close();
|
|
5115
6611
|
this.consoleLines = [];
|
|
5116
6612
|
const { browser, log } = this.options;
|
|
5117
|
-
const shared = this.options.profileDir ?? (0,
|
|
5118
|
-
(0,
|
|
6613
|
+
const shared = this.options.profileDir ?? (0, import_node_path4.join)(stateDir(), "browser-profile");
|
|
6614
|
+
(0, import_node_fs4.mkdirSync)(shared, { recursive: true });
|
|
5119
6615
|
try {
|
|
5120
6616
|
this.cdp = await this.spawnBrowser(shared);
|
|
5121
6617
|
} catch (e) {
|
|
@@ -5123,7 +6619,7 @@ var HeadlessBrowser = class {
|
|
|
5123
6619
|
"warn",
|
|
5124
6620
|
`shared browser profile unavailable (${e.message}); using a temporary one`
|
|
5125
6621
|
);
|
|
5126
|
-
this.tempProfile = (0,
|
|
6622
|
+
this.tempProfile = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "tutticode-profile-"));
|
|
5127
6623
|
this.cdp = await this.spawnBrowser(this.tempProfile);
|
|
5128
6624
|
}
|
|
5129
6625
|
this.cdp.on("event", (event) => this.onEvent(event));
|
|
@@ -5159,7 +6655,7 @@ var HeadlessBrowser = class {
|
|
|
5159
6655
|
"about:blank"
|
|
5160
6656
|
];
|
|
5161
6657
|
return new Promise((resolve3, reject) => {
|
|
5162
|
-
const child = (0,
|
|
6658
|
+
const child = (0, import_node_child_process2.spawn)(browser.path, args, {
|
|
5163
6659
|
// fd 3: commands to the browser, fd 4: its replies and events.
|
|
5164
6660
|
stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"],
|
|
5165
6661
|
windowsHide: true
|
|
@@ -5276,7 +6772,7 @@ function remoteDebuggingFree(args) {
|
|
|
5276
6772
|
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5277
6773
|
function killTree(child) {
|
|
5278
6774
|
if (process.platform === "win32" && child.pid)
|
|
5279
|
-
(0,
|
|
6775
|
+
(0, import_node_child_process2.spawnSync)("taskkill", ["/T", "/F", "/PID", String(child.pid)], {
|
|
5280
6776
|
windowsHide: true
|
|
5281
6777
|
});
|
|
5282
6778
|
else
|
|
@@ -5284,18 +6780,19 @@ function killTree(child) {
|
|
|
5284
6780
|
}
|
|
5285
6781
|
|
|
5286
6782
|
// src/daemon.ts
|
|
5287
|
-
var
|
|
5288
|
-
var
|
|
5289
|
-
var
|
|
5290
|
-
var
|
|
6783
|
+
var import_node_child_process3 = require("node:child_process");
|
|
6784
|
+
var import_node_crypto4 = require("node:crypto");
|
|
6785
|
+
var import_node_fs5 = require("node:fs");
|
|
6786
|
+
var import_node_path5 = require("node:path");
|
|
5291
6787
|
|
|
5292
6788
|
// src/session.ts
|
|
5293
|
-
var
|
|
6789
|
+
var import_node_crypto3 = require("node:crypto");
|
|
5294
6790
|
var import_node_events3 = require("node:events");
|
|
5295
6791
|
|
|
5296
6792
|
// src/page-link.ts
|
|
5297
6793
|
var import_node_events2 = require("node:events");
|
|
5298
6794
|
var HELLO_TIMEOUT_MS = 15e3;
|
|
6795
|
+
var AUTH_TIMEOUT_MS = 1e4;
|
|
5299
6796
|
var CANCEL_GRACE_MS = 5e3;
|
|
5300
6797
|
function maxPartBytes(meta) {
|
|
5301
6798
|
return Math.ceil((meta.seconds + 1) * meta.sampleRate) * 2 * 4 + 1024 * 1024;
|
|
@@ -5349,6 +6846,16 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5349
6846
|
drain;
|
|
5350
6847
|
closedReason;
|
|
5351
6848
|
readyPromise;
|
|
6849
|
+
/** The token ('' = anonymous) of the last `auth` the page took. */
|
|
6850
|
+
authKey;
|
|
6851
|
+
authStatus;
|
|
6852
|
+
/** The page refused `auth` itself: stop sending it. */
|
|
6853
|
+
authUnsupported = false;
|
|
6854
|
+
/** The page's last `error auth` message. */
|
|
6855
|
+
authError;
|
|
6856
|
+
/** Tokens the page refused -> its message. */
|
|
6857
|
+
refused = /* @__PURE__ */ new Map();
|
|
6858
|
+
authWait;
|
|
5352
6859
|
get open() {
|
|
5353
6860
|
return this.closedReason === void 0;
|
|
5354
6861
|
}
|
|
@@ -5360,6 +6867,73 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5360
6867
|
get busy() {
|
|
5361
6868
|
return !!this.current || !!this.drain;
|
|
5362
6869
|
}
|
|
6870
|
+
/**
|
|
6871
|
+
* Sends `auth` unless this socket already carries the same token, and
|
|
6872
|
+
* waits for the page's verdict. A token the page refuses is dropped: the
|
|
6873
|
+
* socket signs in anonymously and the status says why. Never rejects.
|
|
6874
|
+
*/
|
|
6875
|
+
async authenticate(auth, timeoutMs = AUTH_TIMEOUT_MS) {
|
|
6876
|
+
if (this.authUnsupported)
|
|
6877
|
+
return { signedIn: false, unsupported: true };
|
|
6878
|
+
const refused = auth.accessToken && this.refused.get(auth.accessToken);
|
|
6879
|
+
if (refused) {
|
|
6880
|
+
const { accessToken: _dropped2, ...anonymous2 } = auth;
|
|
6881
|
+
return {
|
|
6882
|
+
...await this.authenticate(anonymous2, timeoutMs),
|
|
6883
|
+
rejected: refused
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
const key = auth.accessToken ?? "";
|
|
6887
|
+
if (this.authKey === key && this.authStatus)
|
|
6888
|
+
return this.authStatus;
|
|
6889
|
+
this.authError = void 0;
|
|
6890
|
+
const answer = await this.sendAuth(auth, timeoutMs);
|
|
6891
|
+
if (answer !== "hello") {
|
|
6892
|
+
if (answer !== "closed") {
|
|
6893
|
+
this.authUnsupported = true;
|
|
6894
|
+
this.log(
|
|
6895
|
+
"warn",
|
|
6896
|
+
answer === "protocol" ? "the page does not know auth: rendering anonymously" : "the page did not answer auth: rendering anonymously"
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
return { signedIn: false, unsupported: true };
|
|
6900
|
+
}
|
|
6901
|
+
const signedIn = !!this.hello?.signedIn;
|
|
6902
|
+
if (!auth.accessToken || signedIn) {
|
|
6903
|
+
this.authKey = key;
|
|
6904
|
+
return this.authStatus = { signedIn };
|
|
6905
|
+
}
|
|
6906
|
+
const rejected = this.authError ?? "The page did not accept the sign-in.";
|
|
6907
|
+
this.log("warn", `page refused the sign-in: ${rejected}`);
|
|
6908
|
+
this.refused.set(auth.accessToken, rejected);
|
|
6909
|
+
const { accessToken: _dropped, ...anonymous } = auth;
|
|
6910
|
+
const again = await this.sendAuth(anonymous, timeoutMs);
|
|
6911
|
+
if (again === "hello") {
|
|
6912
|
+
this.authKey = "";
|
|
6913
|
+
this.authStatus = { signedIn: !!this.hello?.signedIn };
|
|
6914
|
+
}
|
|
6915
|
+
return { signedIn: again === "hello" && !!this.hello?.signedIn, rejected };
|
|
6916
|
+
}
|
|
6917
|
+
sendAuth(auth, timeoutMs) {
|
|
6918
|
+
if (!this.open)
|
|
6919
|
+
return Promise.resolve("closed");
|
|
6920
|
+
return new Promise((resolve3) => {
|
|
6921
|
+
const done = (answer) => {
|
|
6922
|
+
clearTimeout(timer);
|
|
6923
|
+
this.off("close", onClose);
|
|
6924
|
+
this.authWait = void 0;
|
|
6925
|
+
resolve3(answer);
|
|
6926
|
+
};
|
|
6927
|
+
const onClose = () => done("closed");
|
|
6928
|
+
const timer = setTimeout(() => done("timeout"), timeoutMs);
|
|
6929
|
+
this.once("close", onClose);
|
|
6930
|
+
this.authWait = (answer) => {
|
|
6931
|
+
if (answer !== "auth")
|
|
6932
|
+
done(answer);
|
|
6933
|
+
};
|
|
6934
|
+
this.send({ type: "auth", ...auth });
|
|
6935
|
+
});
|
|
6936
|
+
}
|
|
5363
6937
|
/** Runs one command. Never rejects: failures are an `error` outcome. */
|
|
5364
6938
|
async run(command, options) {
|
|
5365
6939
|
if (!this.open)
|
|
@@ -5475,8 +7049,13 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5475
7049
|
}
|
|
5476
7050
|
onMessage(message) {
|
|
5477
7051
|
if (message.type === "hello") {
|
|
5478
|
-
if (this.hello)
|
|
7052
|
+
if (this.hello) {
|
|
7053
|
+
if (message.protocol === BRIDGE_PROTOCOL) {
|
|
7054
|
+
this.hello = message;
|
|
7055
|
+
this.authWait?.("hello");
|
|
7056
|
+
}
|
|
5479
7057
|
return;
|
|
7058
|
+
}
|
|
5480
7059
|
if (message.protocol !== BRIDGE_PROTOCOL) {
|
|
5481
7060
|
const why = `The page speaks bridge protocol ${message.protocol}, this tutticode speaks ${BRIDGE_PROTOCOL}: update tutticode (npm i -g tutticode@latest).`;
|
|
5482
7061
|
this.emit("bad-hello", why);
|
|
@@ -5496,6 +7075,20 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5496
7075
|
return;
|
|
5497
7076
|
}
|
|
5498
7077
|
const running = this.current;
|
|
7078
|
+
if (message.type === "error" && id === void 0 && message.code === "auth") {
|
|
7079
|
+
this.log("warn", `page error auth: ${message.message}`);
|
|
7080
|
+
this.authError = message.message;
|
|
7081
|
+
if (this.authKey) {
|
|
7082
|
+
this.authKey = void 0;
|
|
7083
|
+
this.authStatus = void 0;
|
|
7084
|
+
}
|
|
7085
|
+
this.authWait?.("auth");
|
|
7086
|
+
return;
|
|
7087
|
+
}
|
|
7088
|
+
if (message.type === "error" && id === void 0 && this.authWait) {
|
|
7089
|
+
this.log("warn", `page error ${message.code}: ${message.message}`);
|
|
7090
|
+
return this.authWait("protocol");
|
|
7091
|
+
}
|
|
5499
7092
|
if (message.type === "error" && id === void 0) {
|
|
5500
7093
|
this.log("warn", `page error ${message.code}: ${message.message}`);
|
|
5501
7094
|
return this.fail(message.code, message.message, message.report);
|
|
@@ -5531,7 +7124,8 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5531
7124
|
kind: "error",
|
|
5532
7125
|
code: message.code,
|
|
5533
7126
|
message: message.message,
|
|
5534
|
-
...message.report ? { report: message.report } : {}
|
|
7127
|
+
...message.report ? { report: message.report } : {},
|
|
7128
|
+
...message.limit ? { limit: message.limit } : {}
|
|
5535
7129
|
});
|
|
5536
7130
|
case "done": {
|
|
5537
7131
|
if (!running.meta)
|
|
@@ -5663,7 +7257,13 @@ var Session = class extends import_node_events3.EventEmitter {
|
|
|
5663
7257
|
const job = this.queue.then(async () => {
|
|
5664
7258
|
try {
|
|
5665
7259
|
const link = await this.ensurePage(options.signal);
|
|
5666
|
-
|
|
7260
|
+
if (!options.auth)
|
|
7261
|
+
return await link.run(command, options);
|
|
7262
|
+
const auth = await link.authenticate(
|
|
7263
|
+
options.auth,
|
|
7264
|
+
this.options.authTimeoutMs
|
|
7265
|
+
);
|
|
7266
|
+
return { ...await link.run(command, options), auth };
|
|
5667
7267
|
} catch (e) {
|
|
5668
7268
|
if (e instanceof BridgeError)
|
|
5669
7269
|
return { kind: "error", code: e.code, message: e.message };
|
|
@@ -5811,14 +7411,14 @@ Page console:
|
|
|
5811
7411
|
// src/daemon.ts
|
|
5812
7412
|
function daemonKey(config) {
|
|
5813
7413
|
const { verbose: _verbose, ...identity } = config;
|
|
5814
|
-
return (0,
|
|
7414
|
+
return (0, import_node_crypto4.createHash)("sha256").update(
|
|
5815
7415
|
JSON.stringify({
|
|
5816
7416
|
...identity,
|
|
5817
7417
|
devOrigins: [...config.devOrigins ?? []].sort()
|
|
5818
7418
|
})
|
|
5819
7419
|
).digest("hex").slice(0, 12);
|
|
5820
7420
|
}
|
|
5821
|
-
var daemonFile = (key) => (0,
|
|
7421
|
+
var daemonFile = (key) => (0, import_node_path5.join)(stateDir(), `daemon-${key}.json`);
|
|
5822
7422
|
async function runDaemon(config, log) {
|
|
5823
7423
|
const controlToken = newToken();
|
|
5824
7424
|
let active = 0;
|
|
@@ -5846,22 +7446,22 @@ async function runDaemon(config, log) {
|
|
|
5846
7446
|
const session = new Session(options);
|
|
5847
7447
|
const port = await session.start();
|
|
5848
7448
|
const key = daemonKey(config);
|
|
5849
|
-
const
|
|
7449
|
+
const file2 = {
|
|
5850
7450
|
pid: process.pid,
|
|
5851
7451
|
port,
|
|
5852
7452
|
token: controlToken,
|
|
5853
|
-
version:
|
|
7453
|
+
version: VERSION2
|
|
5854
7454
|
};
|
|
5855
|
-
(0,
|
|
7455
|
+
(0, import_node_fs5.writeFileSync)(daemonFile(key), JSON.stringify(file2), { mode: 384 });
|
|
5856
7456
|
log("info", `daemon ${key} listening on 127.0.0.1:${port} (${config.mode})`);
|
|
5857
7457
|
const shutdown = async (why) => {
|
|
5858
7458
|
log("info", `daemon exiting: ${why}`);
|
|
5859
7459
|
try {
|
|
5860
7460
|
const current = JSON.parse(
|
|
5861
|
-
(0,
|
|
7461
|
+
(0, import_node_fs5.readFileSync)(daemonFile(key), "utf8")
|
|
5862
7462
|
);
|
|
5863
7463
|
if (current.pid === process.pid)
|
|
5864
|
-
(0,
|
|
7464
|
+
(0, import_node_fs5.rmSync)(daemonFile(key), { force: true });
|
|
5865
7465
|
} catch {
|
|
5866
7466
|
}
|
|
5867
7467
|
await session.close();
|
|
@@ -5882,14 +7482,14 @@ function serveControl(socket, session, setStatusSink) {
|
|
|
5882
7482
|
socket.send(JSON.stringify(value));
|
|
5883
7483
|
};
|
|
5884
7484
|
socket.once("message", async (data) => {
|
|
5885
|
-
let
|
|
7485
|
+
let request2;
|
|
5886
7486
|
try {
|
|
5887
|
-
|
|
7487
|
+
request2 = JSON.parse(String(data));
|
|
5888
7488
|
} catch {
|
|
5889
7489
|
return socket.close(1008, "bad request");
|
|
5890
7490
|
}
|
|
5891
7491
|
setStatusSink((message) => send({ type: "status", message }));
|
|
5892
|
-
if (
|
|
7492
|
+
if (request2.type === "info") {
|
|
5893
7493
|
try {
|
|
5894
7494
|
await session.ensurePage(abort.signal);
|
|
5895
7495
|
send({
|
|
@@ -5912,28 +7512,33 @@ function serveControl(socket, session, setStatusSink) {
|
|
|
5912
7512
|
}
|
|
5913
7513
|
return socket.close(1e3);
|
|
5914
7514
|
}
|
|
5915
|
-
if (
|
|
7515
|
+
if (request2.type !== "run" || !request2.command)
|
|
5916
7516
|
return socket.close(1008, "bad request");
|
|
5917
|
-
const outcome = await session.run(
|
|
5918
|
-
timeoutMs:
|
|
7517
|
+
const outcome = await session.run(request2.command, {
|
|
7518
|
+
timeoutMs: request2.timeoutMs ?? 6e5,
|
|
5919
7519
|
signal: abort.signal,
|
|
5920
|
-
onProgress: (progress) => send(progress)
|
|
7520
|
+
onProgress: (progress) => send(progress),
|
|
7521
|
+
...request2.auth ? { auth: request2.auth } : {}
|
|
5921
7522
|
});
|
|
5922
7523
|
if (outcome.kind === "rendered") {
|
|
5923
|
-
const id =
|
|
7524
|
+
const id = request2.command.type === "open" ? "open" : request2.command.id;
|
|
5924
7525
|
for (const [part, bytes] of outcome.parts)
|
|
5925
7526
|
for (const frame of frameParts(id, part, bytes))
|
|
5926
7527
|
socket.send(frame);
|
|
5927
7528
|
send({
|
|
5928
7529
|
type: "result",
|
|
5929
|
-
outcome: {
|
|
7530
|
+
outcome: {
|
|
7531
|
+
kind: "rendered",
|
|
7532
|
+
meta: outcome.meta,
|
|
7533
|
+
...outcome.auth ? { auth: outcome.auth } : {}
|
|
7534
|
+
}
|
|
5930
7535
|
});
|
|
5931
7536
|
} else
|
|
5932
7537
|
send({ type: "result", outcome });
|
|
5933
7538
|
socket.close(1e3);
|
|
5934
7539
|
});
|
|
5935
7540
|
}
|
|
5936
|
-
async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
|
|
7541
|
+
async function runViaDaemon(options, command, timeoutMs, callbacks = {}, auth) {
|
|
5937
7542
|
let socket;
|
|
5938
7543
|
try {
|
|
5939
7544
|
socket = await connectDaemon(options);
|
|
@@ -5942,7 +7547,7 @@ async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
|
|
|
5942
7547
|
}
|
|
5943
7548
|
return controlCall(
|
|
5944
7549
|
socket,
|
|
5945
|
-
{ type: "run", command, timeoutMs },
|
|
7550
|
+
{ type: "run", command, timeoutMs, ...auth ? { auth } : {} },
|
|
5946
7551
|
callbacks
|
|
5947
7552
|
);
|
|
5948
7553
|
}
|
|
@@ -5955,7 +7560,7 @@ async function daemonInfo(options, callbacks = {}) {
|
|
|
5955
7560
|
}
|
|
5956
7561
|
return controlCall(socket, { type: "info" }, callbacks);
|
|
5957
7562
|
}
|
|
5958
|
-
function controlCall(socket,
|
|
7563
|
+
function controlCall(socket, request2, callbacks) {
|
|
5959
7564
|
return new Promise((resolve3) => {
|
|
5960
7565
|
const assembler = new PartAssembler();
|
|
5961
7566
|
const parts = /* @__PURE__ */ new Map();
|
|
@@ -6014,7 +7619,7 @@ function controlCall(socket, request, callbacks) {
|
|
|
6014
7619
|
})
|
|
6015
7620
|
);
|
|
6016
7621
|
socket.on("error", () => void 0);
|
|
6017
|
-
socket.send(JSON.stringify(
|
|
7622
|
+
socket.send(JSON.stringify(request2));
|
|
6018
7623
|
});
|
|
6019
7624
|
}
|
|
6020
7625
|
function openControlSocket(port, token, timeoutMs = 3e3) {
|
|
@@ -6031,7 +7636,7 @@ function openControlSocket(port, token, timeoutMs = 3e3) {
|
|
|
6031
7636
|
}
|
|
6032
7637
|
function readDaemonFile(key) {
|
|
6033
7638
|
try {
|
|
6034
|
-
return JSON.parse((0,
|
|
7639
|
+
return JSON.parse((0, import_node_fs5.readFileSync)(daemonFile(key), "utf8"));
|
|
6035
7640
|
} catch {
|
|
6036
7641
|
return void 0;
|
|
6037
7642
|
}
|
|
@@ -6043,7 +7648,7 @@ async function connectDaemon({
|
|
|
6043
7648
|
}) {
|
|
6044
7649
|
const key = daemonKey(config);
|
|
6045
7650
|
const existing = readDaemonFile(key);
|
|
6046
|
-
if (existing && existing.version ===
|
|
7651
|
+
if (existing && existing.version === VERSION2) {
|
|
6047
7652
|
try {
|
|
6048
7653
|
return await openControlSocket(existing.port, existing.token);
|
|
6049
7654
|
} catch {
|
|
@@ -6053,11 +7658,11 @@ async function connectDaemon({
|
|
|
6053
7658
|
);
|
|
6054
7659
|
}
|
|
6055
7660
|
}
|
|
6056
|
-
const lock = (0,
|
|
6057
|
-
const owner = (0,
|
|
6058
|
-
if (
|
|
7661
|
+
const lock = (0, import_node_path5.join)(stateDir(), `daemon-${key}.lock`);
|
|
7662
|
+
const owner = (0, import_node_crypto4.randomUUID)();
|
|
7663
|
+
if (takeLock2(lock, owner)) {
|
|
6059
7664
|
try {
|
|
6060
|
-
const child = (0,
|
|
7665
|
+
const child = (0, import_node_child_process3.spawn)(
|
|
6061
7666
|
process.execPath,
|
|
6062
7667
|
[
|
|
6063
7668
|
entry,
|
|
@@ -6070,19 +7675,19 @@ async function connectDaemon({
|
|
|
6070
7675
|
log("info", `spawned daemon ${key} (pid ${child.pid})`);
|
|
6071
7676
|
return await waitForDaemon(key, existing?.pid, child.pid);
|
|
6072
7677
|
} finally {
|
|
6073
|
-
(0,
|
|
7678
|
+
(0, import_node_fs5.rmSync)(lock, { force: true });
|
|
6074
7679
|
}
|
|
6075
7680
|
}
|
|
6076
7681
|
return waitForDaemon(key, existing?.pid);
|
|
6077
7682
|
}
|
|
6078
|
-
function
|
|
7683
|
+
function takeLock2(lock, owner) {
|
|
6079
7684
|
try {
|
|
6080
|
-
const age = Date.now() - ((0,
|
|
7685
|
+
const age = Date.now() - ((0, import_node_fs5.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs ?? 0);
|
|
6081
7686
|
if (age > 2e4)
|
|
6082
|
-
(0,
|
|
6083
|
-
const fd = (0,
|
|
6084
|
-
(0,
|
|
6085
|
-
(0,
|
|
7687
|
+
(0, import_node_fs5.rmSync)(lock, { force: true });
|
|
7688
|
+
const fd = (0, import_node_fs5.openSync)(lock, "wx");
|
|
7689
|
+
(0, import_node_fs5.writeFileSync)(fd, owner);
|
|
7690
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
6086
7691
|
return true;
|
|
6087
7692
|
} catch {
|
|
6088
7693
|
return false;
|
|
@@ -6091,10 +7696,10 @@ function takeLock(lock, owner) {
|
|
|
6091
7696
|
async function waitForDaemon(key, stalePid, newPid) {
|
|
6092
7697
|
const deadline = Date.now() + 15e3;
|
|
6093
7698
|
while (Date.now() < deadline) {
|
|
6094
|
-
const
|
|
6095
|
-
if (
|
|
7699
|
+
const file2 = readDaemonFile(key);
|
|
7700
|
+
if (file2 && file2.pid !== stalePid && (newPid === void 0 || file2.pid === newPid) && file2.version === VERSION2) {
|
|
6096
7701
|
try {
|
|
6097
|
-
return await openControlSocket(
|
|
7702
|
+
return await openControlSocket(file2.port, file2.token);
|
|
6098
7703
|
} catch {
|
|
6099
7704
|
}
|
|
6100
7705
|
}
|
|
@@ -6105,6 +7710,370 @@ async function waitForDaemon(key, stalePid, newPid) {
|
|
|
6105
7710
|
);
|
|
6106
7711
|
}
|
|
6107
7712
|
|
|
7713
|
+
// src/login.ts
|
|
7714
|
+
var import_node_child_process4 = require("node:child_process");
|
|
7715
|
+
var import_node_crypto5 = require("node:crypto");
|
|
7716
|
+
var import_node_fs6 = require("node:fs");
|
|
7717
|
+
var import_node_http2 = require("node:http");
|
|
7718
|
+
var import_node_path6 = require("node:path");
|
|
7719
|
+
var LoginError = class extends Error {
|
|
7720
|
+
constructor(reason, message) {
|
|
7721
|
+
super(message);
|
|
7722
|
+
this.reason = reason;
|
|
7723
|
+
}
|
|
7724
|
+
};
|
|
7725
|
+
var LOOPBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
7726
|
+
function sitePage(origin, locale, path) {
|
|
7727
|
+
return `${origin}${locale === "en" ? "" : `/${locale}`}${path}`;
|
|
7728
|
+
}
|
|
7729
|
+
function pkcePair() {
|
|
7730
|
+
const verifier = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
|
|
7731
|
+
const challenge = (0, import_node_crypto5.createHash)("sha256").update(verifier).digest("base64url");
|
|
7732
|
+
return { verifier, challenge };
|
|
7733
|
+
}
|
|
7734
|
+
function loginUrl(options) {
|
|
7735
|
+
const url = new URL(
|
|
7736
|
+
sitePage(options.origin, options.locale, TUTTICODE_OAUTH_PATHS.login)
|
|
7737
|
+
);
|
|
7738
|
+
url.search = new URLSearchParams({
|
|
7739
|
+
client_id: TUTTICODE_CLIENT_ID,
|
|
7740
|
+
redirect_uri: options.redirectUri,
|
|
7741
|
+
code_challenge: options.challenge,
|
|
7742
|
+
code_challenge_method: "S256",
|
|
7743
|
+
state: options.state,
|
|
7744
|
+
scope: TUTTICODE_SCOPE,
|
|
7745
|
+
resource: tutticodeResourceUrl(options.origin),
|
|
7746
|
+
device_name: options.deviceName
|
|
7747
|
+
}).toString();
|
|
7748
|
+
return url.toString();
|
|
7749
|
+
}
|
|
7750
|
+
var page = (title, text) => `<!doctype html><meta charset="utf-8"><title>${title}</title><p>${text}</p>`;
|
|
7751
|
+
async function startLoopback(options) {
|
|
7752
|
+
let resolveCode;
|
|
7753
|
+
let rejectCode;
|
|
7754
|
+
const code = new Promise((resolve3, reject) => {
|
|
7755
|
+
resolveCode = resolve3;
|
|
7756
|
+
rejectCode = reject;
|
|
7757
|
+
});
|
|
7758
|
+
code.catch(() => void 0);
|
|
7759
|
+
let settled = false;
|
|
7760
|
+
const finish = (fn) => {
|
|
7761
|
+
if (settled)
|
|
7762
|
+
return;
|
|
7763
|
+
settled = true;
|
|
7764
|
+
clearTimeout(timer);
|
|
7765
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
7766
|
+
fn();
|
|
7767
|
+
setImmediate(() => server.close());
|
|
7768
|
+
};
|
|
7769
|
+
const server = (0, import_node_http2.createServer)((req, res) => {
|
|
7770
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
7771
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
7772
|
+
res.writeHead(404).end();
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
const params = url.searchParams;
|
|
7776
|
+
if (settled || params.get("state") !== options.state) {
|
|
7777
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(
|
|
7778
|
+
page(
|
|
7779
|
+
"TuttiCode",
|
|
7780
|
+
"This sign-in link does not belong to the running tutticode login. Run tutticode login again."
|
|
7781
|
+
)
|
|
7782
|
+
);
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7785
|
+
const error = params.get("error");
|
|
7786
|
+
if (error) {
|
|
7787
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in cancelled. You can close this tab."));
|
|
7788
|
+
finish(
|
|
7789
|
+
() => rejectCode(
|
|
7790
|
+
error === "access_denied" ? new LoginError(
|
|
7791
|
+
"denied",
|
|
7792
|
+
"The sign-in was cancelled in the browser."
|
|
7793
|
+
) : new LoginError("server", `The sign-in failed (${error}).`)
|
|
7794
|
+
)
|
|
7795
|
+
);
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
if (normalizeOrigin(params.get("iss") ?? "") !== options.issuer) {
|
|
7799
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in refused: unexpected issuer."));
|
|
7800
|
+
finish(
|
|
7801
|
+
() => rejectCode(
|
|
7802
|
+
new LoginError(
|
|
7803
|
+
"issuer",
|
|
7804
|
+
`The sign-in came back from ${params.get("iss") ?? "an unnamed issuer"}, not ${options.issuer}.`
|
|
7805
|
+
)
|
|
7806
|
+
)
|
|
7807
|
+
);
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
const value = params.get("code");
|
|
7811
|
+
if (!value) {
|
|
7812
|
+
res.writeHead(400).end();
|
|
7813
|
+
return;
|
|
7814
|
+
}
|
|
7815
|
+
res.writeHead(302, { Location: options.doneUrl }).end();
|
|
7816
|
+
finish(() => resolveCode(value));
|
|
7817
|
+
});
|
|
7818
|
+
const onAbort = () => finish(() => rejectCode(new LoginError("cancelled", "Sign-in cancelled.")));
|
|
7819
|
+
const timer = setTimeout(
|
|
7820
|
+
() => finish(
|
|
7821
|
+
() => rejectCode(
|
|
7822
|
+
new LoginError(
|
|
7823
|
+
"timeout",
|
|
7824
|
+
`No answer from the browser within ${Math.round(
|
|
7825
|
+
(options.timeoutMs ?? LOOPBACK_TIMEOUT_MS) / 6e4
|
|
7826
|
+
)} minutes.`
|
|
7827
|
+
)
|
|
7828
|
+
)
|
|
7829
|
+
),
|
|
7830
|
+
options.timeoutMs ?? LOOPBACK_TIMEOUT_MS
|
|
7831
|
+
);
|
|
7832
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
7833
|
+
await new Promise((resolve3, reject) => {
|
|
7834
|
+
server.once("error", reject);
|
|
7835
|
+
server.listen(0, "127.0.0.1", resolve3);
|
|
7836
|
+
});
|
|
7837
|
+
const port = server.address().port;
|
|
7838
|
+
return {
|
|
7839
|
+
port,
|
|
7840
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
7841
|
+
code,
|
|
7842
|
+
close: () => finish(() => rejectCode(new LoginError("cancelled", "Closed.")))
|
|
7843
|
+
};
|
|
7844
|
+
}
|
|
7845
|
+
async function loopbackLogin(options) {
|
|
7846
|
+
const { verifier, challenge } = pkcePair();
|
|
7847
|
+
const state = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
|
|
7848
|
+
const loopback = await startLoopback({
|
|
7849
|
+
state,
|
|
7850
|
+
issuer: options.origin,
|
|
7851
|
+
doneUrl: sitePage(options.origin, options.locale, "/code/login/done"),
|
|
7852
|
+
timeoutMs: options.timeoutMs,
|
|
7853
|
+
signal: options.signal
|
|
7854
|
+
});
|
|
7855
|
+
const url = loginUrl({
|
|
7856
|
+
origin: options.origin,
|
|
7857
|
+
locale: options.locale,
|
|
7858
|
+
redirectUri: loopback.redirectUri,
|
|
7859
|
+
challenge,
|
|
7860
|
+
state,
|
|
7861
|
+
deviceName: options.deviceName
|
|
7862
|
+
});
|
|
7863
|
+
options.onUrl?.(url);
|
|
7864
|
+
try {
|
|
7865
|
+
await options.openBrowser(url);
|
|
7866
|
+
} catch {
|
|
7867
|
+
}
|
|
7868
|
+
const code = await loopback.code;
|
|
7869
|
+
return exchange(options.origin, options, {
|
|
7870
|
+
grant_type: "authorization_code",
|
|
7871
|
+
code,
|
|
7872
|
+
code_verifier: verifier,
|
|
7873
|
+
redirect_uri: loopback.redirectUri,
|
|
7874
|
+
resource: tutticodeResourceUrl(options.origin)
|
|
7875
|
+
});
|
|
7876
|
+
}
|
|
7877
|
+
async function exchange(origin, deps, params) {
|
|
7878
|
+
try {
|
|
7879
|
+
return tokenResponse(
|
|
7880
|
+
await postOAuth(origin, TUTTICODE_OAUTH_PATHS.token, params, deps)
|
|
7881
|
+
);
|
|
7882
|
+
} catch (e) {
|
|
7883
|
+
if (e instanceof OAuthError)
|
|
7884
|
+
throw new LoginError("server", `The sign-in failed: ${e.message}`);
|
|
7885
|
+
throw e;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
async function startDeviceLogin(origin, deviceName2, deps = {}) {
|
|
7889
|
+
let body;
|
|
7890
|
+
try {
|
|
7891
|
+
body = await postOAuth(
|
|
7892
|
+
origin,
|
|
7893
|
+
TUTTICODE_OAUTH_PATHS.deviceAuthorization,
|
|
7894
|
+
{
|
|
7895
|
+
scope: TUTTICODE_SCOPE,
|
|
7896
|
+
resource: tutticodeResourceUrl(origin),
|
|
7897
|
+
device_name: deviceName2
|
|
7898
|
+
},
|
|
7899
|
+
deps
|
|
7900
|
+
);
|
|
7901
|
+
} catch (e) {
|
|
7902
|
+
if (e instanceof OAuthError)
|
|
7903
|
+
throw new LoginError(
|
|
7904
|
+
"server",
|
|
7905
|
+
e.error === "slow_down" ? "Too many sign-in attempts from this network: try again in an hour." : `The sign-in could not start: ${e.message}`
|
|
7906
|
+
);
|
|
7907
|
+
throw e;
|
|
7908
|
+
}
|
|
7909
|
+
if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof body.verification_uri !== "string")
|
|
7910
|
+
throw new LoginError("server", "The server sent no device code.");
|
|
7911
|
+
return {
|
|
7912
|
+
deviceCode: body.device_code,
|
|
7913
|
+
userCode: body.user_code,
|
|
7914
|
+
verificationUri: body.verification_uri,
|
|
7915
|
+
...typeof body.verification_uri_complete === "string" ? { verificationUriComplete: body.verification_uri_complete } : {},
|
|
7916
|
+
expiresIn: positive(body.expires_in, 600),
|
|
7917
|
+
interval: positive(body.interval, 5)
|
|
7918
|
+
};
|
|
7919
|
+
}
|
|
7920
|
+
var positive = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
|
|
7921
|
+
var SLOW_DOWN_STEP_S = 5;
|
|
7922
|
+
async function pollDeviceToken(origin, device, options = {}) {
|
|
7923
|
+
const now = options.now ?? Date.now;
|
|
7924
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
|
|
7925
|
+
const deadline = now() + device.expiresIn * 1e3;
|
|
7926
|
+
let interval = device.interval;
|
|
7927
|
+
for (; ; ) {
|
|
7928
|
+
await sleep(interval * 1e3);
|
|
7929
|
+
if (options.signal?.aborted)
|
|
7930
|
+
throw new LoginError("cancelled", "Sign-in cancelled.");
|
|
7931
|
+
if (options.stillWanted && !options.stillWanted())
|
|
7932
|
+
throw new LoginError("cancelled", "A newer sign-in replaced this one.");
|
|
7933
|
+
if (now() >= deadline)
|
|
7934
|
+
throw new LoginError(
|
|
7935
|
+
"expired",
|
|
7936
|
+
"The code expired before it was approved: run tutticode login --device again."
|
|
7937
|
+
);
|
|
7938
|
+
try {
|
|
7939
|
+
return tokenResponse(
|
|
7940
|
+
await postOAuth(
|
|
7941
|
+
origin,
|
|
7942
|
+
TUTTICODE_OAUTH_PATHS.token,
|
|
7943
|
+
{
|
|
7944
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
7945
|
+
device_code: device.deviceCode
|
|
7946
|
+
},
|
|
7947
|
+
options
|
|
7948
|
+
)
|
|
7949
|
+
);
|
|
7950
|
+
} catch (e) {
|
|
7951
|
+
if (!(e instanceof OAuthError))
|
|
7952
|
+
continue;
|
|
7953
|
+
switch (e.error) {
|
|
7954
|
+
case "authorization_pending":
|
|
7955
|
+
continue;
|
|
7956
|
+
case "slow_down": {
|
|
7957
|
+
const named = Number(e.body.interval);
|
|
7958
|
+
interval = Number.isFinite(named) && named > interval ? named : interval + SLOW_DOWN_STEP_S;
|
|
7959
|
+
continue;
|
|
7960
|
+
}
|
|
7961
|
+
case "access_denied":
|
|
7962
|
+
throw new LoginError("denied", "The sign-in was declined.");
|
|
7963
|
+
case "expired_token":
|
|
7964
|
+
throw new LoginError(
|
|
7965
|
+
"expired",
|
|
7966
|
+
"The code expired before it was approved: run tutticode login --device again."
|
|
7967
|
+
);
|
|
7968
|
+
default:
|
|
7969
|
+
throw new LoginError("server", `The sign-in failed: ${e.message}`);
|
|
7970
|
+
}
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
async function completeLogin(origin, tokens, deviceName2, deps = {}) {
|
|
7975
|
+
const now = (deps.now ?? Date.now)();
|
|
7976
|
+
const previous = readAccount(origin);
|
|
7977
|
+
try {
|
|
7978
|
+
await saveAccount(origin, storedAccount(tokens, now, { deviceName: deviceName2 }));
|
|
7979
|
+
} catch (e) {
|
|
7980
|
+
await revokeToken(origin, tokens.refresh_token, {
|
|
7981
|
+
...deps,
|
|
7982
|
+
timeoutMs: 5e3
|
|
7983
|
+
});
|
|
7984
|
+
throw e;
|
|
7985
|
+
}
|
|
7986
|
+
const state = await accountState(origin, deps, { envToken: false });
|
|
7987
|
+
if (state.state === "signed-in") {
|
|
7988
|
+
const me = state.me;
|
|
7989
|
+
const current = readAccount(origin);
|
|
7990
|
+
if (current?.accessToken === tokens.access_token)
|
|
7991
|
+
await saveAccount(origin, {
|
|
7992
|
+
...current,
|
|
7993
|
+
userId: me.user.id,
|
|
7994
|
+
email: me.user.email,
|
|
7995
|
+
nickname: me.user.nickname,
|
|
7996
|
+
plan: me.plan,
|
|
7997
|
+
...me.expiresAt ? { grantExpiresAt: me.expiresAt } : {},
|
|
7998
|
+
...me.deviceName ? { deviceName: me.deviceName } : {}
|
|
7999
|
+
});
|
|
8000
|
+
}
|
|
8001
|
+
if (previous && previous.refreshToken !== tokens.refresh_token)
|
|
8002
|
+
await revokeToken(origin, previous.refreshToken, {
|
|
8003
|
+
...deps,
|
|
8004
|
+
timeoutMs: 5e3
|
|
8005
|
+
});
|
|
8006
|
+
return state;
|
|
8007
|
+
}
|
|
8008
|
+
function pendingLoginPath(origin) {
|
|
8009
|
+
const key = (0, import_node_crypto5.createHash)("sha256").update(origin).digest("hex").slice(0, 12);
|
|
8010
|
+
return (0, import_node_path6.join)(stateDir(), `login-pending-${key}.json`);
|
|
8011
|
+
}
|
|
8012
|
+
function writePendingLogin(pending) {
|
|
8013
|
+
const path = pendingLoginPath(pending.origin);
|
|
8014
|
+
writePrivateFile(path, JSON.stringify(pending));
|
|
8015
|
+
return path;
|
|
8016
|
+
}
|
|
8017
|
+
function readPendingLogin(path) {
|
|
8018
|
+
try {
|
|
8019
|
+
const value = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
|
|
8020
|
+
return typeof value.deviceCode === "string" && typeof value.origin === "string" ? value : void 0;
|
|
8021
|
+
} catch {
|
|
8022
|
+
return void 0;
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
function pendingLogin(origin, now = Date.now()) {
|
|
8026
|
+
const pending = readPendingLogin(pendingLoginPath(origin));
|
|
8027
|
+
return pending && Date.parse(pending.expiresAt) > now ? pending : void 0;
|
|
8028
|
+
}
|
|
8029
|
+
function spawnLoginPoller(entry, file2) {
|
|
8030
|
+
const child = (0, import_node_child_process4.spawn)(process.execPath, [entry, "__login-poll", file2], {
|
|
8031
|
+
detached: true,
|
|
8032
|
+
stdio: "ignore",
|
|
8033
|
+
windowsHide: true
|
|
8034
|
+
});
|
|
8035
|
+
child.unref();
|
|
8036
|
+
}
|
|
8037
|
+
async function runLoginPoller(file2, log, deps = {}) {
|
|
8038
|
+
const pending = readPendingLogin(file2);
|
|
8039
|
+
if (!pending) {
|
|
8040
|
+
log("warn", "login poller: no pending sign-in");
|
|
8041
|
+
return false;
|
|
8042
|
+
}
|
|
8043
|
+
const remaining = Math.max(
|
|
8044
|
+
1,
|
|
8045
|
+
Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)
|
|
8046
|
+
);
|
|
8047
|
+
try {
|
|
8048
|
+
const tokens = await pollDeviceToken(
|
|
8049
|
+
pending.origin,
|
|
8050
|
+
{
|
|
8051
|
+
deviceCode: pending.deviceCode,
|
|
8052
|
+
expiresIn: remaining,
|
|
8053
|
+
interval: pending.interval
|
|
8054
|
+
},
|
|
8055
|
+
{
|
|
8056
|
+
...deps,
|
|
8057
|
+
stillWanted: () => readPendingLogin(file2)?.deviceCode === pending.deviceCode
|
|
8058
|
+
}
|
|
8059
|
+
);
|
|
8060
|
+
const state = await completeLogin(
|
|
8061
|
+
pending.origin,
|
|
8062
|
+
tokens,
|
|
8063
|
+
pending.deviceName,
|
|
8064
|
+
deps
|
|
8065
|
+
);
|
|
8066
|
+
log("info", `login poller: signed in (${state.state})`);
|
|
8067
|
+
return true;
|
|
8068
|
+
} catch (e) {
|
|
8069
|
+
log("info", `login poller: ${e.message}`);
|
|
8070
|
+
return false;
|
|
8071
|
+
} finally {
|
|
8072
|
+
if (readPendingLogin(file2)?.deviceCode === pending.deviceCode)
|
|
8073
|
+
(0, import_node_fs6.rmSync)(file2, { force: true });
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
|
|
6108
8077
|
// src/mcp.ts
|
|
6109
8078
|
var import_node_readline = require("node:readline");
|
|
6110
8079
|
|
|
@@ -6114,13 +8083,13 @@ var JSON_RPC_INVALID_REQUEST = -32600;
|
|
|
6114
8083
|
var JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
6115
8084
|
var JSON_RPC_INVALID_PARAMS = -32602;
|
|
6116
8085
|
var JSON_RPC_INTERNAL_ERROR = -32603;
|
|
6117
|
-
var
|
|
8086
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6118
8087
|
var isId2 = (v) => typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
6119
8088
|
function classifyMessage(raw) {
|
|
6120
|
-
if (!
|
|
8089
|
+
if (!isObject3(raw) || raw.jsonrpc !== "2.0") {
|
|
6121
8090
|
return {
|
|
6122
8091
|
kind: "invalid",
|
|
6123
|
-
id:
|
|
8092
|
+
id: isObject3(raw) && isId2(raw.id) ? raw.id : null,
|
|
6124
8093
|
reason: "Not a JSON-RPC 2.0 message"
|
|
6125
8094
|
};
|
|
6126
8095
|
}
|
|
@@ -6134,7 +8103,7 @@ function classifyMessage(raw) {
|
|
|
6134
8103
|
reason: "Missing method"
|
|
6135
8104
|
};
|
|
6136
8105
|
}
|
|
6137
|
-
if (raw.params !== void 0 && !
|
|
8106
|
+
if (raw.params !== void 0 && !isObject3(raw.params)) {
|
|
6138
8107
|
return {
|
|
6139
8108
|
kind: "invalid",
|
|
6140
8109
|
id: isId2(raw.id) ? raw.id : null,
|
|
@@ -6317,22 +8286,22 @@ function createMcpServer(options) {
|
|
|
6317
8286
|
}
|
|
6318
8287
|
|
|
6319
8288
|
// src/reference.ts
|
|
6320
|
-
var
|
|
6321
|
-
var
|
|
8289
|
+
var import_node_fs7 = require("node:fs");
|
|
8290
|
+
var import_node_path7 = require("node:path");
|
|
6322
8291
|
var URI_PREFIX = "tutticode://skills/";
|
|
6323
8292
|
var cache;
|
|
6324
8293
|
function loadReference(dir = skillsDir()) {
|
|
6325
8294
|
if (cache?.dir === dir)
|
|
6326
8295
|
return cache.docs;
|
|
6327
8296
|
const docs = [];
|
|
6328
|
-
if ((0,
|
|
6329
|
-
for (const skill of (0,
|
|
6330
|
-
const root = (0,
|
|
6331
|
-
if (!(0,
|
|
8297
|
+
if ((0, import_node_fs7.existsSync)(dir))
|
|
8298
|
+
for (const skill of (0, import_node_fs7.readdirSync)(dir).sort()) {
|
|
8299
|
+
const root = (0, import_node_path7.join)(dir, skill);
|
|
8300
|
+
if (!(0, import_node_fs7.statSync)(root).isDirectory() || !(0, import_node_fs7.existsSync)((0, import_node_path7.join)(root, "SKILL.md")))
|
|
6332
8301
|
continue;
|
|
6333
|
-
for (const
|
|
6334
|
-
const path = (0,
|
|
6335
|
-
const text = (0,
|
|
8302
|
+
for (const file2 of walk(root)) {
|
|
8303
|
+
const path = (0, import_node_path7.relative)(root, file2).split(import_node_path7.sep).join("/");
|
|
8304
|
+
const text = (0, import_node_fs7.readFileSync)(file2, "utf8").replace(/\r\n/g, "\n");
|
|
6336
8305
|
docs.push({
|
|
6337
8306
|
uri: `${URI_PREFIX}${skill}/${path}`,
|
|
6338
8307
|
skill,
|
|
@@ -6346,9 +8315,9 @@ function loadReference(dir = skillsDir()) {
|
|
|
6346
8315
|
return docs;
|
|
6347
8316
|
}
|
|
6348
8317
|
function walk(dir) {
|
|
6349
|
-
return (0,
|
|
6350
|
-
const path = (0,
|
|
6351
|
-
if ((0,
|
|
8318
|
+
return (0, import_node_fs7.readdirSync)(dir).sort().flatMap((name) => {
|
|
8319
|
+
const path = (0, import_node_path7.join)(dir, name);
|
|
8320
|
+
if ((0, import_node_fs7.statSync)(path).isDirectory())
|
|
6352
8321
|
return walk(path);
|
|
6353
8322
|
return name.endsWith(".md") ? [path] : [];
|
|
6354
8323
|
});
|
|
@@ -6428,8 +8397,8 @@ function count(haystack, needle) {
|
|
|
6428
8397
|
}
|
|
6429
8398
|
|
|
6430
8399
|
// src/tools.ts
|
|
6431
|
-
var
|
|
6432
|
-
var
|
|
8400
|
+
var import_node_os4 = require("node:os");
|
|
8401
|
+
var import_node_path8 = require("node:path");
|
|
6433
8402
|
|
|
6434
8403
|
// ../mcp-protocol/src/code-tools.ts
|
|
6435
8404
|
var CODE_CHECK_TOOL = "code_check";
|
|
@@ -6986,7 +8955,9 @@ function codeFragment(code) {
|
|
|
6986
8955
|
// src/tools.ts
|
|
6987
8956
|
var CODE_OPEN_TOOL = "code_open";
|
|
6988
8957
|
var CODE_REFERENCE_TOOL = "code_reference";
|
|
6989
|
-
var
|
|
8958
|
+
var CODE_ACCOUNT_TOOL = "code_account";
|
|
8959
|
+
var BACKEND_NOTE = "Runs locally: tutticode renders in your own browser with the tuttidaw.com/code engine (free).";
|
|
8960
|
+
var LIMITS_NOTE = `Once TuttiDAW switches its limits on (being rolled out; until then nothing is cut or tagged), renders without sign-in are limited to ${DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds} s (error code "limit") and renders over ${DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds} s carry a spoken "register at TuttiDAW" tag, reported as \`watermark\`: it is not part of the program and is in the stats; never change the code to remove it, tell the user once that \`tutticode login\` (free) removes it.`;
|
|
6990
8961
|
var codeProperty2 = codeCheckTool.inputSchema.properties.code;
|
|
6991
8962
|
var renderProperties2 = {
|
|
6992
8963
|
code: codeProperty2,
|
|
@@ -7064,6 +9035,10 @@ var renderProperties2 = {
|
|
|
7064
9035
|
stemsDir: {
|
|
7065
9036
|
type: "string",
|
|
7066
9037
|
description: "Folder for the stems (default: next to out, <name>.stems)."
|
|
9038
|
+
},
|
|
9039
|
+
brandTag: {
|
|
9040
|
+
type: "boolean",
|
|
9041
|
+
description: 'false: no "Made with TuttiCode" metadata tag in the files. Honoured only when signed in (default true).'
|
|
7067
9042
|
}
|
|
7068
9043
|
};
|
|
7069
9044
|
var localCheckTool = {
|
|
@@ -7073,7 +9048,7 @@ var localCheckTool = {
|
|
|
7073
9048
|
var localRenderTool = {
|
|
7074
9049
|
name: "code_render",
|
|
7075
9050
|
title: "Render TuttiCode to a file",
|
|
7076
|
-
description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${BACKEND_NOTE}`,
|
|
9051
|
+
description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${LIMITS_NOTE} ${BACKEND_NOTE}`,
|
|
7077
9052
|
inputSchema: {
|
|
7078
9053
|
type: "object",
|
|
7079
9054
|
properties: renderProperties2,
|
|
@@ -7152,14 +9127,30 @@ var codeReferenceTool = {
|
|
|
7152
9127
|
openWorldHint: false
|
|
7153
9128
|
}
|
|
7154
9129
|
};
|
|
9130
|
+
var codeAccountTool = {
|
|
9131
|
+
name: CODE_ACCOUNT_TOOL,
|
|
9132
|
+
title: "TuttiCode sign-in and limits",
|
|
9133
|
+
description: "Who renders and with which limits: signedIn, plan, limits.maxSeconds (longest render) and limits.watermarkAfterSeconds (renders longer than this carry a spoken tag; null = never), usageToday, and when not signed in a loginHint. Use it to plan within the limits (keep an anonymous draft at or under watermarkAfterSeconds). Never starts a sign-in: to sign in, run the loginHint command and ask the user to approve the code it prints.",
|
|
9134
|
+
inputSchema: {
|
|
9135
|
+
type: "object",
|
|
9136
|
+
properties: {},
|
|
9137
|
+
additionalProperties: false
|
|
9138
|
+
},
|
|
9139
|
+
annotations: {
|
|
9140
|
+
readOnlyHint: true,
|
|
9141
|
+
idempotentHint: true,
|
|
9142
|
+
openWorldHint: true
|
|
9143
|
+
}
|
|
9144
|
+
};
|
|
7155
9145
|
var LOCAL_TOOLS = [
|
|
7156
9146
|
localCheckTool,
|
|
7157
9147
|
localRenderTool,
|
|
7158
9148
|
localCompareTool,
|
|
7159
9149
|
codeOpenTool,
|
|
7160
|
-
codeReferenceTool
|
|
9150
|
+
codeReferenceTool,
|
|
9151
|
+
codeAccountTool
|
|
7161
9152
|
];
|
|
7162
|
-
var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW.`;
|
|
9153
|
+
var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW. code_account says whether renders are signed in and their limits. On a "limit" error, tell the user its message in one sentence and carry on within the limit; a \`watermark\` in a report is the anonymous voice tag, not part of the program: do not change the code to remove it.`;
|
|
7163
9154
|
var MAX_STORED = 64;
|
|
7164
9155
|
var CHECK_TIMEOUT_MS = 9e4;
|
|
7165
9156
|
var RENDER_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -7178,7 +9169,7 @@ function createToolHandler(deps) {
|
|
|
7178
9169
|
async function check2(args, signal) {
|
|
7179
9170
|
const command = {
|
|
7180
9171
|
type: "check",
|
|
7181
|
-
id: (0,
|
|
9172
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
7182
9173
|
code: args.code,
|
|
7183
9174
|
...args.bpm !== void 0 ? { bpm: args.bpm } : {}
|
|
7184
9175
|
};
|
|
@@ -7210,7 +9201,7 @@ function createToolHandler(deps) {
|
|
|
7210
9201
|
`format ${String(args.format)} does not match out ${out}.`
|
|
7211
9202
|
);
|
|
7212
9203
|
const format = args.format ?? outFormat ?? "wav";
|
|
7213
|
-
const id = (0,
|
|
9204
|
+
const id = (0, import_node_crypto3.randomUUID)();
|
|
7214
9205
|
const length = lengths.length ? { [lengths[0]]: args[lengths[0]] } : void 0;
|
|
7215
9206
|
const command = {
|
|
7216
9207
|
type: "render",
|
|
@@ -7230,7 +9221,8 @@ function createToolHandler(deps) {
|
|
|
7230
9221
|
"tail",
|
|
7231
9222
|
"lufs",
|
|
7232
9223
|
"truePeak",
|
|
7233
|
-
"limit"
|
|
9224
|
+
"limit",
|
|
9225
|
+
"brandTag"
|
|
7234
9226
|
])
|
|
7235
9227
|
if (args[key] !== void 0)
|
|
7236
9228
|
command[key] = args[key];
|
|
@@ -7244,8 +9236,8 @@ function createToolHandler(deps) {
|
|
|
7244
9236
|
});
|
|
7245
9237
|
if (outcome.kind !== "rendered")
|
|
7246
9238
|
return errorResult(outcome);
|
|
7247
|
-
const path = out ?? (0,
|
|
7248
|
-
deps.outDir ?? (0,
|
|
9239
|
+
const path = out ?? (0, import_node_path8.join)(
|
|
9240
|
+
deps.outDir ?? (0, import_node_path8.join)((0, import_node_os4.tmpdir)(), "tutticode"),
|
|
7249
9241
|
`render-${id.slice(0, 8)}.${format}`
|
|
7250
9242
|
);
|
|
7251
9243
|
const files = writeRender(
|
|
@@ -7254,10 +9246,15 @@ function createToolHandler(deps) {
|
|
|
7254
9246
|
path,
|
|
7255
9247
|
typeof args.stemsDir === "string" ? args.stemsDir : void 0
|
|
7256
9248
|
);
|
|
7257
|
-
remember(id, {
|
|
9249
|
+
remember(id, {
|
|
9250
|
+
meta: outcome.meta,
|
|
9251
|
+
analysis: outcome.meta.analysis,
|
|
9252
|
+
watermark: outcome.meta.watermark
|
|
9253
|
+
});
|
|
7258
9254
|
const { meta } = outcome;
|
|
7259
9255
|
return {
|
|
7260
9256
|
id,
|
|
9257
|
+
watermarked: !!meta.watermark,
|
|
7261
9258
|
report: {
|
|
7262
9259
|
id,
|
|
7263
9260
|
kind: "render",
|
|
@@ -7285,7 +9282,9 @@ function createToolHandler(deps) {
|
|
|
7285
9282
|
beatTimes: meta.beatTimes,
|
|
7286
9283
|
check: meta.check,
|
|
7287
9284
|
...meta.analysis ? { analysis: meta.analysis } : {},
|
|
7288
|
-
engineVersion: meta.engineVersion
|
|
9285
|
+
engineVersion: meta.engineVersion,
|
|
9286
|
+
...meta.watermark ? { watermark: meta.watermark, watermarkNote: WATERMARK_LINE } : {},
|
|
9287
|
+
...signInNote(outcome.auth)
|
|
7289
9288
|
}
|
|
7290
9289
|
};
|
|
7291
9290
|
}
|
|
@@ -7325,9 +9324,11 @@ function createToolHandler(deps) {
|
|
|
7325
9324
|
return invalid("Give goals or againstRenderId.");
|
|
7326
9325
|
let analysis;
|
|
7327
9326
|
let rendered;
|
|
9327
|
+
let tagged = target === "render" && !!renders.get(String(args.againstRenderId))?.watermark;
|
|
7328
9328
|
if (args.renderId !== void 0) {
|
|
7329
9329
|
if (args.code !== void 0)
|
|
7330
9330
|
return invalid("Give renderId or code, not both.");
|
|
9331
|
+
tagged ||= !!renders.get(String(args.renderId))?.watermark;
|
|
7331
9332
|
analysis = renders.get(String(args.renderId))?.analysis;
|
|
7332
9333
|
if (!analysis)
|
|
7333
9334
|
return invalid(
|
|
@@ -7345,16 +9346,34 @@ function createToolHandler(deps) {
|
|
|
7345
9346
|
rendered = await render2(renderArgs, signal);
|
|
7346
9347
|
if (!("report" in rendered))
|
|
7347
9348
|
return rendered;
|
|
9349
|
+
tagged ||= rendered.watermarked;
|
|
7348
9350
|
analysis = rendered.report.analysis;
|
|
7349
9351
|
if (!analysis)
|
|
7350
9352
|
return invalid("The render came back without an analysis.");
|
|
7351
9353
|
} else
|
|
7352
9354
|
return invalid("Give renderId or code.");
|
|
7353
9355
|
const result = compareAnalysis(analysis, goals, target);
|
|
9356
|
+
const note = tagged ? { watermarkNote: WATERMARK_LINE } : {};
|
|
7354
9357
|
return textResult(
|
|
7355
|
-
rendered && "report" in rendered ? { ...rendered.report, compare: result } : { id: args.renderId, compare: result }
|
|
9358
|
+
rendered && "report" in rendered ? { ...rendered.report, compare: result, ...note } : { id: args.renderId, compare: result, ...note }
|
|
7356
9359
|
);
|
|
7357
9360
|
}
|
|
9361
|
+
async function account() {
|
|
9362
|
+
const state = deps.account ? await deps.account() : { state: "anonymous" };
|
|
9363
|
+
const signedIn = state.state === "signed-in";
|
|
9364
|
+
return textResult({
|
|
9365
|
+
signedIn,
|
|
9366
|
+
tokenKind: signedIn ? state.tokenKind : "anonymous",
|
|
9367
|
+
...signedIn ? {
|
|
9368
|
+
plan: state.me.plan,
|
|
9369
|
+
usageToday: state.me.usageToday,
|
|
9370
|
+
expiresAt: state.me.expiresAt
|
|
9371
|
+
} : {},
|
|
9372
|
+
limits: accountLimits(state),
|
|
9373
|
+
...signedIn ? {} : { loginHint: LOGIN_HINT },
|
|
9374
|
+
...state.state === "rejected" || state.state === "unreachable" ? { problem: state.message } : {}
|
|
9375
|
+
});
|
|
9376
|
+
}
|
|
7358
9377
|
return async function callTool(name, args, signal) {
|
|
7359
9378
|
switch (name) {
|
|
7360
9379
|
case "code_check":
|
|
@@ -7389,6 +9408,8 @@ function createToolHandler(deps) {
|
|
|
7389
9408
|
}
|
|
7390
9409
|
case CODE_REFERENCE_TOOL:
|
|
7391
9410
|
return reference(args);
|
|
9411
|
+
case CODE_ACCOUNT_TOOL:
|
|
9412
|
+
return account();
|
|
7392
9413
|
default:
|
|
7393
9414
|
return invalid(`Unknown tool ${name}.`);
|
|
7394
9415
|
}
|
|
@@ -7422,12 +9443,26 @@ function errorResult(outcome) {
|
|
|
7422
9443
|
return invalid(`Unexpected ${outcome.kind}.`);
|
|
7423
9444
|
return textResult(
|
|
7424
9445
|
{
|
|
7425
|
-
error: {
|
|
7426
|
-
|
|
9446
|
+
error: {
|
|
9447
|
+
code: outcome.code,
|
|
9448
|
+
message: outcome.message,
|
|
9449
|
+
...outcome.limit ? { limit: outcome.limit } : {}
|
|
9450
|
+
},
|
|
9451
|
+
...outcome.code === "limit" ? { loginHint: LOGIN_HINT } : {},
|
|
9452
|
+
...outcome.report ? { report: outcome.report } : {},
|
|
9453
|
+
...signInNote(outcome.auth)
|
|
7427
9454
|
},
|
|
7428
9455
|
true
|
|
7429
9456
|
);
|
|
7430
9457
|
}
|
|
9458
|
+
function signInNote(auth) {
|
|
9459
|
+
return auth?.rejected ? {
|
|
9460
|
+
signIn: {
|
|
9461
|
+
rejected: auth.rejected,
|
|
9462
|
+
note: "This ran without sign-in. Tell the user to run `tutticode login` again."
|
|
9463
|
+
}
|
|
9464
|
+
} : {};
|
|
9465
|
+
}
|
|
7431
9466
|
var GOAL_KEYS = {
|
|
7432
9467
|
style: (v) => typeof v === "string" && v.length > 0 && v.length <= 64,
|
|
7433
9468
|
lufs: (v) => num(v, -70, 0),
|
|
@@ -7469,10 +9504,11 @@ function serveMcpStdio(options) {
|
|
|
7469
9504
|
backend: options.backend,
|
|
7470
9505
|
styles: options.styles,
|
|
7471
9506
|
codeLink: options.codeLink,
|
|
7472
|
-
canShow: options.canShow
|
|
9507
|
+
canShow: options.canShow,
|
|
9508
|
+
account: options.account
|
|
7473
9509
|
});
|
|
7474
9510
|
const server = createMcpServer({
|
|
7475
|
-
serverInfo: { name: "tutticode", title: "TuttiCode", version:
|
|
9511
|
+
serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION2 },
|
|
7476
9512
|
instructions: MCP_INSTRUCTIONS,
|
|
7477
9513
|
listTools: () => [...LOCAL_TOOLS],
|
|
7478
9514
|
callTool: (name, args, ctx) => callTool(name, args, ctx.signal),
|
|
@@ -7510,7 +9546,7 @@ function serveMcpStdio(options) {
|
|
|
7510
9546
|
});
|
|
7511
9547
|
});
|
|
7512
9548
|
}
|
|
7513
|
-
function sessionBackend(create) {
|
|
9549
|
+
function sessionBackend(create, auth) {
|
|
7514
9550
|
const sessions = /* @__PURE__ */ new Map();
|
|
7515
9551
|
const get = (mode) => {
|
|
7516
9552
|
let session = sessions.get(mode);
|
|
@@ -7521,21 +9557,57 @@ function sessionBackend(create) {
|
|
|
7521
9557
|
return session;
|
|
7522
9558
|
};
|
|
7523
9559
|
return {
|
|
7524
|
-
backend: (command, { mode, signal, timeoutMs }) =>
|
|
9560
|
+
backend: async (command, { mode, signal, timeoutMs }) => {
|
|
9561
|
+
const signIn = await auth?.();
|
|
9562
|
+
const outcome = await get(mode).run(command, {
|
|
9563
|
+
signal,
|
|
9564
|
+
timeoutMs,
|
|
9565
|
+
...signIn ? { auth: signIn.auth } : {}
|
|
9566
|
+
});
|
|
9567
|
+
return signIn?.ended ? {
|
|
9568
|
+
...outcome,
|
|
9569
|
+
auth: { signedIn: false, ...outcome.auth, rejected: signIn.ended }
|
|
9570
|
+
} : outcome;
|
|
9571
|
+
},
|
|
7525
9572
|
close: async () => {
|
|
7526
9573
|
await Promise.all([...sessions.values()].map((s) => s.close()));
|
|
7527
9574
|
}
|
|
7528
9575
|
};
|
|
7529
9576
|
}
|
|
7530
9577
|
|
|
9578
|
+
// src/notices.ts
|
|
9579
|
+
var import_node_fs8 = require("node:fs");
|
|
9580
|
+
var import_node_path9 = require("node:path");
|
|
9581
|
+
var file = () => (0, import_node_path9.join)(stateDir(), "notices.json");
|
|
9582
|
+
function read() {
|
|
9583
|
+
try {
|
|
9584
|
+
const value = JSON.parse((0, import_node_fs8.readFileSync)(file(), "utf8"));
|
|
9585
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
9586
|
+
} catch {
|
|
9587
|
+
return {};
|
|
9588
|
+
}
|
|
9589
|
+
}
|
|
9590
|
+
function firstTime(key, period, now = /* @__PURE__ */ new Date()) {
|
|
9591
|
+
const shown = read();
|
|
9592
|
+
const today = now.toISOString().slice(0, 10);
|
|
9593
|
+
const last = shown[key];
|
|
9594
|
+
if (last && (period === "install" || last === today))
|
|
9595
|
+
return false;
|
|
9596
|
+
try {
|
|
9597
|
+
(0, import_node_fs8.writeFileSync)(file(), JSON.stringify({ ...shown, [key]: today }));
|
|
9598
|
+
} catch {
|
|
9599
|
+
}
|
|
9600
|
+
return true;
|
|
9601
|
+
}
|
|
9602
|
+
|
|
7531
9603
|
// src/setup.ts
|
|
7532
|
-
var
|
|
7533
|
-
var
|
|
7534
|
-
var
|
|
9604
|
+
var import_node_child_process5 = require("node:child_process");
|
|
9605
|
+
var import_node_fs9 = require("node:fs");
|
|
9606
|
+
var import_node_path10 = require("node:path");
|
|
7535
9607
|
var POLICY_NAME = "LocalNetworkAccessAllowedForUrls";
|
|
7536
9608
|
var POLICY_URL = "https://tuttidaw.com";
|
|
7537
9609
|
var exec = (command, args) => {
|
|
7538
|
-
const r2 = (0,
|
|
9610
|
+
const r2 = (0, import_node_child_process5.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
|
|
7539
9611
|
return { status: r2.status, stdout: r2.stdout ?? "" };
|
|
7540
9612
|
};
|
|
7541
9613
|
var WINDOWS_KEYS = [
|
|
@@ -7650,16 +9722,16 @@ function policySteps(undo, platform = process.platform, run = exec) {
|
|
|
7650
9722
|
};
|
|
7651
9723
|
});
|
|
7652
9724
|
return LINUX_POLICY_FILES.map(
|
|
7653
|
-
(
|
|
7654
|
-
describe: `delete ${
|
|
9725
|
+
(file2) => undo ? {
|
|
9726
|
+
describe: `delete ${file2}`,
|
|
7655
9727
|
run() {
|
|
7656
|
-
(0,
|
|
9728
|
+
(0, import_node_fs9.rmSync)(file2, { force: true });
|
|
7657
9729
|
}
|
|
7658
9730
|
} : {
|
|
7659
|
-
describe: `write ${
|
|
9731
|
+
describe: `write ${file2} (${POLICY_NAME}: ["${POLICY_URL}"])`,
|
|
7660
9732
|
run() {
|
|
7661
|
-
(0,
|
|
7662
|
-
(0,
|
|
9733
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path10.dirname)(file2), { recursive: true });
|
|
9734
|
+
(0, import_node_fs9.writeFileSync)(file2, linuxPolicyJson());
|
|
7663
9735
|
}
|
|
7664
9736
|
}
|
|
7665
9737
|
);
|
|
@@ -7675,22 +9747,22 @@ for your user. It allows nothing else, and you can see it at chrome://policy
|
|
|
7675
9747
|
(edge://policy). Undo any time with: tutticode setup --undo`;
|
|
7676
9748
|
|
|
7677
9749
|
// src/skills.ts
|
|
7678
|
-
var
|
|
7679
|
-
var
|
|
7680
|
-
var
|
|
7681
|
-
function targetDir(target, home = (0,
|
|
7682
|
-
return (0,
|
|
9750
|
+
var import_node_fs10 = require("node:fs");
|
|
9751
|
+
var import_node_os5 = require("node:os");
|
|
9752
|
+
var import_node_path11 = require("node:path");
|
|
9753
|
+
function targetDir(target, home = (0, import_node_os5.homedir)()) {
|
|
9754
|
+
return (0, import_node_path11.join)(home, `.${target}`, "skills");
|
|
7683
9755
|
}
|
|
7684
9756
|
function shippedSkills(dir = skillsDir()) {
|
|
7685
|
-
if (!(0,
|
|
9757
|
+
if (!(0, import_node_fs10.existsSync)(dir))
|
|
7686
9758
|
return [];
|
|
7687
|
-
return (0,
|
|
7688
|
-
(name) => (0,
|
|
9759
|
+
return (0, import_node_fs10.readdirSync)(dir).filter(
|
|
9760
|
+
(name) => (0, import_node_fs10.statSync)((0, import_node_path11.join)(dir, name)).isDirectory() && (0, import_node_fs10.existsSync)((0, import_node_path11.join)(dir, name, "SKILL.md"))
|
|
7689
9761
|
).sort();
|
|
7690
9762
|
}
|
|
7691
|
-
function defaultTargets(home = (0,
|
|
9763
|
+
function defaultTargets(home = (0, import_node_os5.homedir)()) {
|
|
7692
9764
|
const found = ["claude", "copilot", "codex"].filter(
|
|
7693
|
-
(t) => (0,
|
|
9765
|
+
(t) => (0, import_node_fs10.existsSync)((0, import_node_path11.join)(home, `.${t}`))
|
|
7694
9766
|
);
|
|
7695
9767
|
return found.length ? [...found] : ["claude"];
|
|
7696
9768
|
}
|
|
@@ -7701,9 +9773,9 @@ function installSkills(destinations, source = skillsDir()) {
|
|
|
7701
9773
|
const installed = [];
|
|
7702
9774
|
for (const destination of destinations) {
|
|
7703
9775
|
for (const skill of skills) {
|
|
7704
|
-
const path = (0,
|
|
7705
|
-
(0,
|
|
7706
|
-
(0,
|
|
9776
|
+
const path = (0, import_node_path11.resolve)(destination, skill);
|
|
9777
|
+
(0, import_node_fs10.rmSync)(path, { recursive: true, force: true });
|
|
9778
|
+
(0, import_node_fs10.cpSync)((0, import_node_path11.join)(source, skill), path, { recursive: true });
|
|
7707
9779
|
installed.push({ skill, path });
|
|
7708
9780
|
}
|
|
7709
9781
|
}
|
|
@@ -7946,7 +10018,19 @@ var afrobeats = {
|
|
|
7946
10018
|
p90: [-8.9, -1.9, -2.9, -4.9, -6.9, -8.9, -9.9, -11.9, -12.9, -17.9],
|
|
7947
10019
|
source: TONAL_BALANCE_SOURCE
|
|
7948
10020
|
},
|
|
7949
|
-
sidechain:
|
|
10021
|
+
sidechain: {
|
|
10022
|
+
ducks: [
|
|
10023
|
+
{
|
|
10024
|
+
trigger: "kick",
|
|
10025
|
+
targets: ["bass"],
|
|
10026
|
+
ratio: 3,
|
|
10027
|
+
attackMs: 1,
|
|
10028
|
+
releaseMs: 80,
|
|
10029
|
+
depthDb: 2
|
|
10030
|
+
}
|
|
10031
|
+
],
|
|
10032
|
+
note: "Clears the kick, no pumping; keep the bounce."
|
|
10033
|
+
},
|
|
7950
10034
|
reverb: "EQ Eight into light Compressor on vocal; Reverb short room for percussion and a darker plate for voice.",
|
|
7951
10035
|
delay: "Echo dotted eighth on guitar or phrase endings; filter repeats away from the lead vocal.",
|
|
7952
10036
|
stereo: "Center kick, bass and lead vocal; spread the guitar and shaker without widening the sub.",
|
|
@@ -8731,7 +10815,19 @@ var boomBap = {
|
|
|
8731
10815
|
p90: [-12.2, -3.2, -2.2, -4.2, -6.2, -8.2, -9.2, -12.2, -16.2, -23.2],
|
|
8732
10816
|
source: TONAL_BALANCE_SOURCE
|
|
8733
10817
|
},
|
|
8734
|
-
sidechain:
|
|
10818
|
+
sidechain: {
|
|
10819
|
+
ducks: [
|
|
10820
|
+
{
|
|
10821
|
+
trigger: "kick",
|
|
10822
|
+
targets: ["bass"],
|
|
10823
|
+
ratio: 3,
|
|
10824
|
+
attackMs: 1,
|
|
10825
|
+
releaseMs: 70,
|
|
10826
|
+
depthDb: 2
|
|
10827
|
+
}
|
|
10828
|
+
],
|
|
10829
|
+
note: "Clears the kick, no EDM pumping."
|
|
10830
|
+
},
|
|
8735
10831
|
reverb: "Redux lightly on the sample, Saturator for body, EQ Eight to trim hiss; short Reverb on snare, rap mostly dry.",
|
|
8736
10832
|
delay: "Echo throws on selected rhyme endings, not across the whole verse.",
|
|
8737
10833
|
stereo: "Centered kick/snare/bass/vocal; sample can be narrow with wider hook answers.",
|
|
@@ -9238,11 +11334,11 @@ var cinematic = {
|
|
|
9238
11334
|
p90: [-11.4, -4.4, -3.4, -3.4, -5.4, -7.4, -8.4, -10.4, -12.4, -18.4],
|
|
9239
11335
|
source: TONAL_BALANCE_SOURCE
|
|
9240
11336
|
},
|
|
9241
|
-
sidechain:
|
|
11337
|
+
sidechain: null,
|
|
9242
11338
|
reverb: "EQ Eight before Hybrid Reverb keeps the hall clear; different predelays establish near piano and distant orchestra.",
|
|
9243
11339
|
delay: "Echo can extend a pulse, but sync it so the ostinato does not become an accidental flam.",
|
|
9244
11340
|
stereo: "Strings and upper brass broad, low drums/bass centered; keep dialogue space in the middle when scoring picture.",
|
|
9245
|
-
lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the
|
|
11341
|
+
lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Thin pad/sub under impacts, no dance pumping. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the brief."
|
|
9246
11342
|
},
|
|
9247
11343
|
arrangement: {
|
|
9248
11344
|
variationEveryBars: 4,
|
|
@@ -10593,7 +12689,7 @@ var disco = {
|
|
|
10593
12689
|
p90: [-12.1, -3.1, -2.1, -5.1, -7.1, -8.1, -9.1, -10.1, -11.1, -17.1],
|
|
10594
12690
|
source: TONAL_BALANCE_SOURCE
|
|
10595
12691
|
},
|
|
10596
|
-
sidechain:
|
|
12692
|
+
sidechain: null,
|
|
10597
12693
|
reverb: "Reverb plate on strings and vocal, small room on drums.",
|
|
10598
12694
|
delay: "Quiet Echo slap on vocal; avoid busy feedback against guitar sixteenths.",
|
|
10599
12695
|
stereo: "Strings wide, rhythm guitar to one side with keys answering; bass and kick centered.",
|
|
@@ -10912,14 +13008,34 @@ var drumAndBass = {
|
|
|
10912
13008
|
"Climbing Keys G Minor 140 bpm"
|
|
10913
13009
|
],
|
|
10914
13010
|
mix: {
|
|
10915
|
-
lufs: { min: -
|
|
13011
|
+
lufs: { min: -9, max: -7 },
|
|
10916
13012
|
tonalBalance: {
|
|
10917
13013
|
p10: [-14.5, -9, -9.5, -12.5, -15, -16, -16.5, -17, -19, -25.5],
|
|
10918
13014
|
p50: [-9.5, -5, -6.5, -9.5, -12, -13, -13.5, -14, -15, -20.5],
|
|
10919
13015
|
p90: [-4.5, -1, -3.5, -6.5, -9, -10, -10.5, -11, -11, -15.5],
|
|
10920
13016
|
source: TONAL_BALANCE_SOURCE
|
|
10921
13017
|
},
|
|
10922
|
-
sidechain:
|
|
13018
|
+
sidechain: {
|
|
13019
|
+
ducks: [
|
|
13020
|
+
{
|
|
13021
|
+
trigger: "kick",
|
|
13022
|
+
targets: ["bass", "pad"],
|
|
13023
|
+
ratio: 4,
|
|
13024
|
+
attackMs: 0.5,
|
|
13025
|
+
releaseMs: 100,
|
|
13026
|
+
depthDb: 4
|
|
13027
|
+
},
|
|
13028
|
+
{
|
|
13029
|
+
trigger: "snare",
|
|
13030
|
+
targets: ["pad"],
|
|
13031
|
+
ratio: 3,
|
|
13032
|
+
attackMs: 0.5,
|
|
13033
|
+
releaseMs: 120,
|
|
13034
|
+
depthDb: 3
|
|
13035
|
+
}
|
|
13036
|
+
],
|
|
13037
|
+
note: "Pads duck to kick and snare; the sub only to the kick."
|
|
13038
|
+
},
|
|
10923
13039
|
reverb: "Short, bright snare reverb; big wash on breakdown pads/vocals.",
|
|
10924
13040
|
delay: "1/4 or 1/8 dotted on vocals; delay throws into drops.",
|
|
10925
13041
|
stereo: "Sub and kick mono, reese mids wide, breaks slightly wide, pads very wide.",
|
|
@@ -11194,7 +13310,7 @@ var dubstep = {
|
|
|
11194
13310
|
},
|
|
11195
13311
|
starterClips: ["Dub Halftime 136 bpm", "Halftime Pattern 136 bpm"],
|
|
11196
13312
|
mix: {
|
|
11197
|
-
lufs: { min: -10, max: -
|
|
13313
|
+
lufs: { min: -10, max: -8 },
|
|
11198
13314
|
tonalBalance: {
|
|
11199
13315
|
p10: [
|
|
11200
13316
|
-11.1,
|
|
@@ -11212,7 +13328,19 @@ var dubstep = {
|
|
|
11212
13328
|
p90: [-1.1, -0.1, -5.1, -9.1, -11.1, -11.1, -11.1, -13.1, -15.1, -21.1],
|
|
11213
13329
|
source: TONAL_BALANCE_SOURCE
|
|
11214
13330
|
},
|
|
11215
|
-
sidechain:
|
|
13331
|
+
sidechain: {
|
|
13332
|
+
ducks: [
|
|
13333
|
+
{
|
|
13334
|
+
trigger: "kick",
|
|
13335
|
+
targets: ["bass"],
|
|
13336
|
+
ratio: 4,
|
|
13337
|
+
attackMs: 0.5,
|
|
13338
|
+
releaseMs: 60,
|
|
13339
|
+
depthDb: 4
|
|
13340
|
+
}
|
|
13341
|
+
],
|
|
13342
|
+
note: "The sub ducks briefly for the kick only."
|
|
13343
|
+
},
|
|
11216
13344
|
reverb: "Dark Reverb on snare and isolated bells; no reverb on the clean sub.",
|
|
11217
13345
|
delay: "Dub-style Echo throws into the gaps, filtered and automated before the next bass call.",
|
|
11218
13346
|
stereo: "Sub and kick mono; widen only the upper bass layer and atmospheric answers.",
|
|
@@ -11857,7 +13985,7 @@ var funk = {
|
|
|
11857
13985
|
p90: [-13.5, -3.5, -2.5, -4.5, -6.5, -7.5, -8.5, -10.5, -12.5, -19.5],
|
|
11858
13986
|
source: TONAL_BALANCE_SOURCE
|
|
11859
13987
|
},
|
|
11860
|
-
sidechain:
|
|
13988
|
+
sidechain: null,
|
|
11861
13989
|
reverb: "Short Reverb room; keep guitar scratches and bass dry.",
|
|
11862
13990
|
delay: "Brief Echo slap on a horn or vocal answer only.",
|
|
11863
13991
|
stereo: "Bass, snare and lead centered; guitar and clav answer from opposite sides.",
|
|
@@ -11874,7 +14002,7 @@ var funk = {
|
|
|
11874
14002
|
{
|
|
11875
14003
|
kind: "breakdown",
|
|
11876
14004
|
where: "Middle",
|
|
11877
|
-
how: "Expose drums and bass; reintroduce clav before horns."
|
|
14005
|
+
how: "Expose the drums (and bass, in a bass feature); reintroduce clav before horns."
|
|
11878
14006
|
},
|
|
11879
14007
|
{
|
|
11880
14008
|
kind: "variation",
|
|
@@ -12120,7 +14248,7 @@ var futureBass = {
|
|
|
12120
14248
|
},
|
|
12121
14249
|
starterClips: [],
|
|
12122
14250
|
mix: {
|
|
12123
|
-
lufs: { min: -10, max: -
|
|
14251
|
+
lufs: { min: -10, max: -8 },
|
|
12124
14252
|
tonalBalance: {
|
|
12125
14253
|
p10: [
|
|
12126
14254
|
-17.8,
|
|
@@ -12138,7 +14266,27 @@ var futureBass = {
|
|
|
12138
14266
|
p90: [-7.8, -1.8, -4.8, -6.8, -6.8, -6.8, -7.8, -8.8, -9.8, -14.8],
|
|
12139
14267
|
source: TONAL_BALANCE_SOURCE
|
|
12140
14268
|
},
|
|
12141
|
-
sidechain:
|
|
14269
|
+
sidechain: {
|
|
14270
|
+
ducks: [
|
|
14271
|
+
{
|
|
14272
|
+
trigger: "kick",
|
|
14273
|
+
targets: ["chords", "pad"],
|
|
14274
|
+
ratio: 8,
|
|
14275
|
+
attackMs: 0.5,
|
|
14276
|
+
releaseMs: 200,
|
|
14277
|
+
depthDb: 8
|
|
14278
|
+
},
|
|
14279
|
+
{
|
|
14280
|
+
trigger: "kick",
|
|
14281
|
+
targets: ["bass"],
|
|
14282
|
+
ratio: 4,
|
|
14283
|
+
attackMs: 0.5,
|
|
14284
|
+
releaseMs: 80,
|
|
14285
|
+
depthDb: 4
|
|
14286
|
+
}
|
|
14287
|
+
],
|
|
14288
|
+
note: "Chord stacks pump hard, release follows the swell; the sub duck is shorter."
|
|
14289
|
+
},
|
|
12142
14290
|
reverb: "EQ Eight clears chord low mids, Saturator adds body, Chorus-Ensemble widens upper layers, then Reverb; keep dry transients in front.",
|
|
12143
14291
|
delay: "Echo dotted eighth on vocal chops; automate throws into gaps rather than washing out the next chord.",
|
|
12144
14292
|
stereo: "Wide chords and chops above a centered sub/kick/snare; mono-check detuned layers.",
|
|
@@ -12410,7 +14558,19 @@ var gospel = {
|
|
|
12410
14558
|
p90: [-13.1, -5.1, -4.1, -4.1, -5.1, -6.1, -7.1, -9.1, -11.1, -17.1],
|
|
12411
14559
|
source: TONAL_BALANCE_SOURCE
|
|
12412
14560
|
},
|
|
12413
|
-
sidechain:
|
|
14561
|
+
sidechain: {
|
|
14562
|
+
ducks: [
|
|
14563
|
+
{
|
|
14564
|
+
trigger: "kick",
|
|
14565
|
+
targets: ["bass"],
|
|
14566
|
+
ratio: 3,
|
|
14567
|
+
attackMs: 1,
|
|
14568
|
+
releaseMs: 80,
|
|
14569
|
+
depthDb: 1.5
|
|
14570
|
+
}
|
|
14571
|
+
],
|
|
14572
|
+
note: "Only where bass masks the kick; no dance pumping."
|
|
14573
|
+
},
|
|
12414
14574
|
reverb: "EQ Eight then Compressor on lead/choir; shared Reverb hall with enough predelay to keep consonants clear.",
|
|
12415
14575
|
delay: "Low Echo throws behind the leader; do not repeat over the choir response.",
|
|
12416
14576
|
stereo: "Lead center, choir spread, organ behind the words; retain mono bass.",
|
|
@@ -12464,7 +14624,7 @@ var guofengPop = {
|
|
|
12464
14624
|
feel: {
|
|
12465
14625
|
kind: "straight",
|
|
12466
14626
|
swingPercent: 50,
|
|
12467
|
-
note: "Soft even-16th R&B-pop beat; the Chinese colour
|
|
14627
|
+
note: "Soft even-16th R&B-pop beat; the Chinese colour is in the melody (pentatonic, slides, grace notes) and woodblock-like rim."
|
|
12468
14628
|
},
|
|
12469
14629
|
keys: ["D major", "E major", "G major", "B minor", "E minor"],
|
|
12470
14630
|
forms: [
|
|
@@ -12769,7 +14929,7 @@ var guofengPop = {
|
|
|
12769
14929
|
reverb: "Large hall on flute, zither and vocal (the misty space is the style), pre-delay for clear words; drums dry.",
|
|
12770
14930
|
delay: "Dotted-1/8 Echo on flute and plucks; vocal throws on line ends.",
|
|
12771
14931
|
stereo: "Zither and strings wide, flute off-centre, vocal centred on top.",
|
|
12772
|
-
lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; master Glue
|
|
14932
|
+
lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; gentle master Glue."
|
|
12773
14933
|
},
|
|
12774
14934
|
arrangement: {
|
|
12775
14935
|
variationEveryBars: 8,
|
|
@@ -12797,7 +14957,7 @@ var guofengPop = {
|
|
|
12797
14957
|
]
|
|
12798
14958
|
},
|
|
12799
14959
|
references: ["Jay Chou", "JJ Lin", "Silence Wang", "Henry Huo", "Isaac Hong"],
|
|
12800
|
-
why: "Guofeng is C-pop with a Chinese accent:
|
|
14960
|
+
why: "Guofeng is C-pop with a Chinese accent: pop/R&B beat and chords under an ornamented pentatonic melody and classical imagery. No Chinese instrument samples yet: Koto / Eastern Harp stand in for guzheng, Flute Mellow / Indian Flute for dizi, Violin Strings with slides for erhu."
|
|
12801
14961
|
};
|
|
12802
14962
|
|
|
12803
14963
|
// ../../src/components/editor-ai/style/cards/house.ts
|
|
@@ -13095,14 +15255,34 @@ var house = {
|
|
|
13095
15255
|
"House 90s C Minor 128 bpm"
|
|
13096
15256
|
],
|
|
13097
15257
|
mix: {
|
|
13098
|
-
lufs: { min: -
|
|
15258
|
+
lufs: { min: -10, max: -8 },
|
|
13099
15259
|
tonalBalance: {
|
|
13100
15260
|
p10: [-15.5, -8.5, -9, -12, -14.5, -16, -17, -18, -20, -26.5],
|
|
13101
15261
|
p50: [-10.5, -4.5, -6, -9, -11.5, -13, -14, -15, -16, -21.5],
|
|
13102
15262
|
p90: [-5.5, -0.5, -3, -6, -8.5, -10, -11, -12, -12, -16.5],
|
|
13103
15263
|
source: TONAL_BALANCE_SOURCE
|
|
13104
15264
|
},
|
|
13105
|
-
sidechain:
|
|
15265
|
+
sidechain: {
|
|
15266
|
+
ducks: [
|
|
15267
|
+
{
|
|
15268
|
+
trigger: "kick",
|
|
15269
|
+
targets: ["chords", "pad"],
|
|
15270
|
+
ratio: 4,
|
|
15271
|
+
attackMs: 1,
|
|
15272
|
+
releaseMs: 200,
|
|
15273
|
+
depthDb: 5
|
|
15274
|
+
},
|
|
15275
|
+
{
|
|
15276
|
+
trigger: "kick",
|
|
15277
|
+
targets: ["bass"],
|
|
15278
|
+
ratio: 3,
|
|
15279
|
+
attackMs: 1,
|
|
15280
|
+
releaseMs: 80,
|
|
15281
|
+
depthDb: 2
|
|
15282
|
+
}
|
|
15283
|
+
],
|
|
15284
|
+
note: "Chords/pads pump (release ~1/8); the offbeat bass only lightly."
|
|
15285
|
+
},
|
|
13106
15286
|
reverb: "Short plate on clap, large hall on breakdown pads/vocals.",
|
|
13107
15287
|
delay: "1/8 dotted or 3/16 on stabs and vocals, filtered.",
|
|
13108
15288
|
stereo: "Kick, bass, clap centered; hats/shaker/perc spread wide.",
|
|
@@ -13734,7 +15914,7 @@ var jpopAnime = {
|
|
|
13734
15914
|
"Downwards A Minor 141 bpm"
|
|
13735
15915
|
],
|
|
13736
15916
|
mix: {
|
|
13737
|
-
lufs: { min: -
|
|
15917
|
+
lufs: { min: -10, max: -8 },
|
|
13738
15918
|
tonalBalance: {
|
|
13739
15919
|
p10: [-20.5, -12, -9.5, -10.5, -12, -13, -14, -15, -18, -26],
|
|
13740
15920
|
p50: [-15.5, -8, -6.5, -7.5, -9, -10, -11, -12, -14, -21],
|
|
@@ -13745,7 +15925,7 @@ var jpopAnime = {
|
|
|
13745
15925
|
reverb: "Bright plate on vocal and snare, hall on strings.",
|
|
13746
15926
|
delay: "Dotted-1/8 vocal delay tucked under.",
|
|
13747
15927
|
stereo: "Doubled guitars hard L/R, strings wide, vocal centred on top.",
|
|
13748
|
-
lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > Limiter
|
|
15928
|
+
lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > one Limiter (ceiling -1 dBTP)."
|
|
13749
15929
|
},
|
|
13750
15930
|
arrangement: {
|
|
13751
15931
|
variationEveryBars: 4,
|
|
@@ -13803,19 +15983,19 @@ var kpop = {
|
|
|
13803
15983
|
"\uCF00\uC774\uD31D",
|
|
13804
15984
|
"\uC544\uC774\uB3CC"
|
|
13805
15985
|
],
|
|
13806
|
-
summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse,
|
|
15986
|
+
summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse, build, EDM/trap chorus, dance break, key-up end.",
|
|
13807
15987
|
tempo: { min: 95, max: 130, typical: 120 },
|
|
13808
15988
|
meter: "4/4",
|
|
13809
15989
|
feel: {
|
|
13810
15990
|
kind: "straight",
|
|
13811
15991
|
swingPercent: 50,
|
|
13812
|
-
note: "Quantized
|
|
15992
|
+
note: "Quantized; trap parts half-time with 32nd hat rolls."
|
|
13813
15993
|
},
|
|
13814
15994
|
keys: ["F# minor", "C# minor", "G minor", "A minor", "Eb major"],
|
|
13815
15995
|
forms: [
|
|
13816
15996
|
{
|
|
13817
15997
|
id: "title-track",
|
|
13818
|
-
name: "Title track
|
|
15998
|
+
name: "Title track, key-up end",
|
|
13819
15999
|
sections: [
|
|
13820
16000
|
{
|
|
13821
16001
|
name: "Intro",
|
|
@@ -13835,9 +16015,9 @@ var kpop = {
|
|
|
13835
16015
|
{
|
|
13836
16016
|
name: "Pre-Chorus",
|
|
13837
16017
|
bars: 8,
|
|
13838
|
-
energy: 0.
|
|
16018
|
+
energy: 0.55,
|
|
13839
16019
|
roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
|
|
13840
|
-
drums: "
|
|
16020
|
+
drums: "build",
|
|
13841
16021
|
bass: "pulse"
|
|
13842
16022
|
},
|
|
13843
16023
|
{
|
|
@@ -13867,9 +16047,9 @@ var kpop = {
|
|
|
13867
16047
|
{
|
|
13868
16048
|
name: "Pre-Chorus",
|
|
13869
16049
|
bars: 8,
|
|
13870
|
-
energy: 0.
|
|
16050
|
+
energy: 0.6,
|
|
13871
16051
|
roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
|
|
13872
|
-
drums: "
|
|
16052
|
+
drums: "build",
|
|
13873
16053
|
bass: "pulse"
|
|
13874
16054
|
},
|
|
13875
16055
|
{
|
|
@@ -13915,7 +16095,7 @@ var kpop = {
|
|
|
13915
16095
|
},
|
|
13916
16096
|
{
|
|
13917
16097
|
id: "beat-switch",
|
|
13918
|
-
name: "Beat switch:
|
|
16098
|
+
name: "Beat switch: trap chorus, dance last chorus",
|
|
13919
16099
|
sections: [
|
|
13920
16100
|
{
|
|
13921
16101
|
name: "Intro",
|
|
@@ -13935,8 +16115,9 @@ var kpop = {
|
|
|
13935
16115
|
{
|
|
13936
16116
|
name: "Pre-Chorus",
|
|
13937
16117
|
bars: 8,
|
|
13938
|
-
energy: 0.
|
|
13939
|
-
roles: ["chords", "pad", "vocal", "fx"]
|
|
16118
|
+
energy: 0.6,
|
|
16119
|
+
roles: ["drums", "chords", "pad", "vocal", "fx"],
|
|
16120
|
+
drums: "build"
|
|
13940
16121
|
},
|
|
13941
16122
|
{
|
|
13942
16123
|
name: "Chorus",
|
|
@@ -13981,6 +16162,7 @@ var kpop = {
|
|
|
13981
16162
|
kick: "X..x..x.X.x.....X..x..x.X.....o.",
|
|
13982
16163
|
snare: "....X.......X.......X.......X...",
|
|
13983
16164
|
clap: "....x.......x.......x.......x...",
|
|
16165
|
+
snap: "....g.......g..o....g.......g...",
|
|
13984
16166
|
hatClosed: "o.x.o.xxo.x.o.x.o.x.o.xxo.x.o...",
|
|
13985
16167
|
hatOpen: "..............................x."
|
|
13986
16168
|
}
|
|
@@ -14014,18 +16196,29 @@ var kpop = {
|
|
|
14014
16196
|
kick: "X..X..X...X.X...",
|
|
14015
16197
|
snare: "....X.......X...",
|
|
14016
16198
|
clap: "....x..x....x..x",
|
|
16199
|
+
snap: "....x.......x...",
|
|
14017
16200
|
hatClosed: "..x...x...x...x."
|
|
14018
16201
|
}
|
|
16202
|
+
},
|
|
16203
|
+
{
|
|
16204
|
+
id: "build",
|
|
16205
|
+
name: "Build: kick out, snare 4ths > 8ths > 16ths",
|
|
16206
|
+
stepsPerBeat: 4,
|
|
16207
|
+
lanes: {
|
|
16208
|
+
snare: "o...o...o...o...x.x.x.x.xxxxXXXX",
|
|
16209
|
+
clap: "....o.......o.......x.......x...",
|
|
16210
|
+
hatClosed: "oooooooooooooooooxoxoxoxxxxxXXXX"
|
|
16211
|
+
}
|
|
14019
16212
|
}
|
|
14020
16213
|
],
|
|
14021
16214
|
fills: [
|
|
14022
16215
|
{
|
|
14023
16216
|
id: "riser-roll",
|
|
14024
16217
|
everyBars: 8,
|
|
14025
|
-
stepsPerBeat:
|
|
14026
|
-
steps:
|
|
14027
|
-
lanes: { snare: "x.x.
|
|
14028
|
-
note: "
|
|
16218
|
+
stepsPerBeat: 8,
|
|
16219
|
+
steps: 24,
|
|
16220
|
+
lanes: { snare: "o.o.x.x.xxxxXXXX........" },
|
|
16221
|
+
note: "Snare roll, kick out, last beat silent."
|
|
14029
16222
|
},
|
|
14030
16223
|
{
|
|
14031
16224
|
id: "drop-gap",
|
|
@@ -14037,8 +16230,8 @@ var kpop = {
|
|
|
14037
16230
|
}
|
|
14038
16231
|
],
|
|
14039
16232
|
rules: [
|
|
14040
|
-
"One groove per section
|
|
14041
|
-
"Stack snare+clap
|
|
16233
|
+
"One groove per section (verse, build, trap, chorus, jersey).",
|
|
16234
|
+
"Stack snare+clap+snap; hits mark choreography."
|
|
14042
16235
|
]
|
|
14043
16236
|
},
|
|
14044
16237
|
bass: {
|
|
@@ -14067,7 +16260,7 @@ var kpop = {
|
|
|
14067
16260
|
velocity: 110
|
|
14068
16261
|
}
|
|
14069
16262
|
],
|
|
14070
|
-
notes: "Synth bass on the dance kick;
|
|
16263
|
+
notes: "Synth bass on the dance kick; 808 in rap/trap."
|
|
14071
16264
|
},
|
|
14072
16265
|
harmony: {
|
|
14073
16266
|
modes: [
|
|
@@ -14086,7 +16279,7 @@ var kpop = {
|
|
|
14086
16279
|
chords: ["i", "bVII", "bVI", "V"],
|
|
14087
16280
|
weight: 2,
|
|
14088
16281
|
barsPerChord: 1,
|
|
14089
|
-
use: "major V
|
|
16282
|
+
use: "major V into the hook"
|
|
14090
16283
|
},
|
|
14091
16284
|
{
|
|
14092
16285
|
chords: ["IVmaj7", "V", "iii7", "vi"],
|
|
@@ -14102,28 +16295,24 @@ var kpop = {
|
|
|
14102
16295
|
}
|
|
14103
16296
|
],
|
|
14104
16297
|
extensions: ["add9", "maj7", "7", "sus4"],
|
|
14105
|
-
voicing: "Close synth/brass stabs
|
|
14106
|
-
harmonicRhythm: "1 chord/bar; rap and dance break vamp
|
|
16298
|
+
voicing: "Close synth/brass stabs; open piano+strings in the bridge.",
|
|
16299
|
+
harmonicRhythm: "1 chord/bar; rap and dance break vamp."
|
|
14107
16300
|
},
|
|
14108
16301
|
melody: {
|
|
14109
16302
|
range: { low: 57, high: 79 },
|
|
14110
16303
|
density: "dense",
|
|
14111
16304
|
notesPerBar: [4, 12],
|
|
14112
|
-
rhythm: "16th verses and rap;
|
|
16305
|
+
rhythm: "16th verses and rap; a short title word repeats in the chorus.",
|
|
14113
16306
|
motifs: [
|
|
14114
16307
|
"One-word title hook repeated 4x",
|
|
14115
16308
|
"Pre-chorus climbs to a held note",
|
|
14116
|
-
"Post-chorus
|
|
16309
|
+
"Post-chorus vocal-chop riff"
|
|
14117
16310
|
]
|
|
14118
16311
|
},
|
|
14119
16312
|
palette: {
|
|
14120
16313
|
drums: {
|
|
14121
|
-
keywords: ["punchy kick", "
|
|
14122
|
-
presets: [
|
|
14123
|
-
"drumrack:Crystal Clear Kit",
|
|
14124
|
-
"drumrack:Street Mind Kit",
|
|
14125
|
-
"drumrack:707 Core Kit"
|
|
14126
|
-
]
|
|
16314
|
+
keywords: ["punchy kick", "snap", "trap hats"],
|
|
16315
|
+
presets: ["drumrack:Street Mind Kit", "drumrack:Clint West Kit"]
|
|
14127
16316
|
},
|
|
14128
16317
|
bass: {
|
|
14129
16318
|
keywords: ["808", "synth bass", "sub"],
|
|
@@ -14143,29 +16332,40 @@ var kpop = {
|
|
|
14143
16332
|
},
|
|
14144
16333
|
lead: {
|
|
14145
16334
|
keywords: ["vocal chop", "pluck", "saw lead"],
|
|
14146
|
-
presets: ["
|
|
16335
|
+
presets: ["sampler:Vocal Chop Ah", "meld:PolyPlucks"]
|
|
14147
16336
|
},
|
|
14148
16337
|
fx: { keywords: ["riser", "impact", "downlifter", "reverse cymbal"] }
|
|
14149
16338
|
},
|
|
14150
16339
|
starterClips: [
|
|
14151
16340
|
"Pop Riddim 124 bpm",
|
|
14152
|
-
"Dancehall Trap 113 bpm",
|
|
14153
16341
|
"LD Core Kit Pop 120 bpm",
|
|
14154
16342
|
"Uplifting Club Vamp F Min 127 bpm"
|
|
14155
16343
|
],
|
|
14156
16344
|
mix: {
|
|
14157
|
-
lufs: { min: -
|
|
16345
|
+
lufs: { min: -11, max: -9 },
|
|
14158
16346
|
tonalBalance: {
|
|
14159
16347
|
p10: [-16.5, -9, -8.5, -11, -13, -14.5, -15.5, -16.5, -19, -26.5],
|
|
14160
16348
|
p50: [-11.5, -5.5, -5.5, -8, -10, -11.5, -12.5, -13.5, -15, -21.5],
|
|
14161
16349
|
p90: [-6.5, -1.5, -2.5, -5, -7, -8.5, -9.5, -10.5, -11, -16.5],
|
|
14162
16350
|
source: TONAL_BALANCE_SOURCE
|
|
14163
16351
|
},
|
|
14164
|
-
sidechain:
|
|
16352
|
+
sidechain: {
|
|
16353
|
+
ducks: [
|
|
16354
|
+
{
|
|
16355
|
+
trigger: "kick",
|
|
16356
|
+
targets: ["pad", "chords", "bass"],
|
|
16357
|
+
ratio: 4,
|
|
16358
|
+
attackMs: 1,
|
|
16359
|
+
releaseMs: 150,
|
|
16360
|
+
depthDb: 5
|
|
16361
|
+
}
|
|
16362
|
+
],
|
|
16363
|
+
note: "Chorus pump; measured dip >= 1 dB."
|
|
16364
|
+
},
|
|
14165
16365
|
reverb: "Bright vocal plate, dry rap; hall only in the bridge.",
|
|
14166
|
-
delay: "Dotted-1/8 throws on hook words
|
|
14167
|
-
stereo: "Wide
|
|
14168
|
-
lowEnd: "Mono sub; Saturator on the 808;
|
|
16366
|
+
delay: "Dotted-1/8 throws on hook words.",
|
|
16367
|
+
stereo: "Wide gang vocals in the chorus; rap centred.",
|
|
16368
|
+
lowEnd: "Mono sub; Saturator on the 808; gentle master Glue, 1 dB true-peak headroom."
|
|
14169
16369
|
},
|
|
14170
16370
|
arrangement: {
|
|
14171
16371
|
variationEveryBars: 4,
|
|
@@ -14173,7 +16373,7 @@ var kpop = {
|
|
|
14173
16373
|
{
|
|
14174
16374
|
kind: "build",
|
|
14175
16375
|
where: "Pre-chorus",
|
|
14176
|
-
how: "Kick out, snare roll, Auto Filter sweep, riser."
|
|
16376
|
+
how: "Kick out (build pattern), snare roll, Auto Filter sweep, riser; 4-6 dB under the chorus; last beat silent."
|
|
14177
16377
|
},
|
|
14178
16378
|
{
|
|
14179
16379
|
kind: "breakdown",
|
|
@@ -14183,7 +16383,7 @@ var kpop = {
|
|
|
14183
16383
|
{
|
|
14184
16384
|
kind: "drop",
|
|
14185
16385
|
where: "Dance break",
|
|
14186
|
-
how: "Instrumental beat switch
|
|
16386
|
+
how: "Instrumental beat switch."
|
|
14187
16387
|
},
|
|
14188
16388
|
{
|
|
14189
16389
|
kind: "key-change",
|
|
@@ -14193,7 +16393,7 @@ var kpop = {
|
|
|
14193
16393
|
]
|
|
14194
16394
|
},
|
|
14195
16395
|
references: ["BTS", "BLACKPINK", "TWICE", "Stray Kids", "NewJeans"],
|
|
14196
|
-
why: "
|
|
16396
|
+
why: "The groove switches genre per section while one short title hook holds the song; builds, stops and the dance break frame the choreography."
|
|
14197
16397
|
};
|
|
14198
16398
|
|
|
14199
16399
|
// ../../src/components/editor-ai/style/cards/lofi-hip-hop.ts
|
|
@@ -14466,7 +16666,19 @@ var lofiHipHop = {
|
|
|
14466
16666
|
p90: [-8.5, -2.5, -2, -4, -6, -8.5, -11.5, -15.5, -19.5, -28.5],
|
|
14467
16667
|
source: TONAL_BALANCE_SOURCE
|
|
14468
16668
|
},
|
|
14469
|
-
sidechain:
|
|
16669
|
+
sidechain: {
|
|
16670
|
+
ducks: [
|
|
16671
|
+
{
|
|
16672
|
+
trigger: "kick",
|
|
16673
|
+
targets: ["keys", "pad"],
|
|
16674
|
+
ratio: 3,
|
|
16675
|
+
attackMs: 5,
|
|
16676
|
+
releaseMs: 180,
|
|
16677
|
+
depthDb: 3
|
|
16678
|
+
}
|
|
16679
|
+
],
|
|
16680
|
+
note: "Gentle breathing: keys and pad dip with the kick."
|
|
16681
|
+
},
|
|
14470
16682
|
reverb: "Small dark room on drums, short plate on keys; low wet levels.",
|
|
14471
16683
|
delay: "Tape-style 1/8 dotted on chops/lead, low feedback, filtered repeats.",
|
|
14472
16684
|
stereo: "Drums mostly mono; keys and textures wide.",
|
|
@@ -14853,7 +17065,19 @@ var mandopopUptempo = {
|
|
|
14853
17065
|
p90: [-8, -2.5, -3, -5, -6.5, -8, -9, -10.5, -11.5, -17],
|
|
14854
17066
|
source: TONAL_BALANCE_SOURCE
|
|
14855
17067
|
},
|
|
14856
|
-
sidechain:
|
|
17068
|
+
sidechain: {
|
|
17069
|
+
ducks: [
|
|
17070
|
+
{
|
|
17071
|
+
trigger: "kick",
|
|
17072
|
+
targets: ["pad", "chords"],
|
|
17073
|
+
ratio: 3,
|
|
17074
|
+
attackMs: 1,
|
|
17075
|
+
releaseMs: 150,
|
|
17076
|
+
depthDb: 3
|
|
17077
|
+
}
|
|
17078
|
+
],
|
|
17079
|
+
note: "Light pump, chorus only."
|
|
17080
|
+
},
|
|
14857
17081
|
reverb: "Plate on vocal and snare; hall only on the breakdown.",
|
|
14858
17082
|
delay: "1/8 vocal echo under the chorus; 1/4 throws.",
|
|
14859
17083
|
stereo: "Vocal loud and centred; doubles wide in the chorus.",
|
|
@@ -14915,7 +17139,7 @@ var modernPop = {
|
|
|
14915
17139
|
feel: {
|
|
14916
17140
|
kind: "straight",
|
|
14917
17141
|
swingPercent: 50,
|
|
14918
|
-
note: "Tight
|
|
17142
|
+
note: "Tight, quantized; swing16-54 on hats only if stiff."
|
|
14919
17143
|
},
|
|
14920
17144
|
keys: ["C major", "G major", "D major", "A minor", "F# minor"],
|
|
14921
17145
|
forms: [
|
|
@@ -15118,13 +17342,13 @@ var modernPop = {
|
|
|
15118
17342
|
stepsPerBeat: 4,
|
|
15119
17343
|
steps: 4,
|
|
15120
17344
|
lanes: {},
|
|
15121
|
-
note: "
|
|
17345
|
+
note: "A silent beat before the chorus."
|
|
15122
17346
|
}
|
|
15123
17347
|
],
|
|
15124
17348
|
rules: [
|
|
15125
17349
|
"Pre-chorus builds: snare 8ths -> 16ths over the last 2 bars, or pull the kick out.",
|
|
15126
17350
|
"Chorus adds a layer the verse lacks (claps, open hats, tambourine).",
|
|
15127
|
-
"
|
|
17351
|
+
"Tight, consistent velocities; punchy, sample-replaced."
|
|
15128
17352
|
]
|
|
15129
17353
|
},
|
|
15130
17354
|
bass: {
|
|
@@ -15146,7 +17370,7 @@ var modernPop = {
|
|
|
15146
17370
|
velocity: 104
|
|
15147
17371
|
}
|
|
15148
17372
|
],
|
|
15149
|
-
notes: "Synth or picked bass doubling the kick
|
|
17373
|
+
notes: "Synth or picked bass doubling the chorus kick; roots, octave pops for lift."
|
|
15150
17374
|
},
|
|
15151
17375
|
harmony: {
|
|
15152
17376
|
modes: [
|
|
@@ -15182,7 +17406,7 @@ var modernPop = {
|
|
|
15182
17406
|
}
|
|
15183
17407
|
],
|
|
15184
17408
|
extensions: ["add9", "sus2", "sus4", "maj7"],
|
|
15185
|
-
voicing: "Open triads with
|
|
17409
|
+
voicing: "Open triads with add9; pads hold, piano/guitar pulses 8ths; chorus voicing an octave up.",
|
|
15186
17410
|
harmonicRhythm: "1 chord per bar; same loop through verse and chorus, with energy doing the work."
|
|
15187
17411
|
},
|
|
15188
17412
|
melody: {
|
|
@@ -15251,11 +17475,23 @@ var modernPop = {
|
|
|
15251
17475
|
p90: [-7.5, -2, -2.5, -5, -7, -8.5, -10, -11.5, -12.5, -18.5],
|
|
15252
17476
|
source: TONAL_BALANCE_SOURCE
|
|
15253
17477
|
},
|
|
15254
|
-
sidechain:
|
|
17478
|
+
sidechain: {
|
|
17479
|
+
ducks: [
|
|
17480
|
+
{
|
|
17481
|
+
trigger: "kick",
|
|
17482
|
+
targets: ["pad", "chords"],
|
|
17483
|
+
ratio: 3,
|
|
17484
|
+
attackMs: 1,
|
|
17485
|
+
releaseMs: 150,
|
|
17486
|
+
depthDb: 3
|
|
17487
|
+
}
|
|
17488
|
+
],
|
|
17489
|
+
note: "Light pump in dance-pop; none in ballads."
|
|
17490
|
+
},
|
|
15255
17491
|
reverb: "Vocal plate, short room on drums; long reverb only as throws.",
|
|
15256
17492
|
delay: "1/4 and 1/8 vocal throws at phrase ends.",
|
|
15257
|
-
stereo: "Doubles
|
|
15258
|
-
lowEnd: "Kick and bass locked; highpass
|
|
17493
|
+
stereo: "Doubles and synths wide in the chorus, narrower verse; low end mono.",
|
|
17494
|
+
lowEnd: "Kick and bass locked; highpass the rest at 100-150 Hz."
|
|
15259
17495
|
},
|
|
15260
17496
|
arrangement: {
|
|
15261
17497
|
variationEveryBars: 8,
|
|
@@ -15294,7 +17530,7 @@ var modernPop = {
|
|
|
15294
17530
|
"The Weeknd",
|
|
15295
17531
|
"Ed Sheeran"
|
|
15296
17532
|
],
|
|
15297
|
-
why: "
|
|
17533
|
+
why: "One memorable hook over barely-changing chords; the arc comes from contrast: sparse verses, a building pre-chorus, a louder, wider, higher chorus, something new every 8 bars."
|
|
15298
17534
|
};
|
|
15299
17535
|
|
|
15300
17536
|
// ../../src/components/editor-ai/style/cards/neo-soul.ts
|
|
@@ -15524,7 +17760,19 @@ var neoSoul = {
|
|
|
15524
17760
|
p90: [-12.5, -3.5, -2.5, -3.5, -5.5, -8.5, -10.5, -13.5, -16.5, -23.5],
|
|
15525
17761
|
source: TONAL_BALANCE_SOURCE
|
|
15526
17762
|
},
|
|
15527
|
-
sidechain:
|
|
17763
|
+
sidechain: {
|
|
17764
|
+
ducks: [
|
|
17765
|
+
{
|
|
17766
|
+
trigger: "kick",
|
|
17767
|
+
targets: ["bass"],
|
|
17768
|
+
ratio: 3,
|
|
17769
|
+
attackMs: 1,
|
|
17770
|
+
releaseMs: 80,
|
|
17771
|
+
depthDb: 1.5
|
|
17772
|
+
}
|
|
17773
|
+
],
|
|
17774
|
+
note: "Only if held bass masks the kick; no audible pumping."
|
|
17775
|
+
},
|
|
15528
17776
|
reverb: "Short dark Reverb room around the band; longer vocal plate audible only between phrases.",
|
|
15529
17777
|
delay: "Filtered Echo throws on selected vocal endings.",
|
|
15530
17778
|
stereo: "Keep lead and bass close and centered; electric piano medium-wide, backing vocals wider.",
|
|
@@ -15796,7 +18044,7 @@ var reggae = {
|
|
|
15796
18044
|
p90: [-9.1, -1.1, -2.1, -5.1, -8.1, -10.1, -12.1, -14.1, -17.1, -23.1],
|
|
15797
18045
|
source: TONAL_BALANCE_SOURCE
|
|
15798
18046
|
},
|
|
15799
|
-
sidechain:
|
|
18047
|
+
sidechain: null,
|
|
15800
18048
|
reverb: "Reverb on rim/organ for a short spring-like splash; EQ Eight cuts lows on the wet return.",
|
|
15801
18049
|
delay: "Echo filtered tape-style throws; automate sends on isolated words or rim hits, then mute the dry source for dub space.",
|
|
15802
18050
|
stereo: "Bass, kick and vocal centered; skank/organ apart; dub returns may travel wider.",
|
|
@@ -16060,7 +18308,19 @@ var reggaeton = {
|
|
|
16060
18308
|
p90: [-6.6, -0.6, -3.6, -6.6, -8.6, -9.6, -9.6, -10.6, -11.6, -16.6],
|
|
16061
18309
|
source: TONAL_BALANCE_SOURCE
|
|
16062
18310
|
},
|
|
16063
|
-
sidechain:
|
|
18311
|
+
sidechain: {
|
|
18312
|
+
ducks: [
|
|
18313
|
+
{
|
|
18314
|
+
trigger: "kick",
|
|
18315
|
+
targets: ["bass"],
|
|
18316
|
+
ratio: 4,
|
|
18317
|
+
attackMs: 0.5,
|
|
18318
|
+
releaseMs: 60,
|
|
18319
|
+
depthDb: 3
|
|
18320
|
+
}
|
|
18321
|
+
],
|
|
18322
|
+
note: "Released before the next tresillo accent."
|
|
18323
|
+
},
|
|
16064
18324
|
reverb: "EQ Eight and Compressor keep the vocal forward; short Reverb on snare, little on bass.",
|
|
16065
18325
|
delay: "Echo on hook endings with filtered repeats, ducked or muted under the next line.",
|
|
16066
18326
|
stereo: "Center kick, snare, sub and lead; widen supporting plucks and vocal doubles only.",
|
|
@@ -16109,7 +18369,7 @@ var rnb = {
|
|
|
16109
18369
|
kind: "swing",
|
|
16110
18370
|
swingPercent: 57,
|
|
16111
18371
|
grooveId: "swing16-57",
|
|
16112
|
-
note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid
|
|
18372
|
+
note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid (double-time, snare on 3: same feel)."
|
|
16113
18373
|
},
|
|
16114
18374
|
keys: ["Eb major", "Ab major", "Db major", "F minor", "C minor"],
|
|
16115
18375
|
forms: [
|
|
@@ -16250,7 +18510,7 @@ var rnb = {
|
|
|
16250
18510
|
stepsPerBeat: 4,
|
|
16251
18511
|
lanes: {
|
|
16252
18512
|
kick: "X.........x.....",
|
|
16253
|
-
|
|
18513
|
+
snap: "....x.......x...",
|
|
16254
18514
|
hatClosed: "x..x..x.o..x..x."
|
|
16255
18515
|
}
|
|
16256
18516
|
}
|
|
@@ -16337,7 +18597,7 @@ var rnb = {
|
|
|
16337
18597
|
{ chords: ["vi9", "ii9", "Imaj7", "IVmaj7"], weight: 1, barsPerChord: 1 }
|
|
16338
18598
|
],
|
|
16339
18599
|
extensions: ["maj7", "maj9", "9", "11", "13", "7sus4"],
|
|
16340
|
-
voicing: "Rootless 4-note keys
|
|
18600
|
+
voicing: "Rootless 4-note keys (3-5-7-9 or 7-9-3-5) around middle C, pads above; chords often land a 16th early.",
|
|
16341
18601
|
harmonicRhythm: "1 chord per bar, or a 2-chord vamp of 2 bars each."
|
|
16342
18602
|
},
|
|
16343
18603
|
melody: {
|
|
@@ -16406,11 +18666,23 @@ var rnb = {
|
|
|
16406
18666
|
p90: [-5.5, -1, -4, -6.5, -8, -9.5, -10.5, -12, -13, -18.5],
|
|
16407
18667
|
source: TONAL_BALANCE_SOURCE
|
|
16408
18668
|
},
|
|
16409
|
-
sidechain:
|
|
18669
|
+
sidechain: {
|
|
18670
|
+
ducks: [
|
|
18671
|
+
{
|
|
18672
|
+
trigger: "kick",
|
|
18673
|
+
targets: ["bass"],
|
|
18674
|
+
ratio: 3,
|
|
18675
|
+
attackMs: 1,
|
|
18676
|
+
releaseMs: 80,
|
|
18677
|
+
depthDb: 3
|
|
18678
|
+
}
|
|
18679
|
+
],
|
|
18680
|
+
note: "Where kick and sub sustain together; or let the 808 be the kick."
|
|
18681
|
+
},
|
|
16410
18682
|
reverb: "Plate on the lead vocal, long hall on ad-libs and stacks; drums mostly dry.",
|
|
16411
18683
|
delay: "1/4 and dotted-1/8 vocal throws, filtered; ping-pong on ad-libs.",
|
|
16412
18684
|
stereo: "Lead vocal centred and intimate, harmony stack and keys wide.",
|
|
16413
|
-
lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; master Glue
|
|
18685
|
+
lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; gentle master Glue."
|
|
16414
18686
|
},
|
|
16415
18687
|
arrangement: {
|
|
16416
18688
|
variationEveryBars: 4,
|
|
@@ -16443,7 +18715,7 @@ var rnb = {
|
|
|
16443
18715
|
]
|
|
16444
18716
|
},
|
|
16445
18717
|
references: ["SZA", "H.E.R.", "Daniel Caesar", "David Tao", "Khalil Fong"],
|
|
16446
|
-
why: "R&B
|
|
18718
|
+
why: "R&B is intimate because it leaves space: a laid-back beat, a deep sub, 7th/9th chords that never fully resolve. The voice is the star (runs, stacks, ad-libs); the beat stays simple."
|
|
16447
18719
|
};
|
|
16448
18720
|
|
|
16449
18721
|
// ../../src/components/editor-ai/style/cards/synth-pop.ts
|
|
@@ -16711,7 +18983,19 @@ var synthPop = {
|
|
|
16711
18983
|
p90: [-12.3, -3.3, -3.3, -5.3, -6.3, -6.3, -7.3, -9.3, -12.3, -18.3],
|
|
16712
18984
|
source: TONAL_BALANCE_SOURCE
|
|
16713
18985
|
},
|
|
16714
|
-
sidechain:
|
|
18986
|
+
sidechain: {
|
|
18987
|
+
ducks: [
|
|
18988
|
+
{
|
|
18989
|
+
trigger: "kick",
|
|
18990
|
+
targets: ["pad"],
|
|
18991
|
+
ratio: 2,
|
|
18992
|
+
attackMs: 2,
|
|
18993
|
+
releaseMs: 150,
|
|
18994
|
+
depthDb: 2
|
|
18995
|
+
}
|
|
18996
|
+
],
|
|
18997
|
+
note: "Barely there: phrasing, not pumping, drives the song."
|
|
18998
|
+
},
|
|
16715
18999
|
reverb: "Reverb plate on snare/vocal; Gate the snare return for a short retro bloom.",
|
|
16716
19000
|
delay: "Echo eighths or slap on the hook, ducked below lead words.",
|
|
16717
19001
|
stereo: "Chorus-Ensemble on pads; center the lead vocal, bass and main hook.",
|
|
@@ -16997,7 +19281,19 @@ var synthwave = {
|
|
|
16997
19281
|
p90: [-10.2, -2.2, -2.2, -5.2, -7.2, -8.2, -9.2, -11.2, -14.2, -20.2],
|
|
16998
19282
|
source: TONAL_BALANCE_SOURCE
|
|
16999
19283
|
},
|
|
17000
|
-
sidechain:
|
|
19284
|
+
sidechain: {
|
|
19285
|
+
ducks: [
|
|
19286
|
+
{
|
|
19287
|
+
trigger: "kick",
|
|
19288
|
+
targets: ["bass", "pad"],
|
|
19289
|
+
ratio: 3,
|
|
19290
|
+
attackMs: 2,
|
|
19291
|
+
releaseMs: 160,
|
|
19292
|
+
depthDb: 3
|
|
19293
|
+
}
|
|
19294
|
+
],
|
|
19295
|
+
note: "A breath, not an EDM vacuum."
|
|
19296
|
+
},
|
|
17001
19297
|
reverb: "Reverb into Gate on the snare return; broad hall on pads.",
|
|
17002
19298
|
delay: "Echo dotted eighth on arp gaps, lowpassed feedback.",
|
|
17003
19299
|
stereo: "Chorus-Ensemble widens pads; lead and snare stay focused, sub mono.",
|
|
@@ -17284,7 +19580,19 @@ var techno = {
|
|
|
17284
19580
|
p90: [-3.5, 0, -3.5, -7.5, -10.5, -11.5, -12.5, -13.5, -14.5, -20.5],
|
|
17285
19581
|
source: TONAL_BALANCE_SOURCE
|
|
17286
19582
|
},
|
|
17287
|
-
sidechain:
|
|
19583
|
+
sidechain: {
|
|
19584
|
+
ducks: [
|
|
19585
|
+
{
|
|
19586
|
+
trigger: "kick",
|
|
19587
|
+
targets: ["bass"],
|
|
19588
|
+
ratio: 6,
|
|
19589
|
+
attackMs: 0.5,
|
|
19590
|
+
releaseMs: 150,
|
|
19591
|
+
depthDb: 8
|
|
19592
|
+
}
|
|
19593
|
+
],
|
|
19594
|
+
note: "The rumble ducks hard, back before the next kick."
|
|
19595
|
+
},
|
|
17288
19596
|
reverb: "Separate Reverb rumble return, then EQ Eight to remove highs and subsonic build-up; dry kick remains clear.",
|
|
17289
19597
|
delay: "Filtered dotted-eighth Echo on stabs; automate feedback at boundaries.",
|
|
17290
19598
|
stereo: "Kick and rumble mono; higher percussion and dub reflections can spread.",
|
|
@@ -17544,14 +19852,26 @@ var trance = {
|
|
|
17544
19852
|
},
|
|
17545
19853
|
starterClips: ["909 Core Kit Off Hat 125 bpm", "Techno Heavy 133 bpm"],
|
|
17546
19854
|
mix: {
|
|
17547
|
-
lufs: { min: -
|
|
19855
|
+
lufs: { min: -10, max: -8 },
|
|
17548
19856
|
tonalBalance: {
|
|
17549
19857
|
p10: [-18, -9, -9, -13, -15, -15, -15, -16, -19, -26],
|
|
17550
19858
|
p50: [-13, -5, -6, -10, -12, -12, -12, -13, -15, -21],
|
|
17551
19859
|
p90: [-8, -1, -3, -7, -9, -9, -9, -10, -11, -16],
|
|
17552
19860
|
source: TONAL_BALANCE_SOURCE
|
|
17553
19861
|
},
|
|
17554
|
-
sidechain:
|
|
19862
|
+
sidechain: {
|
|
19863
|
+
ducks: [
|
|
19864
|
+
{
|
|
19865
|
+
trigger: "kick",
|
|
19866
|
+
targets: ["bass", "pad"],
|
|
19867
|
+
ratio: 4,
|
|
19868
|
+
attackMs: 1,
|
|
19869
|
+
releaseMs: 140,
|
|
19870
|
+
depthDb: 5
|
|
19871
|
+
}
|
|
19872
|
+
],
|
|
19873
|
+
note: "Bass and pad recover into the offbeat."
|
|
19874
|
+
},
|
|
17555
19875
|
reverb: "Long Reverb hall in breakdowns; highpass the return and reduce it at the drop.",
|
|
17556
19876
|
delay: "Echo dotted eighth on the lead, filtered so the dry hook stays legible.",
|
|
17557
19877
|
stereo: "Wide detuned lead and pad above the lows; kick/sub mono. Check the hook in mono.",
|
|
@@ -17857,14 +20177,26 @@ var trap = {
|
|
|
17857
20177
|
"Sinister Run G Min 142 bpm"
|
|
17858
20178
|
],
|
|
17859
20179
|
mix: {
|
|
17860
|
-
lufs: { min: -
|
|
20180
|
+
lufs: { min: -10, max: -8 },
|
|
17861
20181
|
tonalBalance: {
|
|
17862
20182
|
p10: [-12.5, -8, -10, -13, -15.5, -17, -18, -19, -21.5, -28.5],
|
|
17863
20183
|
p50: [-7.5, -4, -7, -10, -12.5, -14, -15, -16, -17.5, -23.5],
|
|
17864
20184
|
p90: [-2.5, 0, -4, -7, -9.5, -11, -12, -13, -13.5, -18.5],
|
|
17865
20185
|
source: TONAL_BALANCE_SOURCE
|
|
17866
20186
|
},
|
|
17867
|
-
sidechain:
|
|
20187
|
+
sidechain: {
|
|
20188
|
+
ducks: [
|
|
20189
|
+
{
|
|
20190
|
+
trigger: "kick",
|
|
20191
|
+
targets: ["bass"],
|
|
20192
|
+
ratio: 4,
|
|
20193
|
+
attackMs: 0.5,
|
|
20194
|
+
releaseMs: 50,
|
|
20195
|
+
depthDb: 4
|
|
20196
|
+
}
|
|
20197
|
+
],
|
|
20198
|
+
note: "Fast and short, or arrange 808 and kick never to clash."
|
|
20199
|
+
},
|
|
17868
20200
|
reverb: "Big dark reverb on the melody; vocal upfront and nearly dry (short room, de-essed); drums dry.",
|
|
17869
20201
|
delay: "1/4 or 1/8 throws on vocal ends and melody.",
|
|
17870
20202
|
stereo: "Melody/pads wide, 808/kick/clap mono, hats slightly off-center.",
|
|
@@ -18151,7 +20483,19 @@ var ukGarage = {
|
|
|
18151
20483
|
p90: [-5.7, -0.7, -2.7, -6.7, -9.7, -10.7, -10.7, -11.7, -12.7, -17.7],
|
|
18152
20484
|
source: TONAL_BALANCE_SOURCE
|
|
18153
20485
|
},
|
|
18154
|
-
sidechain:
|
|
20486
|
+
sidechain: {
|
|
20487
|
+
ducks: [
|
|
20488
|
+
{
|
|
20489
|
+
trigger: "kick",
|
|
20490
|
+
targets: ["bass"],
|
|
20491
|
+
ratio: 3,
|
|
20492
|
+
attackMs: 1,
|
|
20493
|
+
releaseMs: 60,
|
|
20494
|
+
depthDb: 2
|
|
20495
|
+
}
|
|
20496
|
+
],
|
|
20497
|
+
note: "Clears the kick; keep the long gaps full."
|
|
20498
|
+
},
|
|
18155
20499
|
reverb: "Small Reverb room on clap/rim; a darker longer return for isolated vocal chops.",
|
|
18156
20500
|
delay: "Filtered Echo dotted eighth on chops; silence the feedback before busy hook entries.",
|
|
18157
20501
|
stereo: "Vocal chops and hats can answer left/right; kick, snare and sub stay centered.",
|
|
@@ -18274,14 +20618,16 @@ async function dispatch(command, io) {
|
|
|
18274
20618
|
const text = HELP[command.topic ?? "main"] ?? HELP.main;
|
|
18275
20619
|
io.stdout.write(
|
|
18276
20620
|
`${text}
|
|
18277
|
-
${command.topic &&
|
|
20621
|
+
${command.topic && !["setup", "skills", "login", "logout", "whoami"].includes(
|
|
20622
|
+
command.topic
|
|
20623
|
+
) ? `
|
|
18278
20624
|
${HELP.bridge}
|
|
18279
20625
|
` : ""}`
|
|
18280
20626
|
);
|
|
18281
20627
|
return EXIT_OK;
|
|
18282
20628
|
}
|
|
18283
20629
|
case "version":
|
|
18284
|
-
io.stdout.write(`${
|
|
20630
|
+
io.stdout.write(`${VERSION2}
|
|
18285
20631
|
`);
|
|
18286
20632
|
return EXIT_OK;
|
|
18287
20633
|
case "check":
|
|
@@ -18294,6 +20640,15 @@ ${HELP.bridge}
|
|
|
18294
20640
|
return mcp(command.bridge, io);
|
|
18295
20641
|
case "doctor":
|
|
18296
20642
|
return doctor(command, io);
|
|
20643
|
+
case "login":
|
|
20644
|
+
return login(command, io);
|
|
20645
|
+
case "logout":
|
|
20646
|
+
return logout(command.account, io);
|
|
20647
|
+
case "whoami":
|
|
20648
|
+
return whoami(command, io);
|
|
20649
|
+
case "login-poll":
|
|
20650
|
+
await runLoginPoller(command.file, createLogger("login", false));
|
|
20651
|
+
return EXIT_OK;
|
|
18297
20652
|
case "setup":
|
|
18298
20653
|
return setup(command, io);
|
|
18299
20654
|
case "skills-install": {
|
|
@@ -18356,33 +20711,62 @@ async function runJob(job, flags, mode, io, timeoutS) {
|
|
|
18356
20711
|
const onSigint = () => abort.abort();
|
|
18357
20712
|
process.once("SIGINT", onSigint);
|
|
18358
20713
|
const onProgress = progressPrinter(io, flags.verbose);
|
|
20714
|
+
const auth = await jobAuth(accountOrigin(flags.origin), "cli", log);
|
|
20715
|
+
if (auth.ended)
|
|
20716
|
+
io.stderr.write(`tutticode: ${auth.ended}
|
|
20717
|
+
`);
|
|
18359
20718
|
try {
|
|
20719
|
+
let outcome;
|
|
18360
20720
|
if (flags.noDaemon || io.openPage || !io.entry) {
|
|
18361
20721
|
const session = newSession(flags, mode, io, log);
|
|
18362
20722
|
try {
|
|
18363
|
-
|
|
20723
|
+
outcome = await session.run(job, {
|
|
18364
20724
|
timeoutMs: timeoutS * 1e3,
|
|
18365
20725
|
signal: abort.signal,
|
|
18366
|
-
onProgress
|
|
20726
|
+
onProgress,
|
|
20727
|
+
auth: auth.auth
|
|
18367
20728
|
});
|
|
18368
20729
|
} finally {
|
|
18369
20730
|
await session.close();
|
|
18370
20731
|
}
|
|
18371
|
-
}
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
20732
|
+
} else
|
|
20733
|
+
outcome = await runViaDaemon(
|
|
20734
|
+
{ config: daemonConfig(flags, mode), entry: io.entry, log },
|
|
20735
|
+
job,
|
|
20736
|
+
timeoutS * 1e3,
|
|
20737
|
+
{
|
|
20738
|
+
signal: abort.signal,
|
|
20739
|
+
onProgress,
|
|
20740
|
+
onStatus: statusWriter(flags, mode, io)
|
|
20741
|
+
},
|
|
20742
|
+
auth.auth
|
|
20743
|
+
);
|
|
20744
|
+
if (outcome.auth?.rejected)
|
|
20745
|
+
io.stderr.write(`tutticode: ${rejectedLine(outcome.auth)}
|
|
20746
|
+
`);
|
|
20747
|
+
return outcome;
|
|
18382
20748
|
} finally {
|
|
18383
20749
|
process.removeListener("SIGINT", onSigint);
|
|
18384
20750
|
}
|
|
18385
20751
|
}
|
|
20752
|
+
function accountOrigin(origin) {
|
|
20753
|
+
return normalizeOrigin(origin ?? "") ?? BRIDGE_ORIGINS[0];
|
|
20754
|
+
}
|
|
20755
|
+
async function jobAuth(origin, client, log) {
|
|
20756
|
+
try {
|
|
20757
|
+
const token = await resolveToken(origin);
|
|
20758
|
+
return {
|
|
20759
|
+
auth: bridgeAuth(token, client),
|
|
20760
|
+
...token.ended ? { ended: token.ended } : {}
|
|
20761
|
+
};
|
|
20762
|
+
} catch (e) {
|
|
20763
|
+
log("warn", `sign-in unavailable: ${e.message}`);
|
|
20764
|
+
return { auth: bridgeAuth({ kind: "anonymous" }, client) };
|
|
20765
|
+
}
|
|
20766
|
+
}
|
|
20767
|
+
function rejectedLine(auth) {
|
|
20768
|
+
return `the page did not accept your sign-in (${auth.rejected}); this ran without sign-in. Run \`tutticode login\`.`;
|
|
20769
|
+
}
|
|
18386
20770
|
function progressPrinter(io, verbose) {
|
|
18387
20771
|
let last = "";
|
|
18388
20772
|
return (p) => {
|
|
@@ -18395,27 +20779,32 @@ function progressPrinter(io, verbose) {
|
|
|
18395
20779
|
last = line;
|
|
18396
20780
|
};
|
|
18397
20781
|
}
|
|
18398
|
-
async function readProgram(
|
|
18399
|
-
if (
|
|
20782
|
+
async function readProgram(file2, io) {
|
|
20783
|
+
if (file2 === "-") {
|
|
18400
20784
|
const chunks = [];
|
|
18401
20785
|
for await (const chunk of io.stdin)
|
|
18402
20786
|
chunks.push(Buffer.from(chunk));
|
|
18403
20787
|
return Buffer.concat(chunks).toString("utf8");
|
|
18404
20788
|
}
|
|
18405
20789
|
try {
|
|
18406
|
-
return (0,
|
|
20790
|
+
return (0, import_node_fs11.readFileSync)(file2, "utf8");
|
|
18407
20791
|
} catch (e) {
|
|
18408
20792
|
throw new UsageError(
|
|
18409
|
-
`Cannot read ${
|
|
20793
|
+
`Cannot read ${file2}: ${e.code ?? e.message}`
|
|
18410
20794
|
);
|
|
18411
20795
|
}
|
|
18412
20796
|
}
|
|
18413
|
-
function printError(outcome, json,
|
|
20797
|
+
function printError(outcome, json, file2, io) {
|
|
18414
20798
|
if (json)
|
|
18415
20799
|
io.stdout.write(
|
|
18416
20800
|
`${JSON.stringify(
|
|
18417
20801
|
{
|
|
18418
|
-
error: {
|
|
20802
|
+
error: {
|
|
20803
|
+
code: outcome.code,
|
|
20804
|
+
message: outcome.message,
|
|
20805
|
+
...outcome.limit ? { limit: outcome.limit } : {}
|
|
20806
|
+
},
|
|
20807
|
+
...outcome.code === "limit" ? { loginHint: { command: TUTTICODE_LOGIN_HINT_COMMAND } } : {},
|
|
18419
20808
|
...outcome.report ? { report: outcome.report } : {}
|
|
18420
20809
|
},
|
|
18421
20810
|
null,
|
|
@@ -18424,7 +20813,7 @@ function printError(outcome, json, file, io) {
|
|
|
18424
20813
|
`
|
|
18425
20814
|
);
|
|
18426
20815
|
if (outcome.report)
|
|
18427
|
-
for (const line of formatDiagnostics(outcome.report,
|
|
20816
|
+
for (const line of formatDiagnostics(outcome.report, file2))
|
|
18428
20817
|
io.stderr.write(`${line}
|
|
18429
20818
|
`);
|
|
18430
20819
|
io.stderr.write(`tutticode: ${outcome.code}: ${outcome.message}
|
|
@@ -18436,7 +20825,7 @@ async function check(command, io) {
|
|
|
18436
20825
|
const outcome = await runJob(
|
|
18437
20826
|
{
|
|
18438
20827
|
type: "check",
|
|
18439
|
-
id: (0,
|
|
20828
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
18440
20829
|
code,
|
|
18441
20830
|
...command.bpm !== void 0 ? { bpm: command.bpm } : {}
|
|
18442
20831
|
},
|
|
@@ -18467,8 +20856,15 @@ async function check(command, io) {
|
|
|
18467
20856
|
}
|
|
18468
20857
|
async function render(command, io) {
|
|
18469
20858
|
const code = await readProgram(command.file, io);
|
|
20859
|
+
const brandTagOff = command.options.brandTag === false || process.env.TUTTICODE_BRAND_TAG === "0";
|
|
18470
20860
|
const outcome = await runJob(
|
|
18471
|
-
{
|
|
20861
|
+
{
|
|
20862
|
+
type: "render",
|
|
20863
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
20864
|
+
code,
|
|
20865
|
+
...command.options,
|
|
20866
|
+
...brandTagOff ? { brandTag: false } : {}
|
|
20867
|
+
},
|
|
18472
20868
|
command.bridge,
|
|
18473
20869
|
modeFor(command.bridge, io),
|
|
18474
20870
|
io,
|
|
@@ -18499,6 +20895,27 @@ async function render(command, io) {
|
|
|
18499
20895
|
io.stdout.write(`${line}
|
|
18500
20896
|
`);
|
|
18501
20897
|
}
|
|
20898
|
+
if (outcome.meta.watermark)
|
|
20899
|
+
io.stderr.write(`${WATERMARK_LINE}
|
|
20900
|
+
`);
|
|
20901
|
+
const anonymous = outcome.auth ? !outcome.auth.signedIn : false;
|
|
20902
|
+
if (anonymous && brandTagOff && firstTime("brand-tag-anonymous", "install"))
|
|
20903
|
+
io.stderr.write(
|
|
20904
|
+
'The "Made with TuttiCode" file tag can be turned off after `tutticode login`.\n'
|
|
20905
|
+
);
|
|
20906
|
+
if (anonymous && io.interactive) {
|
|
20907
|
+
if (firstTime("terms", "install"))
|
|
20908
|
+
io.stderr.write(
|
|
20909
|
+
`TuttiCode renders are subject to ${tutticodeTermsUrl(
|
|
20910
|
+
accountOrigin(command.bridge.origin)
|
|
20911
|
+
)}
|
|
20912
|
+
`
|
|
20913
|
+
);
|
|
20914
|
+
if (firstTime("sign-in-hint", "day"))
|
|
20915
|
+
io.stderr.write(
|
|
20916
|
+
"Sign in free for longer renders without the voice tag: tutticode login\n"
|
|
20917
|
+
);
|
|
20918
|
+
}
|
|
18502
20919
|
return EXIT_OK;
|
|
18503
20920
|
}
|
|
18504
20921
|
async function open(command, io) {
|
|
@@ -18537,15 +20954,17 @@ ${url}
|
|
|
18537
20954
|
}
|
|
18538
20955
|
async function mcp(flags, io) {
|
|
18539
20956
|
const log = createLogger("mcp", flags.verbose);
|
|
20957
|
+
const origin = accountOrigin(flags.origin);
|
|
18540
20958
|
const { backend, close } = sessionBackend(
|
|
18541
20959
|
(mode) => newSession(
|
|
18542
20960
|
flags,
|
|
18543
20961
|
flags.show ? "show" : mode,
|
|
18544
20962
|
{ ...io, stderr: process.stderr },
|
|
18545
20963
|
log
|
|
18546
|
-
)
|
|
20964
|
+
),
|
|
20965
|
+
() => jobAuth(origin, "mcp", log)
|
|
18547
20966
|
);
|
|
18548
|
-
log("info", `mcp server ${
|
|
20967
|
+
log("info", `mcp server ${VERSION2} starting`);
|
|
18549
20968
|
await serveMcpStdio({
|
|
18550
20969
|
input: io.stdin,
|
|
18551
20970
|
output: io.stdout,
|
|
@@ -18558,7 +20977,8 @@ async function mcp(flags, io) {
|
|
|
18558
20977
|
code
|
|
18559
20978
|
),
|
|
18560
20979
|
// --headless: no window, not even for code_open
|
|
18561
|
-
canShow: flags.show !== false
|
|
20980
|
+
canShow: flags.show !== false,
|
|
20981
|
+
account: () => accountState(origin)
|
|
18562
20982
|
});
|
|
18563
20983
|
await close();
|
|
18564
20984
|
return EXIT_OK;
|
|
@@ -18642,14 +21062,313 @@ async function doctor(command, io) {
|
|
|
18642
21062
|
)}) in ${info.browser.version ?? "the browser"}` : `${info.browser.version ?? "the browser"} accepted no Local Network Access grant (it may not enforce it)`
|
|
18643
21063
|
);
|
|
18644
21064
|
}
|
|
21065
|
+
await accountChecks(accountOrigin(flags.origin), command.fix, add);
|
|
18645
21066
|
const ok = results.every((r2) => r2.ok || r2.check === "local network access");
|
|
18646
21067
|
if (command.json)
|
|
18647
21068
|
io.stdout.write(
|
|
18648
|
-
`${JSON.stringify({ ok, version:
|
|
21069
|
+
`${JSON.stringify({ ok, version: VERSION2, checks: results }, null, 2)}
|
|
18649
21070
|
`
|
|
18650
21071
|
);
|
|
18651
21072
|
return ok ? EXIT_OK : EXIT_BRIDGE;
|
|
18652
21073
|
}
|
|
21074
|
+
async function accountChecks(origin, fix, add) {
|
|
21075
|
+
const state = await accountState(origin);
|
|
21076
|
+
switch (state.state) {
|
|
21077
|
+
case "signed-in":
|
|
21078
|
+
add(
|
|
21079
|
+
"account",
|
|
21080
|
+
true,
|
|
21081
|
+
`signed in as ${signedInAs(state.me)}${state.tokenKind === "pat" ? " with TUTTICODE_TOKEN" : ""}`
|
|
21082
|
+
);
|
|
21083
|
+
break;
|
|
21084
|
+
case "anonymous":
|
|
21085
|
+
add("account", true, `not signed in: ${anonymousLimitsText()}`);
|
|
21086
|
+
break;
|
|
21087
|
+
case "rejected":
|
|
21088
|
+
add(
|
|
21089
|
+
"account",
|
|
21090
|
+
false,
|
|
21091
|
+
`sign-in rejected (${state.message}): run tutticode login`
|
|
21092
|
+
);
|
|
21093
|
+
break;
|
|
21094
|
+
case "unreachable":
|
|
21095
|
+
add("account", false, `could not check the sign-in: ${state.message}`);
|
|
21096
|
+
}
|
|
21097
|
+
if (envToken())
|
|
21098
|
+
add(
|
|
21099
|
+
"token override",
|
|
21100
|
+
true,
|
|
21101
|
+
"TUTTICODE_TOKEN is set: it wins over the stored sign-in"
|
|
21102
|
+
);
|
|
21103
|
+
const path = credentialsPath();
|
|
21104
|
+
if (!(0, import_node_fs11.existsSync)(path))
|
|
21105
|
+
return;
|
|
21106
|
+
let access = checkFileAccess(path);
|
|
21107
|
+
if (fix && access && !access.ok) {
|
|
21108
|
+
try {
|
|
21109
|
+
fixFileAccess(path);
|
|
21110
|
+
} catch (e) {
|
|
21111
|
+
if (!(e instanceof PrivateDirError))
|
|
21112
|
+
throw e;
|
|
21113
|
+
add("credentials folder", false, e.message);
|
|
21114
|
+
}
|
|
21115
|
+
access = checkFileAccess(path);
|
|
21116
|
+
}
|
|
21117
|
+
if (!access)
|
|
21118
|
+
add("credentials file", true, `${path} (permissions not checked)`);
|
|
21119
|
+
else
|
|
21120
|
+
add(
|
|
21121
|
+
"credentials file",
|
|
21122
|
+
access.ok,
|
|
21123
|
+
access.ok ? `${path}: ${access.detail}` : `${path} is too open (${access.detail}): run tutticode doctor --fix`
|
|
21124
|
+
);
|
|
21125
|
+
}
|
|
21126
|
+
function anonymousLimitsText() {
|
|
21127
|
+
const limits = accountLimits({ state: "anonymous" });
|
|
21128
|
+
return `renders up to ${limits.maxSeconds} s, a voice tag over ${limits.watermarkAfterSeconds} s (sign in free: tutticode login)`;
|
|
21129
|
+
}
|
|
21130
|
+
async function login(command, io) {
|
|
21131
|
+
const origin = accountOrigin(command.account.origin);
|
|
21132
|
+
const locale = command.account.locale ?? "en";
|
|
21133
|
+
ensurePrivateDir();
|
|
21134
|
+
if (!command.noWait && !io.interactive) {
|
|
21135
|
+
io.stderr.write(
|
|
21136
|
+
`tutticode: login needs a person: run it in a terminal, or use ${TUTTICODE_LOGIN_HINT_COMMAND}
|
|
21137
|
+
`
|
|
21138
|
+
);
|
|
21139
|
+
return EXIT_USAGE;
|
|
21140
|
+
}
|
|
21141
|
+
if (envToken())
|
|
21142
|
+
io.stderr.write(
|
|
21143
|
+
"Note: TUTTICODE_TOKEN is set; it wins over this sign-in until you unset it.\n"
|
|
21144
|
+
);
|
|
21145
|
+
const name = deviceName();
|
|
21146
|
+
const abort = new AbortController();
|
|
21147
|
+
const onSigint = () => abort.abort();
|
|
21148
|
+
process.once("SIGINT", onSigint);
|
|
21149
|
+
try {
|
|
21150
|
+
let tokens;
|
|
21151
|
+
if (!command.device) {
|
|
21152
|
+
const remote = !io.openBrowser && (!!process.env.SSH_CONNECTION || process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY);
|
|
21153
|
+
tokens = await loopbackLogin({
|
|
21154
|
+
origin,
|
|
21155
|
+
locale,
|
|
21156
|
+
deviceName: name,
|
|
21157
|
+
signal: abort.signal,
|
|
21158
|
+
openBrowser: io.openBrowser ?? (remote ? () => void 0 : openExternal),
|
|
21159
|
+
onUrl: (url) => io.stderr.write(
|
|
21160
|
+
remote ? `No browser can be opened here. Open this link in a browser on this computer:
|
|
21161
|
+
${url}
|
|
21162
|
+
Or sign in with a code on any device: tutticode login --device
|
|
21163
|
+
` : `Opening your browser to sign in. If it does not open, go to:
|
|
21164
|
+
${url}
|
|
21165
|
+
`
|
|
21166
|
+
)
|
|
21167
|
+
});
|
|
21168
|
+
} else {
|
|
21169
|
+
const device = await startDeviceLogin(origin, name);
|
|
21170
|
+
const complete = device.verificationUriComplete ?? device.verificationUri;
|
|
21171
|
+
if (command.noWait) {
|
|
21172
|
+
const file2 = writePendingLogin({
|
|
21173
|
+
origin,
|
|
21174
|
+
deviceCode: device.deviceCode,
|
|
21175
|
+
userCode: device.userCode,
|
|
21176
|
+
verificationUri: device.verificationUri,
|
|
21177
|
+
...device.verificationUriComplete ? { verificationUriComplete: device.verificationUriComplete } : {},
|
|
21178
|
+
expiresAt: new Date(
|
|
21179
|
+
Date.now() + device.expiresIn * 1e3
|
|
21180
|
+
).toISOString(),
|
|
21181
|
+
interval: device.interval,
|
|
21182
|
+
deviceName: name
|
|
21183
|
+
});
|
|
21184
|
+
if (io.spawnLoginPoller)
|
|
21185
|
+
io.spawnLoginPoller(file2);
|
|
21186
|
+
else if (io.entry)
|
|
21187
|
+
spawnLoginPoller(io.entry, file2);
|
|
21188
|
+
else
|
|
21189
|
+
throw new Error("Cannot start the background sign-in.");
|
|
21190
|
+
if (command.json)
|
|
21191
|
+
io.stdout.write(
|
|
21192
|
+
`${JSON.stringify(
|
|
21193
|
+
{
|
|
21194
|
+
verificationUri: device.verificationUri,
|
|
21195
|
+
verificationUriComplete: complete,
|
|
21196
|
+
userCode: device.userCode,
|
|
21197
|
+
expiresIn: device.expiresIn
|
|
21198
|
+
},
|
|
21199
|
+
null,
|
|
21200
|
+
2
|
|
21201
|
+
)}
|
|
21202
|
+
`
|
|
21203
|
+
);
|
|
21204
|
+
else
|
|
21205
|
+
io.stdout.write(
|
|
21206
|
+
`To sign in, open ${complete}
|
|
21207
|
+
(or open ${device.verificationUri} and enter the code ${device.userCode})
|
|
21208
|
+
within ${Math.round(
|
|
21209
|
+
device.expiresIn / 60
|
|
21210
|
+
)} minutes. tutticode finishes the sign-in in the background once it is approved; until then renders run without sign-in.
|
|
21211
|
+
`
|
|
21212
|
+
);
|
|
21213
|
+
return EXIT_OK;
|
|
21214
|
+
}
|
|
21215
|
+
io.stderr.write(
|
|
21216
|
+
`To sign in, open ${device.verificationUri} on any device and enter the code
|
|
21217
|
+
|
|
21218
|
+
${device.userCode}
|
|
21219
|
+
|
|
21220
|
+
or open ${complete}
|
|
21221
|
+
or scan it with a phone:
|
|
21222
|
+
|
|
21223
|
+
${renderUnicodeCompact(
|
|
21224
|
+
complete,
|
|
21225
|
+
{ border: 1 }
|
|
21226
|
+
)}
|
|
21227
|
+
|
|
21228
|
+
Waiting for approval (the code expires in ${Math.round(
|
|
21229
|
+
device.expiresIn / 60
|
|
21230
|
+
)} minutes)...
|
|
21231
|
+
`
|
|
21232
|
+
);
|
|
21233
|
+
tokens = await pollDeviceToken(origin, device, {
|
|
21234
|
+
...io.loginPoll,
|
|
21235
|
+
signal: abort.signal
|
|
21236
|
+
});
|
|
21237
|
+
}
|
|
21238
|
+
const state = await completeLogin(origin, tokens, name);
|
|
21239
|
+
io.stdout.write(
|
|
21240
|
+
state.state === "signed-in" ? `Signed in as ${signedInAs(state.me)}
|
|
21241
|
+
` : "Signed in.\n"
|
|
21242
|
+
);
|
|
21243
|
+
return EXIT_OK;
|
|
21244
|
+
} catch (e) {
|
|
21245
|
+
if (e instanceof LoginError || e instanceof ApiUnreachable) {
|
|
21246
|
+
io.stderr.write(`tutticode: ${e.message}
|
|
21247
|
+
`);
|
|
21248
|
+
return EXIT_BRIDGE;
|
|
21249
|
+
}
|
|
21250
|
+
throw e;
|
|
21251
|
+
} finally {
|
|
21252
|
+
process.removeListener("SIGINT", onSigint);
|
|
21253
|
+
}
|
|
21254
|
+
}
|
|
21255
|
+
async function logout(flags, io) {
|
|
21256
|
+
const origin = accountOrigin(flags.origin);
|
|
21257
|
+
const accountPage = `${origin}${ACCOUNT_PATH}`;
|
|
21258
|
+
(0, import_node_fs11.rmSync)(pendingLoginPath(origin), { force: true });
|
|
21259
|
+
if (envToken())
|
|
21260
|
+
io.stderr.write(
|
|
21261
|
+
`TUTTICODE_TOKEN is set: it keeps signing tutticode in until you unset it. Revoke it on the account page: ${accountPage}
|
|
21262
|
+
`
|
|
21263
|
+
);
|
|
21264
|
+
const stored = readAccount(origin);
|
|
21265
|
+
if (!stored) {
|
|
21266
|
+
io.stdout.write(`Not signed in on this computer (${origin}).
|
|
21267
|
+
`);
|
|
21268
|
+
return EXIT_OK;
|
|
21269
|
+
}
|
|
21270
|
+
const revoked = await revokeToken(origin, stored.refreshToken);
|
|
21271
|
+
await saveAccount(origin, void 0);
|
|
21272
|
+
if (!revoked)
|
|
21273
|
+
io.stderr.write(
|
|
21274
|
+
`Warning: ${origin} could not be reached to end the sign-in; it is deleted here. Revoke this device on the account page: ${accountPage}
|
|
21275
|
+
`
|
|
21276
|
+
);
|
|
21277
|
+
io.stdout.write(
|
|
21278
|
+
`Signed out${stored.email ? ` (${stored.email})` : ""}. Other devices and access tokens: ${accountPage}
|
|
21279
|
+
`
|
|
21280
|
+
);
|
|
21281
|
+
return EXIT_OK;
|
|
21282
|
+
}
|
|
21283
|
+
async function whoami(command, io) {
|
|
21284
|
+
const origin = accountOrigin(command.account.origin);
|
|
21285
|
+
const state = await accountState(origin);
|
|
21286
|
+
const pending = state.state === "signed-in" ? void 0 : pendingLogin(origin);
|
|
21287
|
+
if (command.json)
|
|
21288
|
+
io.stdout.write(`${JSON.stringify(whoamiJson(state, pending), null, 2)}
|
|
21289
|
+
`);
|
|
21290
|
+
else
|
|
21291
|
+
for (const line of whoamiLines(state, pending))
|
|
21292
|
+
io.stdout.write(`${line}
|
|
21293
|
+
`);
|
|
21294
|
+
if (state.state === "signed-in")
|
|
21295
|
+
return EXIT_OK;
|
|
21296
|
+
return state.state === "anonymous" ? EXIT_ANONYMOUS : EXIT_BRIDGE;
|
|
21297
|
+
}
|
|
21298
|
+
function whoamiJson(state, pending) {
|
|
21299
|
+
const pendingJson = pending ? {
|
|
21300
|
+
pendingLogin: {
|
|
21301
|
+
userCode: pending.userCode,
|
|
21302
|
+
verificationUri: pending.verificationUri,
|
|
21303
|
+
expiresAt: pending.expiresAt
|
|
21304
|
+
}
|
|
21305
|
+
} : {};
|
|
21306
|
+
switch (state.state) {
|
|
21307
|
+
case "signed-in": {
|
|
21308
|
+
const { me } = state;
|
|
21309
|
+
return {
|
|
21310
|
+
signedIn: true,
|
|
21311
|
+
tokenKind: state.tokenKind,
|
|
21312
|
+
user: me.user,
|
|
21313
|
+
plan: me.plan,
|
|
21314
|
+
limits: me.limits,
|
|
21315
|
+
usageToday: me.usageToday,
|
|
21316
|
+
expiresAt: me.expiresAt,
|
|
21317
|
+
deviceName: me.deviceName,
|
|
21318
|
+
...me.termsUpdate ? { termsUpdate: me.termsUpdate } : {}
|
|
21319
|
+
};
|
|
21320
|
+
}
|
|
21321
|
+
case "anonymous":
|
|
21322
|
+
return {
|
|
21323
|
+
signedIn: false,
|
|
21324
|
+
tokenKind: "anonymous",
|
|
21325
|
+
limits: accountLimits(state),
|
|
21326
|
+
loginHint: LOGIN_HINT,
|
|
21327
|
+
...pendingJson
|
|
21328
|
+
};
|
|
21329
|
+
default:
|
|
21330
|
+
return {
|
|
21331
|
+
signedIn: false,
|
|
21332
|
+
tokenKind: state.tokenKind,
|
|
21333
|
+
error: {
|
|
21334
|
+
code: state.state === "rejected" ? state.code ?? "rejected" : "unreachable",
|
|
21335
|
+
message: state.message
|
|
21336
|
+
},
|
|
21337
|
+
...pendingJson
|
|
21338
|
+
};
|
|
21339
|
+
}
|
|
21340
|
+
}
|
|
21341
|
+
function whoamiLines(state, pending) {
|
|
21342
|
+
const waiting = pending ? [
|
|
21343
|
+
`A sign-in is waiting for approval: open ${pending.verificationUriComplete ?? pending.verificationUri} (code ${pending.userCode}).`
|
|
21344
|
+
] : [];
|
|
21345
|
+
switch (state.state) {
|
|
21346
|
+
case "signed-in": {
|
|
21347
|
+
const { me } = state;
|
|
21348
|
+
const until = me.expiresAt ? `, until ${me.expiresAt.slice(0, 10)}` : "";
|
|
21349
|
+
const lines = [
|
|
21350
|
+
`Signed in as ${signedInAs(me)}${me.user.nickname ? ` - ${me.user.nickname}` : ""}`,
|
|
21351
|
+
state.tokenKind === "pat" ? `With TUTTICODE_TOKEN (an access token)${until}` : `Signed-in device: ${me.deviceName ?? "this computer"}${until}`,
|
|
21352
|
+
`Limits: renders up to ${me.limits.maxSeconds} s, no voice tag`,
|
|
21353
|
+
`Today: ${me.usageToday.renders} render${me.usageToday.renders === 1 ? "" : "s"}, ${Math.round(me.usageToday.seconds)} s of audio`
|
|
21354
|
+
];
|
|
21355
|
+
if (me.termsUpdate && firstTime(`terms-update:${me.termsUpdate}`, "install"))
|
|
21356
|
+
lines.push(
|
|
21357
|
+
`The TuttiCode terms changed: ${me.termsUpdate} (the next tutticode login accepts them)`
|
|
21358
|
+
);
|
|
21359
|
+
return lines;
|
|
21360
|
+
}
|
|
21361
|
+
case "anonymous":
|
|
21362
|
+
return [`Not signed in: ${anonymousLimitsText()}.`, ...waiting];
|
|
21363
|
+
case "rejected":
|
|
21364
|
+
return [
|
|
21365
|
+
state.message.includes("tutticode login") ? state.message : `Your sign-in was rejected (${state.message}). Run tutticode login.`,
|
|
21366
|
+
...waiting
|
|
21367
|
+
];
|
|
21368
|
+
case "unreachable":
|
|
21369
|
+
return [`Could not check the sign-in: ${state.message}`];
|
|
21370
|
+
}
|
|
21371
|
+
}
|
|
18653
21372
|
async function setup(command, io) {
|
|
18654
21373
|
const steps = policySteps(command.undo);
|
|
18655
21374
|
if (command.undo) {
|
|
@@ -18727,7 +21446,7 @@ function askOnTerminal(io) {
|
|
|
18727
21446
|
}
|
|
18728
21447
|
|
|
18729
21448
|
// src/bin.ts
|
|
18730
|
-
setSkillsDir((0,
|
|
21449
|
+
setSkillsDir((0, import_node_path12.join)(__dirname, "skills"));
|
|
18731
21450
|
main(process.argv.slice(2), {
|
|
18732
21451
|
stdout: process.stdout,
|
|
18733
21452
|
stderr: process.stderr,
|