@notis_ai/cli 0.2.0-beta.178.1 → 0.2.0-beta.1792022492092.1
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/README.md +548 -98
- package/config/portal-route-roots.json +47 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +32901 -5934
- package/dist/base-skills/notis-apps/SKILL.md +76 -51
- package/dist/base-skills/notis-apps/references/architecture.md +116 -164
- package/dist/base-skills/notis-apps/references/design.md +26 -107
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +52 -74
- package/dist/base-skills/notis-apps/references/release.md +105 -96
- package/dist/base-skills/notis-apps/references/sdk.md +115 -60
- package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
- package/dist/base-skills/notis-apps/references/views.md +145 -0
- package/dist/base-skills/notis-cli/SKILL.md +29 -12
- package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +6 -5
- package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
- package/dist/base-skills/notis-query/SKILL.md +34 -60
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -59
- package/dist/base-skills/notis-query/references/documents.md +53 -39
- package/dist/base-skills/notis-query/references/query.md +57 -524
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
- package/dist/sdk/src/components/DocumentPage.tsx +144 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/config.ts +4 -26
- package/dist/sdk/src/documents.ts +2 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +4 -2
- package/dist/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
- package/dist/sdk/src/hooks/useQuery.ts +7 -4
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +36 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +42 -5
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +5 -3
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +163 -13
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +96 -72
- package/dist/sdk/src/vite.ts +11 -1
- package/dist/skill-sync/index.js +571 -80
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +858 -233
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1028 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +175 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +16 -5
- package/skills/notis-apps/cli.md +323 -163
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
- package/skills/notis-onboarding/BRIEF.md +7 -3
- package/src/command-specs/apps.js +2 -1
- package/src/command-specs/auth.js +2 -2
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +2 -0
- package/src/command-specs/index.js +11 -5
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +242 -1
- package/src/command-specs/spaces.js +645 -0
- package/src/command-specs/tools.js +0 -6
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +92 -29
- package/src/runtime/app-boundary-validator.js +7 -4
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +321 -253
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +18 -1
- package/src/runtime/profiles.js +5 -107
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +26 -4
- package/src/runtime/skill-sync/index.ts +230 -35
- package/src/runtime/skill-sync/local-scanner.ts +188 -29
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/sync-plan.ts +16 -9
- package/src/runtime/skill-sync/types.ts +32 -6
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +10 -7
- package/src/runtime/sync-skills.d.ts +3 -1
- package/src/runtime/sync-skills.js +10 -2
- package/src/runtime/transport.js +8 -6
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker.js +14 -2
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +7 -10
- package/template/app/globals.css +1 -0
- package/src/command-specs/reports.js +0 -97
|
@@ -6,10 +6,13 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { promisify } from "util";
|
|
8
8
|
|
|
9
|
-
import type { CloudSkill, LocalSkill, NotisSyncState, SkillWriteOutcome } from "./types";
|
|
9
|
+
import type { BundleFile, CloudSkill, LocalSkill, NotisSyncState, SkillWriteOutcome } from "./types";
|
|
10
|
+
import { NATIVE_IDENTITY_FILE, assertNativePull, checkedNativeIdentity, isNativeSkill,
|
|
11
|
+
nativeFileDigest, nativeFrontmatterName, type NativeSkillIdentity } from './native-identity';
|
|
10
12
|
|
|
11
13
|
const HOME_DIR = os.homedir();
|
|
12
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
+
class NativeSkillIdentityError extends Error {}
|
|
13
16
|
|
|
14
17
|
export const AGENTS_DIR = path.join(HOME_DIR, ".agents");
|
|
15
18
|
export const LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
@@ -45,6 +48,9 @@ const EXCLUDED_TOP_LEVEL_ROOT_NAMES = new Set([
|
|
|
45
48
|
]);
|
|
46
49
|
|
|
47
50
|
export interface SkillSyncPaths {
|
|
51
|
+
/** Explicit process-independent home for a confined sync invocation. */
|
|
52
|
+
homeDir?: string;
|
|
53
|
+
isolatedNamespace?: string;
|
|
48
54
|
agentsDir: string;
|
|
49
55
|
syncRoot: string;
|
|
50
56
|
legacySkillsDir: string;
|
|
@@ -96,14 +102,21 @@ function sanitizePathSegment(value: string): string {
|
|
|
96
102
|
export function getSkillSyncPathsForUser(
|
|
97
103
|
authUserId: string,
|
|
98
104
|
agentsDir: string = AGENTS_DIR,
|
|
105
|
+
namespace?: string | null,
|
|
106
|
+
syncHome?: string,
|
|
99
107
|
): SkillSyncPaths {
|
|
100
108
|
const safeUserId = sanitizePathSegment(authUserId);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
109
|
+
const homeDir = syncHome ? path.resolve(syncHome) : undefined;
|
|
110
|
+
const resolvedAgentsDir = homeDir ? path.join(homeDir, '.agents') : path.resolve(agentsDir);
|
|
111
|
+
const syncRoot = homeDir ? path.join(homeDir, '.notis', 'skills') : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
|
|
112
|
+
const userRoot = namespace
|
|
113
|
+
? path.join(syncRoot, 'environments', sanitizePathSegment(namespace), 'users', safeUserId)
|
|
114
|
+
: path.join(syncRoot, "users", safeUserId);
|
|
104
115
|
const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
|
|
105
116
|
|
|
106
117
|
return {
|
|
118
|
+
...(homeDir ? { homeDir } : {}),
|
|
119
|
+
...(namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {}),
|
|
107
120
|
agentsDir: resolvedAgentsDir,
|
|
108
121
|
syncRoot,
|
|
109
122
|
legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
|
|
@@ -163,17 +176,29 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
// Interpreter caches a script run leaves beside itself are not skill content: they
|
|
180
|
+
// must neither mark a skill as changed nor ship in its bundle (app bundling applies
|
|
181
|
+
// the same rule).
|
|
182
|
+
const IGNORED_SKILL_DIRECTORIES = new Set(["__pycache__", ".pytest_cache"]);
|
|
183
|
+
|
|
184
|
+
function isIgnoredSkillEntry(entry: Dirent): boolean {
|
|
185
|
+
if (entry.name === ".DS_Store") return true;
|
|
186
|
+
if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
|
|
187
|
+
return /\.py[co]$/i.test(entry.name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function listFilesRecursive(dirPath: string, rootPath = dirPath, strictLinks = false): Promise<string[]> {
|
|
167
191
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
168
192
|
const nested = await Promise.all(
|
|
169
193
|
entries.map(async (entry) => {
|
|
170
|
-
if (entry.name ===
|
|
194
|
+
if (isIgnoredSkillEntry(entry) || (dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE)) {
|
|
171
195
|
return [];
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
const fullPath = path.join(dirPath, entry.name);
|
|
199
|
+
if (strictLinks && entry.isSymbolicLink()) throw new Error('Native Skill bundles cannot contain symbolic links.');
|
|
175
200
|
if (entry.isDirectory()) {
|
|
176
|
-
return listFilesRecursive(fullPath);
|
|
201
|
+
return listFilesRecursive(fullPath, rootPath, strictLinks);
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
if (entry.isFile()) {
|
|
@@ -326,17 +351,17 @@ function defaultTopLevelSkillSources(
|
|
|
326
351
|
{ label: "agents", root: paths.legacySkillsDir, priority: 2 },
|
|
327
352
|
{
|
|
328
353
|
label: "codex",
|
|
329
|
-
root: path.join(HOME_DIR, ".codex", "skills"),
|
|
354
|
+
root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
|
|
330
355
|
priority: 3,
|
|
331
356
|
},
|
|
332
357
|
{
|
|
333
358
|
label: "cursor",
|
|
334
|
-
root: path.join(HOME_DIR, ".cursor", "skills"),
|
|
359
|
+
root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
|
|
335
360
|
priority: 4,
|
|
336
361
|
},
|
|
337
362
|
{
|
|
338
363
|
label: "claude",
|
|
339
|
-
root: path.join(HOME_DIR, ".claude", "skills"),
|
|
364
|
+
root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
|
|
340
365
|
priority: 5,
|
|
341
366
|
},
|
|
342
367
|
);
|
|
@@ -680,6 +705,7 @@ export async function scanLocalSkills(
|
|
|
680
705
|
await ensureCanonicalSkillsDir(paths);
|
|
681
706
|
|
|
682
707
|
const sourceUrls = await readSkillLock(paths);
|
|
708
|
+
const previousState = await readSyncState(paths);
|
|
683
709
|
const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
|
|
684
710
|
const skills: Array<LocalSkill | null> = await Promise.all(
|
|
685
711
|
entries.map(async (entry) => {
|
|
@@ -692,7 +718,17 @@ export async function scanLocalSkills(
|
|
|
692
718
|
|
|
693
719
|
const skillDir = path.join(paths.skillsDir, entry.name);
|
|
694
720
|
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
695
|
-
|
|
721
|
+
let marker: string | null = null;
|
|
722
|
+
// Classify provenance BEFORE reading potentially unavailable content.
|
|
723
|
+
// Failure to scan a managed folder is not evidence that it is absent.
|
|
724
|
+
try { marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), 'utf8'); }
|
|
725
|
+
catch (error) {
|
|
726
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
727
|
+
throw new NativeSkillIdentityError('Native Skill provenance could not be read; existing files were retained.');
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference)
|
|
731
|
+
|| Object.values(previousState.nativeHistory || {}).some(row => row.names.includes(entry.name));
|
|
696
732
|
try {
|
|
697
733
|
const skillMd = await fs.readFile(skillMdPath, "utf8");
|
|
698
734
|
const { description } = parseFrontMatter(skillMd);
|
|
@@ -704,11 +740,27 @@ export async function scanLocalSkills(
|
|
|
704
740
|
folderHash,
|
|
705
741
|
directoryPath: skillDir,
|
|
706
742
|
};
|
|
743
|
+
if (marker !== null) {
|
|
744
|
+
let identity: NativeSkillIdentity;
|
|
745
|
+
try { identity = checkedNativeIdentity(JSON.parse(marker)); }
|
|
746
|
+
catch { throw new NativeSkillIdentityError('Native Skill provenance is unreadable; do not upload it as a new Skill.'); }
|
|
747
|
+
let current = true;
|
|
748
|
+
for (const [name, expected] of Object.entries(identity.files)) {
|
|
749
|
+
try { if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false; }
|
|
750
|
+
catch { current = false; }
|
|
751
|
+
}
|
|
752
|
+
const editedName = nativeFrontmatterName(skillMd);
|
|
753
|
+
Object.assign(skill, { cloudId: identity.cloudId, nativeReference: identity.reference,
|
|
754
|
+
nativeScope: identity.scope, nativeBaseFolderHash: identity.folderHash, nativeFilesCurrent: current,
|
|
755
|
+
displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName });
|
|
756
|
+
}
|
|
707
757
|
if (sourceUrls[entry.name]) {
|
|
708
758
|
skill.sourceUrl = sourceUrls[entry.name];
|
|
709
759
|
}
|
|
710
760
|
return skill;
|
|
711
|
-
} catch {
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error instanceof NativeSkillIdentityError) throw error;
|
|
763
|
+
if (knownNative) throw new NativeSkillIdentityError('Native Skill files could not be read; existing files were retained.');
|
|
712
764
|
return null;
|
|
713
765
|
}
|
|
714
766
|
}),
|
|
@@ -725,6 +777,11 @@ function normalizeSyncState(
|
|
|
725
777
|
if (!state || state.version !== 1 || typeof state.skills !== "object") {
|
|
726
778
|
return null;
|
|
727
779
|
}
|
|
780
|
+
if (state.nativeHistory && (typeof state.nativeHistory !== 'object' || Array.isArray(state.nativeHistory)
|
|
781
|
+
|| Object.values(state.nativeHistory).some(row => !row || !Array.isArray(row.names)
|
|
782
|
+
|| row.names.some(name => typeof name !== 'string')))) {
|
|
783
|
+
throw new Error('Native Skill sync provenance is unreadable; existing files were retained.');
|
|
784
|
+
}
|
|
728
785
|
const normalizeStoredAgentTargets = (
|
|
729
786
|
targets: Partial<NotisSyncState["skills"][string]["agentTargets"]> | null | undefined,
|
|
730
787
|
): NotisSyncState["skills"][string]["agentTargets"] => ({
|
|
@@ -736,6 +793,7 @@ function normalizeSyncState(
|
|
|
736
793
|
|
|
737
794
|
return {
|
|
738
795
|
version: 1,
|
|
796
|
+
...(state.nativeHistory ? { nativeHistory: state.nativeHistory } : {}),
|
|
739
797
|
lastSyncedAt:
|
|
740
798
|
typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
|
|
741
799
|
skills: Object.fromEntries(
|
|
@@ -839,22 +897,59 @@ async function createZipFromDirectory(directoryPath: string): Promise<string> {
|
|
|
839
897
|
os.tmpdir(),
|
|
840
898
|
`notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`,
|
|
841
899
|
);
|
|
842
|
-
const parentDir = path.dirname(directoryPath);
|
|
843
900
|
const directoryName = path.basename(directoryPath);
|
|
901
|
+
// Zip a copy holding exactly the files the folder hash covers, so ignored
|
|
902
|
+
// caches never ship.
|
|
903
|
+
const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
|
|
904
|
+
try {
|
|
905
|
+
const stagedDir = path.join(stagingRoot, directoryName);
|
|
906
|
+
for (const filePath of await listFilesRecursive(directoryPath)) {
|
|
907
|
+
const target = path.join(stagedDir, path.relative(directoryPath, filePath));
|
|
908
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
909
|
+
await fs.copyFile(filePath, target);
|
|
910
|
+
}
|
|
911
|
+
await fs.mkdir(stagedDir, { recursive: true });
|
|
912
|
+
|
|
913
|
+
if (process.platform === "win32") {
|
|
914
|
+
// Windows PowerShell's Compress-Archive writes entry names with
|
|
915
|
+
// backslashes, which unzip on the cloud computer as literal file names
|
|
916
|
+
// ("cached\\SKILL.md"). bsdtar, shipped in System32 since Windows 10
|
|
917
|
+
// 1803, writes the forward slashes the zip format requires. Use the
|
|
918
|
+
// system copy explicitly: on a Git Bash PATH, "tar" resolves to GNU tar,
|
|
919
|
+
// which cannot write zip archives.
|
|
920
|
+
const systemTar = path.join(
|
|
921
|
+
process.env.SystemRoot || "C:\\Windows",
|
|
922
|
+
"System32",
|
|
923
|
+
"tar.exe",
|
|
924
|
+
);
|
|
925
|
+
if (await pathExists(systemTar)) {
|
|
926
|
+
await execFileAsync(systemTar, [
|
|
927
|
+
"-c",
|
|
928
|
+
"--format",
|
|
929
|
+
"zip",
|
|
930
|
+
"-f",
|
|
931
|
+
zipPath,
|
|
932
|
+
"-C",
|
|
933
|
+
stagingRoot,
|
|
934
|
+
directoryName,
|
|
935
|
+
]);
|
|
936
|
+
return zipPath;
|
|
937
|
+
}
|
|
938
|
+
await execFileAsync("powershell.exe", [
|
|
939
|
+
"-NoProfile",
|
|
940
|
+
"-Command",
|
|
941
|
+
`Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
|
942
|
+
]);
|
|
943
|
+
return zipPath;
|
|
944
|
+
}
|
|
844
945
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
"-Command",
|
|
849
|
-
`Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
|
850
|
-
]);
|
|
946
|
+
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
947
|
+
cwd: stagingRoot,
|
|
948
|
+
});
|
|
851
949
|
return zipPath;
|
|
950
|
+
} finally {
|
|
951
|
+
await fs.rm(stagingRoot, { recursive: true, force: true });
|
|
852
952
|
}
|
|
853
|
-
|
|
854
|
-
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
855
|
-
cwd: parentDir,
|
|
856
|
-
});
|
|
857
|
-
return zipPath;
|
|
858
953
|
}
|
|
859
954
|
|
|
860
955
|
async function extractZipToDirectory(
|
|
@@ -927,13 +1022,19 @@ async function preserveUnmanagedFiles(
|
|
|
927
1022
|
existingDir: string,
|
|
928
1023
|
stagingDir: string,
|
|
929
1024
|
previouslyApplied?: readonly string[],
|
|
1025
|
+
strictConflicts = false,
|
|
930
1026
|
): Promise<void> {
|
|
931
1027
|
const managed = previouslyApplied ? new Set(previouslyApplied) : null;
|
|
932
1028
|
for (const existingFile of await listFilesRecursive(existingDir)) {
|
|
933
1029
|
const relativePath = toPosixRelativePath(existingDir, existingFile);
|
|
934
1030
|
if (managed?.has(relativePath)) continue;
|
|
935
1031
|
const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
|
|
936
|
-
if (await pathExists(stagedPath))
|
|
1032
|
+
if (await pathExists(stagedPath)) {
|
|
1033
|
+
if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
|
|
1034
|
+
throw new Error('An incoming native Skill file conflicts with a local file. Local files were retained.');
|
|
1035
|
+
}
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
937
1038
|
await fs.mkdir(path.dirname(stagedPath), { recursive: true });
|
|
938
1039
|
await fs.copyFile(existingFile, stagedPath);
|
|
939
1040
|
}
|
|
@@ -951,7 +1052,7 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
|
|
951
1052
|
async function replaceSkillDirectoryAtomically(
|
|
952
1053
|
skillDir: string,
|
|
953
1054
|
populateDir: (stagingDir: string) => Promise<void>,
|
|
954
|
-
preserve?: { previouslyApplied?: readonly string[] },
|
|
1055
|
+
preserve?: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; native?: boolean; legacyBaseline?: boolean },
|
|
955
1056
|
): Promise<void> {
|
|
956
1057
|
const parentDir = path.dirname(skillDir);
|
|
957
1058
|
const skillName = path.basename(skillDir);
|
|
@@ -970,15 +1071,44 @@ async function replaceSkillDirectoryAtomically(
|
|
|
970
1071
|
let cleanupError: Error | null = null;
|
|
971
1072
|
|
|
972
1073
|
try {
|
|
1074
|
+
if (preserve?.native && preserve.expectedFolderHash === undefined) {
|
|
1075
|
+
let existing = false;
|
|
1076
|
+
try { await fs.lstat(skillDir); existing = true; }
|
|
1077
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
1078
|
+
if (existing) throw new Error('Existing native Skill files need a readable baseline before replacement.');
|
|
1079
|
+
}
|
|
1080
|
+
if (preserve?.expectedFolderHash !== undefined
|
|
1081
|
+
&& (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
1082
|
+
throw new Error('Local Skill files changed before sync; the newer local edits were retained.');
|
|
1083
|
+
}
|
|
973
1084
|
await populateDir(stagingDir);
|
|
974
1085
|
|
|
975
1086
|
if (preserve && (await pathExists(skillDir))) {
|
|
976
|
-
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied
|
|
1087
|
+
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied,
|
|
1088
|
+
preserve.native && !preserve.legacyBaseline);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (preserve?.native) {
|
|
1092
|
+
const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
|
|
1093
|
+
const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, 'utf8')));
|
|
1094
|
+
marker.folderHash = await computeFolderHash(stagingDir);
|
|
1095
|
+
await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 0o600 });
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (preserve?.expectedFolderHash !== undefined
|
|
1099
|
+
&& (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
1100
|
+
throw new Error('Local Skill files changed during sync; the newer local edits were retained.');
|
|
977
1101
|
}
|
|
978
1102
|
|
|
979
1103
|
if (await pathExists(skillDir)) {
|
|
980
1104
|
await fs.rename(skillDir, backupDir);
|
|
981
1105
|
movedExisting = true;
|
|
1106
|
+
if (preserve?.native && preserve.expectedFolderHash === undefined) {
|
|
1107
|
+
throw new Error('Native Skill files appeared during sync; existing files were retained.');
|
|
1108
|
+
}
|
|
1109
|
+
if (preserve?.expectedFolderHash !== undefined && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
|
|
1110
|
+
throw new Error('Local Skill files changed before publication; the newer local edits were retained.');
|
|
1111
|
+
}
|
|
982
1112
|
}
|
|
983
1113
|
|
|
984
1114
|
await fs.rename(stagingDir, skillDir);
|
|
@@ -1032,17 +1162,38 @@ export async function createSkillBundleBase64(
|
|
|
1032
1162
|
}
|
|
1033
1163
|
}
|
|
1034
1164
|
|
|
1165
|
+
export async function captureNativeSkillFiles(skill: LocalSkill): Promise<BundleFile[]> {
|
|
1166
|
+
const hash = createHash('sha256');
|
|
1167
|
+
const files: BundleFile[] = [];
|
|
1168
|
+
for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
|
|
1169
|
+
const relative = path.relative(skill.directoryPath, filePath);
|
|
1170
|
+
const bytes = await fs.readFile(filePath);
|
|
1171
|
+
hash.update(relative); hash.update('\0'); hash.update(bytes); hash.update('\0');
|
|
1172
|
+
files.push({ path: relative.split(path.sep).join('/'), content_b64: bytes.toString('base64') });
|
|
1173
|
+
}
|
|
1174
|
+
if (hash.digest('hex') !== skill.folderHash) throw new Error('Local Skill files changed before upload. Sync again with the current files.');
|
|
1175
|
+
return files;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1035
1178
|
export async function writeCloudSkillToDisk(
|
|
1036
1179
|
skill: CloudSkill,
|
|
1037
1180
|
bundleBytes?: Buffer,
|
|
1038
1181
|
paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
|
|
1039
|
-
options: { previouslyApplied?: readonly string[] } = {},
|
|
1182
|
+
options: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; legacyBaseline?: boolean } = {},
|
|
1040
1183
|
): Promise<SkillWriteOutcome | false> {
|
|
1184
|
+
if (isNativeSkill(skill)) {
|
|
1185
|
+
assertNativePull({ skills: [{ ...skill, name: 'native-' + skill.id }], skill_sync_protocol: 2, sync_scope: skill.native_scope,
|
|
1186
|
+
sync_enabled: true, last_synced_at: null });
|
|
1187
|
+
if (bundleBytes?.length) throw new Error('Native Skills require their verified complete file response.');
|
|
1188
|
+
}
|
|
1041
1189
|
const skillDir = path.join(
|
|
1042
1190
|
paths.skillsDir,
|
|
1043
1191
|
safeName(skill.name, paths.skillsDir),
|
|
1044
1192
|
);
|
|
1045
|
-
const preserve = { previouslyApplied: options.previouslyApplied
|
|
1193
|
+
const preserve = { previouslyApplied: options.previouslyApplied, expectedFolderHash: options.expectedFolderHash,
|
|
1194
|
+
native: isNativeSkill(skill),
|
|
1195
|
+
// An unchanged legacy folder upgrading in place: shared files take the cloud version.
|
|
1196
|
+
legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== undefined };
|
|
1046
1197
|
|
|
1047
1198
|
if (bundleBytes?.length) {
|
|
1048
1199
|
const bundlePath = path.join(
|
|
@@ -1088,6 +1239,14 @@ export async function writeCloudSkillToDisk(
|
|
|
1088
1239
|
);
|
|
1089
1240
|
appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
|
|
1090
1241
|
}
|
|
1242
|
+
if (skill.native_reference) {
|
|
1243
|
+
const identity: NativeSkillIdentity = { schema: 1, cloudId: skill.id, reference: skill.native_reference,
|
|
1244
|
+
displayName: skill.display_name || skill.name, frontmatterName: nativeFrontmatterName(skill.skill_md || ''),
|
|
1245
|
+
scope: skill.native_scope!, folderHash: await computeFolderHash(stagingDir),
|
|
1246
|
+
files: Object.fromEntries((skill.bundle_files || []).map(file => [file.path, nativeFileDigest(Buffer.from(file.content_b64, 'base64'))])) };
|
|
1247
|
+
await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 0o600 });
|
|
1248
|
+
appliedFiles.push(NATIVE_IDENTITY_FILE);
|
|
1249
|
+
}
|
|
1091
1250
|
}, preserve);
|
|
1092
1251
|
return { written: true, appliedFiles: appliedFiles.sort() };
|
|
1093
1252
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { CloudSkill, NativeSkillReference, SyncedSkill, SyncPullResponse } from './types';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const NATIVE_IDENTITY_FILE = '.notis-native-skill.json';
|
|
5
|
+
export interface NativeSkillIdentity {
|
|
6
|
+
schema: 1;
|
|
7
|
+
cloudId: string;
|
|
8
|
+
reference: NativeSkillReference;
|
|
9
|
+
displayName: string;
|
|
10
|
+
frontmatterName: string;
|
|
11
|
+
scope: string;
|
|
12
|
+
folderHash: string;
|
|
13
|
+
files: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
16
|
+
|
|
17
|
+
export function checkedNativeReference(value: unknown, cloudId?: string): NativeSkillReference {
|
|
18
|
+
const reference = value as NativeSkillReference;
|
|
19
|
+
if (!reference || typeof reference !== 'object' || Array.isArray(reference)
|
|
20
|
+
|| Object.keys(reference).sort().join(',') !== 'access_revision,kind,skill_id,version'
|
|
21
|
+
|| reference.kind !== 'native_standalone_skill' || !UUID.test(reference.skill_id)
|
|
22
|
+
|| (cloudId !== undefined && reference.skill_id !== cloudId)
|
|
23
|
+
|| !Number.isSafeInteger(reference.access_revision) || reference.access_revision < 0
|
|
24
|
+
|| !reference.version || Object.keys(reference.version).sort().join(',') !== 'digest,revision'
|
|
25
|
+
|| !Number.isSafeInteger(reference.version.revision) || reference.version.revision < 1
|
|
26
|
+
|| !/^[a-f0-9]{64}$/.test(reference.version.digest)) {
|
|
27
|
+
throw new Error('Native Skill identity is unavailable. Refresh before syncing.');
|
|
28
|
+
}
|
|
29
|
+
return structuredClone(reference);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nativeDirectory(reference: NativeSkillReference): string {
|
|
33
|
+
return 'native-' + checkedNativeReference(reference).skill_id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sameNativeReference(left?: NativeSkillReference, right?: NativeSkillReference): boolean {
|
|
37
|
+
return Boolean(left && right && left.skill_id === right.skill_id
|
|
38
|
+
&& left.access_revision === right.access_revision && left.version.revision === right.version.revision
|
|
39
|
+
&& left.version.digest === right.version.digest);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isNativeSkill(skill: CloudSkill): boolean {
|
|
43
|
+
return skill.source === 'native' || skill.native_reference !== undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertNativePull(response: SyncPullResponse): void {
|
|
47
|
+
const ids = new Set<string>();
|
|
48
|
+
for (const skill of response.skills) {
|
|
49
|
+
if (!isNativeSkill(skill)) continue;
|
|
50
|
+
const reference = checkedNativeReference(skill.native_reference, skill.id);
|
|
51
|
+
if (response.skill_sync_protocol !== 2 || skill.name !== nativeDirectory(reference) || ids.has(skill.id)
|
|
52
|
+
|| !/^[a-f0-9]{64}$/.test(response.sync_scope || '') || skill.native_scope !== response.sync_scope
|
|
53
|
+
|| !Array.isArray(skill.bundle_files) || skill.bundle_hydration_failed || typeof skill.skill_md !== 'string') {
|
|
54
|
+
throw new Error('Native Skill inventory is incomplete. Existing local files were retained.');
|
|
55
|
+
}
|
|
56
|
+
const paths = new Set<string>();
|
|
57
|
+
let entrypoint = false;
|
|
58
|
+
for (const file of skill.bundle_files) {
|
|
59
|
+
if (typeof file.path !== 'string' || typeof file.content_b64 !== 'string'
|
|
60
|
+
|| file.path.startsWith('/') || file.path.includes('\\') || file.path.includes('\0')
|
|
61
|
+
|| file.path.split('/').some(part => !part || part === '.' || part === '..')
|
|
62
|
+
|| paths.has(file.path.toLowerCase()) || file.path.toLowerCase() === NATIVE_IDENTITY_FILE) {
|
|
63
|
+
throw new Error('Native Skill bundle has a conflicting managed path.');
|
|
64
|
+
}
|
|
65
|
+
paths.add(file.path.toLowerCase());
|
|
66
|
+
if (file.path === 'SKILL.md') {
|
|
67
|
+
entrypoint = Buffer.from(file.content_b64, 'base64').equals(Buffer.from(skill.skill_md, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!entrypoint) throw new Error('Native Skill instructions and files do not match.');
|
|
71
|
+
ids.add(skill.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function nativeFileDigest(bytes: Buffer): string {
|
|
76
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function nativeFrontmatterName(markdown: string): string {
|
|
80
|
+
const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
81
|
+
return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, '') || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function checkedNativeIdentity(value: unknown): NativeSkillIdentity {
|
|
85
|
+
const identity = value as NativeSkillIdentity;
|
|
86
|
+
if (!identity || identity.schema !== 1 || typeof identity.cloudId !== 'string'
|
|
87
|
+
|| typeof identity.displayName !== 'string' || typeof identity.frontmatterName !== 'string'
|
|
88
|
+
|| !/^[a-f0-9]{64}$/.test(identity.scope) || !/^[a-f0-9]{64}$/.test(identity.folderHash)
|
|
89
|
+
|| !identity.files || !Object.keys(identity.files).includes('SKILL.md')
|
|
90
|
+
|| Object.entries(identity.files).some(([name, hash]) => name.startsWith('/') || name.includes('\\')
|
|
91
|
+
|| name.split('/').some(part => !part || part === '.' || part === '..') || !/^[a-f0-9]{64}$/.test(hash))) {
|
|
92
|
+
throw new Error('Native Skill provenance is incomplete. Existing files were retained.');
|
|
93
|
+
}
|
|
94
|
+
return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nativeState(skill: CloudSkill): Partial<SyncedSkill> {
|
|
98
|
+
if (!isNativeSkill(skill)) return {};
|
|
99
|
+
return { nativeReference: checkedNativeReference(skill.native_reference, skill.id),
|
|
100
|
+
...(skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {}),
|
|
101
|
+
nativeDisplayName: skill.display_name || skill.name, nativeScope: skill.native_scope, nativeUnavailable: false };
|
|
102
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CloudSkill, LocalSkill, NotisSyncState, SyncPullResponse } from './types';
|
|
2
|
+
import { checkedNativeReference, isNativeSkill, sameNativeReference } from './native-identity';
|
|
3
|
+
|
|
4
|
+
/** Folder names may change; existing managed cloud identity may not. */
|
|
5
|
+
export function bindNativeFolders(response: SyncPullResponse, locals: LocalSkill[], state: NotisSyncState): SyncPullResponse {
|
|
6
|
+
const nativeIds = new Set(response.skills.filter(isNativeSkill).map(skill => skill.id));
|
|
7
|
+
for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
|
|
8
|
+
const byId = new Map<string, LocalSkill>();
|
|
9
|
+
for (const local of locals) {
|
|
10
|
+
const id = local.cloudId || state.skills[local.name]?.cloudId;
|
|
11
|
+
if (!id || (!nativeIds.has(id) && !local.nativeReference)) continue;
|
|
12
|
+
if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
|
|
13
|
+
if (byId.has(id)) throw new Error('Two local folders claim the same native Skill. Keep both copies and resolve the conflict.');
|
|
14
|
+
byId.set(id, local);
|
|
15
|
+
}
|
|
16
|
+
const skills = response.skills.map(skill => isNativeSkill(skill) && byId.has(skill.id)
|
|
17
|
+
? { ...skill, name: byId.get(skill.id)!.name } : skill);
|
|
18
|
+
for (const skill of skills.filter(isNativeSkill)) {
|
|
19
|
+
if (skills.some(other => other.id !== skill.id && other.name === skill.name)) {
|
|
20
|
+
throw new Error('A native Skill and another Skill share a local folder. Existing files were retained.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { ...response, skills };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function previousNativeState(state: NotisSyncState, skill: CloudSkill) {
|
|
27
|
+
const named = state.skills[skill.name];
|
|
28
|
+
return named?.cloudId === skill.id ? named : Object.values(state.skills).find(entry => entry.cloudId === skill.id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nativeLocalReady(skill: CloudSkill, local?: LocalSkill): boolean {
|
|
32
|
+
return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash
|
|
33
|
+
&& local.cloudId === skill.id && local.nativeScope === skill.native_scope
|
|
34
|
+
&& sameNativeReference(local.nativeReference, skill.native_reference));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A pre-native folder (no identity marker, no native state) the cloud now serves
|
|
38
|
+
* as a native Skill. It stays usable as before until its upgrade write lands. */
|
|
39
|
+
export function pendingLegacyUpgrade(skill: CloudSkill, local: LocalSkill | undefined, state: NotisSyncState): boolean {
|
|
40
|
+
if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
|
|
41
|
+
const previous = previousNativeState(state, skill);
|
|
42
|
+
return Boolean(previous && !previous.nativeReference);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function nativeLinkRows(response: SyncPullResponse, locals: LocalSkill[], state?: NotisSyncState): CloudSkill[] {
|
|
46
|
+
return response.skills.map(skill => {
|
|
47
|
+
const local = locals.find(row => row.name === skill.name);
|
|
48
|
+
return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state))
|
|
49
|
+
? { ...skill, status: 'disabled' } : skill;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function retainMissingNativeState(previous: NotisSyncState, next: NotisSyncState): NotisSyncState {
|
|
54
|
+
const present = new Set(Object.values(next.skills).map(row => row.cloudId));
|
|
55
|
+
const retained = Object.fromEntries(Object.entries(previous.skills)
|
|
56
|
+
.filter(([, row]) => (row.nativeReference || row.nativeUnavailable) && !present.has(row.cloudId))
|
|
57
|
+
.map(([name, row]) => [name, { ...row, nativeUnavailable: true, verifiedAgentLinks: {} }]));
|
|
58
|
+
const history = structuredClone(previous.nativeHistory || {});
|
|
59
|
+
for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
|
|
60
|
+
if (!row.nativeReference && !row.nativeUnavailable) continue;
|
|
61
|
+
const names = history[row.cloudId]?.names || [];
|
|
62
|
+
history[row.cloudId] = { names: [...new Set([...names, name])] };
|
|
63
|
+
}
|
|
64
|
+
return { ...next, skills: { ...retained, ...next.skills },
|
|
65
|
+
...(Object.keys(history).length ? { nativeHistory: history } : {}) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hasNativeFolderHistory(state: NotisSyncState, name: string): boolean {
|
|
69
|
+
return Object.values(state.nativeHistory || {}).some(row => row.names.includes(name));
|
|
70
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { CloudSkill, LocalSkill, NotisSyncState } from "./types";
|
|
2
|
+
import { isNativeSkill } from './native-identity';
|
|
3
|
+
import { hasNativeFolderHistory } from './native-plan';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* One cloud row per local folder. Several active rows can carry the same skill name
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
+
* (duplicates created by earlier pushes), and every one of them writes to the same
|
|
8
|
+
* directory. Applying them all in
|
|
7
9
|
* one pass replaced that directory several times per sync and left whichever row came
|
|
8
10
|
* last on disk. The newest revision wins, deterministically.
|
|
9
11
|
*/
|
|
@@ -23,25 +25,30 @@ export function getPushCandidates(
|
|
|
23
25
|
syncState: NotisSyncState,
|
|
24
26
|
cloudCuratedSkillNames: ReadonlySet<string> = new Set(),
|
|
25
27
|
cloudSkillNames?: ReadonlySet<string>,
|
|
26
|
-
|
|
28
|
+
cloudSkills: CloudSkill[] = [],
|
|
27
29
|
): LocalSkill[] {
|
|
28
30
|
return localSkills.filter((skill) => {
|
|
29
31
|
if (cloudCuratedSkillNames.has(skill.name)) {
|
|
30
32
|
return false;
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
34
|
+
const previous = syncState.skills[skill.name]
|
|
35
|
+
|| (skill.cloudId ? Object.values(syncState.skills).find(row => row.cloudId === skill.cloudId) : undefined);
|
|
36
|
+
const native = cloudSkills.find(row => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
|
|
37
|
+
if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
|
|
38
|
+
return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope
|
|
39
|
+
&& (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
|
|
37
40
|
}
|
|
38
|
-
const previous = syncState.skills[skill.name];
|
|
39
41
|
if (!previous) {
|
|
42
|
+
if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
|
|
40
43
|
return true;
|
|
41
44
|
}
|
|
42
45
|
if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
|
|
43
46
|
return false;
|
|
44
47
|
}
|
|
45
48
|
return previous.folderHash !== skill.folderHash;
|
|
49
|
+
}).map(skill => {
|
|
50
|
+
const previous = syncState.skills[skill.name]
|
|
51
|
+
|| Object.values(syncState.skills).find(row => row.cloudId === skill.cloudId);
|
|
52
|
+
return { ...skill, ...(skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous!.cloudId } : {}) };
|
|
46
53
|
});
|
|
47
54
|
}
|