@prover-coder-ai/docker-git 1.0.9 → 1.0.11
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/.package.json.release.bak +4 -4
- package/CHANGELOG.md +12 -0
- package/dist/main.js +204 -179
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-clone.js +2 -1
- package/dist/src/docker-git/cli/parser-options.js +8 -0
- package/dist/src/docker-git/cli/parser-scrap.js +74 -0
- package/dist/src/docker-git/cli/parser.js +4 -1
- package/dist/src/docker-git/cli/usage.js +6 -0
- package/dist/src/docker-git/program.js +5 -2
- package/package.json +4 -4
- package/src/docker-git/cli/parser-clone.ts +2 -1
- package/src/docker-git/cli/parser-options.ts +10 -0
- package/src/docker-git/cli/parser-scrap.ts +106 -0
- package/src/docker-git/cli/parser.ts +25 -22
- package/src/docker-git/cli/usage.ts +6 -0
- package/src/docker-git/program.ts +28 -21
- package/tests/docker-git/parser.test.ts +64 -11
|
@@ -34,7 +34,8 @@ export const parseClone = (args) => {
|
|
|
34
34
|
const withRef = resolvedRepo.repoRef !== undefined && raw.repoRef === undefined
|
|
35
35
|
? { ...withDefaults, repoRef: resolvedRepo.repoRef }
|
|
36
36
|
: withDefaults;
|
|
37
|
+
const openSsh = raw.openSsh ?? true;
|
|
37
38
|
const create = yield* _(buildCreateCommand(withRef));
|
|
38
|
-
return { ...create, waitForClone: true };
|
|
39
|
+
return { ...create, waitForClone: true, openSsh };
|
|
39
40
|
});
|
|
40
41
|
};
|
|
@@ -16,6 +16,8 @@ const valueOptionSpecs = [
|
|
|
16
16
|
{ flag: "--env-project", key: "envProjectPath" },
|
|
17
17
|
{ flag: "--codex-auth", key: "codexAuthPath" },
|
|
18
18
|
{ flag: "--codex-home", key: "codexHome" },
|
|
19
|
+
{ flag: "--archive", key: "archivePath" },
|
|
20
|
+
{ flag: "--mode", key: "scrapMode" },
|
|
19
21
|
{ flag: "--label", key: "label" },
|
|
20
22
|
{ flag: "--token", key: "token" },
|
|
21
23
|
{ flag: "--scopes", key: "scopes" },
|
|
@@ -29,10 +31,14 @@ const valueOptionSpecByFlag = new Map(valueOptionSpecs.map((spec) => [spec.flag,
|
|
|
29
31
|
const booleanFlagUpdaters = {
|
|
30
32
|
"--up": (raw) => ({ ...raw, up: true }),
|
|
31
33
|
"--no-up": (raw) => ({ ...raw, up: false }),
|
|
34
|
+
"--ssh": (raw) => ({ ...raw, openSsh: true }),
|
|
35
|
+
"--no-ssh": (raw) => ({ ...raw, openSsh: false }),
|
|
32
36
|
"--force": (raw) => ({ ...raw, force: true }),
|
|
33
37
|
"--force-env": (raw) => ({ ...raw, forceEnv: true }),
|
|
34
38
|
"--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
|
|
35
39
|
"--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
|
|
40
|
+
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
41
|
+
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
36
42
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
37
43
|
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
38
44
|
};
|
|
@@ -51,6 +57,8 @@ const valueFlagUpdaters = {
|
|
|
51
57
|
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
|
|
52
58
|
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
|
|
53
59
|
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
|
|
60
|
+
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
|
|
61
|
+
scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
|
|
54
62
|
label: (raw, value) => ({ ...raw, label: value }),
|
|
55
63
|
token: (raw, value) => ({ ...raw, token: value }),
|
|
56
64
|
scopes: (raw, value) => ({ ...raw, scopes: value }),
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Either, Match } from "effect";
|
|
2
|
+
import { parseProjectDirWithOptions } from "./parser-shared.js";
|
|
3
|
+
const missingRequired = (option) => ({
|
|
4
|
+
_tag: "MissingRequiredOption",
|
|
5
|
+
option
|
|
6
|
+
});
|
|
7
|
+
const invalidScrapAction = (value) => ({
|
|
8
|
+
_tag: "InvalidOption",
|
|
9
|
+
option: "scrap",
|
|
10
|
+
reason: `unknown action: ${value}`
|
|
11
|
+
});
|
|
12
|
+
const defaultSessionArchiveDir = ".orch/scrap/session";
|
|
13
|
+
const invalidScrapMode = (value) => ({
|
|
14
|
+
_tag: "InvalidOption",
|
|
15
|
+
option: "--mode",
|
|
16
|
+
reason: `unknown value: ${value} (expected session)`
|
|
17
|
+
});
|
|
18
|
+
const parseScrapMode = (raw) => {
|
|
19
|
+
const value = raw?.trim();
|
|
20
|
+
if (!value || value.length === 0) {
|
|
21
|
+
return Either.right("session");
|
|
22
|
+
}
|
|
23
|
+
if (value === "session") {
|
|
24
|
+
return Either.right("session");
|
|
25
|
+
}
|
|
26
|
+
if (value === "recipe") {
|
|
27
|
+
// Backwards/semantic alias: "recipe" behaves like "session" (git state + rebuildable deps).
|
|
28
|
+
return Either.right("session");
|
|
29
|
+
}
|
|
30
|
+
return Either.left(invalidScrapMode(value));
|
|
31
|
+
};
|
|
32
|
+
const makeScrapExportCommand = (projectDir, archivePath, mode) => ({
|
|
33
|
+
_tag: "ScrapExport",
|
|
34
|
+
projectDir,
|
|
35
|
+
archivePath,
|
|
36
|
+
mode
|
|
37
|
+
});
|
|
38
|
+
const makeScrapImportCommand = (projectDir, archivePath, wipe, mode) => ({
|
|
39
|
+
_tag: "ScrapImport",
|
|
40
|
+
projectDir,
|
|
41
|
+
archivePath,
|
|
42
|
+
wipe,
|
|
43
|
+
mode
|
|
44
|
+
});
|
|
45
|
+
// CHANGE: parse scrap session export/import commands
|
|
46
|
+
// WHY: store a small reproducible snapshot (git state + secrets) instead of large caches like node_modules
|
|
47
|
+
// QUOTE(ТЗ): "не должно быть старого режима где он качает весь шлак типо node_modules"
|
|
48
|
+
// REF: user-request-2026-02-15
|
|
49
|
+
// SOURCE: n/a
|
|
50
|
+
// FORMAT THEOREM: forall argv: parseScrap(argv) = cmd -> deterministic(cmd)
|
|
51
|
+
// PURITY: CORE
|
|
52
|
+
// EFFECT: Effect<Command, ParseError, never>
|
|
53
|
+
// INVARIANT: export/import always resolves a projectDir
|
|
54
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
55
|
+
export const parseScrap = (args) => {
|
|
56
|
+
const action = args[0]?.trim();
|
|
57
|
+
if (!action || action.length === 0) {
|
|
58
|
+
return Either.left(missingRequired("scrap <action>"));
|
|
59
|
+
}
|
|
60
|
+
const rest = args.slice(1);
|
|
61
|
+
return Match.value(action).pipe(Match.when("export", () => Either.flatMap(parseProjectDirWithOptions(rest), ({ projectDir, raw }) => Either.map(parseScrapMode(raw.scrapMode), (mode) => {
|
|
62
|
+
const archivePathRaw = raw.archivePath?.trim();
|
|
63
|
+
if (archivePathRaw && archivePathRaw.length > 0) {
|
|
64
|
+
return makeScrapExportCommand(projectDir, archivePathRaw, mode);
|
|
65
|
+
}
|
|
66
|
+
return makeScrapExportCommand(projectDir, defaultSessionArchiveDir, mode);
|
|
67
|
+
}))), Match.when("import", () => Either.flatMap(parseProjectDirWithOptions(rest), ({ projectDir, raw }) => {
|
|
68
|
+
const archivePath = raw.archivePath?.trim();
|
|
69
|
+
if (!archivePath || archivePath.length === 0) {
|
|
70
|
+
return Either.left(missingRequired("--archive"));
|
|
71
|
+
}
|
|
72
|
+
return Either.map(parseScrapMode(raw.scrapMode), (mode) => makeScrapImportCommand(projectDir, archivePath, raw.wipe ?? true, mode));
|
|
73
|
+
})), Match.orElse(() => Either.left(invalidScrapAction(action))));
|
|
74
|
+
};
|
|
@@ -6,6 +6,7 @@ import { parseClone } from "./parser-clone.js";
|
|
|
6
6
|
import { buildCreateCommand } from "./parser-create.js";
|
|
7
7
|
import { parseRawOptions } from "./parser-options.js";
|
|
8
8
|
import { parsePanes } from "./parser-panes.js";
|
|
9
|
+
import { parseScrap } from "./parser-scrap.js";
|
|
9
10
|
import { parseSessions } from "./parser-sessions.js";
|
|
10
11
|
import { parseState } from "./parser-state.js";
|
|
11
12
|
import { usageText } from "./usage.js";
|
|
@@ -38,5 +39,7 @@ export const parseArgs = (args) => {
|
|
|
38
39
|
_tag: "UnknownCommand",
|
|
39
40
|
command: command ?? ""
|
|
40
41
|
};
|
|
41
|
-
return Match.value(command)
|
|
42
|
+
return Match.value(command)
|
|
43
|
+
.pipe(Match.when("create", () => parseCreate(rest)), Match.when("init", () => parseCreate(rest)), Match.when("clone", () => parseClone(rest)), Match.when("attach", () => parseAttach(rest)), Match.when("tmux", () => parseAttach(rest)), Match.when("panes", () => parsePanes(rest)), Match.when("terms", () => parsePanes(rest)), Match.when("terminals", () => parsePanes(rest)), Match.when("sessions", () => parseSessions(rest)), Match.when("scrap", () => parseScrap(rest)), Match.when("help", () => Either.right(helpCommand)), Match.when("ps", () => Either.right(statusCommand)), Match.when("status", () => Either.right(statusCommand)), Match.when("down-all", () => Either.right(downAllCommand)), Match.when("stop-all", () => Either.right(downAllCommand)), Match.when("kill-all", () => Either.right(downAllCommand)), Match.when("menu", () => Either.right(menuCommand)), Match.when("ui", () => Either.right(menuCommand)), Match.when("auth", () => parseAuth(rest)), Match.when("state", () => parseState(rest)))
|
|
44
|
+
.pipe(Match.orElse(() => Either.left(unknownCommandError)));
|
|
42
45
|
};
|
|
@@ -4,6 +4,7 @@ docker-git create --repo-url <url> [options]
|
|
|
4
4
|
docker-git clone <url> [options]
|
|
5
5
|
docker-git attach [<url>] [options]
|
|
6
6
|
docker-git panes [<url>] [options]
|
|
7
|
+
docker-git scrap <action> [<url>] [options]
|
|
7
8
|
docker-git sessions [list] [<url>] [options]
|
|
8
9
|
docker-git sessions kill <pid> [<url>] [options]
|
|
9
10
|
docker-git sessions logs <pid> [<url>] [options]
|
|
@@ -18,6 +19,7 @@ Commands:
|
|
|
18
19
|
clone Create + run container and clone repo
|
|
19
20
|
attach, tmux Open tmux workspace for a docker-git project
|
|
20
21
|
panes, terms List tmux panes for a docker-git project
|
|
22
|
+
scrap Export/import project scrap (session snapshot + rebuildable deps)
|
|
21
23
|
sessions List/kill/log container terminal processes
|
|
22
24
|
ps, status Show docker compose status for all docker-git projects
|
|
23
25
|
down-all Stop all docker-git containers (docker compose down)
|
|
@@ -41,9 +43,13 @@ Options:
|
|
|
41
43
|
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
|
|
42
44
|
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
|
|
43
45
|
--project-dir <path> Project directory for attach (default: .)
|
|
46
|
+
--archive <path> Scrap snapshot directory (default: .orch/scrap/session)
|
|
47
|
+
--mode <session> Scrap mode (default: session)
|
|
48
|
+
--wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
|
|
44
49
|
--lines <n> Tail last N lines for sessions logs (default: 200)
|
|
45
50
|
--include-default Show default/system processes in sessions list
|
|
46
51
|
--up | --no-up Run docker compose up after init (default: --up)
|
|
52
|
+
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
|
|
47
53
|
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
|
|
48
54
|
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
49
55
|
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
@@ -2,6 +2,7 @@ import { createProject } from "@effect-template/lib/usecases/actions";
|
|
|
2
2
|
import { authCodexLogin, authCodexLogout, authCodexStatus, authGithubLogin, authGithubLogout, authGithubStatus } from "@effect-template/lib/usecases/auth";
|
|
3
3
|
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
4
4
|
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects";
|
|
5
|
+
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap";
|
|
5
6
|
import { stateCommit, stateInit, statePath, statePull, statePush, stateStatus, stateSync } from "@effect-template/lib/usecases/state-repo";
|
|
6
7
|
import { killTerminalProcess, listTerminalSessions, tailTerminalLogs } from "@effect-template/lib/usecases/terminal-sessions";
|
|
7
8
|
import { Effect, Match, pipe } from "effect";
|
|
@@ -19,7 +20,9 @@ const setExitCode = (code) => Effect.sync(() => {
|
|
|
19
20
|
});
|
|
20
21
|
const logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
|
|
21
22
|
const logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
|
|
22
|
-
const handleNonBaseCommand = (command) => Match.value(command)
|
|
23
|
+
const handleNonBaseCommand = (command) => Match.value(command)
|
|
24
|
+
.pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)), Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)))
|
|
25
|
+
.pipe(Match.exhaustive);
|
|
23
26
|
// CHANGE: compose CLI program with typed errors and shell effects
|
|
24
27
|
// WHY: keep a thin entry layer over pure parsing and template generation
|
|
25
28
|
// QUOTE(ТЗ): "CLI команду... создавать докер образы"
|
|
@@ -30,7 +33,7 @@ const handleNonBaseCommand = (command) => Match.value(command).pipe(Match.when({
|
|
|
30
33
|
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
|
|
31
34
|
// INVARIANT: help is printed without side effects beyond logs
|
|
32
35
|
// COMPLEXITY: O(n) where n = |files|
|
|
33
|
-
export const program = pipe(readCommand, Effect.flatMap((command) => Match.value(command).pipe(Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)), Match.when({ _tag: "Create" }, (create) => createProject(create)), Match.when({ _tag: "Status" }, () => listProjectStatus), Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)))), Effect.catchTag("FileExistsError", (error) => pipe(Effect.logWarning(renderError(error)), Effect.asVoid)), Effect.catchTag("DockerAccessError", logWarningAndExit), Effect.catchTag("DockerCommandError", logWarningAndExit), Effect.catchTag("AuthError", logWarningAndExit), Effect.catchTag("CommandFailedError", logWarningAndExit), Effect.matchEffect({
|
|
36
|
+
export const program = pipe(readCommand, Effect.flatMap((command) => Match.value(command).pipe(Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)), Match.when({ _tag: "Create" }, (create) => createProject(create)), Match.when({ _tag: "Status" }, () => listProjectStatus), Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)))), Effect.catchTag("FileExistsError", (error) => pipe(Effect.logWarning(renderError(error)), Effect.asVoid)), Effect.catchTag("DockerAccessError", logWarningAndExit), Effect.catchTag("DockerCommandError", logWarningAndExit), Effect.catchTag("AuthError", logWarningAndExit), Effect.catchTag("CommandFailedError", logWarningAndExit), Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit), Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit), Effect.catchTag("ScrapWipeRefusedError", logErrorAndExit), Effect.matchEffect({
|
|
34
37
|
onFailure: (error) => isParseError(error)
|
|
35
38
|
? logErrorAndExit(error)
|
|
36
39
|
: pipe(Effect.logError(renderError(error)), Effect.flatMap(() => Effect.fail(error))),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prover-coder-ai/docker-git",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Minimal Vite-powered TypeScript console starter using Effect",
|
|
5
5
|
"main": "dist/src/docker-git/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"build:app": "vite build --ssr src/app/main.ts",
|
|
45
45
|
"dev": "vite build --watch --ssr src/app/main.ts",
|
|
46
46
|
"prelint": "pnpm -C ../lib build",
|
|
47
|
-
"lint": "
|
|
48
|
-
"lint:tests": "
|
|
49
|
-
"lint:effect": "
|
|
47
|
+
"lint": "PATH=../../scripts:$PATH vibecode-linter src/",
|
|
48
|
+
"lint:tests": "PATH=../../scripts:$PATH vibecode-linter tests/",
|
|
49
|
+
"lint:effect": "PATH=../../scripts:$PATH eslint --config eslint.effect-ts-check.config.mjs .",
|
|
50
50
|
"prebuild:docker-git": "pnpm -C ../lib build",
|
|
51
51
|
"build:docker-git": "tsc -p tsconfig.build.json",
|
|
52
52
|
"check": "pnpm run typecheck",
|
|
@@ -49,7 +49,8 @@ export const parseClone = (args: ReadonlyArray<string>): Either.Either<Command,
|
|
|
49
49
|
const withRef = resolvedRepo.repoRef !== undefined && raw.repoRef === undefined
|
|
50
50
|
? { ...withDefaults, repoRef: resolvedRepo.repoRef }
|
|
51
51
|
: withDefaults
|
|
52
|
+
const openSsh = raw.openSsh ?? true
|
|
52
53
|
const create = yield* _(buildCreateCommand(withRef))
|
|
53
|
-
return { ...create, waitForClone: true }
|
|
54
|
+
return { ...create, waitForClone: true, openSsh }
|
|
54
55
|
})
|
|
55
56
|
}
|
|
@@ -20,6 +20,8 @@ interface ValueOptionSpec {
|
|
|
20
20
|
| "envProjectPath"
|
|
21
21
|
| "codexAuthPath"
|
|
22
22
|
| "codexHome"
|
|
23
|
+
| "archivePath"
|
|
24
|
+
| "scrapMode"
|
|
23
25
|
| "label"
|
|
24
26
|
| "token"
|
|
25
27
|
| "scopes"
|
|
@@ -46,6 +48,8 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
|
|
|
46
48
|
{ flag: "--env-project", key: "envProjectPath" },
|
|
47
49
|
{ flag: "--codex-auth", key: "codexAuthPath" },
|
|
48
50
|
{ flag: "--codex-home", key: "codexHome" },
|
|
51
|
+
{ flag: "--archive", key: "archivePath" },
|
|
52
|
+
{ flag: "--mode", key: "scrapMode" },
|
|
49
53
|
{ flag: "--label", key: "label" },
|
|
50
54
|
{ flag: "--token", key: "token" },
|
|
51
55
|
{ flag: "--scopes", key: "scopes" },
|
|
@@ -65,10 +69,14 @@ type ValueKey = ValueOptionSpec["key"]
|
|
|
65
69
|
const booleanFlagUpdaters: Readonly<Record<string, (raw: RawOptions) => RawOptions>> = {
|
|
66
70
|
"--up": (raw) => ({ ...raw, up: true }),
|
|
67
71
|
"--no-up": (raw) => ({ ...raw, up: false }),
|
|
72
|
+
"--ssh": (raw) => ({ ...raw, openSsh: true }),
|
|
73
|
+
"--no-ssh": (raw) => ({ ...raw, openSsh: false }),
|
|
68
74
|
"--force": (raw) => ({ ...raw, force: true }),
|
|
69
75
|
"--force-env": (raw) => ({ ...raw, forceEnv: true }),
|
|
70
76
|
"--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
|
|
71
77
|
"--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
|
|
78
|
+
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
79
|
+
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
72
80
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
73
81
|
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
74
82
|
}
|
|
@@ -88,6 +96,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
|
|
|
88
96
|
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
|
|
89
97
|
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
|
|
90
98
|
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
|
|
99
|
+
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
|
|
100
|
+
scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
|
|
91
101
|
label: (raw, value) => ({ ...raw, label: value }),
|
|
92
102
|
token: (raw, value) => ({ ...raw, token: value }),
|
|
93
103
|
scopes: (raw, value) => ({ ...raw, scopes: value }),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Either, Match } from "effect"
|
|
2
|
+
|
|
3
|
+
import type { Command, ParseError } from "@effect-template/lib/core/domain"
|
|
4
|
+
|
|
5
|
+
import { parseProjectDirWithOptions } from "./parser-shared.js"
|
|
6
|
+
|
|
7
|
+
const missingRequired = (option: string): ParseError => ({
|
|
8
|
+
_tag: "MissingRequiredOption",
|
|
9
|
+
option
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const invalidScrapAction = (value: string): ParseError => ({
|
|
13
|
+
_tag: "InvalidOption",
|
|
14
|
+
option: "scrap",
|
|
15
|
+
reason: `unknown action: ${value}`
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const defaultSessionArchiveDir = ".orch/scrap/session"
|
|
19
|
+
|
|
20
|
+
const invalidScrapMode = (value: string): ParseError => ({
|
|
21
|
+
_tag: "InvalidOption",
|
|
22
|
+
option: "--mode",
|
|
23
|
+
reason: `unknown value: ${value} (expected session)`
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const parseScrapMode = (raw: string | undefined): Either.Either<"session", ParseError> => {
|
|
27
|
+
const value = raw?.trim()
|
|
28
|
+
if (!value || value.length === 0) {
|
|
29
|
+
return Either.right("session")
|
|
30
|
+
}
|
|
31
|
+
if (value === "session") {
|
|
32
|
+
return Either.right("session")
|
|
33
|
+
}
|
|
34
|
+
if (value === "recipe") {
|
|
35
|
+
// Backwards/semantic alias: "recipe" behaves like "session" (git state + rebuildable deps).
|
|
36
|
+
return Either.right("session")
|
|
37
|
+
}
|
|
38
|
+
return Either.left(invalidScrapMode(value))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const makeScrapExportCommand = (projectDir: string, archivePath: string, mode: "session"): Command => ({
|
|
42
|
+
_tag: "ScrapExport",
|
|
43
|
+
projectDir,
|
|
44
|
+
archivePath,
|
|
45
|
+
mode
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const makeScrapImportCommand = (
|
|
49
|
+
projectDir: string,
|
|
50
|
+
archivePath: string,
|
|
51
|
+
wipe: boolean,
|
|
52
|
+
mode: "session"
|
|
53
|
+
): Command => ({
|
|
54
|
+
_tag: "ScrapImport",
|
|
55
|
+
projectDir,
|
|
56
|
+
archivePath,
|
|
57
|
+
wipe,
|
|
58
|
+
mode
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// CHANGE: parse scrap session export/import commands
|
|
62
|
+
// WHY: store a small reproducible snapshot (git state + secrets) instead of large caches like node_modules
|
|
63
|
+
// QUOTE(ТЗ): "не должно быть старого режима где он качает весь шлак типо node_modules"
|
|
64
|
+
// REF: user-request-2026-02-15
|
|
65
|
+
// SOURCE: n/a
|
|
66
|
+
// FORMAT THEOREM: forall argv: parseScrap(argv) = cmd -> deterministic(cmd)
|
|
67
|
+
// PURITY: CORE
|
|
68
|
+
// EFFECT: Effect<Command, ParseError, never>
|
|
69
|
+
// INVARIANT: export/import always resolves a projectDir
|
|
70
|
+
// COMPLEXITY: O(n) where n = |argv|
|
|
71
|
+
export const parseScrap = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> => {
|
|
72
|
+
const action = args[0]?.trim()
|
|
73
|
+
if (!action || action.length === 0) {
|
|
74
|
+
return Either.left(missingRequired("scrap <action>"))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rest = args.slice(1)
|
|
78
|
+
|
|
79
|
+
return Match.value(action).pipe(
|
|
80
|
+
Match.when(
|
|
81
|
+
"export",
|
|
82
|
+
() =>
|
|
83
|
+
Either.flatMap(
|
|
84
|
+
parseProjectDirWithOptions(rest),
|
|
85
|
+
({ projectDir, raw }) =>
|
|
86
|
+
Either.map(parseScrapMode(raw.scrapMode), (mode) => {
|
|
87
|
+
const archivePathRaw = raw.archivePath?.trim()
|
|
88
|
+
if (archivePathRaw && archivePathRaw.length > 0) {
|
|
89
|
+
return makeScrapExportCommand(projectDir, archivePathRaw, mode)
|
|
90
|
+
}
|
|
91
|
+
return makeScrapExportCommand(projectDir, defaultSessionArchiveDir, mode)
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
Match.when("import", () =>
|
|
96
|
+
Either.flatMap(parseProjectDirWithOptions(rest), ({ projectDir, raw }) => {
|
|
97
|
+
const archivePath = raw.archivePath?.trim()
|
|
98
|
+
if (!archivePath || archivePath.length === 0) {
|
|
99
|
+
return Either.left(missingRequired("--archive"))
|
|
100
|
+
}
|
|
101
|
+
return Either.map(parseScrapMode(raw.scrapMode), (mode) =>
|
|
102
|
+
makeScrapImportCommand(projectDir, archivePath, raw.wipe ?? true, mode))
|
|
103
|
+
})),
|
|
104
|
+
Match.orElse(() => Either.left(invalidScrapAction(action)))
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -8,6 +8,7 @@ import { parseClone } from "./parser-clone.js"
|
|
|
8
8
|
import { buildCreateCommand } from "./parser-create.js"
|
|
9
9
|
import { parseRawOptions } from "./parser-options.js"
|
|
10
10
|
import { parsePanes } from "./parser-panes.js"
|
|
11
|
+
import { parseScrap } from "./parser-scrap.js"
|
|
11
12
|
import { parseSessions } from "./parser-sessions.js"
|
|
12
13
|
import { parseState } from "./parser-state.js"
|
|
13
14
|
import { usageText } from "./usage.js"
|
|
@@ -48,26 +49,28 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
|
|
|
48
49
|
command: command ?? ""
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
return Match.value(command)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
return Match.value(command)
|
|
53
|
+
.pipe(
|
|
54
|
+
Match.when("create", () => parseCreate(rest)),
|
|
55
|
+
Match.when("init", () => parseCreate(rest)),
|
|
56
|
+
Match.when("clone", () => parseClone(rest)),
|
|
57
|
+
Match.when("attach", () => parseAttach(rest)),
|
|
58
|
+
Match.when("tmux", () => parseAttach(rest)),
|
|
59
|
+
Match.when("panes", () => parsePanes(rest)),
|
|
60
|
+
Match.when("terms", () => parsePanes(rest)),
|
|
61
|
+
Match.when("terminals", () => parsePanes(rest)),
|
|
62
|
+
Match.when("sessions", () => parseSessions(rest)),
|
|
63
|
+
Match.when("scrap", () => parseScrap(rest)),
|
|
64
|
+
Match.when("help", () => Either.right(helpCommand)),
|
|
65
|
+
Match.when("ps", () => Either.right(statusCommand)),
|
|
66
|
+
Match.when("status", () => Either.right(statusCommand)),
|
|
67
|
+
Match.when("down-all", () => Either.right(downAllCommand)),
|
|
68
|
+
Match.when("stop-all", () => Either.right(downAllCommand)),
|
|
69
|
+
Match.when("kill-all", () => Either.right(downAllCommand)),
|
|
70
|
+
Match.when("menu", () => Either.right(menuCommand)),
|
|
71
|
+
Match.when("ui", () => Either.right(menuCommand)),
|
|
72
|
+
Match.when("auth", () => parseAuth(rest)),
|
|
73
|
+
Match.when("state", () => parseState(rest))
|
|
74
|
+
)
|
|
75
|
+
.pipe(Match.orElse(() => Either.left(unknownCommandError)))
|
|
73
76
|
}
|
|
@@ -7,6 +7,7 @@ docker-git create --repo-url <url> [options]
|
|
|
7
7
|
docker-git clone <url> [options]
|
|
8
8
|
docker-git attach [<url>] [options]
|
|
9
9
|
docker-git panes [<url>] [options]
|
|
10
|
+
docker-git scrap <action> [<url>] [options]
|
|
10
11
|
docker-git sessions [list] [<url>] [options]
|
|
11
12
|
docker-git sessions kill <pid> [<url>] [options]
|
|
12
13
|
docker-git sessions logs <pid> [<url>] [options]
|
|
@@ -21,6 +22,7 @@ Commands:
|
|
|
21
22
|
clone Create + run container and clone repo
|
|
22
23
|
attach, tmux Open tmux workspace for a docker-git project
|
|
23
24
|
panes, terms List tmux panes for a docker-git project
|
|
25
|
+
scrap Export/import project scrap (session snapshot + rebuildable deps)
|
|
24
26
|
sessions List/kill/log container terminal processes
|
|
25
27
|
ps, status Show docker compose status for all docker-git projects
|
|
26
28
|
down-all Stop all docker-git containers (docker compose down)
|
|
@@ -44,9 +46,13 @@ Options:
|
|
|
44
46
|
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
|
|
45
47
|
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
|
|
46
48
|
--project-dir <path> Project directory for attach (default: .)
|
|
49
|
+
--archive <path> Scrap snapshot directory (default: .orch/scrap/session)
|
|
50
|
+
--mode <session> Scrap mode (default: session)
|
|
51
|
+
--wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
|
|
47
52
|
--lines <n> Tail last N lines for sessions logs (default: 200)
|
|
48
53
|
--include-default Show default/system processes in sessions list
|
|
49
54
|
--up | --no-up Run docker compose up after init (default: --up)
|
|
55
|
+
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
|
|
50
56
|
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
|
|
51
57
|
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
52
58
|
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
12
12
|
import { renderError } from "@effect-template/lib/usecases/errors"
|
|
13
13
|
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
|
|
14
|
+
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap"
|
|
14
15
|
import {
|
|
15
16
|
stateCommit,
|
|
16
17
|
stateInit,
|
|
@@ -68,27 +69,30 @@ type NonBaseCommand = Exclude<
|
|
|
68
69
|
>
|
|
69
70
|
|
|
70
71
|
const handleNonBaseCommand = (command: NonBaseCommand) =>
|
|
71
|
-
Match.value(command)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
Match.value(command)
|
|
73
|
+
.pipe(
|
|
74
|
+
Match.when({ _tag: "StatePath" }, () => statePath),
|
|
75
|
+
Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)),
|
|
76
|
+
Match.when({ _tag: "StateStatus" }, () => stateStatus),
|
|
77
|
+
Match.when({ _tag: "StatePull" }, () => statePull),
|
|
78
|
+
Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)),
|
|
79
|
+
Match.when({ _tag: "StatePush" }, () => statePush),
|
|
80
|
+
Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)),
|
|
81
|
+
Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)),
|
|
82
|
+
Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)),
|
|
83
|
+
Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)),
|
|
84
|
+
Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)),
|
|
85
|
+
Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)),
|
|
86
|
+
Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)),
|
|
87
|
+
Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)),
|
|
88
|
+
Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)),
|
|
89
|
+
Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)),
|
|
90
|
+
Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)),
|
|
91
|
+
Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)),
|
|
92
|
+
Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
|
|
93
|
+
Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd))
|
|
94
|
+
)
|
|
95
|
+
.pipe(Match.exhaustive)
|
|
92
96
|
|
|
93
97
|
// CHANGE: compose CLI program with typed errors and shell effects
|
|
94
98
|
// WHY: keep a thin entry layer over pure parsing and template generation
|
|
@@ -121,6 +125,9 @@ export const program = pipe(
|
|
|
121
125
|
Effect.catchTag("DockerCommandError", logWarningAndExit),
|
|
122
126
|
Effect.catchTag("AuthError", logWarningAndExit),
|
|
123
127
|
Effect.catchTag("CommandFailedError", logWarningAndExit),
|
|
128
|
+
Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit),
|
|
129
|
+
Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit),
|
|
130
|
+
Effect.catchTag("ScrapWipeRefusedError", logErrorAndExit),
|
|
124
131
|
Effect.matchEffect({
|
|
125
132
|
onFailure: (error) =>
|
|
126
133
|
isParseError(error)
|