@shardflux/cli 0.14.1 → 0.14.2

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
@@ -1,11 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.2
4
+
5
+ - `shard agents open` accepts conversation keys, VM keys and conversation ids, reattaching the existing Claude Code or Codex session in its conversation worktree. The menu bar's Open action uses the same path; legacy agents retain their existing behavior.
6
+ - Start local projects without Git history: initialize a repository on `main` when needed and create one empty initial commit, preserving existing files and staged work. Use a command-local Shardflux identity when Git identity is missing, and report actionable `git_error` failures.
7
+
3
8
  ## 0.14.1
4
9
 
5
10
  - `shard ssh-proxy` connects faster: host trust (host key pin and device key) runs alongside the tunnel start, with no guest byte reaching OpenSSH before the host key is pinned, and a reconnect checks its device key with one read.
6
11
 
7
12
  ## 0.14.0
8
13
 
14
+ - Mirror native conversation transcripts when the guest includes its empty append-lock file.
15
+
9
16
  - Ask for device approval before preparing the VM-backed local worktree; report a failed preparation as the request's result and bound preparation to 30 seconds.
10
17
  - Keep desktop programs running after a command returns with detached `setsid -f` guidance.
11
18
  - Allocate one VM per new ACP conversation, with cached initial model choices and live updates from that same VM.
package/README.md CHANGED
@@ -1153,6 +1153,9 @@ Set `--reachable 30m` when opening a workspace to choose how long waiting agents
1153
1153
  ## Cloud agents (0.13.2+)
1154
1154
 
1155
1155
  Run `shard ./` in your project to start Claude Code or Codex, usable immediately while setup runs in the background.
1156
+
1157
+ New project folders are ready to use: `shard ./` and `shard agents new` initialize Git on `main` when needed and create an empty initial commit (CLI 0.14.2+). Existing files and staged work stay yours; the initial commit contains no files. Repositories without commits keep their current branch. If Git identity is missing, that one commit uses `Shardflux <noreply@shardflux.dev>` without changing your Git configuration.
1158
+
1156
1159
  Your **machine** carries credentials, tools, dotfiles, logins and agent config; the project's **template** carries
1157
1160
  toolchains, dependencies, services and project secrets. Both are private filesystem templates and include secrets
1158
1161
  as ordinary files. Browser logins and Claude/Codex config belong to the machine. Set up tools and secrets normally inside the VM;
@@ -8,6 +8,7 @@ import { AgentStateStore } from "./agent-state.js";
8
8
  import { attachTerminal, cellConnector } from "./agent-terminal.js";
9
9
  import { prepareProjectTemplate } from "./project-template-setup.js";
10
10
  import { projectIdentity, projectLabels } from "./project-identity.js";
11
+ import { ensureProjectGit } from "./project-git.js";
11
12
  import { json } from "./format.js";
12
13
  import { resolveWorkspace } from "./commands.js";
13
14
  import { savedMachine } from "./filesystem-state.js";
@@ -21,6 +22,8 @@ import { rememberLaunch } from "./agent-live.js";
21
22
  import { prepareAgentMachine } from "./agent-machine.js";
22
23
  import { installedTargets } from "./agent-targets.js";
23
24
  import { openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
25
+ import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
26
+ import { conversationTerminal } from "./conversation-terminal.js";
24
27
  const active = (session) => session.state === 'running' || session.state === 'starting';
25
28
  /** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
26
29
  export function disambiguatedSlug(slug, identity) {
@@ -169,6 +172,10 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
169
172
  if (agent !== 'main')
170
173
  agentName(agent);
171
174
  const labels = projectLabels(project, agent, app);
175
+ const local = like ? projectIdentity(path) : project;
176
+ // A cloud-only --like launch must not initialize an unrelated local checkout.
177
+ if (!like?.labels.repo_remote || like.labels.repo_remote === (local.remote ?? `file://${local.folder}`))
178
+ await ensureProjectGit(ctx, local.folder);
172
179
  const cloud = ctx.cloud();
173
180
  let existing = await cloud.workspaces.findByKey(`${project.slug}/${agent}`, { includeDeleted: false, signal: ctx.signal });
174
181
  const owner = agent === 'main' ? existing : like ?? await cloud.workspaces.findByKey(`${project.slug}/main`, { includeDeleted: false, signal: ctx.signal });
@@ -237,18 +244,52 @@ export const AGENT_HANDLERS = {
237
244
  }
238
245
  await syncCredentials(ctx);
239
246
  let ws;
240
- if (agent.includes('/'))
241
- ws = await ctx.cloud().workspace(agent, { create: false }).open();
247
+ let conversation;
248
+ if (/^conv_[a-z0-9]{26}$/.test(agent)) {
249
+ conversation = await resumeConversation(ctx, await getConversation(ctx, agent));
250
+ ws = await conversationWorkspace(ctx, conversation);
251
+ }
242
252
  else {
243
- const project = projectIdentity('./');
244
- if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
245
- throw new UsageError('agent must be a workspace key or an agent name');
246
- ws = await ctx.cloud().workspace(`${project.slug}/${agent}`, { create: false }).open();
253
+ let key = agent;
254
+ if (!agent.includes('/')) {
255
+ const project = projectIdentity('./');
256
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
257
+ throw new UsageError('agent must be a workspace key or an agent name');
258
+ key = `${project.slug}/${agent}`;
259
+ }
260
+ try {
261
+ ws = await ctx.cloud().workspace(key, { create: false }).open();
262
+ }
263
+ catch (error) {
264
+ if (!missing(error))
265
+ throw error;
266
+ // A conversation key differs from its VM key (and survives VM moves).
267
+ let matches;
268
+ try {
269
+ matches = (await listConversations(ctx)).filter(c => c.key === key);
270
+ }
271
+ catch (e) {
272
+ if (conversationsUnavailable(e))
273
+ throw error;
274
+ throw e;
275
+ }
276
+ if (!matches.length)
277
+ throw error;
278
+ if (matches.length !== 1)
279
+ throw new UsageError('Use the conversation id.');
280
+ conversation = await resumeConversation(ctx, matches[0]);
281
+ ws = await conversationWorkspace(ctx, conversation);
282
+ }
283
+ // The menu bar supplies workspace_key from agents list, not the conversation key.
284
+ if (!conversation && ws.labels.shard_conversation) {
285
+ conversation = await resumeConversation(ctx, await getConversation(ctx, ws.labels.shard_conversation));
286
+ ws = await conversationWorkspace(ctx, conversation);
287
+ }
247
288
  }
248
289
  if (values.reachable !== undefined)
249
290
  ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
250
291
  const projectKey = typeof values.project === 'string' ? values.project : ws.labels.repo_remote || ws.labels.shard_project || ws.key;
251
- const app = values.app ?? new AgentStateStore(ctx.store.dir).app(projectKey, ws.labels.app === 'codex' ? 'codex' : 'claude');
292
+ const app = conversation?.harness ?? values.app ?? new AgentStateStore(ctx.store.dir).app(projectKey, ws.labels.app === 'codex' ? 'codex' : 'claude');
252
293
  if (app === 'claude' && !await cloudClaudeLogin(ctx)) {
253
294
  progress(ctx, 'Signing in to cloud Claude (once per account)…');
254
295
  await loginClaudeInWorkspace(ctx, ws);
@@ -258,6 +299,8 @@ export const AGENT_HANDLERS = {
258
299
  if (ws.labels.shard_layer !== 'machine' && !(await savedMachine(ctx))?.template)
259
300
  await syncAgentConfig(ctx, ws);
260
301
  await prepareAgentMachine(ctx, ws);
302
+ if (conversation)
303
+ return conversationTerminal(ctx, ws, conversation, projectKey, values);
261
304
  return terminal(ctx, ws, app, (ws.labels.agent ?? agent).split('/').at(-1), projectKey, values);
262
305
  },
263
306
  };
@@ -16,7 +16,7 @@ export interface GitRepository {
16
16
  }
17
17
  export type GitRepositories = Record<string, GitRepository>;
18
18
  export type GitRun = (args: string[]) => Promise<string>;
19
- export declare function gitRunner(root: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): GitRun;
19
+ export declare function gitRunner(root: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal, remoteHelper?: boolean): GitRun;
20
20
  export declare function agentName(name: string): string;
21
21
  export declare class AgentGit {
22
22
  readonly root: string;
package/dist/agent-git.js CHANGED
@@ -2,18 +2,25 @@ import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
- import { UsageError } from "./errors.js";
5
+ import { GitError, UsageError } from "./errors.js";
6
6
  import { AgentStateStore } from "./agent-state.js";
7
7
  import { gitPath } from "./git-remote-helper.js";
8
8
  const execute = promisify(execFile);
9
9
  const DAY = 86_400_000;
10
- export function gitRunner(root, env = process.env, signal) {
10
+ export function gitRunner(root, env = process.env, signal, remoteHelper = true) {
11
11
  return async (args) => {
12
- const result = await execute('git', ['--no-pager', '-C', root, ...args], {
13
- env: { ...env, PATH: gitPath(env), GIT_TERMINAL_PROMPT: '0', GIT_MERGE_AUTOEDIT: 'no' },
14
- timeout: 120_000, maxBuffer: 4 * 1024 * 1024, signal,
15
- });
16
- return result.stdout.trim();
12
+ try {
13
+ const result = await execute('git', ['--no-pager', '-C', root, ...args], {
14
+ env: { ...env, PATH: remoteHelper ? gitPath(env) : env.PATH, GIT_TERMINAL_PROMPT: '0', GIT_MERGE_AUTOEDIT: 'no' },
15
+ timeout: 120_000, maxBuffer: 4 * 1024 * 1024, signal,
16
+ });
17
+ return result.stdout.trim();
18
+ }
19
+ catch (error) {
20
+ if (signal?.aborted)
21
+ throw error;
22
+ throw new GitError(root, error);
23
+ }
17
24
  };
18
25
  }
19
26
  export function agentName(name) {
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
3
3
  import { homedir, tmpdir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
- import { CodedError } from "./errors.js";
5
+ import { CodedError, UsageError } from "./errors.js";
6
6
  import { AgentStateStore } from "./agent-state.js";
7
7
  import { projectIdentity } from "./project-identity.js";
8
8
  import { localManifest, projectGit } from "./project-sync-manifest.js";
@@ -135,7 +135,7 @@ async function initializeProjectGit(cell, folder, root, signal, populate = false
135
135
  return;
136
136
  }
137
137
  if (top !== folder)
138
- throw new Error('project sync requires the Git repository root; run shard there');
138
+ throw new UsageError('project sync requires the Git repository root; run shard there');
139
139
  const existing = await cell.exec.run(['git', '-C', root, 'rev-parse', '--verify', 'HEAD'], { timeoutMs: 5000, signal });
140
140
  if (existing.exitCode !== 0 && existing.exitCode !== 128)
141
141
  throw new Error('could not check workspace Git history');
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.14.1";
1
+ export declare const CLI_VERSION = "0.14.2";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
package/dist/args.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
- export const CLI_VERSION = '0.14.1';
15
+ export const CLI_VERSION = '0.14.2';
16
16
  export const GLOBAL_OPTIONS = {
17
17
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
18
18
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -62,6 +62,10 @@ export async function mirrorConversation(ctx, record, source) {
62
62
  throw new Error('Project transcript copy includes files outside the mirror bound.');
63
63
  let count = 0;
64
64
  for (const entry of manifest.entries) {
65
+ // Flush holds this empty regular file while appending the transcript.
66
+ // It is synchronization metadata, not native history to mirror.
67
+ if (entry.path === '.lock' && !entry.symlink && entry.size === 0 && entry.sha256 === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
68
+ continue;
65
69
  if (entry.symlink || entry.path.includes('/') || !/^[a-zA-Z0-9_.-]+\.(jsonl|json)$/.test(entry.path))
66
70
  throw new Error('Project transcript copy has an unsafe entry.');
67
71
  const file = join(dir, entry.path), expected = existsSync(file) ? createHash('sha256').update(readFileSync(file)).digest('hex') : '';
@@ -1,3 +1,8 @@
1
- import type { Handler } from './commands.js';
1
+ import type { Workspace } from '@shardflux/sdk';
2
+ import type { Ctx, Handler } from './commands.js';
3
+ import type { Values } from './args.js';
4
+ import type { Conversation } from './conversations.js';
5
+ /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
6
+ export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values): Promise<number>;
2
7
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
3
8
  export declare const launchConversationProject: Handler;
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { AgentStateStore } from "./agent-state.js";
3
3
  import { projectIdentity, projectLabels } from "./project-identity.js";
4
- import { createConversation, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
4
+ import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
5
5
  import { terminal } from "./agent-commands.js";
6
6
  import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
7
7
  import { syncAgentCredentials } from "./agent-credentials.js";
@@ -13,6 +13,11 @@ import { ensureMenubar, MenubarManager } from "./menubar.js";
13
13
  import { UsageError } from "./errors.js";
14
14
  import { localResumeTerminal } from "./conversation-local-resume.js";
15
15
  import { agentReachability } from "./agent-reachability.js";
16
+ /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
17
+ export async function conversationTerminal(ctx, ws, c, project, values) {
18
+ await restoreConversationBinding(ctx, c, ws);
19
+ return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id });
20
+ }
16
21
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
17
22
  export const launchConversationProject = async (ctx, values, [path = './', explicit]) => {
18
23
  const project = projectIdentity(path), store = new AgentStateStore(ctx.store.dir);
@@ -35,8 +40,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
35
40
  const next = await resumeConversation(ctx, c);
36
41
  rememberConversation(ctx, next, project.folder);
37
42
  const ws = await conversationWorkspace(ctx, next);
38
- await restoreConversationBinding(ctx, next, ws);
39
- return terminal(ctx, ws, app, next.key.split('/').at(-1), project.remote ?? project.folder, values, undefined, next.worktree_path, { id: next.id, session: next.harness_session_id });
43
+ return conversationTerminal(ctx, ws, next, project.remote ?? project.folder, values);
40
44
  }
41
45
  await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
42
46
  const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
@@ -70,5 +74,5 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
70
74
  if (!(await savedMachine(ctx))?.template)
71
75
  await syncAgentConfig(ctx, ws);
72
76
  await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
73
- return terminal(ctx, ws, app, c.key.split('/').at(-1), project.remote ?? project.folder, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id });
77
+ return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values);
74
78
  };
@@ -3,6 +3,7 @@ import { ShardfluxApiError } from '@shardflux/sdk';
3
3
  import { AgentStateStore } from "./agent-state.js";
4
4
  import { gitRunner } from "./agent-git.js";
5
5
  import { projectIdentity } from "./project-identity.js";
6
+ import { ensureProjectGit } from "./project-git.js";
6
7
  import { seedAgentRepository } from "./agent-repository.js";
7
8
  import { UsageError } from "./errors.js";
8
9
  import { conversationClient } from "./conversation-commands.js";
@@ -87,7 +88,8 @@ export async function restoreConversationBinding(ctx, c, ws) {
87
88
  }
88
89
  export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID()) {
89
90
  const project = projectIdentity(folder);
90
- // Detect the optional control-plane feature before applying the new Git contract to old APIs.
91
+ await ensureProjectGit(ctx, project.folder);
92
+ // Both the conversation API and its legacy workspace fallback need a usable local HEAD.
91
93
  await conversationClient(ctx).conversations.list({ limit: 1 });
92
94
  const git = gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal);
93
95
  const commit = await git(['rev-parse', 'HEAD']);
package/dist/errors.d.ts CHANGED
@@ -28,6 +28,13 @@ declare class HintedError extends Error {
28
28
  export declare class UsageError extends HintedError {
29
29
  name: string;
30
30
  }
31
+ /** Keep Git exit statuses for optional probes while reporting actionable CLI errors. */
32
+ export declare class GitError extends HintedError {
33
+ name: string;
34
+ readonly code: string | number | undefined;
35
+ readonly stderr: string;
36
+ constructor(folder: string, cause: unknown);
37
+ }
31
38
  /**
32
39
  * A refusal `shard` detects itself, with a stable code, its facts in `details` and the next step as `hint` (exit 1),
33
40
  * e.g. `repository_too_large` (0.13.3+) for Git history over the initial sync limit.
package/dist/errors.js CHANGED
@@ -36,6 +36,22 @@ class HintedError extends Error {
36
36
  export class UsageError extends HintedError {
37
37
  name = 'UsageError';
38
38
  }
39
+ /** Keep Git exit statuses for optional probes while reporting actionable CLI errors. */
40
+ export class GitError extends HintedError {
41
+ name = 'GitError';
42
+ code;
43
+ stderr;
44
+ constructor(folder, cause) {
45
+ const failure = cause;
46
+ const detail = failure.stderr?.trim() || failure.message || 'Git command failed';
47
+ super(`Git failed in ${folder}: ${detail}\n${failure.code === 'ENOENT' ? 'Install Git and make sure git is on PATH, then run shard again.' : 'Resolve this Git error, then run shard again.'}`, failure.code === 'ENOENT'
48
+ ? 'Install Git and make sure git is on PATH, then run shard again.'
49
+ : `Check Git permissions, configuration and repository state with git -C ${JSON.stringify(folder)} status, resolve the error above, then run shard again.`);
50
+ this.code = failure.code;
51
+ this.stderr = failure.stderr ?? '';
52
+ this.cause = cause;
53
+ }
54
+ }
39
55
  /**
40
56
  * A refusal `shard` detects itself, with a stable code, its facts in `details` and the next step as `hint` (exit 1),
41
57
  * e.g. `repository_too_large` (0.13.3+) for Git history over the initial sync limit.
@@ -199,6 +215,8 @@ export function describeError(err) {
199
215
  return { code: 'timeout', message: err.message, last_state: err.session.state, details: { session_id: err.session.session_id } };
200
216
  if (err instanceof UsageError)
201
217
  return { code: 'usage', message: err.message };
218
+ if (err instanceof GitError)
219
+ return { code: 'git_error', message: err.message, retryable: false };
202
220
  if (err instanceof CodedError)
203
221
  return { code: err.code, message: err.message, retryable: false, ...(err.details ? { details: err.details } : {}) };
204
222
  if (err instanceof CheckoutTimeoutError) {
@@ -0,0 +1,3 @@
1
+ import type { Ctx } from './commands.js';
2
+ /** Prepare only the selected project root. Never stage files or initialize a nested repository. */
3
+ export declare function ensureProjectGit(ctx: Ctx, folder: string): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { gitRunner } from "./agent-git.js";
4
+ import { GitError } from "./errors.js";
5
+ /** Prepare only the selected project root. Never stage files or initialize a nested repository. */
6
+ export async function ensureProjectGit(ctx, folder) {
7
+ const git = gitRunner(folder, { ...process.env, ...ctx.io.env, LC_ALL: 'C' }, ctx.signal, false);
8
+ let initialized = false;
9
+ try {
10
+ const top = await git(['rev-parse', '--show-toplevel']);
11
+ if (realpathSync(top) !== realpathSync(folder))
12
+ return;
13
+ }
14
+ catch (error) {
15
+ if (!(error instanceof GitError) || error.code !== 128 || !error.stderr.includes('not a git repository'))
16
+ throw error;
17
+ if (existsSync(join(folder, '.git')))
18
+ throw error;
19
+ await git(['init', '-b', 'main']);
20
+ initialized = true;
21
+ }
22
+ try {
23
+ await git(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
24
+ return;
25
+ }
26
+ catch (error) {
27
+ if (!(error instanceof GitError) || error.code !== 1)
28
+ throw error;
29
+ // An absent branch is unborn; a broken existing ref is a Git failure, not permission to replace it.
30
+ const branch = await git(['symbolic-ref', 'HEAD']);
31
+ const absent = await git(['show-ref', '--verify', '--quiet', branch]).then(() => false, (refError) => {
32
+ if (!(refError instanceof GitError) || refError.code !== 1)
33
+ throw refError;
34
+ return true;
35
+ });
36
+ if (!absent)
37
+ throw error;
38
+ }
39
+ // --only with no paths makes an empty commit even when the user already has staged files.
40
+ const commit = ['-c', 'core.hooksPath=/dev/null', 'commit', '--allow-empty', '--only', '-m', 'Initial commit'];
41
+ try {
42
+ await git(commit);
43
+ }
44
+ catch (error) {
45
+ if (!(error instanceof GitError) || !/Author identity unknown|Committer identity unknown|unable to auto-detect email address|empty ident name|no (?:name|email) was given/.test(error.stderr))
46
+ throw error;
47
+ await git(['-c', 'user.name=Shardflux', '-c', 'user.email=noreply@shardflux.dev', ...commit]);
48
+ }
49
+ ctx.io.stderr.write(initialized
50
+ ? `Initialized a git repository in ${folder} (shard tracks your project with git).\n`
51
+ : `Created an initial empty commit in ${folder}.\n`);
52
+ }
@@ -3,6 +3,7 @@ import { execFile } from 'node:child_process';
3
3
  import { constants } from 'node:fs';
4
4
  import { lstat, open, readdir, readlink, realpath } from 'node:fs/promises';
5
5
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
6
+ import { GitError } from "./errors.js";
6
7
  export const MAX_SYNC_FILE_BYTES = 64 * 1024 * 1024;
7
8
  export const MAX_SYNC_ENTRIES = 20_000;
8
9
  export function validSyncPath(path) {
@@ -46,13 +47,19 @@ export function parseManifest(manifest) {
46
47
  }
47
48
  export async function projectGit(folder, args, signal, input = '') {
48
49
  return new Promise((resolve, reject) => {
49
- const child = execFile('git', ['-C', folder, '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false', '-c', 'core.hooksPath=/dev/null', ...args], { encoding: 'utf8', timeout: 30_000, maxBuffer: 4 << 20, signal, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0' } }, (error, stdout) => {
50
- if (error) {
51
- const failure = Object.assign(new Error(error.message, { cause: error }), { code: error.code, stdout });
52
- reject(failure);
53
- }
54
- else
50
+ const child = execFile('git', ['-C', folder, '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false', '-c', 'core.hooksPath=/dev/null', ...args], { encoding: 'utf8', timeout: 30_000, maxBuffer: 4 << 20, signal, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0' } }, (error, stdout, stderr) => {
51
+ if (!error) {
55
52
  resolve(stdout);
53
+ return;
54
+ }
55
+ if (signal?.aborted) {
56
+ const abortError = error;
57
+ reject(abortError);
58
+ return;
59
+ }
60
+ const failure = new GitError(folder, Object.assign(error, { stderr }));
61
+ Object.assign(failure, { stdout });
62
+ reject(failure);
56
63
  });
57
64
  child.stdin?.on('error', () => undefined); // Early Git refusal is reported by the callback.
58
65
  child.stdin?.end(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
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",