Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4b5305f
test(integration): cli-send — UXF transfer command surface pin
May 15, 2026
ee41241
test+fix(invoice): #156 — sphere-cli invoice command surface coverage
May 15, 2026
b92ec09
test(integration): #156 — sphere-cli nametag command surface coverage
May 15, 2026
42ed2bb
test(integration): #156 — sphere-cli l1-balance command surface coverage
May 15, 2026
e0004d1
test(integration): #156 — sphere-cli faucet/topup command surface cov…
May 15, 2026
9c3055a
test(integration): #156 — multiaddress CLI + cross-address isolation
May 15, 2026
ec7fadf
test(integration): #156 — wallet profile CLI + cross-profile isolation
May 15, 2026
cedc71c
test(integration): #156 — wallet state commands (history/sync/receive…
May 15, 2026
d660c00
test(integration): #156 — assets / asset-info token registry surface
May 15, 2026
cb39efe
test(integration): #156 — wallet lifecycle: clear, config, init mnemo…
May 15, 2026
55ae1ea
Merge PR #13 — #156 comprehensive CLI integration coverage
vrogojin May 15, 2026
744afa7
test(integration): #156 — swap CLI offline tier (help + arg validation)
May 15, 2026
8dfe8b4
test(integration): #156 — swap CLI e2e (Docker escrow + testnet relay…
May 15, 2026
379a8d8
test(integration): #156 — crypto/util CLI coverage expansion
May 15, 2026
78d593a
test(integration): #156 — init --nametag combined flow
May 15, 2026
e377376
test(integration): #156 — group + market CLI offline tiers
May 15, 2026
04c3e2d
fix(tests): #156 — review follow-ups on PR #14
May 15, 2026
d5e1504
Merge PR #14 — #156 swap (Docker e2e) + crypto/util + init-nametag + …
vrogojin May 15, 2026
2b810c9
chore(tests): switch escrow image default to published v0.2
May 15, 2026
dce4163
Merge PR #15 — chore: default escrow image to published v0.2
vrogojin May 15, 2026
375854e
test(swap): #163 — fix flaky full-settlement e2e via parallel deposits
May 20, 2026
b6f6e14
test(swap): amend #163 — investigation surfaces escrow CAS-mismatch a…
May 20, 2026
e976333
chore(test): default escrow image to v0.3 (issue #195 fix landed)
May 20, 2026
4781eac
Merge pull request #16 from unicity-sphere/fix/issue-163-swap-full-se…
vrogojin May 20, 2026
0c3b998
fix(invoice): accept @nametag (and chain pubkey / alpha1) as --target
May 22, 2026
89166a1
ci: bump pinned sphere-sdk SHA — refactor branch deleted; pin to inte…
May 22, 2026
0d9e5e6
feat(invoice)(#226): add `sphere invoice deliver` subcommand
May 22, 2026
3d64d73
Merge fix/invoice-create-nametag-resolution into feat/invoice-deliver…
May 23, 2026
cde6ac7
Merge integration/all-fixes into feat/invoice-deliver-226
May 23, 2026
bf40221
fix(invoice)(#226): explicit return after process.exit in invoice-del…
May 23, 2026
b36ab25
Merge pull request #18 from unicity-sphere/feat/invoice-deliver-226
May 23, 2026
d4e79a9
fix(daemon)(#19): keep --detach child alive past process.disconnect()
May 23, 2026
971ef31
test(integration)(#19): pin daemon detach lifecycle
May 23, 2026
96c23ed
fix(daemon)(#19): swallow disconnect throw from parent-side race
May 23, 2026
ddc8a67
Merge pull request #20 from unicity-sphere/fix/daemon-detach-immediat…
vrogojin May 23, 2026
4541399
fix(cli)(#21): throw ExitSignal from process.exit wrapper so handlers…
May 23, 2026
b0e104c
docs(cli)(#21): refresh stale comment in invoice-deliver fall-through…
May 23, 2026
c626879
Merge pull request #22 from unicity-sphere/fix/invoice-status-crash-21
vrogojin May 23, 2026
032a2b2
feat(cli)(#23): bootstrap Profile providers; prompt on legacy wallets
May 23, 2026
3c68384
chore(cli)(#23): drop unused buildLegacyOnlyProviders export
May 23, 2026
fe9038e
Merge pull request #25 from unicity-sphere/feat/issue-23-profile-prov…
vrogojin May 23, 2026
8880893
fix(cli)(#24): use 'full' sync mode for invoice-status and invoice-list
May 23, 2026
05d1157
Merge pull request #26 from unicity-sphere/fix/issue-24-invoice-statu…
vrogojin May 23, 2026
5d95c50
fix(cli)(#23): detectWalletKind ignores empty `{}` wallet.json placeh…
May 23, 2026
ead69f1
Merge pull request #27 from unicity-sphere/fix/detect-wallet-kind-emp…
vrogojin May 23, 2026
aec12f1
fix(cli)(sphere-sdk#247): refuse CLI when a daemon holds the OrbitDB …
May 24, 2026
e73ddb7
Merge pull request #28 from unicity-sphere/fix/issue-247-daemon-gate
May 24, 2026
60f69d0
fix(cli)(sphere-sdk#282): route `wallet use` confirmation to STDERR
May 26, 2026
1278714
Merge pull request #29 from unicity-sphere/fix/issue-282-wallet-use-s…
vrogojin May 26, 2026
196e9a8
feat(cli)(sphere-sdk#394): wire publishToIpfs + cidFetchGateways in b…
vrogojin Jun 4, 2026
b64f85c
ci: bump pinned sphere-sdk SHA to current main tip
Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,30 @@ jobs:
- name: Clone sphere-sdk sibling
# Pin to a specific commit SHA (not a branch name) for supply-chain
# integrity — a branch pointer can be force-pushed or rebased,
# silently changing the code CI builds against. This SHA points at
# the tip of `refactor/extract-cli-to-sphere-cli` at the time of
# this commit; that branch contains `bc07e89 feat(cli-extraction)`
# which promoted CLI-consumed types (CreateInvoiceRequest,
# PayInvoiceParams, encrypt/decrypt helpers, etc.) to the public
# module surface. Those exports have not yet landed on `main` or
# in any published npm version.
# silently changing the code CI builds against.
#
# This SHA is the tip of sphere-sdk `main` ("merge: PR #395 #394
# automated CID delivery re-enabled + 512 KiB inline cap + demo
# playbook"). That commit exports the symbols `src/shared/sphere-
# providers.ts` consumes from `@unicitylabs/sphere-sdk/impl/nodejs`:
# `createUxfCarPublisher`, `DEFAULT_IPFS_GATEWAYS`,
# `PublishToIpfsCallback`. It also exposes `AccountingModule.
# deliverInvoice`, which `invoice-deliver` (PR #18 / issue #226)
# calls. Pinning to `main` (rather than the previous integration-
# branch tip 02cb4550) avoids the "unable to read tree" failure
# when an integration tip is rebased away.
#
# Bump this SHA when a new sphere-sdk commit is required; remove
# this whole workaround once sphere-sdk publishes v0.7.1+ to npm
# with the post-extraction exports.
env:
SPHERE_SDK_SHA: 86468103ac25271b96a338f64349dd0eb472689f
SPHERE_SDK_SHA: 3f3dadf9d03eb29db87f062921751f24bfefdec8
run: |
git clone https://github.com/unicity-sphere/sphere-sdk.git ../../sphere-sdk
# The pinned SHA may not be on a branch tip after future merges;
# fetch it explicitly before checkout so the workflow keeps
# working when sphere-sdk main advances past this commit.
git -C ../../sphere-sdk fetch origin "$SPHERE_SDK_SHA" || true
git -C ../../sphere-sdk checkout --detach "$SPHERE_SDK_SHA"

- name: "Build sphere-sdk (required for file: dependency to resolve types)"
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/integration-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ jobs:
cache: npm

# See ci.yml for the rationale behind the sibling-clone workaround.
# Kept identical here so a nightly run is hermetic w.r.t. ci.yml state.
# Kept identical here (same SHA pin) so a nightly run is hermetic
# w.r.t. ci.yml state — bump both together when needed.
- name: Clone sphere-sdk sibling
env:
SPHERE_SDK_SHA: 02cb4550facae0bea58c3b04aceaf3059599464b
run: |
git clone --depth 1 --branch refactor/extract-cli-to-sphere-cli \
https://github.com/unicity-sphere/sphere-sdk.git ../../sphere-sdk
git clone https://github.com/unicity-sphere/sphere-sdk.git ../../sphere-sdk
git -C ../../sphere-sdk fetch origin "$SPHERE_SDK_SHA" || true
git -C ../../sphere-sdk checkout --detach "$SPHERE_SDK_SHA"

- name: Build sphere-sdk (required for file: dependency to resolve types)
run: |
Expand Down
40 changes: 32 additions & 8 deletions src/host/sphere-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@

import * as fs from 'node:fs';
import { Sphere } from '@unicitylabs/sphere-sdk';
import { createNodeProviders } from '@unicitylabs/sphere-sdk/impl/nodejs';
import type { NetworkType } from '@unicitylabs/sphere-sdk';
import {
buildSphereProviders,
detectWalletKind,
} from '../shared/sphere-providers.js';

// All paths are CWD-relative by design — matches legacy-cli behaviour so the
// same wallet is visible whether invoked via `sphere wallet …` (legacy) or
Expand Down Expand Up @@ -48,9 +51,25 @@ function loadConfig(): CliConfig {
export async function initSphere(): Promise<Sphere> {
const config = loadConfig();

const providers = createNodeProviders({
network: config.network,
dataDir: config.dataDir,
// Issue #23 — same gate as the legacy CLI bootstrap. Host commands
// cannot operate against a pre-migration wallet because the new
// Profile-backed token storage would start empty and silently miss
// every token the user has on the legacy IPNS-pointer path. Surface
// the migration step explicitly instead of silently mis-routing.
const kind = detectWalletKind(config.dataDir);
if (kind === 'legacy') {
throw new Error(
`Legacy wallet detected at ${config.dataDir} (file storage + IPNS sync).\n` +
'`sphere host` requires the new Profile storage. Migrate via:\n' +
' sphere wallet migrate # dry-run summary first\n' +
' sphere wallet migrate --apply # commit the import\n' +
'See GitHub sphere-cli#23 for context.',
);
}

const providers = buildSphereProviders({
network: config.network,
dataDir: config.dataDir,
tokensDir: config.tokensDir,
});

Expand All @@ -62,11 +81,16 @@ export async function initSphere(): Promise<Sphere> {
}

const { sphere } = await Sphere.init({
storage: providers.storage,
transport: providers.transport,
oracle: providers.oracle,
network: config.network,
storage: providers.storage,
tokenStorage: providers.tokenStorage,
transport: providers.transport,
oracle: providers.oracle,
network: config.network,
autoGenerate: false,
// sphere-sdk #394 — pass through the UXF CID-delivery wiring so
// sends of > RELAY_SAFE_CAP_BYTES bundles can promote to CID.
...(providers.publishToIpfs ? { publishToIpfs: providers.publishToIpfs } : {}),
...(providers.cidFetchGateways ? { cidFetchGateways: providers.cidFetchGateways } : {}),
});

return sphere;
Expand Down
11 changes: 11 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ describe('buildLegacyArgv dispatcher', () => {
expect(buildLegacyArgv('wallet', ['create', 'foo', '--network', 'testnet']))
.toEqual(['wallet', 'create', 'foo', '--network', 'testnet']);
});
// Issue #23 — `sphere wallet migrate` is the user-facing entry
// point for moving a legacy IPNS-pointer wallet into the new
// Profile storage. Lock the dispatch shape so a future namespace
// refactor doesn't silently route it elsewhere.
it('`wallet migrate` falls through to legacy `wallet migrate`', () => {
expect(buildLegacyArgv('wallet', ['migrate'])).toEqual(['wallet', 'migrate']);
});
it('`wallet migrate --apply` preserves the apply flag', () => {
expect(buildLegacyArgv('wallet', ['migrate', '--apply']))
.toEqual(['wallet', 'migrate', '--apply']);
});
it('bare `wallet` with no subcommand produces `[wallet]`', () => {
expect(buildLegacyArgv('wallet', [])).toEqual(['wallet']);
});
Expand Down
75 changes: 61 additions & 14 deletions src/legacy/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
* Parse a PID file. Handles both the new JSON format and the legacy plain-text
* format (just a number). Returns null on parse failure or missing file.
*/
function readPidFile(pidFile: string): PidFileData | null {
export function readPidFile(pidFile: string): PidFileData | null {
let raw: string;
try {
raw = fs.readFileSync(pidFile, 'utf8').trim();
Expand Down Expand Up @@ -81,7 +81,7 @@
* Returns false for dead PIDs and for PIDs that are alive but clearly not ours
* (i.e. PID reuse case).
*/
function isDaemonProcessAlive(pid: number): boolean {
export function isDaemonProcessAlive(pid: number): boolean {
if (!isProcessAlive(pid)) return false;
// Best-effort PID reuse detection via /proc/<pid>/comm (Linux only).
try {
Expand Down Expand Up @@ -442,9 +442,13 @@
function log(message: string): void {
const line = message.startsWith('[') ? message : `[${new Date().toISOString()}] ${message}`;
if (logStream) {
// In forked mode the WriteStream IS the log destination — avoid
// double-writing via the (now-redirected) console.log which also
// forwards to the same stream.
logStream.write(line + '\n');
} else {
console.log(line);

Check warning on line 450 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 450 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
}
console.log(line);
}

// =============================================================================
Expand Down Expand Up @@ -541,10 +545,10 @@
const stream = fs.createWriteStream(config.logFile, { flags: 'a' });
logStream = stream;
// Redirect console output to log file
const origLog = console.log;

Check warning on line 548 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 548 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
const origErr = console.error;
const origWarn = console.warn;
console.log = (...a: unknown[]) => { stream.write(a.map(String).join(' ') + '\n'); };

Check warning on line 551 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 551 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
console.error = (...a: unknown[]) => { stream.write('[ERROR] ' + a.map(String).join(' ') + '\n'); };
console.warn = (...a: unknown[]) => { stream.write('[WARN] ' + a.map(String).join(' ') + '\n'); };

Expand All @@ -569,12 +573,27 @@
throw e;
}

// Disconnect from parent
if (process.disconnect) process.disconnect();
// Disconnect from parent's IPC channel if one exists. The parent's
// detachDaemon call passes 'ipc' in stdio (Fix issue #19) so the channel
// is normally open here; the `process.connected` guard handles the
// edge case of running with a non-IPC stdio (test harnesses, manual
// invocation of `daemon start --_forked`). Calling `process.disconnect()`
// without a live channel throws "IPC channel is not open", which under
// `stdio: 'ignore'` would crash the child silently with no log trail.
//
// The try/catch handles a residual race: the parent's child.disconnect()
// closes the channel at the OS layer, but the JS 'disconnect' event
// (which flips process.connected to false) is async — there's a microtask
// window where process.connected reads true while the underlying channel
// is already torn down, in which case disconnect() throws. Swallowing
// here is correct: the goal state (channel closed) already holds.
if (process.connected && process.disconnect) {
try { process.disconnect(); } catch { /* already torn down by parent */ }
}

// Restore on exit for cleanup logging
process.on('exit', () => {
console.log = origLog;

Check warning on line 596 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 596 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
console.error = origErr;
console.warn = origWarn;
});
Expand Down Expand Up @@ -697,14 +716,21 @@
// =============================================================================

function detachDaemon(args: string[], flags: DaemonFlags): void {
// Build the resolved config just to get the PID file path
// Resolve config once so we have BOTH the PID file path (for the advisory
// already-running check below) AND the log file path (so we can open it in
// the parent and inherit the fd into the child — see fork() call below).
let pidFile: string;
let logFile: string;
try {
const rawConfig = buildConfigFromFlags(flags);
const config = resolveConfig(rawConfig);
pidFile = config.pidFile;
logFile = config.logFile;
} catch {
pidFile = getDefaultPidFile();
// Match the default emitted by the success message below so the operator
// sees a consistent path. resolveConfig() would normally produce this.
logFile = '.sphere-cli/daemon.log';
}

// Check if already running. Note: this check is advisory only — the forked
Expand All @@ -723,22 +749,43 @@
// Build child args: replace --detach with --_forked, keep everything else
const childArgs = ['daemon', 'start', '--_forked', ...args.filter(a => a !== '--detach')];

// Fork the child process
// Fix issue #19: Open the log file in the parent and pass its fd as the
// child's stdout and stderr. The previous `stdio: 'ignore'` discarded both
// streams, so any failure between fork() and the child's first WriteStream
// flush — including the silent crash from `process.disconnect()` throwing
// on a missing IPC channel — was invisible: no log, no PID-file cleanup,
// just a stale pid. With the fd inherited at OS level, any thrown error,
// uncaught exception, or stderr emission lands in the log file before any
// Node-level streaming machinery is required.
//
// The 'ipc' entry is required by child_process.fork() (Node throws
// "Forked processes must have an IPC channel" without it). The parent
// does not send messages over the channel — it exists solely to satisfy
// fork's contract. The child's runDaemon() calls process.disconnect()
// (guarded on process.connected) to release the channel from the child's
// event loop after PID-file write.
ensureDir(logFile);
const logFd = fs.openSync(logFile, 'a');
fs.writeSync(
logFd,
`[${new Date().toISOString()}] sphere daemon: forking child (parent PID ${process.pid})\n`,
);
const child = fork(process.argv[1], childArgs, {
detached: true,
stdio: 'ignore',
stdio: ['ignore', logFd, logFd, 'ipc'],
});
// Child has inherited its own copy of the fd; close the parent's reference.
fs.closeSync(logFd);

// Don't keep the parent alive waiting for the child. We also disconnect
// the parent's end of the IPC channel so process.exit(0) below doesn't
// need to forcibly tear down a live handle.
child.unref();
if (child.connected) child.disconnect();

console.log(`Daemon started in background (PID ${child.pid})`);

Check warning on line 786 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 786 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
console.log(`PID file: ${pidFile}`);

Check warning on line 787 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 787 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn

if (flags.logFile) {
console.log(`Log file: ${flags.logFile}`);
} else {
console.log('Log file: .sphere-cli/daemon.log');
}
console.log(`Log file: ${logFile}`);

Check warning on line 788 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 788 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn

process.exit(0);
}
Expand All @@ -753,7 +800,7 @@

const pidData = readPidFile(pidFile);
if (!pidData) {
console.log('No daemon running (PID file not found).');

Check warning on line 803 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 803 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
return;
}
const pid = pidData.pid;
Expand All @@ -762,12 +809,12 @@
// appears to be a different (non-node) process, treat as stale so we don't
// SIGTERM someone else's editor/shell.
if (!isDaemonProcessAlive(pid)) {
console.log(`Stale PID file (process ${pid} not running or reused). Cleaning up.`);

Check warning on line 812 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 812 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn
safeUnlink(pidFile);
return;
}

console.log(`Stopping daemon (PID ${pid})...`);

Check warning on line 817 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (22.x)

Unexpected console statement. Only these console methods are allowed: error, warn

Check warning on line 817 in src/legacy/daemon.ts

View workflow job for this annotation

GitHub Actions / lint + typecheck + test (20.x)

Unexpected console statement. Only these console methods are allowed: error, warn

// Fix D-6: SIGTERM may race against process exit (ESRCH). Treat as already-dead.
try {
Expand Down
Loading
Loading