@gajae-code/coding-agent 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/bin/gjc.js +4 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +60 -0
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +10 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/utils/changelog.d.ts +1 -0
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +181 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +21 -4
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/model-profile-activation.ts +55 -7
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -3
- package/src/defaults/gjc/skills/team/SKILL.md +5 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/gjc-plugins/compiler.ts +351 -0
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +109 -0
- package/src/extensibility/gjc-plugins/installer.ts +434 -0
- package/src/extensibility/gjc-plugins/loader.ts +3 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/gjc-plugins/observability.ts +84 -0
- package/src/extensibility/gjc-plugins/paths.ts +1 -1
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/gjc-plugins/registry.ts +180 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/gjc-plugins/schema.ts +250 -20
- package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
- package/src/extensibility/gjc-plugins/types.ts +199 -3
- package/src/extensibility/gjc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/gjc-runtime/launch-tmux.ts +58 -15
- package/src/gjc-runtime/psmux-detect.ts +239 -0
- package/src/gjc-runtime/team-runtime.ts +56 -23
- package/src/gjc-runtime/tmux-common.ts +85 -3
- package/src/gjc-runtime/tmux-sessions.ts +111 -9
- package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/main.ts +14 -3
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +44 -11
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +50 -11
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +228 -0
- package/src/notifications/lifecycle-control-runtime.ts +400 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +433 -8
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +18 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +166 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +739 -12
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/index.ts +3 -1
- package/src/utils/changelog.ts +8 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"main": "./src/index.ts",
|
|
29
29
|
"types": "./dist/types/index.d.ts",
|
|
30
30
|
"bin": {
|
|
31
|
-
"gjc": "
|
|
31
|
+
"gjc": "bin/gjc.js"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "bun scripts/build-binary.ts",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
53
53
|
"@babel/parser": "^7.29.3",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@gajae-code/stats": "0.7.
|
|
56
|
-
"@gajae-code/agent-core": "0.7.
|
|
57
|
-
"@gajae-code/ai": "0.7.
|
|
58
|
-
"@gajae-code/natives": "0.7.
|
|
59
|
-
"@gajae-code/tui": "0.7.
|
|
60
|
-
"@gajae-code/utils": "0.7.
|
|
55
|
+
"@gajae-code/stats": "0.7.5",
|
|
56
|
+
"@gajae-code/agent-core": "0.7.5",
|
|
57
|
+
"@gajae-code/ai": "0.7.5",
|
|
58
|
+
"@gajae-code/natives": "0.7.5",
|
|
59
|
+
"@gajae-code/tui": "0.7.5",
|
|
60
|
+
"@gajae-code/utils": "0.7.5",
|
|
61
61
|
"@puppeteer/browsers": "^2.13.0",
|
|
62
62
|
"@types/turndown": "5.0.6",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
|
@@ -74,13 +74,15 @@
|
|
|
74
74
|
"zod": "4.4.3"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@types/bun": "^1.3.14"
|
|
77
|
+
"@types/bun": "^1.3.14",
|
|
78
|
+
"@types/ws": "^8.5.13"
|
|
78
79
|
},
|
|
79
80
|
"engines": {
|
|
80
81
|
"bun": ">=1.3.14"
|
|
81
82
|
},
|
|
82
83
|
"files": [
|
|
83
84
|
"src",
|
|
85
|
+
"bin",
|
|
84
86
|
"scripts",
|
|
85
87
|
"examples",
|
|
86
88
|
"README.md",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// G004 real-tmux smoke: exercises forceCloseGjcTmuxSession against LIVE tmux
|
|
2
|
+
// sessions (tmux 3.6a). Proves the wrapper hard-kills GJC-managed live panes
|
|
3
|
+
// (where remove refuses), refuses non-GJC sessions, and enforces session-id
|
|
4
|
+
// matching. Produces durable evidence; not part of the unit suite.
|
|
5
|
+
import assert from "node:assert";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildGjcTmuxExactOptionTarget,
|
|
9
|
+
buildGjcTmuxProfileCommands,
|
|
10
|
+
resolveGjcTmuxCommand,
|
|
11
|
+
} from "../src/gjc-runtime/tmux-common";
|
|
12
|
+
import { forceCloseGjcTmuxSession, removeGjcTmuxSession, statusGjcTmuxSession } from "../src/gjc-runtime/tmux-sessions";
|
|
13
|
+
|
|
14
|
+
const tmux = resolveGjcTmuxCommand(process.env);
|
|
15
|
+
|
|
16
|
+
function sh(args: string[]): { code: number; err: string } {
|
|
17
|
+
const r = Bun.spawnSync([tmux, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
18
|
+
return { code: r.exitCode, err: r.stderr.toString().trim() };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeRawSession(name: string): void {
|
|
22
|
+
const r = sh(["new-session", "-d", "-s", name, "sleep 600"]);
|
|
23
|
+
if (r.code !== 0) throw new Error(`failed to create tmux session ${name}: ${r.err}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tagAsGjc(name: string, sessionId?: string): void {
|
|
27
|
+
const target = buildGjcTmuxExactOptionTarget(name);
|
|
28
|
+
for (const cmd of buildGjcTmuxProfileCommands(target, process.env, { sessionId })) {
|
|
29
|
+
const r = sh(cmd.args);
|
|
30
|
+
if (r.code !== 0) throw new Error(`failed to tag ${name} (${cmd.description}): ${r.err}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function exists(name: string): boolean {
|
|
35
|
+
return sh(["has-session", "-t", `=${name}`]).code === 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function killQuiet(name: string): void {
|
|
39
|
+
sh(["kill-session", "-t", `=${name}`]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
43
|
+
const live = `gjc_g004live_${suffix}`;
|
|
44
|
+
const raw = `g004raw_${suffix}`;
|
|
45
|
+
const mism = `gjc_g004mism_${suffix}`;
|
|
46
|
+
const cleanup: string[] = [live, raw, mism];
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// 1. GJC-managed LIVE session: remove refuses, force-close hard-kills.
|
|
50
|
+
makeRawSession(live);
|
|
51
|
+
tagAsGjc(live, "sess-g004");
|
|
52
|
+
const status = statusGjcTmuxSession(live);
|
|
53
|
+
assert.equal(status.profile, "1", "session must be recognized as GJC-managed");
|
|
54
|
+
assert.ok(status.panePids.length > 0, "session must have a live pane (sleep)");
|
|
55
|
+
console.log(`[g004] live GJC session up: ${live} panePids=${status.panePids.length}`);
|
|
56
|
+
|
|
57
|
+
let removeRefused = false;
|
|
58
|
+
try {
|
|
59
|
+
removeGjcTmuxSession(live);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
removeRefused = /gjc_tmux_session_live/.test(String(e));
|
|
62
|
+
}
|
|
63
|
+
assert.ok(removeRefused, "removeGjcTmuxSession must REFUSE a live pane");
|
|
64
|
+
console.log("[g004] remove refused live session (expected)");
|
|
65
|
+
|
|
66
|
+
const closed = forceCloseGjcTmuxSession(live, process.env, "sess-g004");
|
|
67
|
+
assert.equal(closed.name, live);
|
|
68
|
+
assert.ok(!exists(live), "force-close must hard-kill the live GJC session");
|
|
69
|
+
console.log("[g004] force-close hard-killed the live GJC session (id-matched)");
|
|
70
|
+
|
|
71
|
+
// 2. Non-GJC (untagged) session: force-close must refuse.
|
|
72
|
+
makeRawSession(raw);
|
|
73
|
+
let notManaged = false;
|
|
74
|
+
try {
|
|
75
|
+
forceCloseGjcTmuxSession(raw, process.env);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
notManaged = /gjc_tmux_session_(not_managed|not_found)/.test(String(e));
|
|
78
|
+
}
|
|
79
|
+
assert.ok(notManaged, "force-close must refuse a non-GJC tmux session");
|
|
80
|
+
assert.ok(exists(raw), "non-GJC session must be left untouched");
|
|
81
|
+
killQuiet(raw);
|
|
82
|
+
console.log("[g004] force-close refused + preserved non-GJC session (expected)");
|
|
83
|
+
|
|
84
|
+
// 3. GJC session with a MISMATCHED expected session id: must refuse.
|
|
85
|
+
makeRawSession(mism);
|
|
86
|
+
tagAsGjc(mism, "sess-real");
|
|
87
|
+
let idMismatch = false;
|
|
88
|
+
try {
|
|
89
|
+
forceCloseGjcTmuxSession(mism, process.env, "sess-WRONG");
|
|
90
|
+
} catch (e) {
|
|
91
|
+
idMismatch = /gjc_tmux_session_id_mismatch/.test(String(e));
|
|
92
|
+
}
|
|
93
|
+
assert.ok(idMismatch, "force-close must refuse on session-id mismatch");
|
|
94
|
+
assert.ok(exists(mism), "mismatched session must be left untouched");
|
|
95
|
+
console.log("[g004] force-close refused on session-id mismatch (expected)");
|
|
96
|
+
|
|
97
|
+
console.log("[g004] PASS: forceCloseGjcTmuxSession verified against live tmux");
|
|
98
|
+
} finally {
|
|
99
|
+
for (const name of cleanup) killQuiet(name);
|
|
100
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// G005 real end-to-end daemon-orchestration smoke.
|
|
2
|
+
//
|
|
3
|
+
// Drives the lifecycle orchestrator's create -> close path with REAL effects:
|
|
4
|
+
// a real fsynced file-backed idempotency ledger, a real audit JSONL, and a real
|
|
5
|
+
// tmux session spawned/closed via the actual tmux helpers. Proves the daemon
|
|
6
|
+
// orchestration turns a `session_create` frame into a genuinely-spawned,
|
|
7
|
+
// GJC-tagged tmux session and a `session_close` frame into a real hard-close,
|
|
8
|
+
// with idempotent re-ack. Not part of the unit suite.
|
|
9
|
+
import assert from "node:assert";
|
|
10
|
+
import * as crypto from "node:crypto";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
buildGjcTmuxExactOptionTarget,
|
|
17
|
+
buildGjcTmuxProfileCommands,
|
|
18
|
+
resolveGjcTmuxCommand,
|
|
19
|
+
} from "../src/gjc-runtime/tmux-common";
|
|
20
|
+
import { forceCloseGjcTmuxSession, statusGjcTmuxSession } from "../src/gjc-runtime/tmux-sessions";
|
|
21
|
+
import type { SessionCloseFrame, SessionCreateFrame } from "../src/notifications/index";
|
|
22
|
+
import {
|
|
23
|
+
type AuditEvent,
|
|
24
|
+
handleLifecycleRequest,
|
|
25
|
+
type LedgerDoc,
|
|
26
|
+
type LedgerStore,
|
|
27
|
+
type OrchestratorDeps,
|
|
28
|
+
} from "../src/notifications/lifecycle-orchestrator";
|
|
29
|
+
|
|
30
|
+
const tmux = resolveGjcTmuxCommand(process.env);
|
|
31
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "gjc-g005-"));
|
|
32
|
+
const ledgerPath = path.join(tmpRoot, "idempotency.json");
|
|
33
|
+
const auditPath = path.join(tmpRoot, "audit.jsonl");
|
|
34
|
+
const created: string[] = [];
|
|
35
|
+
|
|
36
|
+
function sh(args: string[]): number {
|
|
37
|
+
return Bun.spawnSync([tmux, ...args], { stdout: "pipe", stderr: "pipe" }).exitCode;
|
|
38
|
+
}
|
|
39
|
+
function exists(name: string): boolean {
|
|
40
|
+
return sh(["has-session", "-t", `=${name}`]) === 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Real fsynced + atomic file ledger store.
|
|
44
|
+
const store: LedgerStore = {
|
|
45
|
+
async read(): Promise<LedgerDoc> {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(ledgerPath, "utf8")) as LedgerDoc;
|
|
48
|
+
} catch {
|
|
49
|
+
return { version: 1, entries: {} };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async write(doc: LedgerDoc): Promise<void> {
|
|
53
|
+
const tmp = `${ledgerPath}.${process.pid}.tmp`;
|
|
54
|
+
const fd = fs.openSync(tmp, "w", 0o600);
|
|
55
|
+
fs.writeSync(fd, JSON.stringify(doc));
|
|
56
|
+
fs.fsyncSync(fd);
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
fs.renameSync(tmp, ledgerPath);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const auditLines: AuditEvent[] = [];
|
|
63
|
+
const deps: OrchestratorDeps = {
|
|
64
|
+
pairedChatId: "42",
|
|
65
|
+
now: () => Date.now(),
|
|
66
|
+
store,
|
|
67
|
+
audit: e => {
|
|
68
|
+
auditLines.push(e);
|
|
69
|
+
fs.appendFileSync(auditPath, `${JSON.stringify(e)}\n`, { mode: 0o600 });
|
|
70
|
+
},
|
|
71
|
+
allowCreate: () => true,
|
|
72
|
+
writeStartupPrompt: async (requestId, prompt) => {
|
|
73
|
+
if (prompt === undefined) return undefined;
|
|
74
|
+
const ref = path.join(tmpRoot, `prompt-${requestId}`);
|
|
75
|
+
const fd = fs.openSync(ref, "w", 0o600);
|
|
76
|
+
fs.writeSync(fd, prompt);
|
|
77
|
+
fs.fsyncSync(fd);
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
return ref;
|
|
80
|
+
},
|
|
81
|
+
// REAL spawn: create a GJC-tagged tmux session carrying the authoritative id.
|
|
82
|
+
spawnCreate: async (_frame, ids) => {
|
|
83
|
+
const name = `gjc_g005_${ids.intendedSessionId}`;
|
|
84
|
+
created.push(name);
|
|
85
|
+
if (sh(["new-session", "-d", "-s", name, "sleep 600"]) !== 0) {
|
|
86
|
+
throw new Error(`tmux spawn failed for ${name}`);
|
|
87
|
+
}
|
|
88
|
+
const target = buildGjcTmuxExactOptionTarget(name);
|
|
89
|
+
for (const cmd of buildGjcTmuxProfileCommands(target, process.env, {
|
|
90
|
+
sessionId: ids.intendedSessionId,
|
|
91
|
+
})) {
|
|
92
|
+
if (sh(cmd.args) !== 0) throw new Error(`tag failed for ${name}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
sessionId: ids.intendedSessionId,
|
|
96
|
+
tmuxSession: name,
|
|
97
|
+
endpointUrl: "ws://127.0.0.1:0",
|
|
98
|
+
topicThreadId: "1",
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
// REAL close: hard-close the GJC-managed tmux session, id-matched.
|
|
102
|
+
closeSession: async target => {
|
|
103
|
+
forceCloseGjcTmuxSession(target.tmuxSession ?? "", process.env, target.sessionId);
|
|
104
|
+
return { processGone: !exists(target.tmuxSession ?? "") };
|
|
105
|
+
},
|
|
106
|
+
resumeSession: async () => ({ ambiguous: [] }),
|
|
107
|
+
newLifecycleRequestId: () => `lc-${crypto.randomUUID()}`,
|
|
108
|
+
newSessionId: () => `s${crypto.randomUUID().slice(0, 8)}`,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const createFrame: SessionCreateFrame = {
|
|
112
|
+
type: "session_create",
|
|
113
|
+
requestId: "lc_g005",
|
|
114
|
+
lifecycleRequestId: "lc_g005",
|
|
115
|
+
intendedSessionId: `g005${Date.now().toString(36)}`,
|
|
116
|
+
updateId: 100,
|
|
117
|
+
chatId: "42",
|
|
118
|
+
token: "control-token",
|
|
119
|
+
target: { kind: "existing_path", path: tmpRoot },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
async function main(): Promise<void> {
|
|
123
|
+
// 1. CREATE -> real tmux session spawned + GJC-tagged with the authoritative id.
|
|
124
|
+
const createOut = await handleLifecycleRequest(createFrame, deps);
|
|
125
|
+
assert.equal(createOut.status, "ok", "create must succeed");
|
|
126
|
+
const session = createOut.status === "ok" ? createOut.entry.tmuxSession! : "";
|
|
127
|
+
assert.ok(exists(session), "real tmux session must exist after create");
|
|
128
|
+
const status = statusGjcTmuxSession(session);
|
|
129
|
+
assert.equal(status.profile, "1", "spawned session must be GJC-managed");
|
|
130
|
+
assert.equal(status.sessionId, createFrame.intendedSessionId, "authoritative session id propagated to tmux tag");
|
|
131
|
+
console.log(`[g005] CREATE -> live tmux session ${session} (id=${status.sessionId})`);
|
|
132
|
+
|
|
133
|
+
// 2. Idempotent re-ack: same updateId+body must NOT spawn a second session.
|
|
134
|
+
const before = created.length;
|
|
135
|
+
const dupOut = await handleLifecycleRequest(createFrame, deps);
|
|
136
|
+
assert.equal(dupOut.status, "ok", "duplicate create re-acks ok");
|
|
137
|
+
assert.equal(created.length, before, "duplicate create must NOT spawn a second session");
|
|
138
|
+
console.log("[g005] DUPLICATE create re-acked, no second spawn (idempotent)");
|
|
139
|
+
|
|
140
|
+
// 3. CLOSE -> real hard-close of the GJC-managed session, id-matched.
|
|
141
|
+
const closeFrame: SessionCloseFrame = {
|
|
142
|
+
type: "session_close",
|
|
143
|
+
requestId: "lc_g005_close",
|
|
144
|
+
updateId: 101,
|
|
145
|
+
chatId: "42",
|
|
146
|
+
token: "control-token",
|
|
147
|
+
target: { sessionId: createFrame.intendedSessionId, tmuxSession: session },
|
|
148
|
+
force: true,
|
|
149
|
+
};
|
|
150
|
+
const closeOut = await handleLifecycleRequest(closeFrame, deps);
|
|
151
|
+
assert.equal(closeOut.status, "ok", "close must succeed");
|
|
152
|
+
assert.ok(!exists(session), "real tmux session must be gone after close");
|
|
153
|
+
console.log(`[g005] CLOSE -> hard-closed ${session} (id-matched)`);
|
|
154
|
+
|
|
155
|
+
// 4. Durable ledger + audit redaction.
|
|
156
|
+
const doc = JSON.parse(fs.readFileSync(ledgerPath, "utf8")) as LedgerDoc;
|
|
157
|
+
assert.equal(doc.entries["42:100"]?.state, "success");
|
|
158
|
+
assert.equal(doc.entries["42:101"]?.state, "success");
|
|
159
|
+
const auditBlob = fs.readFileSync(auditPath, "utf8");
|
|
160
|
+
assert.ok(!auditBlob.includes("control-token"), "audit must never contain the control token");
|
|
161
|
+
assert.ok(
|
|
162
|
+
auditLines.some(a => a.event === "spawn_started"),
|
|
163
|
+
"audit records spawn_started",
|
|
164
|
+
);
|
|
165
|
+
console.log("[g005] durable fsynced ledger + token-redacted audit verified");
|
|
166
|
+
|
|
167
|
+
console.log("[g005] PASS: real create->close daemon orchestration over live tmux");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main()
|
|
171
|
+
.then(() => {
|
|
172
|
+
for (const n of created) sh(["kill-session", "-t", `=${n}`]);
|
|
173
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
174
|
+
process.exit(0);
|
|
175
|
+
})
|
|
176
|
+
.catch(err => {
|
|
177
|
+
for (const n of created) sh(["kill-session", "-t", `=${n}`]);
|
|
178
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
179
|
+
console.error("[g005] FAIL", err);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// G011 real daemon-path smoke.
|
|
2
|
+
//
|
|
3
|
+
// Drives a PARSED /session_* command through the LIVE native
|
|
4
|
+
// NotificationControlServer + a real loopback WebSocket client (NO injected/fake
|
|
5
|
+
// seams), wired to the real orchestrator (with stubbed spawn/close effects so the
|
|
6
|
+
// focus is the authenticated wire path, not tmux). Asserts:
|
|
7
|
+
// - a wrong-token handshake is rejected (control token gates the endpoint);
|
|
8
|
+
// - a valid frame is forwarded with the control token STRIPPED from payloadJson;
|
|
9
|
+
// - the host response is routed back to the client by requestId;
|
|
10
|
+
// - control discovery exists while running and is removed after stop.
|
|
11
|
+
import assert from "node:assert";
|
|
12
|
+
import * as crypto from "node:crypto";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import WebSocket from "ws";
|
|
17
|
+
|
|
18
|
+
// Import the WORKTREE native build directly (not @gajae-code/natives, which can
|
|
19
|
+
// resolve to a different checkout in this dev environment).
|
|
20
|
+
import { NotificationControlServer } from "../../natives/native/index.js";
|
|
21
|
+
import { parseLifecycleCommand } from "../src/notifications/lifecycle-commands";
|
|
22
|
+
import { attachLifecycleControl, fileAudit, fileLedgerStore } from "../src/notifications/lifecycle-control-runtime";
|
|
23
|
+
import type { OrchestratorDeps } from "../src/notifications/lifecycle-orchestrator";
|
|
24
|
+
|
|
25
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gjc-g011-"));
|
|
26
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
27
|
+
const ownerId = "daemon-g011";
|
|
28
|
+
const closed: string[] = [];
|
|
29
|
+
|
|
30
|
+
function deps(): OrchestratorDeps {
|
|
31
|
+
return {
|
|
32
|
+
pairedChatId: "42",
|
|
33
|
+
now: () => Date.now(),
|
|
34
|
+
store: fileLedgerStore(path.join(tmp, "idempotency.json")),
|
|
35
|
+
audit: fileAudit(path.join(tmp, "audit.jsonl")),
|
|
36
|
+
allowCreate: () => true,
|
|
37
|
+
writeStartupPrompt: async () => undefined,
|
|
38
|
+
// Stubbed effects: this smoke proves the WIRE path, not tmux (covered by g005).
|
|
39
|
+
spawnCreate: async (_f, ids) => ({
|
|
40
|
+
sessionId: ids.intendedSessionId,
|
|
41
|
+
tmuxSession: `gjc-${ids.intendedSessionId}`,
|
|
42
|
+
endpointUrl: "ws://127.0.0.1:0",
|
|
43
|
+
topicThreadId: "1",
|
|
44
|
+
}),
|
|
45
|
+
closeSession: async t => {
|
|
46
|
+
closed.push(t.sessionId);
|
|
47
|
+
return { processGone: true };
|
|
48
|
+
},
|
|
49
|
+
resumeSession: async () => ({ ambiguous: [] }),
|
|
50
|
+
newLifecycleRequestId: () => `lc-${crypto.randomUUID()}`,
|
|
51
|
+
newSessionId: () => `s${crypto.randomUUID().slice(0, 8)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function send(ws: WebSocket, frame: unknown): void {
|
|
56
|
+
ws.send(JSON.stringify(frame));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main(): Promise<void> {
|
|
60
|
+
const server = new NotificationControlServer(token, ownerId, tmp);
|
|
61
|
+
|
|
62
|
+
// Wire the real orchestrator to the real native control server.
|
|
63
|
+
const d = deps();
|
|
64
|
+
attachLifecycleControl(server as never, d);
|
|
65
|
+
|
|
66
|
+
const ep = (await server.start()) as { url: string };
|
|
67
|
+
assert.ok(ep.url.startsWith("ws://127.0.0.1:"), `loopback url, got ${ep.url}`);
|
|
68
|
+
const controlJson = path.join(tmp, "notifications", "control.json");
|
|
69
|
+
assert.ok(fs.existsSync(controlJson), "control discovery must exist while running");
|
|
70
|
+
// The control discovery file MUST NOT persist the privileged control token:
|
|
71
|
+
// the daemon holds it in memory and is the only client.
|
|
72
|
+
const controlRaw = fs.readFileSync(controlJson, "utf8");
|
|
73
|
+
assert.ok(!controlRaw.includes(token), "control token MUST NOT be persisted in control.json");
|
|
74
|
+
assert.ok(
|
|
75
|
+
!("token" in (JSON.parse(controlRaw) as Record<string, unknown>)),
|
|
76
|
+
"control.json must omit any token field",
|
|
77
|
+
);
|
|
78
|
+
console.log(`[g011] live control endpoint ${ep.url}; discovery present, no persisted token`);
|
|
79
|
+
|
|
80
|
+
// 1. Wrong-token handshake must be rejected.
|
|
81
|
+
await new Promise<void>(resolve => {
|
|
82
|
+
const bad = new WebSocket(`${ep.url}/?token=wrong`);
|
|
83
|
+
bad.on("open", () => {
|
|
84
|
+
throw new Error("wrong-token handshake must NOT open");
|
|
85
|
+
});
|
|
86
|
+
bad.on("error", () => {
|
|
87
|
+
console.log("[g011] wrong-token handshake rejected (expected)");
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 2. Valid client: send a PARSED /session_close command through the real wire.
|
|
93
|
+
const parsed = parseLifecycleCommand("/session_close sess-g011");
|
|
94
|
+
assert.equal(parsed.kind, "close", "parser must yield a close command");
|
|
95
|
+
const requestId = "lc-g011-1";
|
|
96
|
+
const frame = {
|
|
97
|
+
type: "session_close",
|
|
98
|
+
requestId,
|
|
99
|
+
updateId: 1,
|
|
100
|
+
chatId: "42",
|
|
101
|
+
token,
|
|
102
|
+
target: parsed.kind === "close" ? parsed.target : { sessionId: "sess-g011" },
|
|
103
|
+
force: true,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const response = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
107
|
+
const ws = new WebSocket(`${ep.url}/?token=${token}`);
|
|
108
|
+
const timer = setTimeout(() => reject(new Error("timed out")), 5000);
|
|
109
|
+
ws.on("open", () => send(ws, frame));
|
|
110
|
+
ws.on("message", data => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
ws.close();
|
|
113
|
+
resolve(JSON.parse(String(data)) as Record<string, unknown>);
|
|
114
|
+
});
|
|
115
|
+
ws.on("error", reject);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.equal(response.type, "session_close_response", "response routed back to client");
|
|
119
|
+
assert.equal(response.requestId, requestId, "response correlated by requestId");
|
|
120
|
+
assert.equal(response.sessionId, "sess-g011");
|
|
121
|
+
assert.deepEqual(closed, ["sess-g011"], "orchestrator close effect ran exactly once");
|
|
122
|
+
console.log("[g011] parsed command -> real wire -> orchestrator -> routed response OK");
|
|
123
|
+
|
|
124
|
+
// 3. The real orchestrator path must NOT leak the control token into its
|
|
125
|
+
// durable audit log or idempotency ledger (g002 separately proves the native
|
|
126
|
+
// payloadJson boundary is token-stripped).
|
|
127
|
+
for (const f of ["audit.jsonl", "idempotency.json"]) {
|
|
128
|
+
const p = path.join(tmp, f);
|
|
129
|
+
if (fs.existsSync(p)) {
|
|
130
|
+
assert.ok(!fs.readFileSync(p, "utf8").includes(token), `control token leaked into ${f}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log("[g011] no control-token leak in audit/ledger on the real path");
|
|
134
|
+
|
|
135
|
+
// 4. Stop removes control discovery.
|
|
136
|
+
server.stop();
|
|
137
|
+
await new Promise(r => setTimeout(r, 50));
|
|
138
|
+
assert.ok(!fs.existsSync(controlJson), "control discovery removed after stop");
|
|
139
|
+
console.log("[g011] control discovery removed after stop");
|
|
140
|
+
|
|
141
|
+
console.log("[g011] PASS: real daemon-path (parse -> native control endpoint -> orchestrator -> reply)");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main()
|
|
145
|
+
.then(() => {
|
|
146
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
147
|
+
process.exit(0);
|
|
148
|
+
})
|
|
149
|
+
.catch(err => {
|
|
150
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
151
|
+
console.error("[g011] FAIL", err);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
});
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { APP_NAME, getProjectDir } from "@gajae-code/utils";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { resolveOrDefaultProjectRegistryPath } from "../discovery/helpers";
|
|
10
|
+
import { installGjcPluginBundle, isGjcPluginBundleSource, readRegistry } from "../extensibility/gjc-plugins";
|
|
10
11
|
import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
|
|
11
12
|
import {
|
|
12
13
|
getInstalledPluginsRegistryPath,
|
|
@@ -48,6 +49,8 @@ export interface PluginCommandArgs {
|
|
|
48
49
|
disable?: string;
|
|
49
50
|
set?: string;
|
|
50
51
|
scope?: "user" | "project";
|
|
52
|
+
user?: boolean;
|
|
53
|
+
project?: boolean;
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -109,6 +112,10 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
|
|
|
109
112
|
result.flags.dryRun = true;
|
|
110
113
|
} else if (arg === "-l" || arg === "--local") {
|
|
111
114
|
result.flags.local = true;
|
|
115
|
+
} else if (arg === "--user") {
|
|
116
|
+
result.flags.user = true;
|
|
117
|
+
} else if (arg === "--project") {
|
|
118
|
+
result.flags.project = true;
|
|
112
119
|
} else if (arg === "--enable" && i + 1 < args.length) {
|
|
113
120
|
result.flags.enable = args[++i];
|
|
114
121
|
} else if (arg === "--disable" && i + 1 < args.length) {
|
|
@@ -345,7 +352,14 @@ async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]):
|
|
|
345
352
|
async function handleInstall(
|
|
346
353
|
manager: PluginManager,
|
|
347
354
|
packages: string[],
|
|
348
|
-
flags: {
|
|
355
|
+
flags: {
|
|
356
|
+
json?: boolean;
|
|
357
|
+
force?: boolean;
|
|
358
|
+
dryRun?: boolean;
|
|
359
|
+
scope?: "user" | "project";
|
|
360
|
+
user?: boolean;
|
|
361
|
+
project?: boolean;
|
|
362
|
+
},
|
|
349
363
|
): Promise<void> {
|
|
350
364
|
if (packages.length === 0) {
|
|
351
365
|
console.error(chalk.red(`Usage: ${APP_NAME} plugin install <package[@version]>[features] ...`));
|
|
@@ -360,6 +374,32 @@ async function handleInstall(
|
|
|
360
374
|
const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
|
|
361
375
|
|
|
362
376
|
for (const spec of packages) {
|
|
377
|
+
// GJC plugin bundle classifier: a source containing gajae-plugin.json (or a
|
|
378
|
+
// git/tarball source) routes to the bundle installer BEFORE marketplace/npm.
|
|
379
|
+
if (await isGjcPluginBundleSource(spec)) {
|
|
380
|
+
if (flags.user === flags.project) {
|
|
381
|
+
console.error(
|
|
382
|
+
chalk.red(`GJC plugin bundle install requires exactly one of --user or --project for "${spec}".`),
|
|
383
|
+
);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const scope: "user" | "project" = flags.user ? "user" : "project";
|
|
387
|
+
try {
|
|
388
|
+
const res = await installGjcPluginBundle(spec, { scope, cwd: process.cwd(), force: flags.force });
|
|
389
|
+
if (flags.json) {
|
|
390
|
+
console.log(JSON.stringify({ name: res.entry.name, status: res.status, scope }, null, 2));
|
|
391
|
+
} else {
|
|
392
|
+
console.log(
|
|
393
|
+
chalk.green(`${theme.status.success} ${res.status} GJC plugin ${res.entry.name} (${scope})`),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error(chalk.red(`${theme.status.error} Failed to install GJC plugin ${spec}: ${err}`));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
363
403
|
const target = classifyInstallTarget(spec, knownMarketplaces);
|
|
364
404
|
|
|
365
405
|
if (target.type === "marketplace") {
|
|
@@ -462,13 +502,16 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
|
|
|
462
502
|
const npmPlugins = await manager.list();
|
|
463
503
|
const mktMgr = await makeMarketplaceManager();
|
|
464
504
|
const mktPlugins = await mktMgr.listInstalledPlugins();
|
|
505
|
+
const cwd = getProjectDir();
|
|
506
|
+
const [gjcUser, gjcProject] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
|
|
507
|
+
const gjcBundles = [...gjcUser.plugins, ...gjcProject.plugins];
|
|
465
508
|
|
|
466
509
|
if (flags.json) {
|
|
467
|
-
console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2));
|
|
510
|
+
console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins, gjc: gjcBundles }, null, 2));
|
|
468
511
|
return;
|
|
469
512
|
}
|
|
470
513
|
|
|
471
|
-
if (npmPlugins.length === 0 && mktPlugins.length === 0) {
|
|
514
|
+
if (npmPlugins.length === 0 && mktPlugins.length === 0 && gjcBundles.length === 0) {
|
|
472
515
|
console.log(chalk.dim("No plugins installed"));
|
|
473
516
|
console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install <package>`));
|
|
474
517
|
return;
|
|
@@ -510,6 +553,26 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
|
|
|
510
553
|
console.log(` ${plugin.id} (${version})${scopeLabel}${shadowLabel}`);
|
|
511
554
|
}
|
|
512
555
|
}
|
|
556
|
+
|
|
557
|
+
if (gjcBundles.length > 0) {
|
|
558
|
+
if (npmPlugins.length > 0 || mktPlugins.length > 0) console.log();
|
|
559
|
+
console.log(chalk.bold("GJC Plugin Bundles:\n"));
|
|
560
|
+
for (const plugin of gjcBundles) {
|
|
561
|
+
const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
|
|
562
|
+
const scopeLabel = chalk.dim(` (${plugin.scope})`);
|
|
563
|
+
const disabledCount = plugin.disabledSurfaceIds.length;
|
|
564
|
+
const quarantineCount = plugin.quarantine?.length ?? 0;
|
|
565
|
+
const detail = [
|
|
566
|
+
disabledCount > 0 ? `${disabledCount} disabled` : null,
|
|
567
|
+
quarantineCount > 0 ? `${quarantineCount} quarantined` : null,
|
|
568
|
+
]
|
|
569
|
+
.filter((v): v is string => Boolean(v))
|
|
570
|
+
.join(", ");
|
|
571
|
+
console.log(
|
|
572
|
+
`${status} ${plugin.name}@${plugin.version}${scopeLabel}${detail ? chalk.dim(` — ${detail}`) : ""}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
513
576
|
}
|
|
514
577
|
|
|
515
578
|
async function handleLink(manager: PluginManager, paths: string[], flags: { json?: boolean }): Promise<void> {
|
package/src/cli.ts
CHANGED
|
@@ -64,7 +64,7 @@ async function showHelp(config: CliConfig): Promise<void> {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function installRuntimeGlobals(): Promise<void> {
|
|
67
|
-
const
|
|
67
|
+
const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
|
|
68
68
|
// Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
|
|
69
69
|
// discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
|
|
70
70
|
// can't toggle from JS, so we patch globalThis.fetch to pass
|
|
@@ -75,7 +75,24 @@ async function installRuntimeGlobals(): Promise<void> {
|
|
|
75
75
|
// Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
|
|
76
76
|
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
|
|
77
77
|
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
|
|
78
|
-
|
|
78
|
+
delete process.env.MallocStackLogging;
|
|
79
|
+
delete process.env.MallocStackLoggingNoCompact;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
|
|
83
|
+
for (const arg of argv) {
|
|
84
|
+
if (isSubcommand(arg)) return false;
|
|
85
|
+
if (flags.includes(arg)) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasRootHelpFlag(argv: string[]): boolean {
|
|
91
|
+
return hasRootFastFlag(argv, rootHelpFlags);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasRootVersionFlag(argv: string[]): boolean {
|
|
95
|
+
return hasRootFastFlag(argv, versionFlags);
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
class RootHelpCommand extends Command {
|
|
@@ -195,7 +212,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
195
212
|
await runSmokeTest();
|
|
196
213
|
return;
|
|
197
214
|
}
|
|
198
|
-
if (
|
|
215
|
+
if (hasRootHelpFlag(argv)) {
|
|
199
216
|
const { renderRootHelp } = await import("@gajae-code/utils/cli");
|
|
200
217
|
const { getExtraHelpText } = await import("./cli/fast-help");
|
|
201
218
|
renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
|
|
@@ -205,7 +222,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
205
222
|
}
|
|
206
223
|
return;
|
|
207
224
|
}
|
|
208
|
-
if (
|
|
225
|
+
if (hasRootVersionFlag(argv)) {
|
|
209
226
|
process.stdout.write(`${APP_NAME}/${VERSION}\n`);
|
|
210
227
|
return;
|
|
211
228
|
}
|
package/src/commands/plugin.ts
CHANGED
|
@@ -49,6 +49,8 @@ export default class Plugin extends Command {
|
|
|
49
49
|
description: 'Install scope: "user" (default) or "project"',
|
|
50
50
|
options: ["user", "project"],
|
|
51
51
|
}),
|
|
52
|
+
user: Flags.boolean({ description: "Install GJC plugin bundle into the user root" }),
|
|
53
|
+
project: Flags.boolean({ description: "Install GJC plugin bundle into the project root" }),
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
async run(): Promise<void> {
|
|
@@ -69,6 +71,8 @@ export default class Plugin extends Command {
|
|
|
69
71
|
disable: flags.disable,
|
|
70
72
|
set: flags.set,
|
|
71
73
|
scope: flags.scope as "user" | "project" | undefined,
|
|
74
|
+
user: flags.user,
|
|
75
|
+
project: flags.project,
|
|
72
76
|
},
|
|
73
77
|
};
|
|
74
78
|
|