@shardflux/cli 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * (step-up, password change, TOTP change) is saved at once; a saved session the API rejects (401) is removed.
11
11
  * Every credential the command loaded is redacted from what it prints on failure.
12
12
  */
13
+ import { aboutWait } from "./filesystem-commands.js";
13
14
  import { homedir, hostname } from 'node:os';
14
15
  import { statSync } from 'node:fs';
15
16
  import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shardflux/sdk';
@@ -34,6 +35,7 @@ import { NPM_CLIENTS, npmLatestAll, npmRegistry, withRegistryLatest } from "./re
34
35
  import { AuthConfigError, EXIT, ExecutionPendingError, InterruptedError, UsageError, describeError, exitCodeFor, ownHint, redact } from "./errors.js";
35
36
  import { errorText, feedbackCommand, json, time } from "./format.js";
36
37
  import { HANDLERS } from "./handlers.js";
38
+ import { SETUP_VM_ENV, setupGuard } from "./machine-setup.js";
37
39
  import { makeFetch } from "./http.js";
38
40
  import { Input } from "./input.js";
39
41
  import { TimingRecorder, notices, timingJson, timingText } from "./timing.js";
@@ -112,6 +114,12 @@ function hintFor(info, cred, file, command = '') {
112
114
  if (quota)
113
115
  return quota;
114
116
  }
117
+ // 0.20.0+: builds past the plan's slots queue; only a full queue is refused (429, Retry-After).
118
+ if (info.code === 'rate_limited' && reasonOf(info) === 'template_build_queue_full') {
119
+ const queued = typeof info.details?.queued === 'number' ? info.details.queued : undefined;
120
+ const retry = info.retry_after_seconds ?? (typeof info.details?.retry_after_seconds === 'number' ? info.details.retry_after_seconds : undefined);
121
+ return `${queued ?? 'Several'} build${queued === 1 ? '' : 's'} already wait for your plan's build slots; nothing was started. Run the same command again${retry ? ` in ${aboutWait(retry)}` : ' when one finishes'}`;
122
+ }
115
123
  const policyHint = spendPolicyHint(info);
116
124
  if (policyHint)
117
125
  return policyHint;
@@ -678,7 +686,7 @@ export async function run(argv, io, opts = {}) {
678
686
  // `shard update` brings the app in itself; `shard update --check` (the menu bar of an app before 0.3.2 runs it every
679
687
  // 6 hours) is how such an app learns of a newer one with no action from the person.
680
688
  const updating = command === 'update' && parsed.values.check !== true;
681
- if (opts.runtime && !updating && !['menubar', 'device-helper'].includes(parsed.command.path[0])) {
689
+ if (opts.runtime && !updating && !io.env[SETUP_VM_ENV] && !['menubar', 'device-helper'].includes(parsed.command.path[0])) {
682
690
  refreshMenubarInBackground({ dir: store.dir, home: io.env.HOME || homedir(), runtime: opts.runtime, env: { ...process.env, ...io.env } });
683
691
  }
684
692
  // The update check runs next to the command (not for `version`, which reports it itself).
@@ -712,11 +720,15 @@ export async function run(argv, io, opts = {}) {
712
720
  io.stdout.write(setupGuideText(agentSetupGuide(ctx)));
713
721
  return EXIT.ok;
714
722
  }
723
+ // The machine setup agent's own shard (0.20.0+): only exec, writes from standard input and reads on its machine VM.
724
+ const refused = setupGuard(io.env, command, parsed.values, parsed.positionals);
725
+ if (refused)
726
+ return report(new UsageError(refused));
715
727
  const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? (ctx) => runDeviceHelper(ctx, { service: parsed.values.service === true }) : command === 'menubar refresh' ? runMenubarRefresh : parsed.command === PROJECT_COMMAND ? launchProjectCommand : parsed.command === ONBOARDING_COMMAND ? async (ctx, values) => await tryLinuxDesktop(ctx) ? 0 : terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
716
728
  if (!handler)
717
729
  return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
718
730
  // Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
719
- if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward') {
731
+ if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward' && !io.env[SETUP_VM_ENV]) {
720
732
  // An updated shard restarts this computer's CLIProxyAPI forwarder on itself (a no-op unless the version changed).
721
733
  await refreshForwarderAgent(io.env, ctx.store.dir).catch(() => false);
722
734
  await housekeeping(ctx);
@@ -21,9 +21,12 @@ export declare function startPhase(phase: StartPhase): void;
21
21
  * write to. A file the launcher already removed is not created again.
22
22
  */
23
23
  export declare function privateProgressFile(file: string): boolean;
24
- /** Atomic: a sibling written exclusively (O_EXCL never follows a link), then renamed over the file. Never throws. */
24
+ /**
25
+ * Atomic: a sibling written exclusively (O_EXCL never follows a link), then renamed over the file. Never throws. The
26
+ * start's phases and `shard machine setup`'s (0.20.0+) use the same one-line document.
27
+ */
25
28
  export declare function writeProgress(file: string, doc: {
26
- phase: StartPhase;
29
+ phase: string;
27
30
  line: string;
28
31
  }): boolean;
29
32
  export {};
@@ -57,7 +57,10 @@ export function privateProgressFile(file) {
57
57
  return false;
58
58
  }
59
59
  }
60
- /** Atomic: a sibling written exclusively (O_EXCL never follows a link), then renamed over the file. Never throws. */
60
+ /**
61
+ * Atomic: a sibling written exclusively (O_EXCL never follows a link), then renamed over the file. Never throws. The
62
+ * start's phases and `shard machine setup`'s (0.20.0+) use the same one-line document.
63
+ */
61
64
  export function writeProgress(file, doc) {
62
65
  if (!privateProgressFile(file))
63
66
  return false;
@@ -6,6 +6,8 @@ export interface OnboardingDeps {
6
6
  report?: typeof onboardingReport;
7
7
  startHelper?: () => void;
8
8
  loginCodex?: (ctx: Ctx) => Promise<void>;
9
+ /** Right after the Agents step (0.20.0+): `shard machine setup --from-this-computer --if-needed`, detached. */
10
+ startMachineSetup?: (ctx: Ctx) => boolean;
9
11
  }
10
12
  /** Linux's first run orchestrates the same auth, setup and live inspection used by the Mac onboarding. */
11
13
  export declare function terminalOnboarding(ctx: Ctx, values: Values, deps?: OnboardingDeps): Promise<number>;
@@ -6,6 +6,7 @@ import { foundApps, onboardingReport, parseApps } from "./onboarding.js";
6
6
  import { requireTermsAcceptance, showLegalNotice } from "./legal.js";
7
7
  import { json } from "./format.js";
8
8
  import { UsageError } from "./errors.js";
9
+ import { startMachineSetupInBackground } from "./machine-setup.js";
9
10
  const handlers = { ...ACCOUNT_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...CODEX_CLOUD_HANDLERS };
10
11
  async function loginCodex(ctx) {
11
12
  const child = spawn('codex', ['login', ...(!ctx.io.env.DISPLAY && !ctx.io.env.WAYLAND_DISPLAY ? ['--device-auth'] : [])], { env: ctx.io.env, stdio: 'inherit', signal: ctx.signal });
@@ -61,11 +62,15 @@ export async function terminalOnboarding(ctx, values, deps = {}) {
61
62
  }
62
63
  }
63
64
  state = await report(ctx, inspect);
65
+ // The machine sets itself up from this computer in the background; it never holds the onboarding back.
66
+ const settingUp = state.ready && (deps.startMachineSetup ?? startMachineSetupInBackground)(ctx);
64
67
  if (ctx.json)
65
68
  ctx.io.stdout.write(json({ onboarding: state, next: 'shard ./' }));
66
69
  else {
67
70
  for (const c of state.connections.filter(c => c.id === 'account' || c.picked))
68
71
  ctx.io.stdout.write(`${c.id}: ${c.fact}${c.action?.command && c.state !== 'verified' ? ` Run ${c.action.command.join(' ')}.` : ''}\n`);
72
+ if (settingUp)
73
+ ctx.io.stdout.write('Your agent is setting up your cloud machine from this computer in the background (shard machine setup --status).\n');
69
74
  if (state.ready)
70
75
  ctx.io.stdout.write('Ready. Run shard ./ in your project.\n');
71
76
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -47,7 +47,7 @@
47
47
  "smol-toml": "1.9.0",
48
48
  "snappyjs": "0.7.0",
49
49
  "yaml": "2.9.1",
50
- "@shardflux/sdk": "^0.25.0"
50
+ "@shardflux/sdk": "^0.25.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@eslint/js": "10.0.1",
@@ -58,9 +58,9 @@
58
58
  "typescript-eslint": "8.70.1"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@shardflux/desktop-linux-x64": "0.1.2",
62
- "@shardflux/desktop-linux-arm64": "0.1.2",
63
- "@shardflux/menubar-darwin": "^0.7.0"
61
+ "@shardflux/desktop-linux-x64": "0.1.5",
62
+ "@shardflux/desktop-linux-arm64": "0.1.5",
63
+ "@shardflux/menubar-darwin": "^0.8.0"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "node scripts/build.mjs",