turnzero 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- You do not install it. The MCP server gives your AI tool each command line to run, and `npx` fetches this package from npm when that line runs. It needs Node.js 24 or later.
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
- - `authorize` gives this computer a token to act on your account, and `logout` revokes it.
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/
@@ -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 = "npx";
74
- var WINDOWS_LINE_PROGRAM = "npx.cmd";
75
- var LINE_RUNNER = `${LINE_PROGRAM} -y`;
76
- var WINDOWS_LINE_RUNNER = `${WINDOWS_LINE_PROGRAM} -y`;
77
- var LINE_GRANT_HEAD = /^echo \S+ \| $/;
78
- var windowsLineOf = (line) => {
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 Windows form was asked for a line that does not open with ${LINE_RUNNER}, after an optional echo of a grant`);
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}${WINDOWS_LINE_RUNNER} ${line.slice(at + runner.length)}`;
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) => `${LINE_RUNNER} ${TURNZERO_CLOUD_PACKAGE}@${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
- commandRunnerOf(commandTagOf(addressOrigin)),
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.11",
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 ? LOCAL_APP_DATA_VARIABLE : CONFIG_HOME_VARIABLE] ?? "").trim();
541
- const base = named !== "" && paths.isAbsolute(named) ? named : windows ? paths.join(home, ...LOCAL_APP_DATA_FALLBACK) : paths.join(home, CONFIG_HOME_FALLBACK);
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 writes` : declared.computerToken === true ? `, and otherwise this computer's token for the platform's address from its credential file, which authorize writes, and never a minted token from ${MINTED_TOKEN_VARIABLE}` : ""}.`;
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 = readCredentialFile(io2, place.path);
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 = readCredentialFile(io2, credentialFileOf(io2).path);
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 ${AUTHORIZE_SUBCOMMAND}${originWordsOf(origin)} to authorize this computer again.`;
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 SIGNED_IN_ROUTE_OPENER = "A tool signed in to the platform authorizes this computer instead:";
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 firstActLines = (origin, kind, pending, label) => [
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 = readCredentialFile(io2, place.path);
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
- const lines = [...opener !== null ? [opener] : [], ...opening !== "" ? [opening] : [], ...firstActLines(origin, kind, pending, label)];
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 = readCredentialFile(io2, place.path);
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 escaped = segment.replace(UNSAFE_CHARACTER, percentOf);
1552
- escaped = escaped.replace(/[. ]+$/u, (run) => [...run].map(percentOf).join(""));
1553
- return DEVICE_NAME.test(escaped) ? `_${escaped}` : escaped;
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} writes.`
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} writes, sent only to that address; with neither, it prints the variable route first and then, where the address is admitted, how to authorize this computer`;
3026
- var DEPLOY_AFTER_AUTHORIZE = `Once the line the ${MINT_TOKEN_ACTION} call answers has printed authorized:, run this deploy line again. ${NOTHING_SENT_OR_STARTED}`;
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
- if (await firstAct(io2, origin2.origin, "authorized computer", io2.print, SIGNED_IN_ROUTE_OPENER) === COMMAND_STATUS.done) {
3213
- io2.print(DEPLOY_AFTER_AUTHORIZE);
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, how to authorize this computer.",
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
- `A tool signed in to the platform makes the ${MINT_TOKEN_ACTION} call the run prints with its challenge, runs the line that call answers, then runs the deploy line again. The credential file is ${CREDENTIAL_FILE_WORDS}.`,
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 CHECK_AFTER_AUTHORIZE = `Once the line the ${MINT_TOKEN_ACTION} call answers has printed authorized:, run this check line again. ${NOTHING_SENT_OR_CHECKED}`;
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} writes.`
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} writes, sent only to that address; with neither, it prints the variable route first and then, where the address is admitted, how to authorize this computer`;
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
- if (await firstAct(io2, origin2.origin, "authorized computer", io2.print, SIGNED_IN_ROUTE_OPENER) === COMMAND_STATUS.done) {
3586
- io2.print(CHECK_AFTER_AUTHORIZE);
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, onWindows = NPM_NEEDS_SHELL) => onWindows && ended.interrupted === true ? INTERRUPTED_ON_WINDOWS : null;
4322
- var afterInterruptedChildLineOf = (ended, what, onWindows = NPM_NEEDS_SHELL) => afterInterruptLineOf(ended, onWindows) === null ? null : `${what} has ended. cmd may ask "Terminate batch job (Y/N)?" next: either answer returns the prompt.`;
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, windows = LINES_IN_WINDOWS_FORM) => windows ? windowsLineOf(line) : 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 = readCredentialFile(io2, place.path);
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: hostname2, port } = new URL(origin);
5190
- const bare = hostname2.replace(/^\[|\]$/g, "");
5191
- for (const given of [origin, host, ...port !== "" ? [`${bare}:${port}`] : [], hostname2, bare]) if (given !== "") said = said.split(given).join(NAMED_ADDRESS);
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 runLineOf = (origin, windows) => machineLineOf(commandLineOf(origin, RUN_SUBCOMMAND, origin !== DEFAULT_COMMAND_ORIGIN ? { origin } : {}), windows);
6677
- 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}/. A new application has one environment, production, so that deploy goes there; call create_environment first and follow its answer to try versions on development.`;
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 = commandRunnerOf("latest");
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, run through npx from the public npm registry by your platform's tag, latest on production and next on development.`;
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, onWindows = ON_WINDOWS) => {
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 (onWindows && PATH_OPTION_NAMES.includes(name) && value.includes('"')) return { kind: "refused", sentence: quoteInPathSentence(name) };
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
- say(`${SERVED} --help prints the usage.`);
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
- return parsed.subcommand.run(parsed.call, io2);
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.13.0",
3
+ "version": "0.14.0",
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",
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
  },