mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-07 08:31:00 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0537417793 | ||
|
|
86c3419210 |
No files matched your search
@@ -280,3 +280,76 @@ test("mergeSnippetImportPayload keeps existing empty snippet packages", () => {
|
||||
|
||||
assert.deepEqual(result.snippetPackages, ["local-empty", "imported", "imported/ops"]);
|
||||
});
|
||||
|
||||
test("snippet export and import preserve the all-hosts scope", () => {
|
||||
const exported = buildSnippetExportPayload({
|
||||
snippets: [
|
||||
snippet({
|
||||
id: "all-hosts",
|
||||
label: "Patch everything",
|
||||
command: "apt-get update",
|
||||
targetsAllHosts: true,
|
||||
}),
|
||||
],
|
||||
snippetPackages: [],
|
||||
exportedAt: "2026-10-11T00:00:00.000Z",
|
||||
});
|
||||
|
||||
// New exports carry the all-hosts scope, so they must advertise a version
|
||||
// that pre-change releases do not silently accept while dropping the field.
|
||||
assert.equal(exported.version, 3);
|
||||
const parsed = parseSnippetImportPayload(JSON.stringify(exported));
|
||||
const result = mergeSnippetImportPayload({
|
||||
existingSnippets: [],
|
||||
existingSnippetPackages: [],
|
||||
payload: parsed,
|
||||
conflictAction: "skip",
|
||||
createId: () => "imported-all-hosts",
|
||||
});
|
||||
|
||||
assert.equal(exported.snippets[0].targetsAllHosts, true);
|
||||
assert.equal("targets" in exported.snippets[0], false);
|
||||
assert.equal(parsed.snippets[0].targetsAllHosts, true);
|
||||
assert.equal(result.snippets[0].targetsAllHosts, true);
|
||||
assert.deepEqual(result.snippets[0].targets, []);
|
||||
assert.equal(result.snippets[0].targetGroups, undefined);
|
||||
});
|
||||
|
||||
test("snippet import resolves all-hosts scope against dynamic groups", () => {
|
||||
const parsed = parseSnippetImportPayload(JSON.stringify({
|
||||
kind: "netcatty.snippets",
|
||||
version: 2,
|
||||
exportedAt: "2026-10-11T00:00:00.000Z",
|
||||
snippetPackages: [],
|
||||
snippets: [
|
||||
{
|
||||
label: "All hosts beats groups",
|
||||
command: "uptime",
|
||||
targetGroups: ["Production"],
|
||||
targetsAllHosts: true,
|
||||
},
|
||||
{
|
||||
label: "Explicit false stays unscoped",
|
||||
command: "whoami",
|
||||
targetsAllHosts: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
assert.equal(parsed.snippets[0].targetsAllHosts, true);
|
||||
assert.equal(parsed.snippets[0].targetGroups, undefined);
|
||||
assert.deepEqual(parsed.snippets[0].targetGroups ?? [], []);
|
||||
assert.deepEqual([...(parsed.snippets[0].tags ?? [])], []);
|
||||
assert.equal(parsed.snippets[1].targetsAllHosts, undefined);
|
||||
const merged = mergeSnippetImportPayload({
|
||||
existingSnippets: [],
|
||||
existingSnippetPackages: [],
|
||||
payload: parsed,
|
||||
conflictAction: "skip",
|
||||
createId: () => "import-1",
|
||||
});
|
||||
assert.equal(merged.snippets[0].targetsAllHosts, true);
|
||||
assert.deepEqual(merged.snippets[0].targetGroups, undefined);
|
||||
assert.equal(merged.snippets[1].targetsAllHosts, undefined);
|
||||
assert.deepEqual(merged.snippets[1].targetGroups, undefined);
|
||||
});
|
||||
+46
-11
@@ -4,7 +4,11 @@ import { normalizeVaultOrder } from "./vaultOrder";
|
||||
import { normalizeGroupTargetPaths } from "./hostGroupPathMutations";
|
||||
|
||||
export const SNIPPET_EXPORT_KIND = "netcatty.snippets" as const;
|
||||
export const SNIPPET_EXPORT_VERSION = 2 as const;
|
||||
// Version 3 introduced the portable all-hosts scope (targetsAllHosts); older
|
||||
// releases silently drop that field when they accept a version-2 payload, so
|
||||
// new exports advertise 3 while imports still accept versions 1 and 2.
|
||||
export const SNIPPET_EXPORT_VERSION = 3 as const;
|
||||
export const SNIPPET_EXPORT_VERSION_PREVIOUS = 2 as const;
|
||||
export const SNIPPET_EXPORT_VERSION_LEGACY = 1 as const;
|
||||
|
||||
export type SnippetImportConflictAction = "skip" | "overwrite";
|
||||
@@ -15,6 +19,7 @@ export type SnippetExportItem = {
|
||||
tags?: string[];
|
||||
package?: string;
|
||||
targetGroups?: string[];
|
||||
targetsAllHosts?: boolean;
|
||||
shortkey?: string;
|
||||
noAutoRun?: boolean;
|
||||
multiLineRunMode?: Snippet["multiLineRunMode"];
|
||||
@@ -27,7 +32,10 @@ export type SnippetExportItem = {
|
||||
|
||||
export type SnippetExportPayload = {
|
||||
kind: typeof SNIPPET_EXPORT_KIND;
|
||||
version: typeof SNIPPET_EXPORT_VERSION | typeof SNIPPET_EXPORT_VERSION_LEGACY;
|
||||
version:
|
||||
| typeof SNIPPET_EXPORT_VERSION
|
||||
| typeof SNIPPET_EXPORT_VERSION_PREVIOUS
|
||||
| typeof SNIPPET_EXPORT_VERSION_LEGACY;
|
||||
exportedAt: string;
|
||||
snippetPackages: string[];
|
||||
snippets: SnippetExportItem[];
|
||||
@@ -88,12 +96,20 @@ const mergeSnippetPackagePaths = (...groups: string[][]): string[] => {
|
||||
return uniqueStrings(paths);
|
||||
};
|
||||
|
||||
/**
|
||||
* Concrete host-id bindings (targets) are intentionally omitted for portability;
|
||||
* the portable all-hosts scope rides along and suppresses group scope on import.
|
||||
*/
|
||||
const toExportItem = (snippet: Snippet): SnippetExportItem => ({
|
||||
label: snippet.label,
|
||||
command: snippet.command,
|
||||
tags: Array.isArray(snippet.tags) ? [...snippet.tags] : [],
|
||||
package: snippet.package || "",
|
||||
...(snippet.targetGroups !== undefined ? { targetGroups: [...snippet.targetGroups] } : {}),
|
||||
...(snippet.targetsAllHosts
|
||||
? { targetsAllHosts: true }
|
||||
: snippet.targetGroups !== undefined
|
||||
? { targetGroups: [...snippet.targetGroups] }
|
||||
: {}),
|
||||
...(snippet.shortkey ? { shortkey: snippet.shortkey } : {}),
|
||||
...(snippet.noAutoRun ? { noAutoRun: snippet.noAutoRun } : {}),
|
||||
...(snippet.multiLineRunMode ? { multiLineRunMode: snippet.multiLineRunMode } : {}),
|
||||
@@ -134,6 +150,8 @@ const sanitizeImportItem = (value: unknown): SnippetExportItem | null => {
|
||||
const label = typeof value.label === "string" && value.label.trim()
|
||||
? value.label.trim()
|
||||
: fallbackLabel(value.command);
|
||||
// "Apply to all hosts" is a portable scope; when set it wins over group scope.
|
||||
const targetsAllHosts = value.targetsAllHosts === true;
|
||||
return {
|
||||
label,
|
||||
command: value.command,
|
||||
@@ -141,11 +159,14 @@ const sanitizeImportItem = (value: unknown): SnippetExportItem | null => {
|
||||
? uniqueStrings(value.tags)
|
||||
: [],
|
||||
package: typeof value.package === "string" ? value.package.trim() : "",
|
||||
targetGroups: Array.isArray(value.targetGroups)
|
||||
? normalizeGroupTargetPaths(value.targetGroups.filter(
|
||||
(entry): entry is string => typeof entry === "string",
|
||||
))
|
||||
: undefined,
|
||||
targetGroups: targetsAllHosts
|
||||
? undefined
|
||||
: Array.isArray(value.targetGroups)
|
||||
? normalizeGroupTargetPaths(value.targetGroups.filter(
|
||||
(entry): entry is string => typeof entry === "string",
|
||||
))
|
||||
: undefined,
|
||||
targetsAllHosts: targetsAllHosts || undefined,
|
||||
shortkey: typeof value.shortkey === "string" && value.shortkey.trim()
|
||||
? value.shortkey.trim()
|
||||
: undefined,
|
||||
@@ -174,7 +195,11 @@ const parseSnippetImportObject = (parsed: Record<string, unknown>): SnippetExpor
|
||||
if (parsed.kind !== SNIPPET_EXPORT_KIND) {
|
||||
throw new Error("Unsupported snippet import file.");
|
||||
}
|
||||
if (version !== SNIPPET_EXPORT_VERSION && version !== SNIPPET_EXPORT_VERSION_LEGACY) {
|
||||
if (
|
||||
version !== SNIPPET_EXPORT_VERSION &&
|
||||
version !== SNIPPET_EXPORT_VERSION_PREVIOUS &&
|
||||
version !== SNIPPET_EXPORT_VERSION_LEGACY
|
||||
) {
|
||||
throw new Error("Unsupported snippet import file.");
|
||||
}
|
||||
if (!Array.isArray(parsed.snippets)) {
|
||||
@@ -187,7 +212,12 @@ const parseSnippetImportObject = (parsed: Record<string, unknown>): SnippetExpor
|
||||
|
||||
return {
|
||||
kind: SNIPPET_EXPORT_KIND,
|
||||
version: version === SNIPPET_EXPORT_VERSION ? SNIPPET_EXPORT_VERSION : SNIPPET_EXPORT_VERSION_LEGACY,
|
||||
version:
|
||||
version === SNIPPET_EXPORT_VERSION
|
||||
? SNIPPET_EXPORT_VERSION
|
||||
: version === SNIPPET_EXPORT_VERSION_PREVIOUS
|
||||
? SNIPPET_EXPORT_VERSION_PREVIOUS
|
||||
: SNIPPET_EXPORT_VERSION_LEGACY,
|
||||
exportedAt: typeof parsed.exportedAt === "string" ? parsed.exportedAt : "",
|
||||
snippetPackages: collectSnippetPackagePaths(
|
||||
snippets,
|
||||
@@ -244,8 +274,13 @@ const toImportedSnippet = (item: SnippetExportItem, id: string, order?: number):
|
||||
command: item.command,
|
||||
tags: item.tags || [],
|
||||
package: item.package || "",
|
||||
// Host-id bindings are never imported; the all-hosts flag wins over group scope.
|
||||
targets: [],
|
||||
...(item.targetGroups !== undefined ? { targetGroups: [...item.targetGroups] } : {}),
|
||||
...(item.targetsAllHosts
|
||||
? { targetsAllHosts: true }
|
||||
: item.targetGroups !== undefined
|
||||
? { targetGroups: [...item.targetGroups] }
|
||||
: {}),
|
||||
...(item.shortkey ? { shortkey: item.shortkey } : {}),
|
||||
...(item.noAutoRun ? { noAutoRun: item.noAutoRun } : {}),
|
||||
...(item.multiLineRunMode ? { multiLineRunMode: item.multiLineRunMode } : {}),
|
||||
|
||||
Reference in new issue
Block a user