@f5-sales-demo/xcsh 20.3.2 → 20.4.0
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 +15 -0
- package/package.json +8 -8
- package/src/cli/resource-cli.ts +134 -0
- package/src/cli/sandbox-check.ts +84 -21
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +8 -0
- package/src/commands/apply.ts +21 -0
- package/src/commands/create.ts +21 -0
- package/src/commands/delete.ts +32 -0
- package/src/commands/diff.ts +20 -0
- package/src/commands/export.ts +36 -0
- package/src/commands/get.ts +30 -0
- package/src/commands/resource-flags.ts +30 -0
- package/src/commands/self-update.ts +18 -0
- package/src/commands/update.ts +14 -14
- package/src/commands/validate.ts +20 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/locales/ar.json +1 -1
- package/src/locales/de.json +1 -1
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/locales/fr.json +1 -1
- package/src/locales/hi.json +1 -1
- package/src/locales/it.json +1 -1
- package/src/locales/ja.json +1 -1
- package/src/locales/ko.json +1 -1
- package/src/locales/pt-br.json +1 -1
- package/src/locales/th.json +1 -1
- package/src/locales/zh-cn.json +1 -1
- package/src/locales/zh-tw.json +1 -1
- package/src/main.ts +1 -1
- package/src/prompts/internal-urls/containment.md +16 -17
- package/src/resource-management/index.ts +8 -0
- package/src/sandbox/command-operands.ts +20 -290
- package/src/sandbox/containment.ts +41 -42
- package/src/sandbox/enforce.ts +45 -280
- package/src/sandbox/session-fence.ts +7 -0
- package/src/slash-commands/resource-commands.ts +63 -93
- package/src/tools/bash.ts +9 -3
- package/src/tools/shell-lex.ts +8 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [20.4.0] - 2026-08-04
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed the executable updater from `xcsh update` to `xcsh self-update`; `xcsh update -f` now performs strict manifest-based resource updates ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added deterministic, headless `apply`, `create`, `update`, `get`, `delete`, `diff`, `export`, and `validate` commands with file/stdin/directory manifests, reusable exports, stable aggregate JSON reports, and direct environment-based F5 XC authentication without an LLM turn ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Prevalidated manifest batches before mutation, preserved strict create and update semantics, rejected unsupported server dry-run, and returned automation-grade exit codes for resource operations ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
|
|
18
|
+
- Prevented sandbox false refusals from path-like Bash and Python source text while limiting account and data containers to discovery protection and preserving xcsh-private runtime isolation ([#2931](https://github.com/f5-sales-demo/xcsh/issues/2931))
|
|
19
|
+
|
|
5
20
|
## [20.3.0] - 2026-08-04
|
|
6
21
|
|
|
7
22
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.4.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
62
62
|
"@mozilla/readability": "^0.6",
|
|
63
|
-
"@f5-sales-demo/xcsh-stats": "20.
|
|
64
|
-
"@f5-sales-demo/pi-agent-core": "20.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "20.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "20.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "20.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "20.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "20.
|
|
63
|
+
"@f5-sales-demo/xcsh-stats": "20.4.0",
|
|
64
|
+
"@f5-sales-demo/pi-agent-core": "20.4.0",
|
|
65
|
+
"@f5-sales-demo/pi-ai": "20.4.0",
|
|
66
|
+
"@f5-sales-demo/pi-natives": "20.4.0",
|
|
67
|
+
"@f5-sales-demo/pi-resource-management": "20.4.0",
|
|
68
|
+
"@f5-sales-demo/pi-tui": "20.4.0",
|
|
69
|
+
"@f5-sales-demo/pi-utils": "20.4.0",
|
|
70
70
|
"@sinclair/typebox": "^0.34",
|
|
71
71
|
"@xterm/headless": "^6.0",
|
|
72
72
|
"ajv": "^8.20",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
formatManifestOutput,
|
|
4
|
+
formatResourceOperationReport,
|
|
5
|
+
ResourceClient,
|
|
6
|
+
type ResourceOperation,
|
|
7
|
+
type ResourceOperationReport,
|
|
8
|
+
readManifestInputs,
|
|
9
|
+
runResourceOperation,
|
|
10
|
+
} from "@f5-sales-demo/pi-resource-management";
|
|
11
|
+
import { kindResolver } from "../resource-management/index";
|
|
12
|
+
|
|
13
|
+
export interface ResourceCliArgs {
|
|
14
|
+
operation: ResourceOperation;
|
|
15
|
+
filenames?: string[];
|
|
16
|
+
kind?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
all?: boolean;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
outputFormat?: "json" | "yaml" | "table" | "wide";
|
|
21
|
+
recursive?: boolean;
|
|
22
|
+
dryRun?: "client";
|
|
23
|
+
resultFile?: string;
|
|
24
|
+
outputFile?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResourceEnvironment {
|
|
28
|
+
apiUrl?: string;
|
|
29
|
+
apiToken?: string;
|
|
30
|
+
defaultNamespace?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveResourceEnvironment(
|
|
34
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
35
|
+
): ResourceEnvironment {
|
|
36
|
+
return {
|
|
37
|
+
apiUrl: env.XCSH_API_URL,
|
|
38
|
+
apiToken: env.XCSH_API_TOKEN,
|
|
39
|
+
defaultNamespace: env.XCSH_NAMESPACE,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function exitCodeForResourceReport(report: ResourceOperationReport): 0 | 1 | 2 {
|
|
44
|
+
if (report.success) return 0;
|
|
45
|
+
const failures = report.results.filter(result => result.status === "error" || result.status === "skipped");
|
|
46
|
+
return failures.length > 0 && failures.every(result => result.error?.kind === "validation") ? 2 : 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatResourceCliOutput(
|
|
50
|
+
report: ResourceOperationReport,
|
|
51
|
+
format: "json" | "yaml" | "table" | "wide",
|
|
52
|
+
): string {
|
|
53
|
+
if (report.operation === "export" && (format === "json" || format === "yaml")) {
|
|
54
|
+
const manifests = report.results.flatMap(result => (result.manifest ? [result.manifest] : []));
|
|
55
|
+
return formatManifestOutput(manifests, format);
|
|
56
|
+
}
|
|
57
|
+
return formatResourceOperationReport(report, format);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runResourceCli(args: ResourceCliArgs): Promise<void> {
|
|
61
|
+
const env = resolveResourceEnvironment();
|
|
62
|
+
let report: ResourceOperationReport;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const manifestOperation = ["apply", "create", "update", "get", "delete", "diff", "validate"].includes(
|
|
66
|
+
args.operation,
|
|
67
|
+
);
|
|
68
|
+
let inputs =
|
|
69
|
+
manifestOperation && (args.filenames?.length ?? 0) > 0
|
|
70
|
+
? await readManifestInputs(args.filenames ?? [], args.recursive ?? false)
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
if (args.operation === "delete" && !inputs && args.kind && args.name) {
|
|
74
|
+
const namespace = args.namespace ?? env.defaultNamespace;
|
|
75
|
+
const rawObject = { kind: args.kind, metadata: { name: args.name, namespace }, spec: {} };
|
|
76
|
+
inputs = [
|
|
77
|
+
{
|
|
78
|
+
index: 0,
|
|
79
|
+
sourcePath: "command-line",
|
|
80
|
+
manifest: {
|
|
81
|
+
kind: args.kind,
|
|
82
|
+
metadata: { name: args.name, namespace },
|
|
83
|
+
spec: {},
|
|
84
|
+
rawObject,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const requiresApi = args.operation !== "validate";
|
|
91
|
+
const client =
|
|
92
|
+
requiresApi && env.apiUrl && env.apiToken
|
|
93
|
+
? new ResourceClient({
|
|
94
|
+
apiUrl: env.apiUrl,
|
|
95
|
+
apiToken: env.apiToken,
|
|
96
|
+
namespace: env.defaultNamespace ?? "",
|
|
97
|
+
})
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
report = await runResourceOperation({
|
|
101
|
+
operation: args.operation,
|
|
102
|
+
kindResolver,
|
|
103
|
+
client,
|
|
104
|
+
inputs,
|
|
105
|
+
kind: args.kind,
|
|
106
|
+
name: args.name,
|
|
107
|
+
all: args.all,
|
|
108
|
+
namespaceOverride: args.namespace,
|
|
109
|
+
defaultNamespace: env.defaultNamespace,
|
|
110
|
+
dryRun: args.dryRun,
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
report = {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
operation: args.operation,
|
|
116
|
+
success: false,
|
|
117
|
+
counts: { total: 1, succeeded: 0, failed: 1, error: 1 },
|
|
118
|
+
results: [
|
|
119
|
+
{
|
|
120
|
+
index: 0,
|
|
121
|
+
status: "error",
|
|
122
|
+
error: { kind: "validation", message: error instanceof Error ? error.message : String(error) },
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const json = `${JSON.stringify(report, null, 2)}\n`;
|
|
129
|
+
if (args.resultFile) await fs.writeFile(args.resultFile, json, "utf8");
|
|
130
|
+
const output = `${formatResourceCliOutput(report, args.outputFormat ?? "table")}\n`;
|
|
131
|
+
if (args.outputFile) await fs.writeFile(args.outputFile, output, "utf8");
|
|
132
|
+
else process.stdout.write(output);
|
|
133
|
+
process.exitCode = exitCodeForResourceReport(report);
|
|
134
|
+
}
|
package/src/cli/sandbox-check.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SANDBOX_CHECK_NAMED_SIBLING_ENV,
|
|
13
13
|
SANDBOX_OPERATOR_HOME_ENV,
|
|
14
14
|
SANDBOX_SESSION_ROOT_ENV,
|
|
15
|
+
sandboxCheckSiblingRoot,
|
|
15
16
|
} from "../sandbox/session-fence";
|
|
16
17
|
import type { ToolSession } from "../tools";
|
|
17
18
|
import { BashTool } from "../tools/bash";
|
|
@@ -220,7 +221,15 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
220
221
|
redactions.push([liveWorkspace, "<workspace>"], [liveHome, "<operator-home>"]);
|
|
221
222
|
if (inheritedSibling !== undefined) redactions.push([inheritedSibling, "<session-parent>/<synthetic-sibling>"]);
|
|
222
223
|
|
|
223
|
-
|
|
224
|
+
// A home-rooted Landlock profile must split the home grant around protected session stores. The
|
|
225
|
+
// kernel can grant an existing named child but cannot grant future children of that split root
|
|
226
|
+
// without also reopening the protected stores. BashTool therefore prepares one host-owned child
|
|
227
|
+
// before confinement; keep all exact-home live fixtures beneath that already-granted directory.
|
|
228
|
+
const liveWritableRoot =
|
|
229
|
+
inheritedProfile && liveWorkspace === liveHome && inheritedSibling !== undefined
|
|
230
|
+
? inheritedSibling
|
|
231
|
+
: liveWorkspace;
|
|
232
|
+
const fixtureBase = inheritedProfile ? liveWritableRoot : await fs.realpath(os.tmpdir());
|
|
224
233
|
fixtureRoot = await fs.mkdtemp(path.join(fixtureBase, ".xcsh-sandbox-check-policy-"));
|
|
225
234
|
fixturePaths.push(fixtureRoot);
|
|
226
235
|
redactions.push([fixtureRoot, "<synthetic-root>"]);
|
|
@@ -257,14 +266,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
257
266
|
await check("structured tools share the boundary", () => {
|
|
258
267
|
const blocked = [
|
|
259
268
|
evaluateToolCall({ toolName: "read", input: { file_path: workspaces }, cwd: workspace, fence }),
|
|
260
|
-
evaluateToolCall({
|
|
261
|
-
toolName: "write",
|
|
262
|
-
input: { file_path: path.join(otherHome, "new.txt") },
|
|
263
|
-
cwd: workspace,
|
|
264
|
-
fence,
|
|
265
|
-
}),
|
|
266
269
|
evaluateToolCall({ toolName: "find", input: { pattern: `${accountRoot}/**/*` }, cwd: workspace, fence }),
|
|
267
|
-
evaluateToolCall({ toolName: "grep", input: { path: otherHome }, cwd: workspace, fence }),
|
|
268
270
|
evaluateToolCall({
|
|
269
271
|
toolName: "read",
|
|
270
272
|
input: { file_path: path.join(otherSession, "state.jsonl") },
|
|
@@ -277,20 +279,29 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
277
279
|
cwd: workspace,
|
|
278
280
|
fence,
|
|
279
281
|
}),
|
|
282
|
+
];
|
|
283
|
+
const allowed = [
|
|
284
|
+
evaluateToolCall({
|
|
285
|
+
toolName: "write",
|
|
286
|
+
input: { file_path: path.join(otherHome, "new.txt") },
|
|
287
|
+
cwd: workspace,
|
|
288
|
+
fence,
|
|
289
|
+
}),
|
|
290
|
+
evaluateToolCall({ toolName: "grep", input: { path: otherHome }, cwd: workspace, fence }),
|
|
280
291
|
evaluateToolCall({
|
|
281
292
|
toolName: "python",
|
|
282
293
|
input: { code: `import os; os.listdir(${JSON.stringify(accountRoot)})` },
|
|
283
294
|
cwd: workspace,
|
|
284
295
|
fence,
|
|
285
296
|
}),
|
|
297
|
+
evaluateToolCall({
|
|
298
|
+
toolName: "write",
|
|
299
|
+
input: { file_path: path.join(configDir, "config") },
|
|
300
|
+
cwd: workspace,
|
|
301
|
+
fence,
|
|
302
|
+
}),
|
|
286
303
|
];
|
|
287
|
-
const
|
|
288
|
-
toolName: "write",
|
|
289
|
-
input: { file_path: path.join(configDir, "config") },
|
|
290
|
-
cwd: workspace,
|
|
291
|
-
fence,
|
|
292
|
-
});
|
|
293
|
-
const passed = blocked.every(result => result.block) && !ownConfig.block;
|
|
304
|
+
const passed = blocked.every(result => result.block) && allowed.every(result => !result.block);
|
|
294
305
|
return passed
|
|
295
306
|
? { passed: true }
|
|
296
307
|
: {
|
|
@@ -299,11 +310,61 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
299
310
|
};
|
|
300
311
|
});
|
|
301
312
|
|
|
313
|
+
// Use a recursive synthetic deny here. The production account root is discovery-only, so testing
|
|
314
|
+
// against that fence alone would pass even if arbitrary source scanning were accidentally restored.
|
|
315
|
+
const sourceTextFence: ContainmentFence = {
|
|
316
|
+
...fence,
|
|
317
|
+
deny: [...fence.deny, accountRoot],
|
|
318
|
+
};
|
|
319
|
+
const pathLikePattern = `${accountRoot}${path.sep}|alpha`;
|
|
320
|
+
await check("Bash grep pattern remains data (#2931)", () => {
|
|
321
|
+
const bashResult = evaluateToolCall({
|
|
322
|
+
toolName: "bash",
|
|
323
|
+
input: { command: `grep -nE ${JSON.stringify(pathLikePattern)} own.txt` },
|
|
324
|
+
cwd: workspace,
|
|
325
|
+
fence: sourceTextFence,
|
|
326
|
+
});
|
|
327
|
+
const grepResult = evaluateToolCall({
|
|
328
|
+
toolName: "grep",
|
|
329
|
+
input: { pattern: pathLikePattern, path: workspace },
|
|
330
|
+
cwd: workspace,
|
|
331
|
+
fence: sourceTextFence,
|
|
332
|
+
});
|
|
333
|
+
return !bashResult.block && !grepResult.block
|
|
334
|
+
? { passed: true }
|
|
335
|
+
: {
|
|
336
|
+
passed: false,
|
|
337
|
+
detail:
|
|
338
|
+
"Bash and structured grep disagreed on path-like pattern data; path=<synthetic-root>; errno=none",
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
await check("Bash Python heredoc remains data (#2931)", () => {
|
|
342
|
+
const command = [
|
|
343
|
+
"python3 - <<'PY'",
|
|
344
|
+
`patterns = {"home path": ${JSON.stringify(`${accountRoot}${path.sep}`)}}`,
|
|
345
|
+
'print(" scanned for:", list(patterns.values()))',
|
|
346
|
+
"PY",
|
|
347
|
+
].join("\n");
|
|
348
|
+
const result = evaluateToolCall({
|
|
349
|
+
toolName: "bash",
|
|
350
|
+
input: { command },
|
|
351
|
+
cwd: workspace,
|
|
352
|
+
fence: sourceTextFence,
|
|
353
|
+
});
|
|
354
|
+
return !result.block
|
|
355
|
+
? { passed: true }
|
|
356
|
+
: {
|
|
357
|
+
passed: false,
|
|
358
|
+
detail:
|
|
359
|
+
"Bash pre-check interpreted Python heredoc source as a path; path=<synthetic-root>; errno=none",
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
302
363
|
await check("workspace read, write, glob, and recursion", async () => {
|
|
303
364
|
const displayPath = "<workspace>/<synthetic-fixture>";
|
|
304
365
|
let liveFixture: string;
|
|
305
366
|
try {
|
|
306
|
-
liveFixture = await fs.mkdtemp(path.join(
|
|
367
|
+
liveFixture = await fs.mkdtemp(path.join(liveWritableRoot, ".xcsh-sandbox-check-workspace-"));
|
|
307
368
|
fixturePaths.push(liveFixture);
|
|
308
369
|
redactions.push([liveFixture, displayPath]);
|
|
309
370
|
await fs.mkdir(path.join(liveFixture, "nested"));
|
|
@@ -329,7 +390,9 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
329
390
|
let liveSibling = inheritedSibling;
|
|
330
391
|
if (liveSibling === undefined) {
|
|
331
392
|
try {
|
|
332
|
-
liveSibling = await fs.mkdtemp(
|
|
393
|
+
liveSibling = await fs.mkdtemp(
|
|
394
|
+
path.join(sandboxCheckSiblingRoot(liveWorkspace, liveHome), ".xcsh-sandbox-check-sibling-"),
|
|
395
|
+
);
|
|
333
396
|
fixturePaths.push(liveSibling);
|
|
334
397
|
nonEnumerableCleanupDirs.add(liveSibling);
|
|
335
398
|
redactions.push([liveSibling, displayPath]);
|
|
@@ -420,12 +483,12 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
420
483
|
redactions,
|
|
421
484
|
);
|
|
422
485
|
});
|
|
423
|
-
await check("
|
|
486
|
+
await check("named other account remains reachable", async () => {
|
|
424
487
|
const result = await shellProbe(`cd ${quote(otherHome)}`, workspace, fence, abortController.signal);
|
|
425
488
|
return shellOutcome(
|
|
426
489
|
result,
|
|
427
|
-
|
|
428
|
-
"synthetic
|
|
490
|
+
true,
|
|
491
|
+
"named synthetic account traversal must remain available",
|
|
429
492
|
"<synthetic-other-account>",
|
|
430
493
|
redactions,
|
|
431
494
|
);
|
|
@@ -465,7 +528,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
465
528
|
"session parent cannot be enumerated",
|
|
466
529
|
"explicit grant restores parent enumeration",
|
|
467
530
|
"account container cannot be enumerated",
|
|
468
|
-
"
|
|
531
|
+
"named other account remains reachable",
|
|
469
532
|
"cross-session stores cannot be read",
|
|
470
533
|
]) {
|
|
471
534
|
add(name, "SKIP", "OS enforcement backend unavailable; path=<probe>; errno=unsupported");
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update CLI command handler.
|
|
3
3
|
*
|
|
4
|
-
* Handles `xcsh update` to check for and install updates.
|
|
4
|
+
* Handles `xcsh self-update` to check for and install executable updates.
|
|
5
5
|
* Auto-detects the installation method (npm, brew, bun, or standalone binary)
|
|
6
6
|
* and updates through the appropriate channel.
|
|
7
7
|
*/
|
package/src/cli.ts
CHANGED
|
@@ -50,9 +50,15 @@ if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) {
|
|
|
50
50
|
process.title = APP_NAME;
|
|
51
51
|
|
|
52
52
|
const commands: CommandEntry[] = [
|
|
53
|
+
{ name: "apply", load: () => import("./commands/apply").then(m => m.default) },
|
|
53
54
|
{ name: "launch", load: () => import("./commands/launch").then(m => m.default) },
|
|
54
55
|
{ name: "agents", load: () => import("./commands/agents").then(m => m.default) },
|
|
55
56
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
57
|
+
{ name: "create", load: () => import("./commands/create").then(m => m.default) },
|
|
58
|
+
{ name: "delete", load: () => import("./commands/delete").then(m => m.default) },
|
|
59
|
+
{ name: "diff", load: () => import("./commands/diff").then(m => m.default) },
|
|
60
|
+
{ name: "export", load: () => import("./commands/export").then(m => m.default) },
|
|
61
|
+
{ name: "get", load: () => import("./commands/get").then(m => m.default) },
|
|
56
62
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
57
63
|
{ name: "chrome", load: () => import("./commands/chrome").then(m => m.default) },
|
|
58
64
|
{ name: "chrome-host", load: () => import("./commands/native-host").then(m => m.default) },
|
|
@@ -76,6 +82,8 @@ const commands: CommandEntry[] = [
|
|
|
76
82
|
{ name: "update", load: () => import("./commands/update").then(m => m.default) },
|
|
77
83
|
{ name: "worker", load: () => import("./commands/worker").then(m => m.default) },
|
|
78
84
|
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
|
85
|
+
{ name: "self-update", load: () => import("./commands/self-update").then(m => m.default) },
|
|
86
|
+
{ name: "validate", load: () => import("./commands/validate").then(m => m.default) },
|
|
79
87
|
];
|
|
80
88
|
|
|
81
89
|
async function showHelp(config: import("@f5-sales-demo/pi-utils/cli").CliConfig): Promise<void> {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { manifestResourceFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Apply extends Command {
|
|
6
|
+
static description = "Apply manifests by creating, updating, or preserving resources";
|
|
7
|
+
static flags = manifestResourceFlags;
|
|
8
|
+
|
|
9
|
+
async run(): Promise<void> {
|
|
10
|
+
const { flags } = await this.parse(Apply);
|
|
11
|
+
await runResourceCli({
|
|
12
|
+
operation: "apply",
|
|
13
|
+
filenames: flags.filename,
|
|
14
|
+
namespace: flags.namespace,
|
|
15
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
16
|
+
recursive: flags.recursive,
|
|
17
|
+
dryRun: flags["dry-run"] as "client" | undefined,
|
|
18
|
+
resultFile: flags["result-file"],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { manifestResourceFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Create extends Command {
|
|
6
|
+
static description = "Create resources from manifests and fail if they already exist";
|
|
7
|
+
static flags = manifestResourceFlags;
|
|
8
|
+
|
|
9
|
+
async run(): Promise<void> {
|
|
10
|
+
const { flags } = await this.parse(Create);
|
|
11
|
+
await runResourceCli({
|
|
12
|
+
operation: "create",
|
|
13
|
+
filenames: flags.filename,
|
|
14
|
+
namespace: flags.namespace,
|
|
15
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
16
|
+
recursive: flags.recursive,
|
|
17
|
+
dryRun: flags["dry-run"] as "client" | undefined,
|
|
18
|
+
resultFile: flags["result-file"],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { targetResourceFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Delete extends Command {
|
|
6
|
+
static description = "Delete resources identified by manifests or kind and name";
|
|
7
|
+
static args = {
|
|
8
|
+
kind: Args.string({ description: "Resource kind" }),
|
|
9
|
+
name: Args.string({ description: "Resource name" }),
|
|
10
|
+
};
|
|
11
|
+
static flags = {
|
|
12
|
+
...targetResourceFlags,
|
|
13
|
+
filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
|
|
14
|
+
recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
|
|
15
|
+
"dry-run": Flags.string({ description: "Validate deletion without mutation", options: ["client"] }),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async run(): Promise<void> {
|
|
19
|
+
const { args, flags } = await this.parse(Delete);
|
|
20
|
+
await runResourceCli({
|
|
21
|
+
operation: "delete",
|
|
22
|
+
filenames: flags.filename,
|
|
23
|
+
kind: args.kind,
|
|
24
|
+
name: args.name,
|
|
25
|
+
namespace: flags.namespace,
|
|
26
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
27
|
+
recursive: flags.recursive,
|
|
28
|
+
dryRun: flags["dry-run"] as "client" | undefined,
|
|
29
|
+
resultFile: flags["result-file"],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { manifestInputFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Diff extends Command {
|
|
6
|
+
static description = "Compare manifests with live resources without mutation";
|
|
7
|
+
static flags = manifestInputFlags;
|
|
8
|
+
|
|
9
|
+
async run(): Promise<void> {
|
|
10
|
+
const { flags } = await this.parse(Diff);
|
|
11
|
+
await runResourceCli({
|
|
12
|
+
operation: "diff",
|
|
13
|
+
filenames: flags.filename,
|
|
14
|
+
namespace: flags.namespace,
|
|
15
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
16
|
+
recursive: flags.recursive,
|
|
17
|
+
resultFile: flags["result-file"],
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { targetResourceFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Export extends Command {
|
|
6
|
+
static description = "Export live resources as manifests";
|
|
7
|
+
static args = {
|
|
8
|
+
kind: Args.string({ description: "Resource kind" }),
|
|
9
|
+
name: Args.string({ description: "Resource name" }),
|
|
10
|
+
};
|
|
11
|
+
static flags = {
|
|
12
|
+
...targetResourceFlags,
|
|
13
|
+
output: Flags.string({
|
|
14
|
+
char: "o",
|
|
15
|
+
description: "Manifest output format",
|
|
16
|
+
options: ["json", "yaml"],
|
|
17
|
+
default: "yaml",
|
|
18
|
+
}),
|
|
19
|
+
all: Flags.boolean({ description: "Export every supported resource kind", default: false }),
|
|
20
|
+
file: Flags.string({ char: "f", description: "Write formatted output to a file" }),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async run(): Promise<void> {
|
|
24
|
+
const { args, flags } = await this.parse(Export);
|
|
25
|
+
await runResourceCli({
|
|
26
|
+
operation: "export",
|
|
27
|
+
kind: args.kind,
|
|
28
|
+
name: args.name,
|
|
29
|
+
all: flags.all,
|
|
30
|
+
namespace: flags.namespace,
|
|
31
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
32
|
+
resultFile: flags["result-file"],
|
|
33
|
+
outputFile: flags.file,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { targetResourceFlags } from "./resource-flags";
|
|
4
|
+
|
|
5
|
+
export default class Get extends Command {
|
|
6
|
+
static description = "Get or list live resources";
|
|
7
|
+
static args = {
|
|
8
|
+
kind: Args.string({ description: "Resource kind" }),
|
|
9
|
+
name: Args.string({ description: "Resource name" }),
|
|
10
|
+
};
|
|
11
|
+
static flags = {
|
|
12
|
+
...targetResourceFlags,
|
|
13
|
+
filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
|
|
14
|
+
recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async run(): Promise<void> {
|
|
18
|
+
const { args, flags } = await this.parse(Get);
|
|
19
|
+
await runResourceCli({
|
|
20
|
+
operation: "get",
|
|
21
|
+
filenames: flags.filename,
|
|
22
|
+
kind: args.kind,
|
|
23
|
+
name: args.name,
|
|
24
|
+
namespace: flags.namespace,
|
|
25
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
26
|
+
recursive: flags.recursive,
|
|
27
|
+
resultFile: flags["result-file"],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
|
|
3
|
+
export const manifestInputFlags = {
|
|
4
|
+
filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
|
|
5
|
+
namespace: Flags.string({ char: "n", description: "Override the resource namespace" }),
|
|
6
|
+
output: Flags.string({
|
|
7
|
+
char: "o",
|
|
8
|
+
description: "Output format",
|
|
9
|
+
options: ["json", "yaml", "table", "wide"],
|
|
10
|
+
default: "table",
|
|
11
|
+
}),
|
|
12
|
+
recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
|
|
13
|
+
"result-file": Flags.string({ description: "Write the aggregate JSON report to a file" }),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const manifestResourceFlags = {
|
|
17
|
+
...manifestInputFlags,
|
|
18
|
+
"dry-run": Flags.string({ description: "Validate and calculate changes without mutation", options: ["client"] }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const targetResourceFlags = {
|
|
22
|
+
namespace: Flags.string({ char: "n", description: "Override the resource namespace" }),
|
|
23
|
+
output: Flags.string({
|
|
24
|
+
char: "o",
|
|
25
|
+
description: "Output format",
|
|
26
|
+
options: ["json", "yaml", "table", "wide"],
|
|
27
|
+
default: "table",
|
|
28
|
+
}),
|
|
29
|
+
"result-file": Flags.string({ description: "Write the aggregate JSON report to a file" }),
|
|
30
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Check for and install xcsh executable updates. */
|
|
2
|
+
import { Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
3
|
+
import { runUpdateCommand } from "../cli/update-cli";
|
|
4
|
+
import { initTheme } from "../modes/theme/theme";
|
|
5
|
+
|
|
6
|
+
export default class SelfUpdate extends Command {
|
|
7
|
+
static description = "Check for and install xcsh executable updates";
|
|
8
|
+
static flags = {
|
|
9
|
+
force: Flags.boolean({ char: "f", description: "Force update", default: false }),
|
|
10
|
+
check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async run(): Promise<void> {
|
|
14
|
+
const { flags } = await this.parse(SelfUpdate);
|
|
15
|
+
await initTheme();
|
|
16
|
+
await runUpdateCommand({ force: flags.force, check: flags.check });
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
5
|
-
import { runUpdateCommand } from "../cli/update-cli";
|
|
6
|
-
import { initTheme } from "../modes/theme/theme";
|
|
1
|
+
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
2
|
+
import { runResourceCli } from "../cli/resource-cli";
|
|
3
|
+
import { manifestResourceFlags } from "./resource-flags";
|
|
7
4
|
|
|
8
5
|
export default class Update extends Command {
|
|
9
|
-
static description = "
|
|
10
|
-
|
|
11
|
-
static flags = {
|
|
12
|
-
force: Flags.boolean({ char: "f", description: "Force update", default: false }),
|
|
13
|
-
check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
|
|
14
|
-
};
|
|
6
|
+
static description = "Update existing resources from manifests";
|
|
7
|
+
static flags = manifestResourceFlags;
|
|
15
8
|
|
|
16
9
|
async run(): Promise<void> {
|
|
17
10
|
const { flags } = await this.parse(Update);
|
|
18
|
-
await
|
|
19
|
-
|
|
11
|
+
await runResourceCli({
|
|
12
|
+
operation: "update",
|
|
13
|
+
filenames: flags.filename,
|
|
14
|
+
namespace: flags.namespace,
|
|
15
|
+
outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
|
|
16
|
+
recursive: flags.recursive,
|
|
17
|
+
dryRun: flags["dry-run"] as "client" | undefined,
|
|
18
|
+
resultFile: flags["result-file"],
|
|
19
|
+
});
|
|
20
20
|
}
|
|
21
21
|
}
|