Skip to content

Expose app dev session files for agents - #8926

Merged
isaacroldan merged 22 commits into
mainfrom
make-dev-agentic
Oct 22, 2026
Merged

isaacroldan merged 22 commits into
mainfrom
make-dev-agentic

Conversation

@isaacroldan

@isaacroldan isaacroldan commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Agents need to find an existing app dev session and read its current status and output from another terminal. This PR builds on the reusable CLI Kit API in #8952.

WHAT is this pull request doing?

Register one session per app configuration, capture process output in JSONL, and save the CLI's current preview status and errors. Non-interactive sessions remain registered until they stop, and dev work stops if session ownership or output fails. Active dev sessions handle signal cancellation and drain their logs before exit. Function compilation uses the supplied streams so Javy errors appear in both terminal output and the session log.

The withDevSession service maps app metadata to the shared withDevInstance helper in #8952. The command passes app work to that service; there is no session class or command-owned acquisition logic. The app integration uses the shared tee API from #8952. The session files include the PID, session ID, URLs, log path, and details.preview. Preview status reflects the CLI's reported state. The agent workflow and integration tests are included here; file ownership, logging, and their unit tests are in #8952.

Windows shutdown also preserves the CLI process while stopping its children. A one-condition patch to when-exit prevents signal redelivery from terminating the CLI before its queued logs drain.

How to manually test your changes?

  1. Start shopify app dev for a configured app and development store. From another terminal, read .shopify/dev/*.json and the reported logFile.
  2. Start dev again with the same configuration and confirm it reports the existing session.
  3. Introduce an invalid extension import. Check details.preview.state and errors, then correct the import and check the new output and status.
  4. Stop dev and restart with --no-input. Confirm the session ID changes, the log starts fresh, and the session remains registered while dev runs. Press Ctrl+C once and confirm that the process exits and the instance record is removed. On Unix, repeat with SIGTERM.

See docs/cli/dev-instance-agent-workflow.md for the file-reading workflow.

Validation

  • Focused shutdown checks pass on native Windows and Linux with Node 26.12.1: Windows run, Linux run. The 15 Windows and 19 Linux tests cover bootstrap fallback, all 1,000 queued messages plus the shutdown message, instance/lock cleanup, real child-process termination, and normal exit after the postrun hook.
  • Windows tests emit the installed signal events; Unix tests also deliver real OS signals. These are subprocess checks, not an interactive Windows console or live-store test.
  • The final stack passes 164 focused tests locally on macOS with Node 26.1.0. The app type check, changed-file lint, workspace builds, and CLI bundle pass. The two new regressions fail before their fixes: Windows child cleanup ignores killRoot: false, and schema output drops named definitions.
  • The clean-checkout source loader and test TypeScript configuration retain normal type checking in subprocesses. No generated workspace modules are required by the CI tests.
  • The existing compiler regression runs a real failing compiler fixture through the captured streams so Javy diagnostics reach both terminal output and session JSONL.

The larger stress probes below were performed earlier on this implementation; they are separate from the latest focused checks.

Stress testing

The larger local session-API checks used real temporary files and separate child processes:

  • Non-interactive lifetime: let a watcher action finish setup while keeping its service alive. The session remained discoverable until cancellation; a second start reported the existing session.
  • Shutdown with pending work: queued 30,000 complete log lines, a final partial UTF-8 line, and 500 status updates, then sent SIGINT or SIGTERM. Both runs retained all 30,001 records, exited successfully, and removed the status file and lock.
  • Terminal/log parity: sent 20,000 alternating extension-source messages through a delayed output stream, then exercised the real DevSessionLogger and extension WebSocket log handler. All 20,003 records had the expected message and source. Messages without a trailing newline appeared separately; byte output still assembled split UTF-8 into complete lines.
  • Slow reader: blocked a child process's stdout pipe. A producer respecting backpressure paused, then delivered all 20,000 records when reading resumed.
  • Status history: integration tests checked the initial status and ready/updating/error transitions. URL-only changes did not repeat status messages, and the latest status/errors remained available in the snapshot.
  • Actual local executable: rebuilt the bundle, checked pnpm shopify app dev --help, and probed the generated chunk loaded by app dev. Complete messages stayed separate, split UTF-8 was preserved, and JSONL records omitted session IDs.

The ownership races, SIGKILL recovery, 33-second pause/resume, 3,000-update pressure test, 100,001-record burst, and filesystem-error matrix are detailed in the base PR, #8952. Regression cases are covered by committed tests; the larger stress loops were local, ad hoc scripts and are not part of CI.

These checks ran on macOS arm64 with Node v26.1.0. The stress harnesses did not exercise Windows/Linux or remote Shopify update behavior.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • Added a minor changeset for the app-dev behavior

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Oct 15, 2026
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm.

Built from 710d4b4673dfebeba3ab9e6e08046905197986fc. Workflow run.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261015153801

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm.

Built from 2b3b7ddf9a2ee813a1955a702160239f123499b9. Workflow run.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261019115812

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@isaacroldan
isaacroldan force-pushed the make-dev-agentic branch 3 times, most recently from cfd91fb to b06cf68 Compare October 19, 2026 15:40
@isaacroldan isaacroldan changed the title Add a reusable dev-instance lock and JSONL log, and use it in app dev Expose app dev session files for agents Oct 19, 2026
@isaacroldan
isaacroldan changed the base branch from main to codex/dev-instance-support October 19, 2026 15:41
@isaacroldan
isaacroldan added this pull request to stack #8953 October 19, 2026 15:41
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm.

Built from b06cf681d7a1a60c17630b5967ccc46ee43b213d. Workflow run.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261019155212

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@github-actions github-actions Bot added Area: @shopify/app @shopify/app package issues and removed Area: @shopify/cli @shopify/cli package issues labels Oct 19, 2026
@isaacroldan isaacroldan mentioned this pull request Oct 20, 2026
1 of 4 tasks
@isaacroldan
isaacroldan marked this pull request as ready for review October 20, 2026 16:18
@isaacroldan
isaacroldan requested a review from a team as a code owner October 20, 2026 16:18
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/private/node/dev-instance-output.d.ts
import { zod } from '../../public/node/schema.js';
import { Writable, type WritableOptions } from 'stream';
export declare const devInstanceLogOutputSchema: import("../../public/node/json-output-schema.js").JsonOutputSchema<zod.ZodObject<{
    timestamp: zod.ZodString;
    source: zod.ZodString;
    level: zod.ZodOptional<zod.ZodEnum<["debug", "info", "warning", "error"]>>;
    message: zod.ZodString;
}, "strict", zod.ZodTypeAny, {
    source: string;
    message: string;
    timestamp: string;
    level?: "info" | "error" | "debug" | "warning" | undefined;
}, {
    source: string;
    message: string;
    timestamp: string;
    level?: "info" | "error" | "debug" | "warning" | undefined;
}>>;
/** One line of a dev instance's JSON Lines log. */
export type DevInstanceLogEntry = zod.infer<typeof devInstanceLogOutputSchema.schema>;
interface DevInstanceOutputOptions {
    logFile: string;
    logOutput?: Writable;
    isActive: () => boolean;
    onError: (error: unknown) => void;
}
export declare function createDevInstanceOutput({ logFile, logOutput, isActive, onError }: DevInstanceOutputOptions): {
    opened: Promise<any[]>;
    log: (entry: Omit<DevInstanceLogEntry, "timestamp"> & {
        timestamp?: string;
    }) => Promise<void>;
    tee: (target: Writable, { source, stringWritesAreMessages }: {
        source: string;
        stringWritesAreMessages?: boolean;
    }) => DevInstanceOutput;
    flush: () => Promise<PromiseSettledResult<void>[]>;
    close: () => Promise<void>;
};
declare class DevInstanceOutput extends Writable {
    private readonly contexts;
    constructor(options: WritableOptions & {
        write: NonNullable<WritableOptions['write']>;
    });
    write(chunk: string | Uint8Array, encoding?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean;
    end(chunk?: string | Uint8Array | null | (() => void), encoding?: BufferEncoding | (() => void), callback?: () => void): this;
}
export {};
packages/cli-kit/dist/private/node/output-context.d.ts
export interface ConcurrentOutputContext {
    outputPrefix?: string;
    /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */
    stripAnsi?: boolean;
}
export declare function useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T;
/** Returns the source and styling context for the current output write. */
export declare function getConcurrentOutputContext(): ConcurrentOutputContext | undefined;
packages/cli-kit/dist/public/node/dev-instance.d.ts
import { type DevInstanceLogEntry } from '../../private/node/dev-instance-output.js';
import { Writable } from 'stream';
/**
 * A stable error code for JSON consumers when a long-running command refuses to start because another instance of
 * it is already running for the same project.
 */
export declare const DEV_INSTANCE_ALREADY_RUNNING_CODE = "dev_instance_already_running";
/**
 * Describes a running instance of a long-running command such as `app dev` or `theme dev`.
 * It's written to `.shopify/dev/<slug>.json` so agents and other tools can discover the instance.
 */
export interface DevInstanceInfo {
    schemaVersion: 1;
    /** Identifies this run, including when the OS reuses a PID. */
    sessionId: string;
    /** The command that owns the instance, for example `app dev`. */
    command: string;
    /** Distinguishes instances of the same command in the same directory, for example the app configuration file. */
    key?: string;
    pid: number;
    status: 'starting' | 'running';
    startedAt: string;
    updatedAt: string;
    /** The project directory the instance runs for. */
    directory: string;
    /** The JSON Lines file the instance appends its output to. */
    logFile: string;
    /** Command-specific, JSON-serializable details such as URLs and ports. */
    details: {
        [key: string]: unknown;
    };
}
export { devInstanceLogOutputSchema, type DevInstanceLogEntry } from '../../private/node/dev-instance-output.js';
export interface DevInstancePaths {
    /** The folder that holds every dev instance of the project: `.shopify/dev`. */
    folder: string;
    /** The instance description. */
    infoFile: string;
    /** The lock directory, owned by `proper-lockfile`. */
    lockFile: string;
    /** The JSON Lines log. */
    logFile: string;
}
export interface DevInstanceIdentity {
    /** The command that owns the instance, for example `app dev`. */
    command: string;
    /** The project directory. */
    directory: string;
    /** Distinguishes several instances of the same command in the same directory. */
    key?: string;
}
export interface AcquireDevInstanceOptions extends DevInstanceIdentity {
    /** Initial command-specific details. */
    details?: {
        [key: string]: unknown;
    };
    /** Receives the exact JSONL bytes written to the session log. The caller owns this stream. */
    logOutput?: Writable;
}
export interface DevInstanceUpdate {
    status?: DevInstanceInfo['status'];
    /** Merged into the existing details. */
    details?: {
        [key: string]: unknown;
    };
}
export interface DevInstance {
    readonly info: DevInstanceInfo;
    readonly paths: DevInstancePaths;
    /** Aborts when this session loses its lock or output fails. Consumers must stop their owned work. */
    readonly abortSignal: AbortSignal;
    /** Updates and persists the instance description. */
    update: (changes: DevInstanceUpdate) => Promise<void>;
    /** Appends one entry to the JSON Lines log. */
    log: (entry: Omit<DevInstanceLogEntry, 'timestamp'> & {
        timestamp?: string;
    }) => Promise<void>;
    /**
     * Wraps a stream so every line written to it is also appended to the log, without ANSI styling.
     * The returned stream forwards the original bytes to `target` unchanged.
     */
    tee: (target: Writable, options: {
        source: string;
        /** Treat each string write as a complete message. Byte writes remain buffered into lines. */
        stringWritesAreMessages?: boolean;
    }) => Writable;
    /** Closes the log, removes the instance description and releases the lock. Safe to call more than once. */
    release: () => Promise<void>;
}
/**
 * Returns the files used by a dev instance. Every instance of a project shares `.shopify/dev`,
 * with one `<slug>.json`, `<slug>.lock` and `<slug>.jsonl` per command and key.
 *
 * @param identity - The command, directory and optional key of the instance.
 * @returns The paths of the instance files.
 */
export declare function devInstancePaths(identity: DevInstanceIdentity): DevInstancePaths;
/**
 * Marks this process as the running instance of a long-running command for a project.
 *
 * Mutual exclusion uses `proper-lockfile`: an atomic `mkdir` lock whose mtime this process refreshes as a heartbeat.
 * An exclusive startup directory serializes abandoned-lock recovery. An interrupted startup requires explicit
 * cleanup of that directory. A live owner blocks takeover, including when its heartbeat is stale.
 *
 * @param options - The instance identity, initial details.
 * @returns The acquired instance.
 * @throws AbortError with code {@link DEV_INSTANCE_ALREADY_RUNNING_CODE} when another live instance holds the lock.
 */
export declare function acquireDevInstance(options: AcquireDevInstanceOptions): Promise<DevInstance>;
/**
 * Acquires a dev instance and releases it after the callback completes or throws.
 *
 * @param options - The command, project and output options for the instance.
 * @param execute - The work to run while the instance is owned.
 * @returns The callback's result after instance cleanup completes.
 */
export declare function withDevInstance<T>(options: AcquireDevInstanceOptions, execute: (instance: DevInstance) => T | Promise<T>): Promise<T>;
/**
 * Returns the running instance of a command for a project, if there is one.
 *
 * @param identity - The command, directory and optional key of the instance.
 * @returns The instance description, or undefined when no live instance holds the lock.
 */
export declare function readDevInstance(identity: DevInstanceIdentity): Promise<DevInstanceInfo | undefined>;
/**
 * Lists every running dev instance of a project, across commands and keys.
 *
 * @param directory - The project directory.
 * @returns The descriptions of the live instances.
 */
export declare function listDevInstances(directory: string): Promise<DevInstanceInfo[]>;
/**
 * Checks whether a process exists. A process owned by another user counts as alive.
 *
 * @param pid - The process ID.
 * @returns True when the process exists.
 */
export declare function isProcessAlive(pid: number): boolean;

Existing type declarations

packages/cli-kit/dist/private/node/ui/components/ConcurrentOutput.d.ts
@@ -1,3 +1,4 @@
+import { ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext } from '../../output-context.js';
 import { type OutputProcess } from '../../../../public/node/output.js';
 import { AbortSignal } from '../../../../public/node/abort.js';
 import { FunctionComponent } from 'react';
@@ -13,12 +14,6 @@ export interface ConcurrentOutputProps {
     keepRunningAfterProcessesResolve?: boolean;
     useAlternativeColorPalette?: boolean;
 }
-interface ConcurrentOutputContext {
-    outputPrefix?: string;
-    /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */
-    stripAnsi?: boolean;
-}
-declare function useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T;
 /** Runs finite processes concurrently and routes their output through the shared diagnostic context. */
 export declare function runConcurrentProcessesForJson({ processes, abortSignal, }: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void>;
 /**
@@ -55,4 +50,4 @@ export declare function runConcurrentProcessesForJson({ processes, abortSignal,
  * ```
  */
 declare const ConcurrentOutput: FunctionComponent<ConcurrentOutputProps>;
-export { ConcurrentOutput, ConcurrentOutputContext, useConcurrentOutputContext };
\ No newline at end of file
+export { ConcurrentOutput, ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext };
\ No newline at end of file

Base automatically changed from codex/dev-instance-support to main October 21, 2026 13:40
Comment thread docs/cli/app-dev-agent-workflow.md Outdated
Comment thread docs/cli/app-dev-agent-workflow.md Outdated
Comment thread packages/app/src/cli/commands/app/dev.ts
Comment thread packages/app/src/cli/services/dev/ui.integration.test.ts Outdated
@github-actions github-actions Bot added Area: @shopify/cli @shopify/cli package issues and removed Area: @shopify/app @shopify/app package issues labels Oct 22, 2026
@isaacroldan
isaacroldan added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 22, 2026
@isaacroldan
isaacroldan added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 458861f Oct 22, 2026
27 of 29 checks passed
@isaacroldan
isaacroldan deleted the make-dev-agentic branch October 22, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants