@shardflux/cli 0.28.2 → 0.28.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The Shardflux app 0.9.4 (0.28.3+): `shard` installs and updates `@shardflux/desktop-darwin-arm64` / `-x64` and
6
+ `@shardflux/desktop-linux-x64` / `-arm64` 0.9.4 (files with a ⌘2 task, ⌘2 Credentials and Set up from this Mac,
7
+ talking in Safari, new answers stay visible, device request outcomes).
8
+
9
+ - Device request outcomes (0.28.3+, SDK 0.30.3): `shard orchestrator show <input-id>` and `shard task show <task-id>` show device requests, paths and recorded errors, failures first. Device activity keeps the actual error before API delivery.
10
+
11
+ - Input attachments (0.28.3+, SDK 0.30.3): `shard orchestrator say --file <path>` attaches files; repeat the flag, or use `--file - --file-name <name>` to stream stdin. Files-only inputs are supported.
12
+
13
+ - Automatic task landing (0.28.3+): the device helper automatically lands a finished board task's tested result in its linked repository even when the desktop board is closed. It respects Auto land and project landing settings, keeps local edits, waits for a missing folder, retries an interrupted receipt, and records the board only after delivery. Conflicts and unfinished master integration wait for a new result instead of repeating a steer after a service restart.
14
+
15
+ - Linux helper shutdown (0.28.3+): short Linux commands shut down their command-lifetime helper cleanly; service helpers leave stdin with the foreground command.
16
+
17
+ - Task results (0.28.3+, SDK 0.30.3): keep canonical task-result links and automatic resend metadata in orchestrator input output.
18
+
19
+ - Coordinator questions (0.28.3+, SDK 0.30.3): input reads preserve a coordinator wait and its message; internal worker blockers reach their coordinator before a question reaches the person. Existing needs-you answer commands remain unchanged.
20
+
21
+ - Credential setup (0.28.3+, SDK 0.30.3): `shard credentials setup --from-this-computer` brings existing shell, project and login credentials along automatically. `--if-needed` leaves unchanged values alone; `--plan` offers an optional names-only review. Connected agents can request the same secure local transfer through the device helper.
22
+
23
+ - Credential errors (0.28.3+): write-only credential failures never copy upstream prose or details into output, including values shorter than eight bytes and JSON-escaped values. Status and request id remain available.
24
+
5
25
  - The Shardflux app 0.9.3 (0.28.2+): `shard` installs and updates `@shardflux/desktop-darwin-arm64` / `-x64` and
6
26
  `@shardflux/desktop-linux-x64` / `-arm64` 0.9.3 (files open beside your work, pictures and videos picture in picture,
7
27
  ⌘2 shows the agents your orchestrator starts, needs-you questions read whole).
package/README.md CHANGED
@@ -101,7 +101,7 @@ and restarts after a CLI update. It keeps the live twin, `~/Shardflux` files and
101
101
  Use `shard device requests`, `shard device approve <id>` or `shard device deny <id>` for local approvals, and
102
102
  `shard device approvals auto|read|ask` to choose whether your agents ask first (0.28.0+);
103
103
  desktop notifications point to pending requests. On systems without a systemd user session, the helper runs
104
- while your `shard` commands are open. `shard agents open` opens a terminal when launched outside one, using
104
+ while your `shard` commands are open; their input stays with the foreground command. `shard agents open` opens a terminal when launched outside one, using
105
105
  `$TERMINAL`, `x-terminal-emulator` or `gnome-terminal`, or prints the attach command. On macOS the same app lives in
106
106
  the menu bar.
107
107
 
@@ -263,6 +263,9 @@ Finished cloud work lands in your folder by itself: a conversation's commits com
263
263
  board task's result into your linked folder and the project's default branch. `shard settings --auto-land off`
264
264
  (0.28.0+) keeps it in the cloud on this computer (`shard task land` and `shard conversations land` still land on
265
265
  request); `--auto-land on` turns it back on. The Shardflux app's Settings → Projects changes the same switch.
266
+ The device helper brings finished task results back while your computer is reachable, including work requested from
267
+ your phone; it catches up after your computer reconnects. Your local edits stay in place, and a conflicting merge
268
+ waits for a decision instead of overwriting them.
266
269
 
267
270
  Terminal conversations open in the terminal you choose (0.22.0+):
268
271
 
@@ -1529,8 +1532,10 @@ your agent uses the matching machine or project save/rollback commands in its so
1529
1532
  `shard machine open` (0.17.1+): it creates or resumes the machine VM without attaching and prints its key for
1530
1533
  `shard ws exec` and `shard files write`. `shard machine setup --from-this-computer` (0.20.0+) does it for you: your own
1531
1534
  Claude Code (`claude -p`, else Codex) recreates this computer's CLIs and tools, language runtimes, shell config and
1532
- aliases, git identity and dotfiles on the machine, on your subscription, then saves it. On this computer it only reads;
1533
- `~/.ssh`, keychains, `.env`, token and credential files are never read. The apps and the first run start it once,
1535
+ aliases, git identity and dotfiles on the machine, on your subscription, then saves it. That setup agent reads
1536
+ tool and configuration metadata and excludes `~/.ssh`, keychains, `.env`, token and credential files. Optional
1537
+ `--with-credentials` (0.28.3+) runs the trusted local credential transfer afterwards; onboarding includes that transfer.
1538
+ The apps and the first run start machine setup once,
1534
1539
  right after you connect your agents. `shard machine setup --status` shows the machine as your account has it, in the
1535
1540
  same line the apps show: ready, saving (with its place in the queue), or the one command to run next (0.22.0+); `shard
1536
1541
  setup --inspect --json` carries it as `onboarding.machine`. `connections_ready` (and the legacy `ready` field) means
@@ -2100,3 +2105,29 @@ Bare `shard` in a coding-agent shell or without a TTY returns its setup guide us
2100
2105
  Needs-you output prints each question and its optional `option_details` in full, including what each choice does and what follows it. The first suggested answer is the agent’s recommendation. Secret values go through the project’s secure setup, never a question or its ordinary answer.
2101
2106
 
2102
2107
  Conversation creation (0.28.2+) includes the optional launching conversation only when `/v1/meta` advertises `capabilities.conversation_origin: true`. Older APIs receive the existing create body; no rejected create is replayed with different fields.
2108
+
2109
+
2110
+ ## Input attachments (0.28.3+)
2111
+
2112
+ `shard orchestrator say --file <path>` attaches files; repeat the flag, or use `--file - --file-name <name>` to stream stdin. Files-only inputs are supported.
2113
+
2114
+ ```sh
2115
+ shard orchestrator say "Review these" --file spec.pdf --file photo.png
2116
+ cat archive.tar | shard orchestrator say --file - --file-name archive.tar
2117
+ ```
2118
+ File uploads stream to durable API storage, then the input carries their pinned references. Each receiving thread gets the files before its turn, under `~/inputs/`.
2119
+
2120
+ ### Device request outcomes (0.28.3+)
2121
+
2122
+ ```sh
2123
+ shard orchestrator show in_abcdefghijklmnopqrstuvwxyz
2124
+ shard task show task_abcdefghijklmnopqrstuvwxyz
2125
+ ```
2126
+
2127
+ Each request shows what ran, its paths and outcome, and its recorded error. Failures appear first. Add `--json` for the full detail object. Reading starts no computer.
2128
+
2129
+ Scoped vault imports (0.28.3+) use `shard secrets import NAME --from hsec --scope project --project PROJECT` (or `--scope agents --org ORG` while signed in). Re-importing an identical value leaves its version unchanged; a changed value rotates it. The vault sends the value directly to the encrypted write-only API. Failures show a safe status and request id without upstream prose or details that could echo the value.
2130
+
2131
+ ## Credential setup (0.28.3+)
2132
+
2133
+ Run `shard credentials setup --from-this-computer --if-needed` after machine setup to bring your existing credentials along. Machine credentials are available to all your agents; project `.env` credentials stay local and override machine values by name. Values transfer directly from the local source into the write-only secret store and private credential files. The command prints names and delivery progress. Use `--plan` for an optional review, then `--apply PLAN_ID`. If a requested credential is missing everywhere, your agent asks once through the secure credential field.
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.28.2";
1
+ export declare const CLI_VERSION = "0.28.3";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
package/dist/args.js CHANGED
@@ -15,7 +15,7 @@ import { BOARD_COMMANDS } from "./board-args.js";
15
15
  import { ORCHESTRATOR_COMMANDS } from "./orchestrator-args.js";
16
16
  import { UsageError } from "./errors.js";
17
17
  import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
18
- export const CLI_VERSION = '0.28.2';
18
+ export const CLI_VERSION = '0.28.3';
19
19
  export const GLOBAL_OPTIONS = {
20
20
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
21
21
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -244,6 +244,7 @@ const KEY_COMMANDS = [
244
244
  { path: ['machine', 'setup'], summary: 'Let your own Claude Code or Codex set up your machine from this computer (0.20.0+)', positionals: [], options: {
245
245
  'from-this-computer': { type: 'boolean', description: 'Recreate this computer’s development setup on your machine: its CLIs and tools, language runtimes, shell config and aliases, git identity and dotfiles. Required.' },
246
246
  'if-needed': { type: 'boolean', description: 'Only when your machine was never set up from this computer (what the apps run after onboarding).' },
247
+ 'with-credentials': { type: 'boolean', description: 'After the machine is saved, bring existing credentials along with the trusted local credential setup.' },
247
248
  status: { type: 'boolean', description: 'Show the last setup from this computer instead of running one.' },
248
249
  agent: { type: 'string', value: '<claude|codex|cloud>', description: 'Which agent sets it up (default: this computer’s Claude Code, else its Codex, else your cloud Claude in the machine VM).' },
249
250
  timeout: { type: 'string', value: '<duration>', description: 'Time box for the agent (default 10m).' },
@@ -982,6 +983,16 @@ const KEY_COMMANDS = [
982
983
  examples: ['shard executions get acme/agent-7 build-42', 'shard ws executions get acme/agent-7 ex-6f1c2a9e-0d7b-4c1e-9a53-2b8e1f0c4d7a --wait 10m --json'],
983
984
  },
984
985
  { path: ['secrets', 'log'], summary: 'Show credential request and fill audit events', positionals: [], options: {} },
986
+ { path: ['credentials', 'setup'], summary: 'Bring existing credentials from this computer to your machine and project', positionals: [], options: {
987
+ 'from-this-computer': { type: 'boolean', description: 'Discover source names locally and transfer values directly; no value goes to a model.' },
988
+ 'if-needed': { type: 'boolean', description: 'Reconcile existing sources; report needed:false when their stored versions are unchanged.' },
989
+ plan: { type: 'boolean', description: 'Review the names and scopes once before transferring selected sources.' },
990
+ apply: { type: 'string', value: '<plan-id>', description: 'Continue a reviewed setup using its original source descriptors.' },
991
+ deny: { type: 'string', value: '<plan-id>', description: 'Decline a reviewed setup.' },
992
+ picks: { type: 'string', value: '<file|->', description: 'JSON source IDs and scopes for --apply; - reads stdin. No values.' },
993
+ project: { type: 'string', value: '<project-id>', description: 'Project for local credentials (default current context).' },
994
+ path: { type: 'string', value: '<folder>', description: 'Project folder whose .env files to discover (default current folder).' },
995
+ } },
985
996
  { path: ['credentials', 'list'], summary: 'Show pending login requests', positionals: [], options: {} },
986
997
  { path: ['credentials', 'enable'], summary: 'Enable credential fill in a workspace', positionals: [REF], options: {} },
987
998
  { path: ['credentials', 'deny'], summary: 'Deny a login request', positionals: [{ name: 'request-id', required: true }], options: {} },
@@ -84,7 +84,8 @@ export const BOARD_COMMANDS = [
84
84
  path: ['task', 'land'],
85
85
  summary: 'Land a finished task: its result into your folder and the project’s default branch (0.25.0+)',
86
86
  positionals: [{ name: 'id', required: true }],
87
- options: { path: { type: 'string', value: '<folder>', description: 'The folder to land in (default: the project’s folder on this computer, if any).' } },
87
+ options: { path: { type: 'string', value: '<folder>', description: 'The folder to land in (default: the project’s folder on this computer, if any).' },
88
+ automatic: { type: 'boolean', hidden: true, description: 'Device automatic delivery: require Auto land, a linked folder and the master’s tested result.' } },
88
89
  description: [
89
90
  'A task has one result: the branch shard/<task id>, where its master merged its workers’ branches. Landing merges it',
90
91
  'into the project’s default branch on the project hub (a fast-forward, or a merge commit when the merge is clean),',
@@ -1,3 +1,4 @@
1
+ import { deviceOutcomeLines } from "./device-outcomes.js";
1
2
  import { findOrg, resolveOrg } from "./account-scope.js";
2
3
  import { out, principal, str, strs, readSecretValue, askSecretValue } from "./commands.js";
3
4
  import { detailLine, pickAnswers } from "./conversation-commands.js";
@@ -150,6 +151,8 @@ export function taskDetailText(d) {
150
151
  lines.push(` Landed: ${r.landed.how} ${r.landed.ref}`);
151
152
  }
152
153
  }
154
+ if (d.device_requests !== undefined)
155
+ lines.push(...deviceOutcomeLines(d.device_requests));
153
156
  return `${lines.join('\n')}\n`;
154
157
  }
155
158
  /** --now, --next or --later (one at most); undefined when none was given. */
@@ -350,7 +353,7 @@ export const BOARD_HANDLERS = {
350
353
  return 0;
351
354
  },
352
355
  async 'task land'(ctx, values, [id]) {
353
- const o = await landTask(landDeps(ctx), id, { path: str(values.path) });
356
+ const o = await landTask(landDeps(ctx), id, { path: str(values.path), automatic: values.automatic === true });
354
357
  out(ctx, landedText(o), o);
355
358
  return 0;
356
359
  },
@@ -391,7 +394,7 @@ export const BOARD_HANDLERS = {
391
394
  await findOrg(ctx.account(), organizationId);
392
395
  }
393
396
  const save = (value) => {
394
- ctx.addSecret(value);
397
+ ctx.addWriteOnlyValue(value);
395
398
  return cloud.needsYou.answer(id, { credential: { value, scope: target } }, { ...(organizationId ? { organizationId } : {}), signal: ctx.signal });
396
399
  };
397
400
  const value = await readSecretValue(ctx, values, item.credential.name);
@@ -61,6 +61,8 @@ export interface Ctx {
61
61
  };
62
62
  /** A credential this command received (redacted from error output). */
63
63
  addSecret(value: string): void;
64
+ /** A credential value this command writes: redacted like addSecret, and failures report only status-based codes. */
65
+ addWriteOnlyValue(value: string): void;
64
66
  /** The update status for `shard version` (forced: ask the API now). Null when unknown. */
65
67
  versionStatus(force: boolean): Promise<UpdateView | null>;
66
68
  /** The credentials file (config directory from the environment). */
package/dist/commands.js CHANGED
@@ -2349,9 +2349,9 @@ export const KEY_HANDLERS = {
2349
2349
  const project = agent ? null : (await principal(ctx)).project_id;
2350
2350
  const imported = [];
2351
2351
  for (const [i, name] of names.entries()) {
2352
- const value = await vaultValue(from, name, { signal: ctx.signal, ...(keychain ? { keychain } : {}) });
2353
- ctx.addSecret(value);
2354
- imported.push(agent ? await agent.cloud.secrets.createAgent(agent.target, { name: targets[i], value }) : await ctx.cloud().secrets.create(project, { name: targets[i], value }));
2352
+ const value = await vaultValue(from, name, { signal: ctx.signal, env: ctx.io.env, ...(keychain ? { keychain } : {}) });
2353
+ ctx.addWriteOnlyValue(value);
2354
+ imported.push(agent ? await agent.cloud.secrets.importAgent(agent.target, { name: targets[i], value }) : await ctx.cloud().secrets.create(project, { name: targets[i], value }));
2355
2355
  }
2356
2356
  out(ctx, `Imported ${imported.map(s => s.name).join(', ')}. ${SECRET_SECURITY_NOTE}\n`, { data: imported, security_note: SECRET_SECURITY_NOTE });
2357
2357
  return 0;
@@ -2612,7 +2612,7 @@ async function agentSecretCommand(ctx, values, action, ref) {
2612
2612
  if (action === 'create') {
2613
2613
  if (!SECRET_NAME.test(ref ?? ''))
2614
2614
  throw new UsageError('Use an environment variable name for the credential.');
2615
- const value = await readSecretValue(ctx, values, ref), save = (value) => { ctx.addSecret(value); return cloud.secrets.createAgent(target, { name: ref, value, ...(str(values.description) !== undefined ? { description: str(values.description) } : {}) }); };
2615
+ const value = await readSecretValue(ctx, values, ref), save = (value) => { ctx.addWriteOnlyValue(value); return cloud.secrets.createAgent(target, { name: ref, value, ...(str(values.description) !== undefined ? { description: str(values.description) } : {}) }); };
2616
2616
  const r = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `Add ${ref}`, purpose: `Saved encrypted for your agents as ${ref}.`, name: ref }, save);
2617
2617
  out(ctx, describe(r), r);
2618
2618
  return 0;
@@ -2636,7 +2636,7 @@ async function agentSecretCommand(ctx, values, action, ref) {
2636
2636
  return 0;
2637
2637
  }
2638
2638
  if (action === 'rotate') {
2639
- const value = await readSecretValue(ctx, values, r.name), save = (value) => { ctx.addSecret(value); return cloud.secrets.rotateAgent(r.id, value); };
2639
+ const value = await readSecretValue(ctx, values, r.name), save = (value) => { ctx.addWriteOnlyValue(value); return cloud.secrets.rotateAgent(r.id, value); };
2640
2640
  const changed = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `New value for ${r.name}`, purpose: `Replaces the encrypted credential ${r.name}.`, name: r.name }, save);
2641
2641
  out(ctx, describe(changed), changed);
2642
2642
  return 0;
@@ -0,0 +1,8 @@
1
+ import type { Ctx } from './commands.js';
2
+ import { conversationClient } from './conversation-commands.js';
3
+ type Input = Parameters<ReturnType<typeof conversationClient>['deviceRequests']['heartbeat']>[0];
4
+ export declare function credentialHeartbeat(ctx: Ctx, input: Input): Promise<{
5
+ device_id: string;
6
+ online_until: string;
7
+ }>;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ /** Old APIs must keep accepting ordinary helper heartbeats during a rolling upgrade. */
2
+ import { ShardfluxApiError } from '@shardflux/sdk';
3
+ import { conversationClient } from "./conversation-commands.js";
4
+ const probes = new WeakMap();
5
+ export async function credentialHeartbeat(ctx, input) {
6
+ let probe = probes.get(ctx);
7
+ if (!probe || Date.now() - probe.at >= 30_000) {
8
+ probe = { at: Date.now(), pending: conversationClient(ctx).credentialSetups.available().catch(() => false) };
9
+ probes.set(ctx, probe);
10
+ }
11
+ const available = await probe.pending, cloud = conversationClient(ctx);
12
+ try {
13
+ return await cloud.deviceRequests.heartbeat({ ...input, ...(available ? { capabilities: ['credentials_setup.v1'] } : {}) });
14
+ }
15
+ catch (e) {
16
+ if (!available || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
17
+ throw e;
18
+ probes.set(ctx, { at: Date.now(), pending: Promise.resolve(false) });
19
+ return cloud.deviceRequests.heartbeat(input);
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ import type { CredentialSetup, CredentialSetupsApi, SecretsApi, NeedsYouApi } from '@shardflux/sdk';
2
+ import type { Ctx, Handler } from './commands.js';
3
+ import type { CredentialDiscoveryOptions } from './credential-sources.js';
4
+ interface SetupClient {
5
+ credentialSetups: CredentialSetupsApi;
6
+ secrets: SecretsApi;
7
+ needsYou: NeedsYouApi;
8
+ }
9
+ /** Onboarding opts in explicitly. The restricted machine agent never reads the source values. */
10
+ export declare function credentialSetupAfterMachine(ctx: Ctx): Promise<void>;
11
+ export declare function transferCredentialSetup(ctx: Ctx, cloud: SetupClient, plan: CredentialSetup, options: CredentialDiscoveryOptions): Promise<CredentialSetup>;
12
+ export declare const CREDENTIAL_SETUP_HANDLERS: Record<string, Handler>;
13
+ export {};
@@ -0,0 +1,170 @@
1
+ /** Trusted local credential transfer. Models and stdout receive names, sources and status only. */
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { UsageError } from "./errors.js";
5
+ import { captureLocalLoginEnvironment, discoverCredentialSources, readCredentialSource, CredentialSourceError } from "./credential-sources.js";
6
+ const str = (value) => typeof value === 'string' ? value : undefined;
7
+ function client(ctx) { return ctx.sessionSource() ? ctx.account() : ctx.cloud(); }
8
+ /** Onboarding opts in explicitly. The restricted machine agent never reads the source values. */
9
+ export async function credentialSetupAfterMachine(ctx) {
10
+ try {
11
+ const cloud = client(ctx);
12
+ if (!cloud.credentialSetups || !await cloud.credentialSetups.available())
13
+ return;
14
+ const quiet = { ...ctx, json: true, io: { ...ctx.io, stdout: { ...ctx.io.stdout, write: () => true } } };
15
+ await CREDENTIAL_SETUP_HANDLERS['credentials setup'](quiet, { 'from-this-computer': true, 'if-needed': true }, []);
16
+ }
17
+ catch {
18
+ ctx.io.stderr.write('Credential setup is pending. Continue with shard credentials setup --from-this-computer --if-needed.\n');
19
+ }
20
+ }
21
+ const itemResult = (item) => ({ id: item.id, name: item.name, state: item.state, scope: item.selected_scope, project_id: item.project_id, ...(item.delivery ? { delivery: item.delivery } : {}), ...(item.error_code ? { error_code: item.error_code } : {}) });
22
+ function output(ctx, value) { if (ctx.json)
23
+ ctx.io.stdout.write(JSON.stringify(value) + '\n'); }
24
+ export async function transferCredentialSetup(ctx, cloud, plan, options) {
25
+ if (plan.state === 'awaiting_approval')
26
+ return plan;
27
+ if (plan.state === 'denied' || plan.state === 'completed')
28
+ return plan;
29
+ const jobs = plan.items.filter(i => i.state === 'pending' && i.selected_scope !== null).map(async (item) => {
30
+ try {
31
+ const { project_id: sourceProject, ...descriptor } = item;
32
+ const candidate = { ...descriptor, ...(sourceProject ? { project_id: sourceProject } : {}), preferred: true };
33
+ const value = await readCredentialSource(candidate, options);
34
+ ctx.addWriteOnlyValue(value);
35
+ const stored = await cloud.credentialSetups.storeValue(plan.id, item.id, value);
36
+ const result = stored.items.find(i => i.id === item.id);
37
+ if (result)
38
+ output(ctx, { plan_id: plan.id, result: itemResult(result) });
39
+ }
40
+ catch (err) {
41
+ const code = err instanceof CredentialSourceError ? err.code : 'import_failed';
42
+ try {
43
+ const failed = await cloud.credentialSetups.sourceResult(plan.id, item.id, code);
44
+ const result = failed.items.find(i => i.id === item.id);
45
+ if (result)
46
+ output(ctx, { plan_id: plan.id, result: itemResult(result) });
47
+ }
48
+ catch { /* Read authoritative status below; a lost write response may already have stored it. */ }
49
+ }
50
+ });
51
+ await Promise.allSettled(jobs);
52
+ return cloud.credentialSetups.get(plan.id, { signal: ctx.signal });
53
+ }
54
+ async function picks(ctx, path) {
55
+ let value;
56
+ if (path === '-') {
57
+ const chunks = [];
58
+ for await (const chunk of ctx.io.stdin)
59
+ chunks.push(Buffer.from(chunk));
60
+ value = Buffer.concat(chunks).toString('utf8');
61
+ }
62
+ else
63
+ value = await readFile(path, 'utf8');
64
+ try {
65
+ const parsed = JSON.parse(value);
66
+ if (!Array.isArray(parsed) || !parsed.every((item) => {
67
+ if (!item || typeof item !== 'object' || Array.isArray(item))
68
+ return false;
69
+ const i = item;
70
+ return typeof i.id === 'string' && (i.scope === 'agents' || i.scope === 'project') && (i.project_id === undefined || typeof i.project_id === 'string') && Object.keys(i).every(k => ['id', 'scope', 'project_id'].includes(k));
71
+ }))
72
+ throw new Error();
73
+ return parsed;
74
+ }
75
+ catch {
76
+ throw new UsageError('--picks takes a JSON list of discovered source IDs and scopes.');
77
+ }
78
+ }
79
+ export const CREDENTIAL_SETUP_HANDLERS = {
80
+ async 'credentials setup'(ctx, values) {
81
+ const cloud = client(ctx), apply = str(values.apply), deny = str(values.deny), path = str(values.path);
82
+ if (!await cloud.credentialSetups.available())
83
+ throw new UsageError('Credential setup is being prepared. Update Shardflux and try again.');
84
+ if (apply && deny)
85
+ throw new UsageError('Choose --apply or --deny.');
86
+ if (deny) {
87
+ await cloud.needsYou.answer('csetup_' + deny, { decision: 'deny' });
88
+ output(ctx, { plan_id: deny, state: 'denied' });
89
+ if (!ctx.json)
90
+ ctx.io.stdout.write('Credential setup declined.\n');
91
+ return 0;
92
+ }
93
+ if (!apply && values['from-this-computer'] !== true)
94
+ throw new UsageError('Use --from-this-computer to bring your existing credentials along.');
95
+ let projectId = str(values.project) ?? ctx.store.profile(ctx.apiUrl).project_id, organizationId = ctx.store.profile(ctx.apiUrl).organization_id;
96
+ if (!projectId && !apply) {
97
+ try {
98
+ const me = await ctx.cloud().me();
99
+ if (!me.api_key)
100
+ throw new UsageError('Select a project with --project.');
101
+ projectId = me.api_key.project_id;
102
+ organizationId ??= me.api_key.organization_id;
103
+ }
104
+ catch {
105
+ if (!ctx.sessionSource())
106
+ throw new UsageError('Select a project with --project, or sign in first.');
107
+ }
108
+ }
109
+ const home = ctx.io.env.HOME ?? homedir();
110
+ const base = { home, env: ctx.io.env, ...(projectId ? { projectId, projectPath: path ?? process.cwd() } : {}), signal: ctx.signal };
111
+ const env = await captureLocalLoginEnvironment(base), options = { ...base, env };
112
+ let plan, prior = new Map();
113
+ if (apply) {
114
+ const selection = str(values.picks);
115
+ if (selection)
116
+ await cloud.needsYou.answer('csetup_' + apply, { credential_setup: { items: await picks(ctx, selection) } });
117
+ plan = await cloud.credentialSetups.get(apply, { signal: ctx.signal });
118
+ }
119
+ else {
120
+ const candidates = await discoverCredentialSources(options), localOnly = !ctx.sessionSource();
121
+ const normalized = (localOnly ? [...candidates].sort((a, b) => Number(b.suggested_scope === 'project') - Number(a.suggested_scope === 'project')) : candidates).map(c => ({ ...c, ...(localOnly ? { suggested_scope: 'project', project_id: projectId } : {}) }));
122
+ if (localOnly && !projectId)
123
+ throw new UsageError('Select a project with --project.');
124
+ const selected = new Set(), selectedItems = [];
125
+ for (const c of normalized)
126
+ if (c.preferred) {
127
+ const key = JSON.stringify([c.suggested_scope, c.project_id, c.name]);
128
+ if (!selected.has(key)) {
129
+ selected.add(key);
130
+ selectedItems.push(c.id);
131
+ }
132
+ }
133
+ const items = normalized.map(({ id, name, source, suggested_scope, project_id }) => ({ id, name, source, suggested_scope, ...(project_id ? { project_id } : {}) }));
134
+ if (!items.length && values['if-needed'] === true) {
135
+ output(ctx, { needed: false, results: [] });
136
+ if (!ctx.json)
137
+ ctx.io.stdout.write('No new credential sources found.\n');
138
+ return 0;
139
+ }
140
+ try {
141
+ const metadata = await cloud.secrets.listAgents({ ...organizationId ? { organizationId } : {} });
142
+ prior = new Map(metadata.map(s => [s.id, s.current_version]));
143
+ }
144
+ catch { /* An unavailable metadata read does not block the direct transfer. */ }
145
+ plan = await cloud.credentialSetups.create({ items, ...(projectId ? { projectId } : {}), ...(organizationId ? { organizationId } : {}), review: values.plan === true, ...(values.plan === true ? {} : { selectedItems }) });
146
+ if (!prior.size)
147
+ try {
148
+ prior = new Map((await cloud.secrets.listAgents({ organizationId: plan.organization_id })).map(s => [s.id, s.current_version]));
149
+ }
150
+ catch { /* Transfers remain independent of this optional metadata read. */ }
151
+ output(ctx, { plan_id: plan.id, ...(values.plan === true ? { candidates: plan.items } : {}) });
152
+ if (values.plan === true) {
153
+ if (!ctx.json)
154
+ ctx.io.stdout.write('Review the discovered names in Shard, then import the selected sources.\n');
155
+ return 0;
156
+ }
157
+ }
158
+ if (plan.state === 'awaiting_approval') {
159
+ output(ctx, { plan_id: plan.id, state: plan.state, results: plan.items.map(itemResult) });
160
+ return 0;
161
+ }
162
+ const final = await transferCredentialSetup(ctx, cloud, plan, options), failed = final.items.filter(i => i.state === 'failed').length, pending = final.items.filter(i => i.state === 'pending' || i.state === 'stored' && i.delivery?.state !== 'ready').length;
163
+ const stored = final.items.filter(i => i.state === 'stored'), needed = stored.some(i => !i.secret_id || prior.get(i.secret_id) !== i.secret_version);
164
+ const result = { plan_id: final.id, state: final.state, needed, results: final.items.map(itemResult), stored: stored.length, failed, pending };
165
+ output(ctx, result);
166
+ if (!ctx.json)
167
+ ctx.io.stdout.write(`${stored.length} credential${stored.length === 1 ? '' : 's'} stored${failed ? `, ${failed} source${failed === 1 ? '' : 's'} need attention` : ''}${pending ? `, ${pending} delivery pending` : ''}.\n`);
168
+ return failed ? 1 : 0;
169
+ },
170
+ };
@@ -0,0 +1,34 @@
1
+ export type CredentialSourceCode = 'source_unavailable' | 'source_changed' | 'source_not_text' | 'source_empty' | 'source_too_large' | 'source_has_nul' | 'vault_locked' | 'login_expired' | 'import_failed';
2
+ export declare class CredentialSourceError extends Error {
3
+ readonly code: CredentialSourceCode;
4
+ constructor(code: CredentialSourceCode);
5
+ }
6
+ export interface CredentialCandidate {
7
+ id: string;
8
+ name: string;
9
+ source: {
10
+ kind: 'environment' | 'dotenv' | 'shell' | 'vault' | 'cli';
11
+ label: string;
12
+ reference: string;
13
+ };
14
+ suggested_scope: 'agents' | 'project';
15
+ project_id?: string;
16
+ preferred: boolean;
17
+ }
18
+ export interface CredentialDiscoveryOptions {
19
+ home: string;
20
+ env: Readonly<NodeJS.ProcessEnv>;
21
+ projectPath?: string;
22
+ projectId?: string;
23
+ signal?: AbortSignal;
24
+ /** Fixture-injected process boundary. stdout is private; stderr is never returned. */
25
+ run?: (argv: readonly string[], env: Readonly<NodeJS.ProcessEnv>, signal?: AbortSignal) => Promise<Buffer>;
26
+ /** Tests declare the fixture executables. Production resolves only the caller's PATH. */
27
+ availableCommands?: readonly string[];
28
+ }
29
+ export declare function credentialVariableName(name: string): boolean;
30
+ /** Normal local login startup resolves exported vault expressions privately, without a model reading shell files. */
31
+ export declare function captureLocalLoginEnvironment(options: CredentialDiscoveryOptions): Promise<NodeJS.ProcessEnv>;
32
+ /** Reconstruct a source from metadata only, so a review can finish in a later CLI process. */
33
+ export declare function readCredentialSource(candidate: CredentialCandidate, options: CredentialDiscoveryOptions): Promise<string>;
34
+ export declare function discoverCredentialSources(options: CredentialDiscoveryOptions): Promise<CredentialCandidate[]>;