@prover-coder-ai/docker-git 1.0.8 → 1.0.10
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 +61 -32
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-options.js +6 -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 +5 -0
- package/dist/src/docker-git/program.js +5 -2
- package/package.json +4 -4
- package/src/docker-git/cli/parser-options.ts +8 -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 +5 -0
- package/src/docker-git/program.ts +28 -21
- package/tests/docker-git/parser.test.ts +48 -11
|
@@ -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" },
|
|
@@ -33,6 +35,8 @@ const booleanFlagUpdaters = {
|
|
|
33
35
|
"--force-env": (raw) => ({ ...raw, forceEnv: true }),
|
|
34
36
|
"--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
|
|
35
37
|
"--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
|
|
38
|
+
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
39
|
+
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
36
40
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
37
41
|
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
38
42
|
};
|
|
@@ -51,6 +55,8 @@ const valueFlagUpdaters = {
|
|
|
51
55
|
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
|
|
52
56
|
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
|
|
53
57
|
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
|
|
58
|
+
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
|
|
59
|
+
scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
|
|
54
60
|
label: (raw, value) => ({ ...raw, label: value }),
|
|
55
61
|
token: (raw, value) => ({ ...raw, token: value }),
|
|
56
62
|
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,6 +43,9 @@ 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)
|
|
@@ -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.10",
|
|
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",
|
|
@@ -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" },
|
|
@@ -69,6 +73,8 @@ const booleanFlagUpdaters: Readonly<Record<string, (raw: RawOptions) => RawOptio
|
|
|
69
73
|
"--force-env": (raw) => ({ ...raw, forceEnv: true }),
|
|
70
74
|
"--mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: true }),
|
|
71
75
|
"--no-mcp-playwright": (raw) => ({ ...raw, enableMcpPlaywright: false }),
|
|
76
|
+
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
77
|
+
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
72
78
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
73
79
|
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
74
80
|
}
|
|
@@ -88,6 +94,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
|
|
|
88
94
|
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
|
|
89
95
|
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
|
|
90
96
|
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
|
|
97
|
+
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
|
|
98
|
+
scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
|
|
91
99
|
label: (raw, value) => ({ ...raw, label: value }),
|
|
92
100
|
token: (raw, value) => ({ ...raw, token: value }),
|
|
93
101
|
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,6 +46,9 @@ 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)
|
|
@@ -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)
|
|
@@ -6,6 +6,22 @@ import { parseArgs } from "../../src/docker-git/cli/parser.js"
|
|
|
6
6
|
|
|
7
7
|
type CreateCommand = Extract<Command, { _tag: "Create" }>
|
|
8
8
|
|
|
9
|
+
const expectParseErrorTag = (
|
|
10
|
+
args: ReadonlyArray<string>,
|
|
11
|
+
expectedTag: string
|
|
12
|
+
) =>
|
|
13
|
+
Effect.sync(() => {
|
|
14
|
+
const parsed = parseArgs(args)
|
|
15
|
+
Either.match(parsed, {
|
|
16
|
+
onLeft: (error) => {
|
|
17
|
+
expect(error._tag).toBe(expectedTag)
|
|
18
|
+
},
|
|
19
|
+
onRight: () => {
|
|
20
|
+
throw new Error("expected parse error")
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
9
25
|
const parseOrThrow = (args: ReadonlyArray<string>): Command => {
|
|
10
26
|
const parsed = parseArgs(args)
|
|
11
27
|
return Either.match(parsed, {
|
|
@@ -56,17 +72,7 @@ describe("parseArgs", () => {
|
|
|
56
72
|
expect(command.config.volumeName).toBe("dg-repo-issue-9-home")
|
|
57
73
|
}))
|
|
58
74
|
|
|
59
|
-
it.effect("fails on missing repo url", () =>
|
|
60
|
-
Effect.sync(() => {
|
|
61
|
-
Either.match(parseArgs(["create"]), {
|
|
62
|
-
onLeft: (error) => {
|
|
63
|
-
expect(error._tag).toBe("MissingRequiredOption")
|
|
64
|
-
},
|
|
65
|
-
onRight: () => {
|
|
66
|
-
throw new Error("expected parse error")
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
}))
|
|
75
|
+
it.effect("fails on missing repo url", () => expectParseErrorTag(["create"], "MissingRequiredOption"))
|
|
70
76
|
|
|
71
77
|
it.effect("parses clone command with positional repo url", () =>
|
|
72
78
|
expectCreateCommand(["clone", "https://github.com/org/repo.git"], (command) => {
|
|
@@ -169,4 +175,35 @@ describe("parseArgs", () => {
|
|
|
169
175
|
}
|
|
170
176
|
expect(command.message).toBe("sync state")
|
|
171
177
|
}))
|
|
178
|
+
|
|
179
|
+
it.effect("parses scrap export with defaults", () =>
|
|
180
|
+
Effect.sync(() => {
|
|
181
|
+
const command = parseOrThrow(["scrap", "export"])
|
|
182
|
+
if (command._tag !== "ScrapExport") {
|
|
183
|
+
throw new Error("expected ScrapExport command")
|
|
184
|
+
}
|
|
185
|
+
expect(command.projectDir).toBe(".")
|
|
186
|
+
expect(command.archivePath).toBe(".orch/scrap/session")
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
it.effect("fails scrap import without archive", () =>
|
|
190
|
+
expectParseErrorTag(["scrap", "import"], "MissingRequiredOption"))
|
|
191
|
+
|
|
192
|
+
it.effect("parses scrap import wipe defaults", () =>
|
|
193
|
+
Effect.sync(() => {
|
|
194
|
+
const command = parseOrThrow(["scrap", "import", "--archive", "workspace.tar.gz"])
|
|
195
|
+
if (command._tag !== "ScrapImport") {
|
|
196
|
+
throw new Error("expected ScrapImport command")
|
|
197
|
+
}
|
|
198
|
+
expect(command.wipe).toBe(true)
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
it.effect("parses scrap import --no-wipe", () =>
|
|
202
|
+
Effect.sync(() => {
|
|
203
|
+
const command = parseOrThrow(["scrap", "import", "--archive", "workspace.tar.gz", "--no-wipe"])
|
|
204
|
+
if (command._tag !== "ScrapImport") {
|
|
205
|
+
throw new Error("expected ScrapImport command")
|
|
206
|
+
}
|
|
207
|
+
expect(command.wipe).toBe(false)
|
|
208
|
+
}))
|
|
172
209
|
})
|