turnzero 0.13.0-next.1 → 0.14.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/turnzero-cloud.mjs +645 -86
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Turn Zero Cloud connects to your AI coding tool through a Model Context Protocol (MCP) server. Once connected, your AI can code, build, and test backends, connect them to your app, and host your code in the cloud.
|
|
4
4
|
|
|
5
|
-
Turn Zero Cloud is for developers who build with an AI coding tool and do not want to run servers. Your tool writes, builds, and tests the application on your computer. The platform builds it, runs it in production at the application's address, and records its logs, counters, and usage. To start, connect your AI tool: https://turnzero.ai/cloud/getting-started/connect-your-tool/
|
|
5
|
+
Turn Zero Cloud is for developers who build with an AI coding tool and do not want to run servers. Your tool writes, builds, and tests the application on your computer. The platform builds it, runs it in production at the application's address, and records its logs, counters, and usage. To start, connect your AI coding tool: https://turnzero.ai/cloud/getting-started/connect-your-tool/
|
|
6
6
|
|
|
7
7
|
## What this package is
|
|
8
8
|
|
|
9
|
-
The turnzero command-line tool is used by the Turn Zero Cloud MCP server. Your AI tool runs it on your computer for the work the platform cannot do from the cloud: reading your files, writing files, and starting your application.
|
|
9
|
+
The turnzero command-line tool is used by the Turn Zero Cloud MCP server. Your AI coding tool runs it on your computer for the work the platform cannot do from the cloud: reading your files, writing files, and starting your application.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Install it once with `npm install -g turnzero`, then run `turnzero login` once in a terminal of your own and approve the login in your browser. The MCP server then gives your AI coding tool each command line to run, such as `turnzero deploy --application <id>`. On Windows the lines run `turnzero.cmd`. Where the command is not installed, the same line runs through npx as `npx -y turnzero@latest`. It needs Node.js 24 or later.
|
|
12
12
|
|
|
13
13
|
## Subcommands
|
|
14
14
|
|
|
@@ -22,7 +22,8 @@ You do not install it. The MCP server gives your AI tool each command line to ru
|
|
|
22
22
|
- `provision` gets new development credentials for your application and writes them to its environment file.
|
|
23
23
|
- `run` starts your application on your computer with that file's settings.
|
|
24
24
|
- `scaffold` writes a new service into your project from a template, installs it, and runs its tests.
|
|
25
|
-
- `
|
|
25
|
+
- `login` logs this computer in through your browser, and `logout` revokes its token.
|
|
26
|
+
- `authorize` gives this computer a token through your tool, where you ask your tool to.
|
|
26
27
|
- `token request` and `token claim` get a token for another computer or for an unattended run.
|
|
27
28
|
|
|
28
29
|
Every subcommand's options, statuses, and output are on the reference page: https://turnzero.ai/cloud/reference/turnzero-command/
|
package/dist/turnzero-cloud.mjs
CHANGED
|
@@ -61,6 +61,20 @@ var startBodyOf = (application, upload, environment = UPLOAD_ENVIRONMENT) => ({
|
|
|
61
61
|
upload
|
|
62
62
|
});
|
|
63
63
|
var TURNZERO_CLOUD_PACKAGE = "turnzero";
|
|
64
|
+
var LOGIN_SUBCOMMAND = "login";
|
|
65
|
+
var LOGIN_OPTION = Object.freeze({ origin: "--origin", noBrowser: "--no-browser" });
|
|
66
|
+
var loginLineOf = (origin) => commandLineOf(origin, LOGIN_SUBCOMMAND, origin !== DEFAULT_COMMAND_ORIGIN ? { origin } : {});
|
|
67
|
+
var COMPUTER_LOGIN_PATH = "/approve/computer";
|
|
68
|
+
var COMPUTER_LOGIN_QUERY = Object.freeze({ challenge: "challenge", state: "state", port: "port", label: "label" });
|
|
69
|
+
var LOGIN_STATE_FORM = /^[A-Za-z0-9_-]{43}$/;
|
|
70
|
+
var computerLoginAddressOf = (origin, login2) => {
|
|
71
|
+
const query = new URLSearchParams();
|
|
72
|
+
query.set(COMPUTER_LOGIN_QUERY.challenge, login2.challenge);
|
|
73
|
+
query.set(COMPUTER_LOGIN_QUERY.state, login2.state);
|
|
74
|
+
if (login2.port !== null) query.set(COMPUTER_LOGIN_QUERY.port, String(login2.port));
|
|
75
|
+
query.set(COMPUTER_LOGIN_QUERY.label, login2.label);
|
|
76
|
+
return `${origin}${COMPUTER_LOGIN_PATH}?${query.toString()}`;
|
|
77
|
+
};
|
|
64
78
|
var DEPLOY_SUBCOMMAND = "deploy";
|
|
65
79
|
var CHECK_SUBCOMMAND = "check";
|
|
66
80
|
var CHECK_ACTION = "check_deploy";
|
|
@@ -70,26 +84,43 @@ var DEPLOY_OPTION = Object.freeze({ env: "--env", upload: "--upload", path: "--p
|
|
|
70
84
|
var CHECK_OPTION = { application: "--application", path: "--path", origin: "--origin" };
|
|
71
85
|
var HASH_OPTION = Object.freeze({ path: DEPLOY_OPTION.path, list: "--list", origin: DEPLOY_OPTION.origin });
|
|
72
86
|
var HASH_LINE_HEAD = "sha256: ";
|
|
73
|
-
var LINE_PROGRAM =
|
|
74
|
-
var WINDOWS_LINE_PROGRAM =
|
|
75
|
-
var LINE_RUNNER =
|
|
76
|
-
var WINDOWS_LINE_RUNNER =
|
|
77
|
-
var
|
|
78
|
-
var
|
|
87
|
+
var LINE_PROGRAM = TURNZERO_CLOUD_PACKAGE;
|
|
88
|
+
var WINDOWS_LINE_PROGRAM = `${TURNZERO_CLOUD_PACKAGE}.cmd`;
|
|
89
|
+
var LINE_RUNNER = LINE_PROGRAM;
|
|
90
|
+
var WINDOWS_LINE_RUNNER = WINDOWS_LINE_PROGRAM;
|
|
91
|
+
var NPX_LINE_RUNNER = "npx -y";
|
|
92
|
+
var NPX_WINDOWS_LINE_RUNNER = "npx.cmd -y";
|
|
93
|
+
var installLineOf = (origin) => `npm install -g ${TURNZERO_CLOUD_PACKAGE}${origin === DEFAULT_COMMAND_ORIGIN ? "" : `@${commandTagOf(origin)}`}`;
|
|
94
|
+
var npxFormOf = (line, origin) => {
|
|
79
95
|
const runner = `${LINE_RUNNER} `;
|
|
80
96
|
const at = line.indexOf(runner);
|
|
81
97
|
const before = at < 0 ? null : line.slice(0, at);
|
|
82
98
|
if (before === null || before !== "" && !LINE_GRANT_HEAD.test(before)) {
|
|
83
|
-
throw new Error(`a line's
|
|
99
|
+
throw new Error(`a line's npx form was asked for a line that does not open with ${LINE_RUNNER}, after an optional echo of a grant`);
|
|
84
100
|
}
|
|
85
|
-
return `${before}${
|
|
101
|
+
return `${before}${commandRunnerOf(commandTagOf(origin))} ${line.slice(at + runner.length)}`;
|
|
102
|
+
};
|
|
103
|
+
var LINE_GRANT_HEAD = /^echo \S+ \| $/;
|
|
104
|
+
var windowsLineOf = (line) => {
|
|
105
|
+
for (const [head, windows] of [
|
|
106
|
+
[LINE_RUNNER, WINDOWS_LINE_RUNNER],
|
|
107
|
+
[NPX_LINE_RUNNER, NPX_WINDOWS_LINE_RUNNER]
|
|
108
|
+
]) {
|
|
109
|
+
const runner = `${head} `;
|
|
110
|
+
const at = line.indexOf(runner);
|
|
111
|
+
if (at < 0) continue;
|
|
112
|
+
const before = line.slice(0, at);
|
|
113
|
+
if (before !== "" && !LINE_GRANT_HEAD.test(before)) continue;
|
|
114
|
+
return `${before}${windows} ${line.slice(at + runner.length)}`;
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`a line's Windows form was asked for a line that does not open with ${LINE_RUNNER} or ${NPX_LINE_RUNNER}, after an optional echo of a grant`);
|
|
86
117
|
};
|
|
87
118
|
var originWord = (word) => word.includes("[") ? `"${word}"` : word;
|
|
88
|
-
var commandRunnerOf = (tag) => `${
|
|
119
|
+
var commandRunnerOf = (tag) => `${NPX_LINE_RUNNER} ${TURNZERO_CLOUD_PACKAGE}@${tag}`;
|
|
89
120
|
var COMMAND_TAGS = Object.freeze({ production: "latest", other: "next" });
|
|
90
121
|
var commandTagOf = (origin) => origin === DEFAULT_COMMAND_ORIGIN ? COMMAND_TAGS.production : COMMAND_TAGS.other;
|
|
91
122
|
var commandLineOf = (addressOrigin, subcommand, parts = {}) => [
|
|
92
|
-
|
|
123
|
+
(void addressOrigin, LINE_RUNNER),
|
|
93
124
|
subcommand,
|
|
94
125
|
...parts.act !== void 0 ? [parts.act] : [],
|
|
95
126
|
...(parts.options ?? []).map(([name, value]) => `${name} ${value}`),
|
|
@@ -347,7 +378,7 @@ function compareWireVersions(a, b) {
|
|
|
347
378
|
|
|
348
379
|
// src/turnzero_cloud_command/plane_wire.ts
|
|
349
380
|
var PLANE_WIRE_NEEDED = Object.freeze({
|
|
350
|
-
action_payloads: "2026-10-10
|
|
381
|
+
action_payloads: "2026-10-13.10",
|
|
351
382
|
management_api: "2026-10-10.5",
|
|
352
383
|
wire_errors: "2026-10-10.6"
|
|
353
384
|
});
|
|
@@ -530,6 +561,8 @@ var CREDENTIAL_FILE_NAME = "credentials.json";
|
|
|
530
561
|
var CREDENTIAL_FOLDER_MODE = 448;
|
|
531
562
|
var CONFIG_HOME_VARIABLE = "XDG_CONFIG_HOME";
|
|
532
563
|
var CONFIG_HOME_FALLBACK = ".config";
|
|
564
|
+
var WINDOWS_CREDENTIAL_FOLDER_NAME = ".turnzero-cloud";
|
|
565
|
+
var PROFILE_VARIABLE = "USERPROFILE";
|
|
533
566
|
var LOCAL_APP_DATA_VARIABLE = "LOCALAPPDATA";
|
|
534
567
|
var LOCAL_APP_DATA_FALLBACK = ["AppData", "Local"];
|
|
535
568
|
var PENDING_REQUEST_MS = 10 * 6e4;
|
|
@@ -537,11 +570,20 @@ var credentialFileOf = (io2) => {
|
|
|
537
570
|
const windows = io2.windows ?? process.platform === "win32";
|
|
538
571
|
const home = io2.homeFolder ?? homedir();
|
|
539
572
|
const paths = windows ? win32 : posix;
|
|
540
|
-
const named = (io2.env[windows ?
|
|
541
|
-
const base = named !== "" && paths.isAbsolute(named) ? named : windows ?
|
|
542
|
-
const folder = paths.join(base, CREDENTIAL_FOLDER_NAME);
|
|
573
|
+
const named = (io2.env[windows ? PROFILE_VARIABLE : CONFIG_HOME_VARIABLE] ?? "").trim();
|
|
574
|
+
const base = named !== "" && paths.isAbsolute(named) ? named : windows ? home : paths.join(home, CONFIG_HOME_FALLBACK);
|
|
575
|
+
const folder = paths.join(base, windows ? WINDOWS_CREDENTIAL_FOLDER_NAME : CREDENTIAL_FOLDER_NAME);
|
|
543
576
|
return { folder, path: paths.join(folder, CREDENTIAL_FILE_NAME) };
|
|
544
577
|
};
|
|
578
|
+
var oldCredentialFileOf = (io2) => {
|
|
579
|
+
const windows = io2.windows ?? process.platform === "win32";
|
|
580
|
+
if (!windows) return null;
|
|
581
|
+
const home = io2.homeFolder ?? homedir();
|
|
582
|
+
const named = (io2.env[LOCAL_APP_DATA_VARIABLE] ?? "").trim();
|
|
583
|
+
const base = named !== "" && win32.isAbsolute(named) ? named : win32.join(home, ...LOCAL_APP_DATA_FALLBACK);
|
|
584
|
+
const folder = win32.join(base, CREDENTIAL_FOLDER_NAME);
|
|
585
|
+
return { folder, path: win32.join(folder, CREDENTIAL_FILE_NAME) };
|
|
586
|
+
};
|
|
545
587
|
var EMPTY_CREDENTIAL_STATE = Object.freeze({ origins: /* @__PURE__ */ new Map(), pending: /* @__PURE__ */ new Map() });
|
|
546
588
|
var INSTANT_FORM = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
|
|
547
589
|
var LABEL_FORM = /^[^\u0000-\u001f\u007f-\u009f]{1,512}$/;
|
|
@@ -611,7 +653,7 @@ var readCredentialFile = (io2, path) => {
|
|
|
611
653
|
var unreadableFileSentence = (path, code) => `The credential file ${path} could not be read (${code}), so nothing was sent and nothing was changed.`;
|
|
612
654
|
var malformedFileSentence = (path) => `The credential file ${path} is not in the form this command writes, so nothing was sent and nothing was changed; what it holds is not shown. Move it aside or delete it, and authorize this computer again: a token it held works until it expires, list_tokens from your tool shows it by its label, and revoke_token ends it.`;
|
|
613
655
|
var unwritableFileSentence = (path, code) => `The credential file ${path} could not be written (${code}): its folder must stand or be one the command can make, and admit a write.`;
|
|
614
|
-
var readRefusalOf = (read2, path) => "malformed" in read2 ? malformedFileSentence(path) : "unreadable" in read2 ? unreadableFileSentence(path, read2.unreadable) : null;
|
|
656
|
+
var readRefusalOf = (read2, path) => "malformed" in read2 ? malformedFileSentence(read2.path ?? path) : "unreadable" in read2 ? unreadableFileSentence(read2.path ?? path, read2.unreadable) : null;
|
|
615
657
|
var runTagOf = () => randomBytes(6).toString("hex");
|
|
616
658
|
var prepareCredentialFile = (io2, place, tag) => {
|
|
617
659
|
try {
|
|
@@ -645,6 +687,59 @@ var withoutOrigin = (origin) => (state) => ({
|
|
|
645
687
|
pending: new Map([...state.pending].filter(([key]) => key !== origin))
|
|
646
688
|
});
|
|
647
689
|
var pendingLives = (entry, now) => Date.parse(entry.expires_at) > now.getTime();
|
|
690
|
+
var withOldEntries = (old) => (state) => ({
|
|
691
|
+
origins: new Map([...old.origins].filter(([origin]) => !state.origins.has(origin)).concat([...state.origins])),
|
|
692
|
+
pending: new Map([...old.pending].filter(([origin]) => !state.pending.has(origin)).concat([...state.pending]))
|
|
693
|
+
});
|
|
694
|
+
var droppedEntriesSentence = (oldPath, path, dropped) => `The credential file at its old place, ${oldPath}, held ${dropped.length === 1 ? "a token" : "tokens"} for ${dropped.length === 1 ? "an address" : "addresses"} ${path} also holds one for, so ${dropped.length === 1 ? "it was" : "they were"} not brought over: revoke_token from your tool with id ${dropped.map((entry) => entry.token_id).join(", then with id ")} ends ${dropped.length === 1 ? "it" : "each"}.`;
|
|
695
|
+
var unmovedSentence = (oldPath, path, code) => `The credential file could not be moved from its old place, ${oldPath}, to ${path} (${code}), so this run reads it where it stands; the next run moves it again.`;
|
|
696
|
+
var movesWith = (io2) => io2.writeFile !== void 0 && io2.rename !== void 0 && io2.copyMode !== void 0 && io2.makeFolder !== void 0 && io2.removeFile !== void 0 && io2.stat !== void 0;
|
|
697
|
+
var MOVES = /* @__PURE__ */ new WeakMap();
|
|
698
|
+
var readMovingCredentialFile = (io2, place, say) => {
|
|
699
|
+
const old = oldCredentialFileOf(io2);
|
|
700
|
+
if (old === null || old.path === place.path || !movesWith(io2)) return readCredentialFile(io2, place.path);
|
|
701
|
+
const made = MOVES.get(io2);
|
|
702
|
+
if (made !== void 0) {
|
|
703
|
+
const read3 = readCredentialFile(io2, place.path);
|
|
704
|
+
return made.unmoved !== null && "state" in read3 ? { state: withOldEntries(made.unmoved)(read3.state) } : read3;
|
|
705
|
+
}
|
|
706
|
+
MOVES.set(io2, { unmoved: null });
|
|
707
|
+
let standing;
|
|
708
|
+
try {
|
|
709
|
+
standing = io2.stat(old.path);
|
|
710
|
+
} catch {
|
|
711
|
+
standing = null;
|
|
712
|
+
}
|
|
713
|
+
if (standing === null || standing.folder) return readCredentialFile(io2, place.path);
|
|
714
|
+
const oldRead = readCredentialFile(io2, old.path);
|
|
715
|
+
if ("malformed" in oldRead) return { malformed: true, path: old.path };
|
|
716
|
+
if ("unreadable" in oldRead) return { unreadable: oldRead.unreadable, path: old.path };
|
|
717
|
+
const read2 = readCredentialFile(io2, place.path);
|
|
718
|
+
if (!("state" in read2)) return read2;
|
|
719
|
+
const tag = runTagOf();
|
|
720
|
+
let moved;
|
|
721
|
+
try {
|
|
722
|
+
const prepared = prepareCredentialFile(io2, place, tag);
|
|
723
|
+
moved = "unwritten" in prepared ? prepared : updateCredentialFile(io2, place, tag, withOldEntries(oldRead.state));
|
|
724
|
+
} finally {
|
|
725
|
+
dropCredentialWriting(io2, place, tag);
|
|
726
|
+
}
|
|
727
|
+
if (!("written" in moved)) {
|
|
728
|
+
if ("unwritten" in moved) {
|
|
729
|
+
MOVES.set(io2, { unmoved: oldRead.state });
|
|
730
|
+
say(unmovedSentence(old.path, place.path, moved.unwritten));
|
|
731
|
+
return { state: withOldEntries(oldRead.state)(read2.state) };
|
|
732
|
+
}
|
|
733
|
+
return moved;
|
|
734
|
+
}
|
|
735
|
+
const dropped = [...oldRead.state.origins].filter(([origin]) => moved.before.origins.has(origin)).map(([, entry]) => entry);
|
|
736
|
+
try {
|
|
737
|
+
io2.removeFile(old.path);
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
if (dropped.length > 0) say(droppedEntriesSentence(old.path, place.path, dropped));
|
|
741
|
+
return readCredentialFile(io2, place.path);
|
|
742
|
+
};
|
|
648
743
|
|
|
649
744
|
// src/turnzero_cloud_command/grant.ts
|
|
650
745
|
var GRANT_READ_BOUND_MS = 1e4;
|
|
@@ -689,11 +784,23 @@ var unendedReadSentence = (what, reading, closing, wayOn) => `${reading === "err
|
|
|
689
784
|
var NOTHING_PIPED_WAY_ON = "or, to run with nothing piped, close standard input or redirect it from an empty source";
|
|
690
785
|
var pipeWholeWayOn = (aWhat, call, nothingPiped = false) => `Pipe ${aWhat} whole and end the input, as the line ${call} answers does${nothingPiped ? `, ${NOTHING_PIPED_WAY_ON}` : ""}.`;
|
|
691
786
|
|
|
787
|
+
// src/turnzero_cloud_command/line_form.ts
|
|
788
|
+
var onWindows = (io2) => io2.windows ?? process.platform === "win32";
|
|
789
|
+
var ranThroughNpx = (io2) => io2.ranThroughNpx ?? false;
|
|
790
|
+
var loginLineFor = (io2, origin) => {
|
|
791
|
+
const line = loginLineOf(origin);
|
|
792
|
+
return onWindows(io2) ? windowsLineOf(line) : line;
|
|
793
|
+
};
|
|
794
|
+
var loginLinesFor = (io2, origin) => [
|
|
795
|
+
...ranThroughNpx(io2) ? [installLineOf(origin)] : [],
|
|
796
|
+
loginLineFor(io2, origin)
|
|
797
|
+
];
|
|
798
|
+
|
|
692
799
|
// src/turnzero_cloud_command/credential.ts
|
|
693
800
|
var REDACTED_TOKEN = "<minted token>";
|
|
694
801
|
var MINTED_TOKEN_PREFIX_WORDS = `${MINTED_TOKEN_PREFIX.slice(0, -1)} and an underscore`;
|
|
695
802
|
var CREDENTIAL_SOURCES = Object.freeze(["standard_input", "minted_token_variable", "credential_file"]);
|
|
696
|
-
var credentialPlaces = (declared) => declared.places ?? `the command reads the grant from its standard input, as the line ${declared.call} answers pipes it in${declared.mintedToken ? `, and otherwise a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, or this computer's token for the platform's address from its credential file, which authorize
|
|
803
|
+
var credentialPlaces = (declared) => declared.places ?? `the command reads the grant from its standard input, as the line ${declared.call} answers pipes it in${declared.mintedToken ? `, and otherwise a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, or this computer's token for the platform's address from its credential file, which login and authorize write` : declared.computerToken === true ? `, and otherwise this computer's token for the platform's address from its credential file, which login and authorize write, and never a minted token from ${MINTED_TOKEN_VARIABLE}` : ""}.`;
|
|
697
804
|
var unsetVariableWayOn = (declared) => `To run a line you wrote yourself under this computer's token, unset ${MINTED_TOKEN_VARIABLE} and run it again; or pipe ${declared.aGrant}.`;
|
|
698
805
|
var grantWaysOn = (declared) => `Pipe ${declared.aGrant}, as the line ${declared.call} answers pipes it, or, where you wrote the line yourself, pipe nothing to run it under this computer's token.`;
|
|
699
806
|
var emptyGrantRefusal = (declared) => `The ${declared.grant} piped to the command was empty, so nothing was sent and nothing was started. ${grantWaysOn(declared)}`;
|
|
@@ -740,7 +847,7 @@ async function readCredential(io2, declared, again, line = { named: void 0, clos
|
|
|
740
847
|
const origin = entryOriginOf(io2.env, line.named, line.closing);
|
|
741
848
|
if ("refusal" in origin) return origin;
|
|
742
849
|
const place = credentialFileOf(io2);
|
|
743
|
-
const read2 =
|
|
850
|
+
const read2 = readMovingCredentialFile(io2, place, io2.print ?? (() => void 0));
|
|
744
851
|
if (!("state" in read2)) return { refusal: readRefusalOf(read2, place.path) };
|
|
745
852
|
const entry = read2.state.origins.get(origin.origin);
|
|
746
853
|
const unauthorized = (expiredAt) => ({
|
|
@@ -756,8 +863,8 @@ var credentialOriginOf = (env, read2, line) => {
|
|
|
756
863
|
if (read2.source === "minted_token_variable") return mintedTokenOriginOf(env, line.named, line.closing);
|
|
757
864
|
return commandOriginOf(line.named ?? DEFAULT_COMMAND_ORIGIN);
|
|
758
865
|
};
|
|
759
|
-
var fileStandingOf = (io2, origin) => {
|
|
760
|
-
const read2 =
|
|
866
|
+
var fileStandingOf = (io2, origin, say) => {
|
|
867
|
+
const read2 = readMovingCredentialFile(io2, credentialFileOf(io2), say);
|
|
761
868
|
if (!("state" in read2)) return "unread";
|
|
762
869
|
const entry = read2.state.origins.get(origin);
|
|
763
870
|
if (entry === void 0) return "none";
|
|
@@ -776,10 +883,22 @@ var variableReadSentence = (origin, standing) => {
|
|
|
776
883
|
return `${opening}; that file could not be read.`;
|
|
777
884
|
}
|
|
778
885
|
};
|
|
779
|
-
var fileReadSentence = (origin) => `The credential this run read was this computer's token for ${origin}, from its credential file: where the platform no longer accepts it, run ${
|
|
886
|
+
var fileReadSentence = (origin) => `The credential this run read was this computer's token for ${origin}, from its credential file: where the platform no longer accepts it, run ${LOGIN_SUBCOMMAND}${originWordsOf(origin)} to authorize this computer again.`;
|
|
780
887
|
var noTokenSentence = (origin, expiredAt) => expiredAt === null ? `No credential was read, so nothing was sent: ${MINTED_TOKEN_VARIABLE} is not set, and this computer's credential file holds no token for ${origin}.` : `No credential was read, so nothing was sent: ${MINTED_TOKEN_VARIABLE} is not set, and this computer's token for ${origin} in its credential file expired at ${expiredAt}.`;
|
|
781
888
|
var VARIABLE_ROUTE_SENTENCES = `A tool connected by a minted token, or a job, cannot use ${MINT_TOKEN_ACTION}, which takes a sign-in alone. It sets ${MINTED_TOKEN_VARIABLE} from where its token is kept, never by writing the value in a line, and ${ORIGIN_VARIABLE} to the address that minted the token where that is not ${DEFAULT_COMMAND_ORIGIN}, then runs the line again. A tool sets ${MINTED_TOKEN_VARIABLE} for the deploy or check line alone and unsets it once that line ends.`;
|
|
782
|
-
var
|
|
889
|
+
var LOGIN_ROUTE_OPENER = "A person logs this computer in once, in a terminal of their own, and its token then stays on this computer for 30 days:";
|
|
890
|
+
var DEPLOY_FILES_ROUTE_SENTENCE = "A folder within the caps of your tool's deploy_files call deploys now through deploy_files, with no login.";
|
|
891
|
+
var loginRouteLines = (io2, origin) => [LOGIN_ROUTE_OPENER, ...loginLinesFor(io2, origin), DEPLOY_FILES_ROUTE_SENTENCE];
|
|
892
|
+
var loginRouteRefusalOf = (io2, closing) => {
|
|
893
|
+
const place = credentialFileOf(io2);
|
|
894
|
+
const tag = runTagOf();
|
|
895
|
+
try {
|
|
896
|
+
const prepared = prepareCredentialFile(io2, place, tag);
|
|
897
|
+
return "unwritten" in prepared ? `${unwritableFileSentence(place.path, prepared.unwritten)} ${closing}` : null;
|
|
898
|
+
} finally {
|
|
899
|
+
dropCredentialWriting(io2, place, tag);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
783
902
|
|
|
784
903
|
// src/turnzero_cloud_command/guard.ts
|
|
785
904
|
var GRANT_TEXT = new RegExp(`${UPLOAD_GRANT_PREFIX}[A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*`, "g");
|
|
@@ -869,7 +988,8 @@ var mintCallMembers = (kind, challenge, label) => kind === "authorized computer"
|
|
|
869
988
|
var firstActWayOn = (kind) => kind === "authorized computer" ? `Where the platform refuses ${CODE_CHALLENGE_MEMBER}, it does not yet serve this authorization: deploy by the line your tool's deploy call answers until the platform's next release.` : `Where the platform refuses ${CODE_CHALLENGE_MEMBER}, it does not yet serve this mint: until the platform's next release, ${MINT_TOKEN_ACTION} without it answers the token's value as it does today.`;
|
|
870
989
|
var firstActNamed = (kind, origin = DEFAULT_COMMAND_ORIGIN) => `${kind === "authorized computer" ? AUTHORIZE_SUBCOMMAND : `${TOKEN_SUBCOMMAND} ${TOKEN_REQUEST_ACT}`}${originWordsOf(origin)}`;
|
|
871
990
|
var noConnectionLine = (origin) => `Where your tool holds no connection signed in to ${origin}, ${MINT_TOKEN_ACTION} is not among its tools: a person connects the tool and signs in, in a browser, before the call.`;
|
|
872
|
-
var
|
|
991
|
+
var loginWaySentence = (loginLines) => `A person can instead log this computer in themselves, in a terminal of their own, by running ${loginLines.join(", then ")}, and approving the login in the browser.`;
|
|
992
|
+
var firstActLines = (origin, kind, pending, label, loginLines = []) => [
|
|
873
993
|
kind === "authorized computer" ? `To authorize this computer at ${origin}, call ${MINT_TOKEN_ACTION} from your tool, through the connection you signed in with, naming the members below and no others, then run the line it answers once, as given, on this computer.` : `To mint a token whose value never passes through the chat, call ${MINT_TOKEN_ACTION} at ${origin} from your tool, through the connection you signed in with, naming the member below beside the scope_kind, application, grants, label, and expires_in_days the task needs.`,
|
|
874
994
|
// The case this command cannot see (#4273): a tool with no signed-in
|
|
875
995
|
// connection holds no mint_token, and only a person's sign-in mends it.
|
|
@@ -877,20 +997,38 @@ var firstActLines = (origin, kind, pending, label) => [
|
|
|
877
997
|
`${CHALLENGE_LINE_HEAD}${pending.challenge}`,
|
|
878
998
|
CALL_LINE_HEAD,
|
|
879
999
|
...mintCallMembers(kind, pending.challenge, label).map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`),
|
|
880
|
-
kind === "authorized computer" ? `The line keeps the token it mints in this computer's credential file and revokes the one this computer held there before. The token is account-scoped and works from any computer until it expires or revoke_token ends it, and list_tokens shows it by its label. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.` : `It answers a ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} line, which is yours to run in a terminal outside the AI tool, on this computer as this user, and never through the tool: it prints the token's value once to standard output, so pipe it into your secret store's command where the token is a job's. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.`,
|
|
1000
|
+
kind === "authorized computer" ? `The line keeps the token it mints in this computer's credential file and revokes the one this computer held there before. The token is account-scoped and works from any computer until it expires or revoke_token ends it, and list_tokens shows it by its label. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.` : `It answers a ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} line, which is yours to run in a terminal outside the AI coding tool, on this computer as this user, and never through the tool: it prints the token's value once to standard output, so pipe it into your secret store's command where the token is a job's. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.`,
|
|
1001
|
+
...kind === "authorized computer" && loginLines.length > 0 ? [loginWaySentence(loginLines)] : [],
|
|
881
1002
|
firstActWayOn(kind)
|
|
882
1003
|
];
|
|
883
1004
|
var reusedRequestSentence = (origin, pending) => `A request for ${origin} is pending until ${pending.expires_at}, so its challenge is printed again and no new verifier was made.`;
|
|
884
1005
|
var replacedRequestSentence = (origin, expired2) => `The request pending for ${origin} expired at ${expired2.expires_at}, so a new one replaces it: a line minted against the old challenge is refused at its exchange, and a ${MINT_TOKEN_ACTION} call naming the new challenge below answers a fresh one.`;
|
|
885
1006
|
async function firstAct(io2, origin, kind, say, opener = null) {
|
|
1007
|
+
const kept2 = keepPendingRequest(io2, origin, say);
|
|
1008
|
+
if ("status" in kept2) return kept2.status;
|
|
1009
|
+
const { pending, opening } = kept2;
|
|
1010
|
+
const label = computerLabelOf(io2.hostName ?? hostname());
|
|
1011
|
+
const lines = [
|
|
1012
|
+
...opener !== null ? [opener] : [],
|
|
1013
|
+
...opening !== "" ? [opening] : [],
|
|
1014
|
+
...firstActLines(origin, kind, pending, label, kind === "authorized computer" ? loginLinesFor(io2, origin) : [])
|
|
1015
|
+
];
|
|
1016
|
+
let last = -1;
|
|
1017
|
+
for (const [index, line] of lines.entries()) if (line.includes(pending.challenge)) last = index;
|
|
1018
|
+
for (const [index, line] of lines.entries()) {
|
|
1019
|
+
say(line);
|
|
1020
|
+
if (index === last) io2.hide(pending.challenge, REDACTED_CHALLENGE);
|
|
1021
|
+
}
|
|
1022
|
+
return COMMAND_STATUS.done;
|
|
1023
|
+
}
|
|
1024
|
+
function keepPendingRequest(io2, origin, say) {
|
|
886
1025
|
const place = credentialFileOf(io2);
|
|
887
|
-
const read2 =
|
|
1026
|
+
const read2 = readMovingCredentialFile(io2, place, say);
|
|
888
1027
|
if (!("state" in read2)) {
|
|
889
1028
|
say(readRefusalOf(read2, place.path));
|
|
890
|
-
return COMMAND_STATUS.nothing_started;
|
|
1029
|
+
return { status: COMMAND_STATUS.nothing_started };
|
|
891
1030
|
}
|
|
892
1031
|
hideStandingTokens(io2, read2.state);
|
|
893
|
-
const label = computerLabelOf(io2.hostName ?? hostname());
|
|
894
1032
|
const now = io2.clock();
|
|
895
1033
|
const standing = read2.state.pending.get(origin);
|
|
896
1034
|
let pending;
|
|
@@ -908,12 +1046,12 @@ async function firstAct(io2, origin, kind, say, opener = null) {
|
|
|
908
1046
|
const prepared = prepareCredentialFile(io2, place, tag);
|
|
909
1047
|
if ("unwritten" in prepared) {
|
|
910
1048
|
say(`${unwritableFileSentence(place.path, prepared.unwritten)} Nothing was sent, and no request is pending.`);
|
|
911
|
-
return COMMAND_STATUS.nothing_started;
|
|
1049
|
+
return { status: COMMAND_STATUS.nothing_started };
|
|
912
1050
|
}
|
|
913
1051
|
const written = updateCredentialFile(io2, place, tag, withPending(origin, pending));
|
|
914
1052
|
if (!("written" in written)) {
|
|
915
1053
|
say("unwritten" in written ? `${unwritableFileSentence(place.path, written.unwritten)} Nothing was sent, and no request is pending.` : readRefusalOf(written, place.path));
|
|
916
|
-
return COMMAND_STATUS.nothing_started;
|
|
1054
|
+
return { status: COMMAND_STATUS.nothing_started };
|
|
917
1055
|
}
|
|
918
1056
|
} finally {
|
|
919
1057
|
release();
|
|
@@ -921,14 +1059,7 @@ async function firstAct(io2, origin, kind, say, opener = null) {
|
|
|
921
1059
|
}
|
|
922
1060
|
opening = standing !== void 0 ? replacedRequestSentence(origin, standing) : "";
|
|
923
1061
|
}
|
|
924
|
-
|
|
925
|
-
let last = -1;
|
|
926
|
-
for (const [index, line] of lines.entries()) if (line.includes(pending.challenge)) last = index;
|
|
927
|
-
for (const [index, line] of lines.entries()) {
|
|
928
|
-
say(line);
|
|
929
|
-
if (index === last) io2.hide(pending.challenge, REDACTED_CHALLENGE);
|
|
930
|
-
}
|
|
931
|
-
return COMMAND_STATUS.done;
|
|
1062
|
+
return { pending, opening };
|
|
932
1063
|
}
|
|
933
1064
|
var TOKEN_CALL_BOUND_SECONDS = 60;
|
|
934
1065
|
var EXCHANGE_ACT = "exchange";
|
|
@@ -987,16 +1118,16 @@ var mintedTokenOf = (answer) => {
|
|
|
987
1118
|
authorizedComputer: view[AUTHORIZED_COMPUTER_MEMBER] === true
|
|
988
1119
|
};
|
|
989
1120
|
};
|
|
990
|
-
var mintAgainSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN) => `Call ${MINT_TOKEN_ACTION} from your tool again with the challenge ${firstActNamed(kind, origin)} prints, and run the line it answers.`;
|
|
991
|
-
var exchangeUnknownSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN) => `Whether a token was minted is unknown: list_tokens from your tool shows each token by its label, and revoke_token ends one nothing holds. ${mintAgainSentence(kind, origin)}`;
|
|
1121
|
+
var mintAgainSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN, login2 = null) => login2 !== null ? `Run ${login2} again to log this computer in.` : `Call ${MINT_TOKEN_ACTION} from your tool again with the challenge ${firstActNamed(kind, origin)} prints, and run the line it answers.`;
|
|
1122
|
+
var exchangeUnknownSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN, login2 = null) => `Whether a token was minted is unknown: list_tokens from your tool shows each token by its label, and revoke_token ends one nothing holds. ${mintAgainSentence(kind, origin, login2)}`;
|
|
992
1123
|
var noPendingSentence = (origin, kind) => `No request is pending for ${origin} on this computer, so the token code was not presented and nothing was sent: the verifier it needs is kept only on the computer whose ${firstActNamed(kind)} printed its challenge. Run ${firstActNamed(kind, origin)} on this computer, call ${MINT_TOKEN_ACTION} from your tool with the challenge it prints, and run the line it answers.`;
|
|
993
1124
|
var markerDisagreesSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN) => kind === "authorized computer" ? `The exchange minted a token that is not an authorized computer's, and ${AUTHORIZE_SUBCOMMAND} keeps only an authorized computer's, so nothing of its value was printed or kept. Call ${MINT_TOKEN_ACTION} from your tool again naming ${AUTHORIZED_COMPUTER_MEMBER}: true, as ${AUTHORIZE_SUBCOMMAND} prints it, and run the line it answers.` : `The exchange minted an authorized computer's token, and ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} prints only a token that is not one, so nothing of its value was printed or kept. Call ${MINT_TOKEN_ACTION} from your tool again without ${AUTHORIZED_COMPUTER_MEMBER}, as ${TOKEN_SUBCOMMAND} ${TOKEN_REQUEST_ACT} prints it, and run the line it answers; to authorize this computer, run ${AUTHORIZE_SUBCOMMAND}${originWordsOf(origin)}.`;
|
|
994
1125
|
var mintedRevocationLines = (revocation, minted) => "lines" in revocation ? [...revocation.lines, standsSentence(`The token it minted, ${minted.id},`, minted.id, minted.expiresAt)] : [`The token it minted, ${minted.id}, is revoked.`];
|
|
995
1126
|
var authorizedLineOf = (origin, entry) => `${AUTHORIZED_LINE_HEAD}${origin}, label ${entry.label !== null ? JSON.stringify(entry.label) : "none"}, its token expiring ${entry.expires_at !== null ? `at ${entry.expires_at}` : "never"}`;
|
|
996
1127
|
var CLAIMED_LINE_HEAD = "claimed: ";
|
|
997
|
-
async function completeAct(io2, origin, kind, code, userAgent, say, deliver = () => void 0) {
|
|
1128
|
+
async function completeAct(io2, origin, kind, code, userAgent, say, deliver = () => void 0, login2 = null) {
|
|
998
1129
|
const place = credentialFileOf(io2);
|
|
999
|
-
const read2 =
|
|
1130
|
+
const read2 = readMovingCredentialFile(io2, place, say);
|
|
1000
1131
|
if (!("state" in read2)) {
|
|
1001
1132
|
say(`${readRefusalOf(read2, place.path)} ${TOKEN_CODE_UNREAD}`);
|
|
1002
1133
|
return COMMAND_STATUS.nothing_started;
|
|
@@ -1014,7 +1145,7 @@ async function completeAct(io2, origin, kind, code, userAgent, say, deliver = ()
|
|
|
1014
1145
|
const release = io2.whenEnded(() => {
|
|
1015
1146
|
stage.ended = true;
|
|
1016
1147
|
say(
|
|
1017
|
-
stage.now === "unsent" ? `The run was ended before its exchange. ${TOKEN_CODE_UNREAD}` : stage.now === "sent" ? `The run was ended during its exchange. ${exchangeUnknownSentence(kind, origin)}` : `The run was ended after its exchange minted a token, which may not have been ${kind === "authorized computer" ? "kept" : "printed"}: list_tokens from your tool shows it by its label, and revoke_token ends it.`
|
|
1148
|
+
stage.now === "unsent" ? `The run was ended before its exchange. ${TOKEN_CODE_UNREAD}` : stage.now === "sent" ? `The run was ended during its exchange. ${exchangeUnknownSentence(kind, origin, login2)}` : `The run was ended after its exchange minted a token, which may not have been ${kind === "authorized computer" ? "kept" : "printed"}: list_tokens from your tool shows it by its label, and revoke_token ends it.`
|
|
1018
1149
|
);
|
|
1019
1150
|
dropCredentialWriting(io2, place, tag);
|
|
1020
1151
|
});
|
|
@@ -1038,7 +1169,7 @@ async function completeAct(io2, origin, kind, code, userAgent, say, deliver = ()
|
|
|
1038
1169
|
if (late !== null && typeof late.value === "string") io2.hide(late.value, REDACTED_TOKEN);
|
|
1039
1170
|
return COMMAND_STATUS.unknown;
|
|
1040
1171
|
}
|
|
1041
|
-
const unknown = exchangeUnknownSentence(kind, origin);
|
|
1172
|
+
const unknown = exchangeUnknownSentence(kind, origin, login2);
|
|
1042
1173
|
if (answered === null) {
|
|
1043
1174
|
say("The exchange drew no answer.");
|
|
1044
1175
|
say(unknown);
|
|
@@ -1064,7 +1195,7 @@ async function completeAct(io2, origin, kind, code, userAgent, say, deliver = ()
|
|
|
1064
1195
|
return COMMAND_STATUS.unknown;
|
|
1065
1196
|
}
|
|
1066
1197
|
for (const line of refusalLines(EXCHANGE_ACT, answered.status, refusal)) say(line);
|
|
1067
|
-
say(answered.status === 429 ? "Nothing was minted, and the token code was not spent: run the line again after the wait the answer names." : `Nothing was minted. ${mintAgainSentence(kind, origin)}`);
|
|
1198
|
+
say(answered.status === 429 ? "Nothing was minted, and the token code was not spent: run the line again after the wait the answer names." : `Nothing was minted. ${mintAgainSentence(kind, origin, login2)}`);
|
|
1068
1199
|
return COMMAND_STATUS.nothing_started;
|
|
1069
1200
|
}
|
|
1070
1201
|
const minted = answer === null ? null : mintedTokenOf(answer);
|
|
@@ -1144,7 +1275,7 @@ var AUTHORIZE_CREDENTIAL = Object.freeze({
|
|
|
1144
1275
|
bare: `with nothing piped, it keeps a verifier and prints the challenge and the ${MINT_TOKEN_ACTION} call that authorize this computer`
|
|
1145
1276
|
});
|
|
1146
1277
|
var AUTHORIZATION_ORIGIN_VARIABLE_LINE = `the platform's address the authorization reaches where the line names no ${DEPLOY_OPTION.origin}, and the one address beside ${DEFAULT_COMMAND_ORIGIN} and ${DEVELOPMENT_COMMAND_ORIGIN} a ${DEPLOY_OPTION.origin} may name, an address of this machine only where it names it.`;
|
|
1147
|
-
var CREDENTIAL_FILE_WORDS = `${CREDENTIAL_FILE_NAME} in a folder ${CREDENTIAL_FOLDER_NAME} under ${CONFIG_HOME_VARIABLE}, or under .config in your home folder where that is unset, readable by you alone; on Windows under ${LOCAL_APP_DATA_VARIABLE}`;
|
|
1278
|
+
var CREDENTIAL_FILE_WORDS = `${CREDENTIAL_FILE_NAME} in a folder ${CREDENTIAL_FOLDER_NAME} under ${CONFIG_HOME_VARIABLE}, or under .config in your home folder where that is unset, readable by you alone; on Windows in a folder ${WINDOWS_CREDENTIAL_FOLDER_NAME} under your profile folder, ${PROFILE_VARIABLE}, where a file under ${LOCAL_APP_DATA_VARIABLE} from an older version is moved once`;
|
|
1148
1279
|
var AUTHORIZE_AFTER_OWN_ERROR = "a token code the line carried may not have been presented, and it is live until it expires and mints nothing without this computer's verifier; where a token was minted, list_tokens from your tool shows it by its label, and revoke_token ends it";
|
|
1149
1280
|
async function authorize(call, io2) {
|
|
1150
1281
|
const origin = authorizationOriginOf(io2.env, call.named[DEPLOY_OPTION.origin], NOTHING_SENT);
|
|
@@ -1184,7 +1315,7 @@ var AUTHORIZE_ROW = Object.freeze({
|
|
|
1184
1315
|
},
|
|
1185
1316
|
variables: [[ORIGIN_VARIABLE, AUTHORIZATION_ORIGIN_VARIABLE_LINE]],
|
|
1186
1317
|
note: [
|
|
1187
|
-
`Authorizing this computer takes three acts once for each platform address. ${AUTHORIZE_SUBCOMMAND} run with nothing piped prints a challenge and the ${MINT_TOKEN_ACTION} call to make; your tool makes that call through the connection you signed in with, and runs the line it answers once, as given, on this computer, which keeps the token.`,
|
|
1318
|
+
`Authorizing this computer takes three acts once for each platform address, where you ask your tool to authorize it; login, run in a terminal of your own, is your own way. ${AUTHORIZE_SUBCOMMAND} run with nothing piped prints a challenge and the ${MINT_TOKEN_ACTION} call to make; your tool makes that call through the connection you signed in with, and runs the line it answers once, as given, on this computer, which keeps the token.`,
|
|
1188
1319
|
`The token is account-scoped and works from any computer until it expires or revoke_token ends it: keep it as you would a password, list_tokens shows it by this computer's host name, and logout ends it. The credential file is ${CREDENTIAL_FILE_WORDS}.`
|
|
1189
1320
|
],
|
|
1190
1321
|
afterOwnError: AUTHORIZE_AFTER_OWN_ERROR,
|
|
@@ -1548,9 +1679,9 @@ var percentOf = (character) => [...Buffer.from(character, "utf8")].map((byte) =>
|
|
|
1548
1679
|
var escapedSegment = (segment) => {
|
|
1549
1680
|
if (segment === "") return "_";
|
|
1550
1681
|
if (segment === "." || segment === "..") return segment.replace(/\./g, percentOf("."));
|
|
1551
|
-
let
|
|
1552
|
-
|
|
1553
|
-
return DEVICE_NAME.test(
|
|
1682
|
+
let escaped2 = segment.replace(UNSAFE_CHARACTER, percentOf);
|
|
1683
|
+
escaped2 = escaped2.replace(/[. ]+$/u, (run) => [...run].map(percentOf).join(""));
|
|
1684
|
+
return DEVICE_NAME.test(escaped2) ? `_${escaped2}` : escaped2;
|
|
1554
1685
|
};
|
|
1555
1686
|
var shownName = (name) => name.replace(UNSEEN_CHARACTER, percentOf);
|
|
1556
1687
|
var folded = (path) => path.normalize("NFC").toLowerCase();
|
|
@@ -3020,10 +3151,10 @@ var DEPLOY_CREDENTIAL = Object.freeze({
|
|
|
3020
3151
|
call: "the deploy call",
|
|
3021
3152
|
mintedToken: true,
|
|
3022
3153
|
piped: false,
|
|
3023
|
-
places: `the command reads no standard input: it reads a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, as a tool connected by a minted token sets it from where its token is kept, or a job with no tool from its secret store, and otherwise this computer's token for the platform's address from its credential file, which ${AUTHORIZE_SUBCOMMAND}
|
|
3154
|
+
places: `the command reads no standard input: it reads a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, as a tool connected by a minted token sets it from where its token is kept, or a job with no tool from its secret store, and otherwise this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write.`
|
|
3024
3155
|
});
|
|
3025
|
-
var DEPLOY_READS = `no standard input: a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, and otherwise this computer's token for the platform's address from its credential file, which ${AUTHORIZE_SUBCOMMAND}
|
|
3026
|
-
var
|
|
3156
|
+
var DEPLOY_READS = `no standard input: a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, and otherwise this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write, sent only to that address; with neither, it prints the variable route first and then, where the address is admitted, the login route`;
|
|
3157
|
+
var DEPLOY_AFTER_LOGIN = `Once login has printed authorized:, run this deploy line again. ${NOTHING_SENT_OR_STARTED}`;
|
|
3027
3158
|
var PREPARE_ACT = "prepare";
|
|
3028
3159
|
var PREPARE_AGAIN = "Nothing was started. Run the line again: it prepares a fresh upload, which replaces any upload this run prepared.";
|
|
3029
3160
|
var DEPLOY_CALL_BOUND_SECONDS = 60;
|
|
@@ -3209,9 +3340,13 @@ async function deploy(named, io2, flags = /* @__PURE__ */ new Set()) {
|
|
|
3209
3340
|
io2.print(origin2.refusal);
|
|
3210
3341
|
return PROGRESS_EXIT_STATUS.nothing_started;
|
|
3211
3342
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3343
|
+
const unwritable = loginRouteRefusalOf(io2, NOTHING_SENT_OR_STARTED);
|
|
3344
|
+
if (unwritable !== null) {
|
|
3345
|
+
io2.print(unwritable);
|
|
3346
|
+
return PROGRESS_EXIT_STATUS.nothing_started;
|
|
3214
3347
|
}
|
|
3348
|
+
for (const line of loginRouteLines(io2, origin2.origin)) io2.print(line);
|
|
3349
|
+
io2.print(DEPLOY_AFTER_LOGIN);
|
|
3215
3350
|
return PROGRESS_EXIT_STATUS.nothing_started;
|
|
3216
3351
|
}
|
|
3217
3352
|
if ("refusal" in credential) {
|
|
@@ -3225,7 +3360,7 @@ async function deploy(named, io2, flags = /* @__PURE__ */ new Set()) {
|
|
|
3225
3360
|
return PROGRESS_EXIT_STATUS.nothing_started;
|
|
3226
3361
|
}
|
|
3227
3362
|
const origin = sent.origin;
|
|
3228
|
-
const refused = () => credential.source === "credential_file" ? fileReadSentence(origin) : variableReadSentence(origin, fileStandingOf(io2, origin));
|
|
3363
|
+
const refused = () => credential.source === "credential_file" ? fileReadSentence(origin) : variableReadSentence(origin, fileStandingOf(io2, origin, io2.print));
|
|
3229
3364
|
const zipped = zipOf(options.path, io2, NOTHING_STARTED, named[DEPLOY_OPTION.path] !== void 0);
|
|
3230
3365
|
if ("refusal" in zipped) {
|
|
3231
3366
|
io2.print(zipped.refusal);
|
|
@@ -3383,7 +3518,7 @@ async function deploy(named, io2, flags = /* @__PURE__ */ new Set()) {
|
|
|
3383
3518
|
var DEPLOY_AFTER_OWN_ERROR = "the deploy may or may not have started: call read_status from your tool first, make the start-only retry a pending upload it answers carries, and run the line again only where it shows neither";
|
|
3384
3519
|
var DEPLOY_ROW = Object.freeze({
|
|
3385
3520
|
name: DEPLOY_SUBCOMMAND,
|
|
3386
|
-
summary: "Zips the application folder (or takes a .zip file), makes the preparing deploy call under this computer's token or, for a tool connected by a minted token or a job with no tool, under a minted token, then uploads the zip under the upload grant that call answers, starts the deploy, and waits for it, printing each step and the outcome. With no credential, it sends nothing and prints the two routes to one: the minted token's variables first, then, where the address is admitted,
|
|
3521
|
+
summary: "Zips the application folder (or takes a .zip file), makes the preparing deploy call under this computer's token or, for a tool connected by a minted token or a job with no tool, under a minted token, then uploads the zip under the upload grant that call answers, starts the deploy, and waits for it, printing each step and the outcome. With no credential, it sends nothing and prints the two routes to one: the minted token's variables first, then, where the address is admitted, the login route.",
|
|
3387
3522
|
options: DEPLOY_OPTIONS,
|
|
3388
3523
|
credential: DEPLOY_CREDENTIAL,
|
|
3389
3524
|
reads: DEPLOY_READS,
|
|
@@ -3395,9 +3530,9 @@ var DEPLOY_ROW = Object.freeze({
|
|
|
3395
3530
|
},
|
|
3396
3531
|
variables: [[DEPLOY_NO_WAIT_VARIABLE, "any value returns the command once the deploy has started, without waiting for it."]],
|
|
3397
3532
|
note: [
|
|
3398
|
-
`A deploy is one line, which carries no credential: run the line your tool's deploy call answers, or ${DEPLOY_SUBCOMMAND} ${DEPLOY_OPTION.application} <application id>, from the application's folder. It reads no standard input. On an authorized computer it runs under this computer's token, which ${AUTHORIZE_SUBCOMMAND} keeps in the credential file for each platform address, and sends it only to the address it is kept under: ${DEPLOY_OPTION.origin} where the line names one, else the address ${ORIGIN_VARIABLE} holds, else ${DEFAULT_COMMAND_ORIGIN}.`,
|
|
3533
|
+
`A deploy is one line, which carries no credential: run the line your tool's deploy call answers, or ${DEPLOY_SUBCOMMAND} ${DEPLOY_OPTION.application} <application id>, from the application's folder. It reads no standard input. On an authorized computer it runs under this computer's token, which ${LOGIN_SUBCOMMAND} or ${AUTHORIZE_SUBCOMMAND} keeps in the credential file for each platform address, and sends it only to the address it is kept under: ${DEPLOY_OPTION.origin} where the line names one, else the address ${ORIGIN_VARIABLE} holds, else ${DEFAULT_COMMAND_ORIGIN}.`,
|
|
3399
3534
|
`On a computer with no token for that address, or one that has expired, it sends nothing, ends 3, and prints two routes, in this order. A tool connected by a minted token, or a job with no tool, such as a CI job's, cannot use ${MINT_TOKEN_ACTION}: it sets ${MINTED_TOKEN_VARIABLE} from where its token is kept, such as the job's secret store, never by writing the value in a line, and ${ORIGIN_VARIABLE} to the address that minted the token where that is not ${DEFAULT_COMMAND_ORIGIN}, then runs the deploy line again.`,
|
|
3400
|
-
`
|
|
3535
|
+
`Otherwise a person logs this computer in once with ${LOGIN_SUBCOMMAND}, in a terminal of their own, then runs the deploy line again; a folder within the caps of the tool's deploy_files call deploys now through deploy_files, with no login. The credential file is ${CREDENTIAL_FILE_WORDS}.`,
|
|
3401
3536
|
`A tool sets ${MINTED_TOKEN_VARIABLE} for the deploy or check line alone and unsets it once that line ends, since the command cannot unset a variable of the shell that started it. A job's token is minted for the application, with an expiry. The variable's token wins over the credential file: the command makes the preparing call under that token, and sends the token in no other request.`,
|
|
3402
3537
|
`Under the variable, the command sends its token only to the address ${ORIGIN_VARIABLE} holds, or to ${DEFAULT_COMMAND_ORIGIN} where that variable is unset: under the token, ${DEPLOY_OPTION.origin} may name that address alone, and where the line names none the command addresses it. ${DEPLOY_OPTION.env} names the environment where a line names one, ${COMMAND_ENVIRONMENTS.join(" or ")}; where none is named, the platform chooses, and the command deploys to the environment the platform answers.`,
|
|
3403
3538
|
`A progress read answered 502, 503, or 504, which is often temporary, is made again, at most three more times, and an upload answered 503 credential_lookup_busy, which wrote nothing, is made again after the wait it names, at most twice.`,
|
|
@@ -3426,7 +3561,7 @@ var checkNotThePlatform = (status) => `The check call was answered HTTP ${status
|
|
|
3426
3561
|
var checkAnswerUnusable = (status) => `The check call was answered HTTP ${status} with no answer the command can use: refusals, first_deploy, and recorded_health_path, each in its form. Nothing of the answer was printed or used.`;
|
|
3427
3562
|
var CHECK_AGAIN = `${NOTHING_CHECKED} Run the line again.`;
|
|
3428
3563
|
var CHECK_RATE_CAPPED = `${NOTHING_CHECKED} Run the line again after the wait the answer names.`;
|
|
3429
|
-
var
|
|
3564
|
+
var CHECK_AFTER_LOGIN = `Once login has printed authorized:, run this check line again. ${NOTHING_SENT_OR_CHECKED}`;
|
|
3430
3565
|
var checkOptionsOf = (named, cwd) => {
|
|
3431
3566
|
const application = named[CHECK_OPTION.application];
|
|
3432
3567
|
if (application === void 0 || !UPLOAD_ID.test(application)) {
|
|
@@ -3464,9 +3599,9 @@ var CHECK_CREDENTIAL = Object.freeze({
|
|
|
3464
3599
|
call: `the ${CHECK_ACTION} call`,
|
|
3465
3600
|
mintedToken: true,
|
|
3466
3601
|
piped: false,
|
|
3467
|
-
places: `the command reads no standard input: it reads a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, as a tool connected by a minted token sets it from where its token is kept, or a job with no tool from its secret store, and otherwise this computer's token for the platform's address from its credential file, which ${AUTHORIZE_SUBCOMMAND}
|
|
3602
|
+
places: `the command reads no standard input: it reads a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, as a tool connected by a minted token sets it from where its token is kept, or a job with no tool from its secret store, and otherwise this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write.`
|
|
3468
3603
|
});
|
|
3469
|
-
var CHECK_READS = `no standard input: a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, and otherwise this computer's token for the platform's address from its credential file, which ${AUTHORIZE_SUBCOMMAND}
|
|
3604
|
+
var CHECK_READS = `no standard input: a minted token from ${MINTED_TOKEN_VARIABLE} where it is set, and otherwise this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write, sent only to that address; with neither, it prints the variable route first and then, where the address is admitted, the login route`;
|
|
3470
3605
|
var checkAnswerOf = (answer) => {
|
|
3471
3606
|
const { refusals, first_deploy: firstDeploy, recorded_health_path: recordedHealthPath } = answer;
|
|
3472
3607
|
if (!Array.isArray(refusals) || typeof firstDeploy !== "boolean" || typeof recordedHealthPath !== "string") return null;
|
|
@@ -3582,9 +3717,13 @@ async function check(named, io2) {
|
|
|
3582
3717
|
io2.print(origin2.refusal);
|
|
3583
3718
|
return COMMAND_STATUS.nothing_started;
|
|
3584
3719
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
3720
|
+
const unwritable = loginRouteRefusalOf(io2, NOTHING_SENT_OR_CHECKED);
|
|
3721
|
+
if (unwritable !== null) {
|
|
3722
|
+
io2.print(unwritable);
|
|
3723
|
+
return COMMAND_STATUS.nothing_started;
|
|
3587
3724
|
}
|
|
3725
|
+
for (const line of loginRouteLines(io2, origin2.origin)) io2.print(line);
|
|
3726
|
+
io2.print(CHECK_AFTER_LOGIN);
|
|
3588
3727
|
return COMMAND_STATUS.nothing_started;
|
|
3589
3728
|
}
|
|
3590
3729
|
if ("refusal" in credential) {
|
|
@@ -3598,7 +3737,7 @@ async function check(named, io2) {
|
|
|
3598
3737
|
return COMMAND_STATUS.nothing_started;
|
|
3599
3738
|
}
|
|
3600
3739
|
const origin = sent.origin;
|
|
3601
|
-
const refused = () => credential.source === "credential_file" ? fileReadSentence(origin) : variableReadSentence(origin, fileStandingOf(io2, origin));
|
|
3740
|
+
const refused = () => credential.source === "credential_file" ? fileReadSentence(origin) : variableReadSentence(origin, fileStandingOf(io2, origin, io2.print));
|
|
3602
3741
|
const zipped = zipOf(options.path, io2, NOTHING_CHECKED, named[CHECK_OPTION.path] !== void 0, null, "checked");
|
|
3603
3742
|
if ("refusal" in zipped) {
|
|
3604
3743
|
io2.print(zipped.refusal);
|
|
@@ -4318,8 +4457,8 @@ var npmEntryOf = (io2, nodePath) => npmEntryCandidatesOf(io2.env, nodePath).find
|
|
|
4318
4457
|
var npmStartOf = (viaShell = NPM_NEEDS_SHELL, entry = null, nodePath = process.execPath) => entry !== null ? { command: nodePath, args: [entry, "start"] } : viaShell ? { command: "npm start", args: [], shell: true } : { command: "npm", args: ["start"] };
|
|
4319
4458
|
var npmRunOf = (words, viaShell = NPM_NEEDS_SHELL, entry = null, nodePath = process.execPath) => entry !== null ? { command: nodePath, args: [entry, ...words] } : viaShell ? { command: ["npm", ...words].join(" "), args: [], shell: true } : { command: "npm", args: words };
|
|
4320
4459
|
var INTERRUPTED_ON_WINDOWS = 'The application has ended. cmd may ask "Terminate batch job (Y/N)?" next: either answer returns the prompt.';
|
|
4321
|
-
var afterInterruptLineOf = (ended,
|
|
4322
|
-
var afterInterruptedChildLineOf = (ended, what,
|
|
4460
|
+
var afterInterruptLineOf = (ended, onWindows2 = NPM_NEEDS_SHELL) => onWindows2 && ended.interrupted === true ? INTERRUPTED_ON_WINDOWS : null;
|
|
4461
|
+
var afterInterruptedChildLineOf = (ended, what, onWindows2 = NPM_NEEDS_SHELL) => afterInterruptLineOf(ended, onWindows2) === null ? null : `${what} has ended. cmd may ask "Terminate batch job (Y/N)?" next: either answer returns the prompt.`;
|
|
4323
4462
|
var printAfterInterrupt = (io2, line) => {
|
|
4324
4463
|
if (line === null) return;
|
|
4325
4464
|
io2.print("");
|
|
@@ -4428,7 +4567,11 @@ var LIBRARY_TAKE_OPTIONS = Object.freeze([
|
|
|
4428
4567
|
]);
|
|
4429
4568
|
var NOTHING_WRITTEN2 = "Nothing was written.";
|
|
4430
4569
|
var LINES_IN_WINDOWS_FORM = process.platform === "win32";
|
|
4431
|
-
var machineLineOf = (line,
|
|
4570
|
+
var machineLineOf = (line, form = LINES_IN_WINDOWS_FORM, origin = DEFAULT_COMMAND_ORIGIN) => {
|
|
4571
|
+
const windows = typeof form === "boolean" ? form : form.windows;
|
|
4572
|
+
const run = typeof form !== "boolean" && form.npx ? npxFormOf(line, origin) : line;
|
|
4573
|
+
return windows ? windowsLineOf(run) : run;
|
|
4574
|
+
};
|
|
4432
4575
|
var LIBRARY_FOLDER = "system";
|
|
4433
4576
|
var POINTER_FILE = "system.json";
|
|
4434
4577
|
var MANIFEST_FILE2 = "manifest.json";
|
|
@@ -4653,7 +4796,7 @@ var noPackageWayOnOf = (io2, options, name) => {
|
|
|
4653
4796
|
empty = false;
|
|
4654
4797
|
}
|
|
4655
4798
|
if (!empty) return OTHER_WAY_ON;
|
|
4656
|
-
const line = machineLineOf(scaffoldLineOf({ origin: options.origin, template }), io2.windows ?? LINES_IN_WINDOWS_FORM);
|
|
4799
|
+
const line = machineLineOf(scaffoldLineOf({ origin: options.origin, template }), { windows: io2.windows ?? LINES_IN_WINDOWS_FORM, npx: ranThroughNpx({ env: io2.env ?? {}, ranThroughNpx: io2.ranThroughNpx }) }, options.origin);
|
|
4657
4800
|
return {
|
|
4658
4801
|
clause: MAKE_OWN_PACKAGE_FILE,
|
|
4659
4802
|
after: `For a new ${taking.writes}, run the scaffold line at the project's root instead, which writes the application and takes this entry: ${line}`
|
|
@@ -5018,6 +5161,267 @@ var LIBRARY_ROW = Object.freeze({
|
|
|
5018
5161
|
run: libraryTake
|
|
5019
5162
|
});
|
|
5020
5163
|
|
|
5164
|
+
// src/turnzero_cloud_command/login.ts
|
|
5165
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
5166
|
+
import { hostname as hostname2 } from "node:os";
|
|
5167
|
+
var NOTHING_SENT3 = "Nothing was sent and nothing was started.";
|
|
5168
|
+
var LOGIN_ADDRESS_PATH_MAX = 512;
|
|
5169
|
+
var LOGIN_SEND_WAIT_SECONDS = 120;
|
|
5170
|
+
var OPENER_WAIT_MS = 5e3;
|
|
5171
|
+
var newLoginState = () => randomBytes3(32).toString("base64url");
|
|
5172
|
+
var loginAddressOf = (origin, login2) => {
|
|
5173
|
+
const fits = (label) => {
|
|
5174
|
+
const address = computerLoginAddressOf(origin, { ...login2, label });
|
|
5175
|
+
return address.length - origin.length <= LOGIN_ADDRESS_PATH_MAX ? address : null;
|
|
5176
|
+
};
|
|
5177
|
+
let characters = [...login2.label];
|
|
5178
|
+
while (characters.length > 0) {
|
|
5179
|
+
const label = characters.join("").trimEnd();
|
|
5180
|
+
if (label !== "") {
|
|
5181
|
+
const address = fits(label);
|
|
5182
|
+
if (address !== null) return address;
|
|
5183
|
+
}
|
|
5184
|
+
characters = characters.slice(0, -1);
|
|
5185
|
+
}
|
|
5186
|
+
return fits(LABEL_FALLBACK) ?? computerLoginAddressOf(origin, { ...login2, label: LABEL_FALLBACK });
|
|
5187
|
+
};
|
|
5188
|
+
var openerOf = (platform, address) => platform === "darwin" ? { command: "open", args: [address] } : platform === "win32" ? { command: "rundll32", args: ["url.dll,FileProtocolHandler", address] } : { command: "xdg-open", args: [address] };
|
|
5189
|
+
var openerEnvironmentOf = (env) => Object.fromEntries(Object.entries(env).filter(([name]) => !name.toUpperCase().startsWith("TURNZERO_CLOUD_")));
|
|
5190
|
+
var openerRunOf = (io2, address) => {
|
|
5191
|
+
const platform = io2.platform ?? (onWindows(io2) ? "win32" : process.platform);
|
|
5192
|
+
const opener = openerOf(onWindows(io2) ? "win32" : platform, address);
|
|
5193
|
+
return { command: opener.command, args: opener.args, cwd: io2.cwd, env: openerEnvironmentOf(io2.env), output: () => void 0 };
|
|
5194
|
+
};
|
|
5195
|
+
var openBrowser = async (io2, address) => {
|
|
5196
|
+
let cancel = () => void 0;
|
|
5197
|
+
const waited = new Promise((resolve12) => {
|
|
5198
|
+
cancel = io2.schedule(OPENER_WAIT_MS, () => resolve12("waiting"));
|
|
5199
|
+
});
|
|
5200
|
+
const ended = await Promise.race([io2.child(openerRunOf(io2, address)), waited]);
|
|
5201
|
+
cancel();
|
|
5202
|
+
return ended === "waiting" || ended.error === null && ended.status === 0;
|
|
5203
|
+
};
|
|
5204
|
+
var addressLeadSentence = (origin) => `To log this computer in at ${origin}, approve the login on this page:`;
|
|
5205
|
+
var minutesWords = (minutes) => `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
5206
|
+
var minutesLeftOf = (expiresAt, now) => Math.max(1, Math.ceil((Date.parse(expiresAt) - now.getTime()) / 6e4));
|
|
5207
|
+
var openingSentence = (minutes) => `Opening it in your browser. This command waits up to ${minutesWords(minutes)} for the approval.`;
|
|
5208
|
+
var noBrowserSentence = "Open the address in a browser on any computer, approve the login, and paste here the code the page shows.";
|
|
5209
|
+
var unboundSentence = "No port on this computer could be bound for the browser to answer, so paste here the code the page shows once you approve.";
|
|
5210
|
+
var unopenedSentence = "The browser could not be opened here: open the address yourself, approve the login, and paste here the code the page shows if it shows one.";
|
|
5211
|
+
var noSendSentence = (seconds) => `No answer has come from the browser in ${seconds} seconds. Where the page shows a code, paste it here; the browser's answer still completes the login.`;
|
|
5212
|
+
var noTerminalSentence2 = "This run has no terminal to paste a code into, so it waits for the browser's answer alone.";
|
|
5213
|
+
var noWayInSentence = (line, minutes = PENDING_REQUEST_MS / 6e4) => `This run has no terminal to paste a code into and no port for the browser to answer, so nothing was minted and the request stays pending for ${minutesWords(minutes)}. Run ${line} in a terminal of your own.`;
|
|
5214
|
+
var endedSentence = (minutes, line) => `No approval arrived within ${minutesWords(minutes)}, so nothing was minted and nothing was changed. Run ${line} again to log this computer in.`;
|
|
5215
|
+
var stoppedSentence = (line) => `The code prompt was stopped, so nothing was minted. Run ${line} again to log this computer in.`;
|
|
5216
|
+
var notCodeSentence = (line) => `What was pasted is not a token code, so nothing was sent and nothing was minted. Run ${line} again to log this computer in.`;
|
|
5217
|
+
var CODE_PROMPT_WORDS = "Code (not shown), then Enter: ";
|
|
5218
|
+
var escaped = (text) => text.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
5219
|
+
var browserPageOf = (title, sentence) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escaped(title)}</title></head><body style="font-family: system-ui, sans-serif; max-width: 36rem; margin: 3rem auto; padding: 0 1rem; line-height: 1.5"><h1>${escaped(title)}</h1><p>${escaped(sentence)}</p></body></html>`;
|
|
5220
|
+
var answerHeaders = (origin, html) => ({
|
|
5221
|
+
"access-control-allow-origin": origin,
|
|
5222
|
+
"access-control-allow-private-network": "true",
|
|
5223
|
+
"cache-control": "no-store",
|
|
5224
|
+
...html ? { "content-type": "text/html; charset=utf-8" } : {}
|
|
5225
|
+
});
|
|
5226
|
+
var probeAnswer = (origin) => ({ status: 204, headers: answerHeaders(origin, false), body: "" });
|
|
5227
|
+
var outcomeAnswer = (origin, authorized) => ({
|
|
5228
|
+
status: 200,
|
|
5229
|
+
headers: answerHeaders(origin, true),
|
|
5230
|
+
body: authorized ? browserPageOf("This computer is logged in", `The turnzero command on this computer is logged in at ${origin}. You can close this tab.`) : browserPageOf("The login did not complete", "The turnzero command on this computer did not complete the login; its terminal says why and what to run next. You can close this tab.")
|
|
5231
|
+
});
|
|
5232
|
+
var wrongStateAnswer = (origin) => ({
|
|
5233
|
+
status: 400,
|
|
5234
|
+
headers: answerHeaders(origin, true),
|
|
5235
|
+
body: browserPageOf("Not this login", "This address is not the login the turnzero command on this computer is waiting for. Nothing was changed.")
|
|
5236
|
+
});
|
|
5237
|
+
var endedAnswer = (origin) => ({
|
|
5238
|
+
status: 410,
|
|
5239
|
+
headers: answerHeaders(origin, true),
|
|
5240
|
+
body: browserPageOf("This login has ended", "The turnzero command on this computer is no longer waiting for this login. Run turnzero login again.")
|
|
5241
|
+
});
|
|
5242
|
+
var notFoundAnswer = (origin) => ({ status: 404, headers: answerHeaders(origin, false), body: "" });
|
|
5243
|
+
var loopbackHandlerOf = (origin, state, arrive) => {
|
|
5244
|
+
return async (request) => {
|
|
5245
|
+
if (request.path !== "/") return notFoundAnswer(origin);
|
|
5246
|
+
if (request.method === "OPTIONS") return probeAnswer(origin);
|
|
5247
|
+
if (request.method !== "GET") return notFoundAnswer(origin);
|
|
5248
|
+
const named = request.query[COMPUTER_LOGIN_QUERY.state];
|
|
5249
|
+
if (named === void 0 || !LOGIN_STATE_FORM.test(named) || named !== state) return wrongStateAnswer(origin);
|
|
5250
|
+
const code = request.query.code;
|
|
5251
|
+
if (code === void 0) return probeAnswer(origin);
|
|
5252
|
+
const outcome = arrive(code);
|
|
5253
|
+
if (outcome === null) return endedAnswer(origin);
|
|
5254
|
+
return outcomeAnswer(origin, await outcome);
|
|
5255
|
+
};
|
|
5256
|
+
};
|
|
5257
|
+
async function login(call, io2) {
|
|
5258
|
+
const origin = authorizationOriginOf(io2.env, call.named[LOGIN_OPTION.origin], NOTHING_SENT3);
|
|
5259
|
+
if ("refusal" in origin) {
|
|
5260
|
+
io2.print(origin.refusal);
|
|
5261
|
+
return COMMAND_STATUS.nothing_started;
|
|
5262
|
+
}
|
|
5263
|
+
const agent = userAgentOf(io2.env);
|
|
5264
|
+
if ("refusal" in agent) {
|
|
5265
|
+
io2.print(agent.refusal);
|
|
5266
|
+
return COMMAND_STATUS.nothing_started;
|
|
5267
|
+
}
|
|
5268
|
+
const kept2 = keepPendingRequest(io2, origin.origin, io2.print);
|
|
5269
|
+
if ("status" in kept2) return kept2.status;
|
|
5270
|
+
const { pending } = kept2;
|
|
5271
|
+
const again = loginLinesFor(io2, origin.origin).join(", then ");
|
|
5272
|
+
const noBrowser = call.flags.has(LOGIN_OPTION.noBrowser);
|
|
5273
|
+
const state = newLoginState();
|
|
5274
|
+
const endMs = Math.max(1e3, Date.parse(pending.expires_at) - io2.clock().getTime());
|
|
5275
|
+
const minutes = minutesLeftOf(pending.expires_at, io2.clock());
|
|
5276
|
+
let settle = () => void 0;
|
|
5277
|
+
const arrived = new Promise((resolve12) => {
|
|
5278
|
+
settle = resolve12;
|
|
5279
|
+
});
|
|
5280
|
+
let taken = false;
|
|
5281
|
+
let outcome = () => void 0;
|
|
5282
|
+
const outcomeKnown = new Promise((resolve12) => {
|
|
5283
|
+
outcome = resolve12;
|
|
5284
|
+
});
|
|
5285
|
+
const take = (arrival2) => {
|
|
5286
|
+
if (taken) return false;
|
|
5287
|
+
taken = true;
|
|
5288
|
+
settle(arrival2);
|
|
5289
|
+
return true;
|
|
5290
|
+
};
|
|
5291
|
+
let listener = null;
|
|
5292
|
+
if (!noBrowser && io2.listen !== void 0) {
|
|
5293
|
+
listener = await io2.listen(
|
|
5294
|
+
loopbackHandlerOf(origin.origin, state, (code) => {
|
|
5295
|
+
io2.hide(code, REDACTED_TOKEN_CODE);
|
|
5296
|
+
return take({ code, from: "send" }) ? outcomeKnown : null;
|
|
5297
|
+
})
|
|
5298
|
+
);
|
|
5299
|
+
}
|
|
5300
|
+
const label = computerLabelOf(io2.hostName ?? hostname2());
|
|
5301
|
+
const address = loginAddressOf(origin.origin, { challenge: pending.challenge, state, port: listener?.port ?? null, label });
|
|
5302
|
+
io2.print(addressLeadSentence(origin.origin));
|
|
5303
|
+
io2.print(address);
|
|
5304
|
+
io2.hide(pending.challenge, REDACTED_CHALLENGE);
|
|
5305
|
+
let prompt = null;
|
|
5306
|
+
const paste = () => {
|
|
5307
|
+
if (taken || prompt !== null) return;
|
|
5308
|
+
const terminal = io2.terminal();
|
|
5309
|
+
if (terminal === null) {
|
|
5310
|
+
if (listener === null) take({ ended: "no way in" });
|
|
5311
|
+
else io2.print(noTerminalSentence2);
|
|
5312
|
+
return;
|
|
5313
|
+
}
|
|
5314
|
+
let typed = "";
|
|
5315
|
+
let open = true;
|
|
5316
|
+
const close = () => {
|
|
5317
|
+
if (!open) return;
|
|
5318
|
+
open = false;
|
|
5319
|
+
terminal.write("\n");
|
|
5320
|
+
terminal.close();
|
|
5321
|
+
};
|
|
5322
|
+
prompt = { close };
|
|
5323
|
+
terminal.write(CODE_PROMPT_WORDS);
|
|
5324
|
+
terminal.keys((chunk) => {
|
|
5325
|
+
if (!open || taken) return;
|
|
5326
|
+
const step = promptKeys(typed, chunk, "the code prompt", "a fresh login");
|
|
5327
|
+
if ("refusal" in step) {
|
|
5328
|
+
close();
|
|
5329
|
+
take({ ended: "stopped" });
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
typed = step.typed;
|
|
5333
|
+
if (!step.ended) return;
|
|
5334
|
+
close();
|
|
5335
|
+
const code = typed.trim();
|
|
5336
|
+
if (code === "") return;
|
|
5337
|
+
io2.hide(code, REDACTED_TOKEN_CODE);
|
|
5338
|
+
take(isTokenCode(code) ? { code, from: "paste" } : { ended: "not code" });
|
|
5339
|
+
});
|
|
5340
|
+
};
|
|
5341
|
+
const cancels = [];
|
|
5342
|
+
cancels.push(io2.schedule(endMs, () => take({ ended: "time" })));
|
|
5343
|
+
if (noBrowser) {
|
|
5344
|
+
io2.print(noBrowserSentence);
|
|
5345
|
+
paste();
|
|
5346
|
+
} else if (listener === null) {
|
|
5347
|
+
io2.print(unboundSentence);
|
|
5348
|
+
paste();
|
|
5349
|
+
} else if (!await openBrowser(io2, address)) {
|
|
5350
|
+
io2.print(unopenedSentence);
|
|
5351
|
+
paste();
|
|
5352
|
+
} else {
|
|
5353
|
+
io2.print(openingSentence(minutes));
|
|
5354
|
+
cancels.push(
|
|
5355
|
+
io2.schedule(LOGIN_SEND_WAIT_SECONDS * 1e3, () => {
|
|
5356
|
+
if (taken) return;
|
|
5357
|
+
io2.print(noSendSentence(LOGIN_SEND_WAIT_SECONDS));
|
|
5358
|
+
paste();
|
|
5359
|
+
})
|
|
5360
|
+
);
|
|
5361
|
+
}
|
|
5362
|
+
const arrival = await arrived;
|
|
5363
|
+
for (const cancel of cancels) cancel();
|
|
5364
|
+
prompt?.close();
|
|
5365
|
+
try {
|
|
5366
|
+
if ("ended" in arrival) {
|
|
5367
|
+
outcome(false);
|
|
5368
|
+
io2.print(
|
|
5369
|
+
arrival.ended === "time" ? endedSentence(minutes, again) : arrival.ended === "stopped" ? stoppedSentence(again) : arrival.ended === "no way in" ? noWayInSentence(again, minutes) : notCodeSentence(again)
|
|
5370
|
+
);
|
|
5371
|
+
return COMMAND_STATUS.nothing_started;
|
|
5372
|
+
}
|
|
5373
|
+
if (!isTokenCode(arrival.code)) {
|
|
5374
|
+
outcome(false);
|
|
5375
|
+
io2.print(notCodeSentence(again));
|
|
5376
|
+
return COMMAND_STATUS.nothing_started;
|
|
5377
|
+
}
|
|
5378
|
+
const status = await completeAct(io2, origin.origin, "authorized computer", arrival.code, agent.userAgent, io2.print, () => void 0, again);
|
|
5379
|
+
outcome(status === COMMAND_STATUS.done);
|
|
5380
|
+
return status;
|
|
5381
|
+
} finally {
|
|
5382
|
+
if (listener !== null) {
|
|
5383
|
+
await io2.sleep(100);
|
|
5384
|
+
listener.close();
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
var LOGIN_OPTIONS = Object.freeze([
|
|
5389
|
+
{
|
|
5390
|
+
name: LOGIN_OPTION.origin,
|
|
5391
|
+
value: "<origin>",
|
|
5392
|
+
required: false,
|
|
5393
|
+
description: `The platform's address this computer logs in at. Where absent, the address ${ORIGIN_VARIABLE} holds, or ${DEFAULT_COMMAND_ORIGIN} where it is unset. A ${LOGIN_OPTION.origin} the line names must be ${DEFAULT_COMMAND_ORIGIN}, ${DEVELOPMENT_COMMAND_ORIGIN}, or the address ${ORIGIN_VARIABLE} holds.`
|
|
5394
|
+
},
|
|
5395
|
+
{
|
|
5396
|
+
name: LOGIN_OPTION.noBrowser,
|
|
5397
|
+
value: "",
|
|
5398
|
+
required: false,
|
|
5399
|
+
flag: true,
|
|
5400
|
+
description: "Opens no browser and binds no port: open the printed address in a browser on any computer, approve, and paste the code the page shows."
|
|
5401
|
+
}
|
|
5402
|
+
]);
|
|
5403
|
+
var LOGIN_AFTER_OWN_ERROR = "the login may not have completed; where a token was minted, list_tokens from your tool shows it by its label, and revoke_token ends it; run login again to log this computer in";
|
|
5404
|
+
var LOGIN_ROW = Object.freeze({
|
|
5405
|
+
name: LOGIN_SUBCOMMAND,
|
|
5406
|
+
summary: `Logs this computer in at the platform's address: it opens the platform's computer login page (${COMPUTER_LOGIN_PATH}) in your browser, where you approve the login under your sign-in, and keeps the token the platform mints in this computer's credential file, then prints authorized:. Where the browser cannot answer, it asks for the code the page shows. It prints no token, code, or verifier.`,
|
|
5407
|
+
options: LOGIN_OPTIONS,
|
|
5408
|
+
credential: null,
|
|
5409
|
+
reads: "nothing from standard input; with the browser it waits for the page to answer on a port of this computer, and with --no-browser it reads the code the page shows from the terminal, hidden",
|
|
5410
|
+
statuses: {
|
|
5411
|
+
done: "this computer logged in",
|
|
5412
|
+
failed: "a token was minted and not kept, or the token this computer held before was not revoked: the printed lines name revoke_token",
|
|
5413
|
+
unknown: "the exchange drew no answer, or an answer that is not a refusal: list_tokens shows whether a token was minted",
|
|
5414
|
+
nothing_started: "nothing was minted: an option or the credential file was refused, no code arrived in time or could arrive, the prompt was stopped or given none, the platform was too old, or the exchange was refused"
|
|
5415
|
+
},
|
|
5416
|
+
variables: [[ORIGIN_VARIABLE, AUTHORIZATION_ORIGIN_VARIABLE_LINE]],
|
|
5417
|
+
note: [
|
|
5418
|
+
`login is your own way to authorize this computer, run once in a terminal of your own; your tool runs it only where you ask it to. The token is account-scoped and works from any computer for 30 days or until revoke_token or logout ends it: list_tokens shows it by this computer's host name.`,
|
|
5419
|
+
`The credential file is ${CREDENTIAL_FILE_WORDS}.`
|
|
5420
|
+
],
|
|
5421
|
+
afterOwnError: LOGIN_AFTER_OWN_ERROR,
|
|
5422
|
+
run: login
|
|
5423
|
+
});
|
|
5424
|
+
|
|
5021
5425
|
// src/turnzero_cloud_command/logout.ts
|
|
5022
5426
|
var LOGOUT_OPTIONS = Object.freeze([
|
|
5023
5427
|
{
|
|
@@ -5042,7 +5446,7 @@ async function logout(call, io2) {
|
|
|
5042
5446
|
return COMMAND_STATUS.nothing_started;
|
|
5043
5447
|
}
|
|
5044
5448
|
const place = credentialFileOf(io2);
|
|
5045
|
-
const read2 =
|
|
5449
|
+
const read2 = readMovingCredentialFile(io2, place, io2.print);
|
|
5046
5450
|
if (!("state" in read2)) {
|
|
5047
5451
|
io2.print(readRefusalOf(read2, place.path));
|
|
5048
5452
|
return COMMAND_STATUS.nothing_started;
|
|
@@ -5111,6 +5515,106 @@ var LOGOUT_ROW = Object.freeze({
|
|
|
5111
5515
|
run: logout
|
|
5112
5516
|
});
|
|
5113
5517
|
|
|
5518
|
+
// src/turnzero_cloud_command/upgrade_check.ts
|
|
5519
|
+
import { posix as posix3, win32 as win324 } from "node:path";
|
|
5520
|
+
var UPGRADE_CHECK_SUBCOMMANDS = Object.freeze([DEPLOY_SUBCOMMAND, CHECK_SUBCOMMAND, LOGIN_SUBCOMMAND]);
|
|
5521
|
+
var UPGRADE_CHECK_SKIP_VARIABLES = Object.freeze([MINTED_TOKEN_VARIABLE, "CI"]);
|
|
5522
|
+
var UPGRADE_CHECK_FILE_NAME = "upgrade_check.json";
|
|
5523
|
+
var UPGRADE_READ_BOUND_MS = 3e3;
|
|
5524
|
+
var UPGRADE_READ_EVERY_MS = 24 * 60 * 6e4;
|
|
5525
|
+
var DIST_TAGS_ADDRESS = `https://registry.npmjs.org/-/package/${TURNZERO_CLOUD_PACKAGE}/dist-tags`;
|
|
5526
|
+
var upgradeLineOf = (tag, version, running) => `A newer turnzero, ${version}, is published under ${tag} (this is ${running}): npm install -g ${TURNZERO_CLOUD_PACKAGE}@${tag} installs it.`;
|
|
5527
|
+
var VERSION_FORM2 = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
5528
|
+
var newerVersion = (candidate, running) => {
|
|
5529
|
+
const a = VERSION_FORM2.exec(candidate);
|
|
5530
|
+
const b = VERSION_FORM2.exec(running);
|
|
5531
|
+
if (a === null || b === null) return false;
|
|
5532
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
5533
|
+
const difference = Number(a[index]) - Number(b[index]);
|
|
5534
|
+
if (difference !== 0) return difference > 0;
|
|
5535
|
+
}
|
|
5536
|
+
const pa = a[4];
|
|
5537
|
+
const pb = b[4];
|
|
5538
|
+
if (pa === void 0 || pb === void 0) return pa === void 0 && pb !== void 0;
|
|
5539
|
+
const ia = pa.split(".");
|
|
5540
|
+
const ib = pb.split(".");
|
|
5541
|
+
for (let index = 0; index < Math.max(ia.length, ib.length); index += 1) {
|
|
5542
|
+
const x2 = ia[index];
|
|
5543
|
+
const y = ib[index];
|
|
5544
|
+
if (x2 === void 0) return false;
|
|
5545
|
+
if (y === void 0) return true;
|
|
5546
|
+
if (x2 === y) continue;
|
|
5547
|
+
const nx = /^\d+$/.test(x2);
|
|
5548
|
+
const ny = /^\d+$/.test(y);
|
|
5549
|
+
if (nx && ny) return Number(x2) > Number(y);
|
|
5550
|
+
if (nx !== ny) return !nx;
|
|
5551
|
+
return x2 > y;
|
|
5552
|
+
}
|
|
5553
|
+
return false;
|
|
5554
|
+
};
|
|
5555
|
+
var cacheOf = (text) => {
|
|
5556
|
+
try {
|
|
5557
|
+
const parsed = JSON.parse(text);
|
|
5558
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
5559
|
+
const entries = {};
|
|
5560
|
+
for (const [tag, entry] of Object.entries(parsed)) {
|
|
5561
|
+
if (entry !== null && typeof entry === "object" && typeof entry.read_at === "string" && typeof entry.version === "string") {
|
|
5562
|
+
entries[tag] = entry;
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
return entries;
|
|
5566
|
+
} catch {
|
|
5567
|
+
return {};
|
|
5568
|
+
}
|
|
5569
|
+
};
|
|
5570
|
+
var NO_CHECK = Object.freeze({ finish: () => null });
|
|
5571
|
+
var startUpgradeCheck = (io2, subcommand, named, running) => {
|
|
5572
|
+
const read2 = io2.registryFetch;
|
|
5573
|
+
if (read2 === void 0 || !UPGRADE_CHECK_SUBCOMMANDS.includes(subcommand)) return NO_CHECK;
|
|
5574
|
+
if (UPGRADE_CHECK_SKIP_VARIABLES.some((name) => io2.env[name] !== void 0)) return NO_CHECK;
|
|
5575
|
+
const origin = named[DEPLOY_OPTION.origin] ?? io2.env[ORIGIN_VARIABLE] ?? DEFAULT_COMMAND_ORIGIN;
|
|
5576
|
+
const tag = commandTagOf(origin);
|
|
5577
|
+
const place = credentialFileOf(io2);
|
|
5578
|
+
const path = (onWindows(io2) ? win324 : posix3).join(place.folder, UPGRADE_CHECK_FILE_NAME);
|
|
5579
|
+
const now = io2.clock();
|
|
5580
|
+
let cache = {};
|
|
5581
|
+
try {
|
|
5582
|
+
cache = cacheOf(io2.readFile(path).toString("utf8"));
|
|
5583
|
+
} catch {
|
|
5584
|
+
cache = {};
|
|
5585
|
+
}
|
|
5586
|
+
const last = cache[tag];
|
|
5587
|
+
if (last !== void 0 && now.getTime() - Date.parse(last.read_at) < UPGRADE_READ_EVERY_MS) return NO_CHECK;
|
|
5588
|
+
let arrived = null;
|
|
5589
|
+
const controller = new AbortController();
|
|
5590
|
+
const cancel = io2.schedule(UPGRADE_READ_BOUND_MS, () => controller.abort());
|
|
5591
|
+
void (async () => {
|
|
5592
|
+
try {
|
|
5593
|
+
const response = await read2(DIST_TAGS_ADDRESS, { redirect: "error", signal: controller.signal, headers: { accept: "application/json" } });
|
|
5594
|
+
if (!response.ok) return;
|
|
5595
|
+
const tags = await response.json();
|
|
5596
|
+
const version = tags !== null && typeof tags === "object" ? tags[tag] : void 0;
|
|
5597
|
+
if (typeof version !== "string" || !VERSION_FORM2.test(version)) return;
|
|
5598
|
+
arrived = version;
|
|
5599
|
+
try {
|
|
5600
|
+
io2.makeFolder(place.folder, CREDENTIAL_FOLDER_MODE);
|
|
5601
|
+
io2.writeFile(path, `${JSON.stringify({ ...cache, [tag]: { read_at: io2.clock().toISOString(), version } }, null, 2)}
|
|
5602
|
+
`);
|
|
5603
|
+
} catch {
|
|
5604
|
+
}
|
|
5605
|
+
} catch {
|
|
5606
|
+
} finally {
|
|
5607
|
+
cancel();
|
|
5608
|
+
}
|
|
5609
|
+
})();
|
|
5610
|
+
return {
|
|
5611
|
+
finish: () => {
|
|
5612
|
+
controller.abort();
|
|
5613
|
+
return arrived !== null && newerVersion(arrived, running) ? upgradeLineOf(tag, arrived, running) : null;
|
|
5614
|
+
}
|
|
5615
|
+
};
|
|
5616
|
+
};
|
|
5617
|
+
|
|
5114
5618
|
// src/turnzero_cloud_command/provision.ts
|
|
5115
5619
|
var PROVISION_OPTIONS = Object.freeze([
|
|
5116
5620
|
{
|
|
@@ -5186,9 +5690,9 @@ var NAMED_ADDRESS = "the platform's address";
|
|
|
5186
5690
|
var unreachedLine = (origin, error) => {
|
|
5187
5691
|
let said = transportText(error);
|
|
5188
5692
|
if (origin !== DEFAULT_COMMAND_ORIGIN) {
|
|
5189
|
-
const { host, hostname:
|
|
5190
|
-
const bare =
|
|
5191
|
-
for (const given of [origin, host, ...port !== "" ? [`${bare}:${port}`] : [],
|
|
5693
|
+
const { host, hostname: hostname3, port } = new URL(origin);
|
|
5694
|
+
const bare = hostname3.replace(/^\[|\]$/g, "");
|
|
5695
|
+
for (const given of [origin, host, ...port !== "" ? [`${bare}:${port}`] : [], hostname3, bare]) if (given !== "") said = said.split(given).join(NAMED_ADDRESS);
|
|
5192
5696
|
}
|
|
5193
5697
|
return `${PROVISION_ACTION}: the platform could not be reached: ${said}`;
|
|
5194
5698
|
};
|
|
@@ -6673,8 +7177,9 @@ var APPLICATION_FOLDER = "app";
|
|
|
6673
7177
|
var STAGE_FOLDER = "app.scaffold";
|
|
6674
7178
|
var APPLICATION_MANIFEST = "manifest.json";
|
|
6675
7179
|
var CREATE_STEP = "Call create_application from your tool for this new service, and skip this step where you already created its application.";
|
|
6676
|
-
var
|
|
6677
|
-
var
|
|
7180
|
+
var DEPLOY_TARGET_SENTENCE = "The deploy goes to the application's deploy target: production on an application with one environment, as a new one has, and development on one with two, where production receives a version through promote. To try versions on development, call create_environment first and follow its answer.";
|
|
7181
|
+
var runLineOf = (origin, windows) => machineLineOf(commandLineOf(origin, RUN_SUBCOMMAND, origin !== DEFAULT_COMMAND_ORIGIN ? { origin } : {}), windows, origin);
|
|
7182
|
+
var DEPLOY_STEP = `Call deploy from your tool with the application, naming no environment and neither artifact nor upload, and run the line it answers in ${APPLICATION_FOLDER}/. ${DEPLOY_TARGET_SENTENCE}`;
|
|
6678
7183
|
var SHAPE_STEP = `Shape the service to your own before the first deploy: the table and its migration in ${APPLICATION_FOLDER}/migrations, the routes in ${APPLICATION_FOLDER}/src, and the tests, which cover those routes as written.`;
|
|
6679
7184
|
var WEB_SHAPE_STEP = `Shape the app to your own before the first deploy: the page in ${APPLICATION_FOLDER}/client, the table and its migration in ${APPLICATION_FOLDER}/migrations, the routes in ${APPLICATION_FOLDER}/src, and the tests, which cover those routes as written.`;
|
|
6680
7185
|
var WEB_OPEN_STEP = "Open the address the deploy answers in a browser and sign in there: the page lists the signed-in user's own rows, adds one, and answers a question under the plan's AI allowance.";
|
|
@@ -7514,7 +8019,7 @@ async function scaffold(call, io2) {
|
|
|
7514
8019
|
const files = writtenFilesOf(template, options.names, options.fields);
|
|
7515
8020
|
const app = join8(root, APPLICATION_FOLDER);
|
|
7516
8021
|
const stage = join8(root, STAGE_FOLDER);
|
|
7517
|
-
const takeLines = takes.map((take) => machineLineOf(libraryTakeLineOf({ origin, entry: take.entry }), io2.windows ?? LINES_IN_WINDOWS_FORM));
|
|
8022
|
+
const takeLines = takes.map((take) => machineLineOf(libraryTakeLineOf({ origin, entry: take.entry }), { windows: io2.windows ?? LINES_IN_WINDOWS_FORM, npx: ranThroughNpx(io2) }, origin));
|
|
7518
8023
|
let reached = "checks";
|
|
7519
8024
|
const stateNow = () => {
|
|
7520
8025
|
if (reached === "template") return "template";
|
|
@@ -7624,7 +8129,7 @@ async function installAndTest(io2, app, template, taken, origin, left) {
|
|
|
7624
8129
|
return COMMAND_STATUS.failed;
|
|
7625
8130
|
}
|
|
7626
8131
|
io2.print(testedLine(taken));
|
|
7627
|
-
for (const line of nextStepLines(origin, io2.windows ?? LINES_IN_WINDOWS_FORM, left, template)) io2.print(line);
|
|
8132
|
+
for (const line of nextStepLines(origin, { windows: io2.windows ?? LINES_IN_WINDOWS_FORM, npx: ranThroughNpx(io2) }, left, template)) io2.print(line);
|
|
7628
8133
|
return COMMAND_STATUS.done;
|
|
7629
8134
|
}
|
|
7630
8135
|
var SCAFFOLD_ROW = Object.freeze({
|
|
@@ -7665,7 +8170,7 @@ var TOKEN_CREDENTIAL = Object.freeze({
|
|
|
7665
8170
|
mintedToken: false,
|
|
7666
8171
|
bare: `under ${TOKEN_ACTS[0]}, it reads none, and prints the challenge for a ${MINT_TOKEN_ACTION} call`
|
|
7667
8172
|
});
|
|
7668
|
-
var TERMINAL_RULE_SENTENCE = `${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} prints a token's value, so it runs only where its standard error is a terminal, as in a terminal of your own outside the AI tool, its standard output piped or not; here it is not, as under an AI tool's shell, so nothing was read and nothing was sent. Run the line yourself in a terminal outside the AI tool, piping it into your secret store's command where the token is a job's.`;
|
|
8173
|
+
var TERMINAL_RULE_SENTENCE = `${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} prints a token's value, so it runs only where its standard error is a terminal, as in a terminal of your own outside the AI coding tool, its standard output piped or not; here it is not, as under an AI coding tool's shell, so nothing was read and nothing was sent. Run the line yourself in a terminal outside the AI coding tool, piping it into your secret store's command where the token is a job's.`;
|
|
7669
8174
|
async function token(call, io2) {
|
|
7670
8175
|
const claim = call.act === TOKEN_CLAIM_ACT;
|
|
7671
8176
|
const say = claim ? io2.printError ?? io2.print : io2.print;
|
|
@@ -7705,7 +8210,7 @@ async function token(call, io2) {
|
|
|
7705
8210
|
var TOKEN_ROW = Object.freeze({
|
|
7706
8211
|
name: TOKEN_SUBCOMMAND,
|
|
7707
8212
|
acts: TOKEN_ACTS,
|
|
7708
|
-
summary: `Mints a token that is not this computer's, its value never in the chat, in two acts around one ${MINT_TOKEN_ACTION} call your tool makes. ${TOKEN_ACTS[0]} keeps a verifier in this computer's credential file and prints the challenge and the call. ${TOKEN_ACTS[1]}, run by you in a terminal outside the AI tool with the token code the call's line pipes in, presents the code with the verifier and prints the token's value once to standard output, every other line on standard error.`,
|
|
8213
|
+
summary: `Mints a token that is not this computer's, its value never in the chat, in two acts around one ${MINT_TOKEN_ACTION} call your tool makes. ${TOKEN_ACTS[0]} keeps a verifier in this computer's credential file and prints the challenge and the call. ${TOKEN_ACTS[1]}, run by you in a terminal outside the AI coding tool with the token code the call's line pipes in, presents the code with the verifier and prints the token's value once to standard output, every other line on standard error.`,
|
|
7709
8214
|
options: TOKEN_OPTIONS,
|
|
7710
8215
|
credential: TOKEN_CREDENTIAL,
|
|
7711
8216
|
statuses: {
|
|
@@ -7715,7 +8220,7 @@ var TOKEN_ROW = Object.freeze({
|
|
|
7715
8220
|
nothing_started: "nothing was minted: an option, the credential file, the token code, or a standard error that is no terminal was refused, no request was pending, the platform was too old, or the exchange was refused"
|
|
7716
8221
|
},
|
|
7717
8222
|
variables: [[ORIGIN_VARIABLE, AUTHORIZATION_ORIGIN_VARIABLE_LINE]],
|
|
7718
|
-
note: [`${TOKEN_SUBCOMMAND} ${TOKEN_ACTS[0]} may run in a tool, since it prints nothing secret. ${TOKEN_SUBCOMMAND} ${TOKEN_ACTS[1]} is yours, run in a terminal outside the AI tool on the same computer as the same user, and never by a tool: it runs only where its standard error is a terminal, which yours stays when you pipe its standard output into your secret store's command, such as gh secret set ${MINTED_TOKEN_VARIABLE}. Read the line's origin before you run it.`],
|
|
8223
|
+
note: [`${TOKEN_SUBCOMMAND} ${TOKEN_ACTS[0]} may run in a tool, since it prints nothing secret. ${TOKEN_SUBCOMMAND} ${TOKEN_ACTS[1]} is yours, run in a terminal outside the AI coding tool on the same computer as the same user, and never by a tool: it runs only where its standard error is a terminal, which yours stays when you pipe its standard output into your secret store's command, such as gh secret set ${MINTED_TOKEN_VARIABLE}. Read the line's origin before you run it.`],
|
|
7719
8224
|
afterOwnError: AUTHORIZE_AFTER_OWN_ERROR,
|
|
7720
8225
|
run: token
|
|
7721
8226
|
});
|
|
@@ -7724,8 +8229,49 @@ var TOKEN_ROW = Object.freeze({
|
|
|
7724
8229
|
import { spawn } from "node:child_process";
|
|
7725
8230
|
import { chmodSync, closeSync, openSync, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync, writeSync } from "node:fs";
|
|
7726
8231
|
import { mkdirSync, readdirSync as readdirSync2, renameSync, rmSync, statSync as statSync4, write as writeAt } from "node:fs";
|
|
8232
|
+
import { createServer } from "node:http";
|
|
7727
8233
|
import { constants as constants2 } from "node:os";
|
|
8234
|
+
import { sep as sep4 } from "node:path";
|
|
7728
8235
|
import { ReadStream, isatty } from "node:tty";
|
|
8236
|
+
var listenLoopback = (answer) => new Promise((resolve12) => {
|
|
8237
|
+
const server = createServer((request, response) => {
|
|
8238
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
8239
|
+
const query = {};
|
|
8240
|
+
for (const [name, value] of url.searchParams) if (!(name in query)) query[name] = value;
|
|
8241
|
+
const origin = typeof request.headers.origin === "string" ? request.headers.origin : null;
|
|
8242
|
+
answer({ method: request.method ?? "GET", path: url.pathname, query, origin }).then(
|
|
8243
|
+
(answered) => {
|
|
8244
|
+
response.writeHead(answered.status, { ...answered.headers, connection: "close" });
|
|
8245
|
+
response.end(answered.body);
|
|
8246
|
+
},
|
|
8247
|
+
() => {
|
|
8248
|
+
response.writeHead(500, { connection: "close" });
|
|
8249
|
+
response.end();
|
|
8250
|
+
}
|
|
8251
|
+
);
|
|
8252
|
+
});
|
|
8253
|
+
server.once("error", () => resolve12(null));
|
|
8254
|
+
server.listen(0, "127.0.0.1", () => {
|
|
8255
|
+
const address = server.address();
|
|
8256
|
+
if (address === null || typeof address === "string") {
|
|
8257
|
+
server.close();
|
|
8258
|
+
resolve12(null);
|
|
8259
|
+
return;
|
|
8260
|
+
}
|
|
8261
|
+
let closed = false;
|
|
8262
|
+
resolve12({
|
|
8263
|
+
port: address.port,
|
|
8264
|
+
close: () => {
|
|
8265
|
+
if (closed) return;
|
|
8266
|
+
closed = true;
|
|
8267
|
+
server.close();
|
|
8268
|
+
server.closeIdleConnections();
|
|
8269
|
+
setTimeout(() => server.closeAllConnections(), 2e3).unref();
|
|
8270
|
+
}
|
|
8271
|
+
});
|
|
8272
|
+
});
|
|
8273
|
+
});
|
|
8274
|
+
var ranThroughNpxOf = (entry) => entry !== void 0 && entry.includes(`${sep4}_npx${sep4}`);
|
|
7729
8275
|
var TERMINAL = process.platform === "win32" ? { input: "\\\\.\\CONIN$", output: "\\\\.\\CONOUT$", inputFlags: "r+" } : { input: "/dev/tty", output: "/dev/tty", inputFlags: "r" };
|
|
7730
8276
|
var terminalFor = (outputIsTerminal, open) => outputIsTerminal ? open() : null;
|
|
7731
8277
|
var openTerminal = () => {
|
|
@@ -7894,6 +8440,11 @@ var processIo = () => ({
|
|
|
7894
8440
|
writeNewFile: (path, bytes) => writeFileSync(path, bytes, { flag: "wx" }),
|
|
7895
8441
|
terminal: () => terminalFor(process.stdout.isTTY === true, openTerminal),
|
|
7896
8442
|
child: runChild,
|
|
8443
|
+
// The login release's seams (io.ts's LoginSeams; PLD-L0-94; PLD-L0-104).
|
|
8444
|
+
listen: listenLoopback,
|
|
8445
|
+
platform: process.platform,
|
|
8446
|
+
ranThroughNpx: ranThroughNpxOf(process.argv[1]),
|
|
8447
|
+
registryFetch: globalThis.fetch,
|
|
7897
8448
|
...folderSeams(),
|
|
7898
8449
|
// The end waits for what the held act printed to leave the process
|
|
7899
8450
|
// (endAfterOutput).
|
|
@@ -7991,9 +8542,11 @@ var SUBCOMMANDS = Object.freeze([
|
|
|
7991
8542
|
SCAFFOLD_ROW,
|
|
7992
8543
|
AUTHORIZE_ROW,
|
|
7993
8544
|
LOGOUT_ROW,
|
|
7994
|
-
TOKEN_ROW
|
|
8545
|
+
TOKEN_ROW,
|
|
8546
|
+
LOGIN_ROW
|
|
7995
8547
|
]);
|
|
7996
|
-
var SERVED =
|
|
8548
|
+
var SERVED = LINE_RUNNER;
|
|
8549
|
+
var NPX_FORM_SENTENCE = `Where the command is not installed, install it with ${installLineOf(DEFAULT_COMMAND_ORIGIN)}, or run the same line through npx: write ${commandRunnerOf("latest")} for ${LINE_RUNNER}, with next in place of latest on Turn Zero's development platform.`;
|
|
7997
8550
|
var namedWithActs = (subcommand) => subcommand.acts ? `${subcommand.name} ${subcommand.acts.join("|")}` : subcommand.name;
|
|
7998
8551
|
var shown = (option) => option.flag ? option.name : `${option.name} ${option.value}`;
|
|
7999
8552
|
var listed = (names) => names.length < 3 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
@@ -8016,11 +8569,11 @@ var usageLineOf = (subcommand) => {
|
|
|
8016
8569
|
const optional = subcommand.options.filter((option) => !option.required).map((option) => `[${shown(option)}]`);
|
|
8017
8570
|
return `${pipedHead(subcommand)}${SERVED} ${[namedWithActs(subcommand), ...required, ...optional].join(" ")}`;
|
|
8018
8571
|
};
|
|
8019
|
-
var usageHeadOf = (version) => `${TURNZERO_CLOUD_PACKAGE} ${version}: the Turn Zero Cloud command,
|
|
8572
|
+
var usageHeadOf = (version) => `${TURNZERO_CLOUD_PACKAGE} ${version}: the Turn Zero Cloud command, installed from the public npm registry by your platform's tag, latest on production and next on development.`;
|
|
8020
8573
|
var usageText = (version = commandVersion()) => {
|
|
8021
8574
|
const lines = [usageHeadOf(version), "", "Usage:"];
|
|
8022
8575
|
for (const subcommand of SUBCOMMANDS) lines.push(` ${usageLineOf(subcommand)}`);
|
|
8023
|
-
lines.push(` ${SERVED} --version`, ` ${SERVED} --help`, "", WINDOWS_SENTENCE, "", QUOTED_PATH_SENTENCE, "");
|
|
8576
|
+
lines.push(` ${SERVED} --version`, ` ${SERVED} --help`, "", WINDOWS_SENTENCE, "", NPX_FORM_SENTENCE, "", QUOTED_PATH_SENTENCE, "");
|
|
8024
8577
|
for (const subcommand of SUBCOMMANDS) {
|
|
8025
8578
|
lines.push(`${namedWithActs(subcommand)}: ${subcommand.summary}`, "", "Options:");
|
|
8026
8579
|
for (const option of subcommand.options) lines.push(` ${shown(option)}${option.required ? " (required)" : ""}`, ` ${option.description}`);
|
|
@@ -8075,7 +8628,7 @@ var valuePlacesOf = (argv2) => {
|
|
|
8075
8628
|
}
|
|
8076
8629
|
return places;
|
|
8077
8630
|
};
|
|
8078
|
-
var parseArguments = (argv2,
|
|
8631
|
+
var parseArguments = (argv2, onWindows2 = ON_WINDOWS) => {
|
|
8079
8632
|
if (argv2.some((argument) => argument.includes(UPLOAD_GRANT_PREFIX))) return { kind: "refused", sentence: grantArgumentSentence() };
|
|
8080
8633
|
if (argv2.some((argument) => argument.includes(MINTED_TOKEN_PREFIX))) return { kind: "refused", sentence: tokenArgumentSentence() };
|
|
8081
8634
|
const values = valuePlacesOf(argv2);
|
|
@@ -8125,7 +8678,7 @@ var parseArguments = (argv2, onWindows = ON_WINDOWS) => {
|
|
|
8125
8678
|
}
|
|
8126
8679
|
const value = given === void 0 ? void 0 : unquoted(given);
|
|
8127
8680
|
if (value === void 0 || value === "") return { kind: "refused", sentence: `${name} takes a value. ${NOTHING_STARTED2}` };
|
|
8128
|
-
if (
|
|
8681
|
+
if (onWindows2 && PATH_OPTION_NAMES.includes(name) && value.includes('"')) return { kind: "refused", sentence: quoteInPathSentence(name) };
|
|
8129
8682
|
named[name] = value;
|
|
8130
8683
|
}
|
|
8131
8684
|
return { kind: "run", subcommand, call: { act, named, flags } };
|
|
@@ -8160,10 +8713,16 @@ async function runGuarded(argv2, io2) {
|
|
|
8160
8713
|
say(versionLineOf(version, found));
|
|
8161
8714
|
if (parsed.kind === "refused") {
|
|
8162
8715
|
say(carriesTokenCode(argv2) ? `${parsed.sentence} ${TOKEN_CODE_UNREAD}` : parsed.sentence);
|
|
8163
|
-
|
|
8716
|
+
const npx = "ranThroughNpx" in io2 && io2.ranThroughNpx === true;
|
|
8717
|
+
const help = npx ? npxFormOf(`${SERVED} --help`, io2.env[ORIGIN_VARIABLE] ?? DEFAULT_COMMAND_ORIGIN) : `${SERVED} --help`;
|
|
8718
|
+
say(`${io2.windows ?? ON_WINDOWS ? windowsLineOf(help) : help} prints the usage.`);
|
|
8164
8719
|
return COMMAND_STATUS.nothing_started;
|
|
8165
8720
|
}
|
|
8166
|
-
|
|
8721
|
+
const upgrade = startUpgradeCheck(io2, parsed.subcommand.name, parsed.call.named, version);
|
|
8722
|
+
const status = await parsed.subcommand.run(parsed.call, io2);
|
|
8723
|
+
const notice = upgrade.finish();
|
|
8724
|
+
if (notice !== null) say(notice);
|
|
8725
|
+
return status;
|
|
8167
8726
|
}
|
|
8168
8727
|
var outputOf = (argv2, io2) => argv2[0] === TOKEN_SUBCOMMAND && argv2[1] === TOKEN_CLAIM_ACT ? io2.printError ?? io2.print : io2.print;
|
|
8169
8728
|
var NOTHING_REACHED = "no subcommand was reached, so nothing was started";
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turnzero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "The Turn Zero Cloud command, which an AI tool runs on your machine to deploy an application, store a secret, upload a file, download an export, take a library entry, write a local run's settings, start the application locally, and scaffold a new service from a template.",
|
|
5
|
+
"description": "The Turn Zero Cloud command, which an AI coding tool runs on your machine to deploy an application, store a secret, upload a file, download an export, take a library entry, write a local run's settings, start the application locally, and scaffold a new service from a template.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=24"
|
|
9
9
|
},
|
|
10
10
|
"plane_wire": {
|
|
11
|
-
"action_payloads": "2026-10-10
|
|
11
|
+
"action_payloads": "2026-10-13.10",
|
|
12
12
|
"management_api": "2026-10-10.5",
|
|
13
13
|
"wire_errors": "2026-10-10.6"
|
|
14
14
|
},
|