@fnnas-labs/fnos-cli 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +169 -2
- package/THIRD_PARTY_NOTICES.txt +38904 -0
- package/cordis.patch.yml +3 -0
- package/npm/README.dsh.md +49 -0
- package/npm/acceptance-report.cjs +53 -0
- package/npm/dsh-integration.mjs +159 -0
- package/npm/dsh-test-support.mjs +55 -0
- package/npm/dsh.mjs +31 -0
- package/npm/install.cjs +140 -0
- package/npm/package-lib.cjs +274 -0
- package/npm/run.cjs +39 -0
- package/package.json +46 -4
- package/skill/LICENSE +21 -0
- package/skill/SKILL.md +143 -0
- package/skill/THIRD_PARTY_NOTICES.txt +38904 -0
- package/skill/bin/trim-cli-darwin-arm64 +0 -0
- package/skill/bin/trim-cli-darwin-x64 +0 -0
- package/skill/bin/trim-cli-linux-arm64 +0 -0
- package/skill/bin/trim-cli-linux-x64 +0 -0
- package/skill/bin/trim-cli-windows-arm64.exe +0 -0
- package/skill/bin/trim-cli-windows-x64.exe +0 -0
- package/skill/build-provenance.json +44 -0
- package/skill/entries/trim-app.md +63 -0
- package/skill/entries/trim-baidu-netdisk.md +42 -0
- package/skill/entries/trim-docker.md +35 -0
- package/skill/entries/trim-download.md +38 -0
- package/skill/entries/trim-file.md +30 -0
- package/skill/entries/trim-log.md +32 -0
- package/skill/entries/trim-monitor.md +41 -0
- package/skill/entries/trim-network.md +22 -0
- package/skill/entries/trim-photos.md +32 -0
- package/skill/entries/trim-shared.md +54 -0
- package/skill/entries/trim-storage.md +34 -0
- package/skill/entries/trim-system.md +59 -0
- package/skill/entries/trim-user.md +28 -0
- package/skill/manifest.json +43 -0
- package/skill/reference/_conventions.md +76 -0
- package/skill/reference/_index.md +78 -0
- package/skill/reference/app-center.md +234 -0
- package/skill/reference/baidu-netdisk.md +221 -0
- package/skill/reference/dockermgr.md +244 -0
- package/skill/reference/download.md +357 -0
- package/skill/reference/file.md +752 -0
- package/skill/reference/log.md +224 -0
- package/skill/reference/network.md +41 -0
- package/skill/reference/oauth.md +51 -0
- package/skill/reference/photos.md +158 -0
- package/skill/reference/power.md +34 -0
- package/skill/reference/resmon.md +160 -0
- package/skill/reference/stor.md +525 -0
- package/skill/reference/sysinfo.md +133 -0
- package/skill/reference/user.md +139 -0
- package/skill/reference/workflows/device-validation.md +66 -0
- package/skill/reference/workflows/file-routing.md +44 -0
- package/skill/reference/workflows/file-upload-validation.md +63 -0
- package/skill/reference/workflows/photos-routing.md +83 -0
- package/skill/reference/workflows/storage-dangerous-ops.md +53 -0
- package/skill/scripts/fnos-cli +63 -0
- package/skill/scripts/fnos-cli.cmd +36 -0
- package/skill/scripts/fnos-cli.ps1 +31 -0
- package/skill/scripts/trim-cli +63 -0
- package/skill/scripts/trim-cli.cmd +36 -0
- package/skill/scripts/trim-cli.ps1 +31 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { createHash } = require("node:crypto");
|
|
5
|
+
|
|
6
|
+
const runtimePackageJson = JSON.parse(
|
|
7
|
+
fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8")
|
|
8
|
+
);
|
|
9
|
+
const PACKAGE_NAME = runtimePackageJson.name;
|
|
10
|
+
const SKILL_NAME = runtimePackageJson.trimCliSkillName;
|
|
11
|
+
const PRIMARY_COMMAND = runtimePackageJson.cliPrimaryCommand || "trim-cli";
|
|
12
|
+
const OWNERSHIP_FILE = ".fnos-cli-ownership.json";
|
|
13
|
+
if (runtimePackageJson.bin?.[PRIMARY_COMMAND] !== "npm/run.cjs") {
|
|
14
|
+
throw new Error(`Package does not expose the primary command ${PRIMARY_COMMAND}`);
|
|
15
|
+
}
|
|
16
|
+
const PLATFORM_BINARIES = Object.freeze({
|
|
17
|
+
"darwin-arm64": "trim-cli-darwin-arm64",
|
|
18
|
+
"darwin-x64": "trim-cli-darwin-x64",
|
|
19
|
+
"linux-arm64": "trim-cli-linux-arm64",
|
|
20
|
+
"linux-x64": "trim-cli-linux-x64",
|
|
21
|
+
"windows-arm64": "trim-cli-windows-arm64.exe",
|
|
22
|
+
"windows-x64": "trim-cli-windows-x64.exe"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function platformKey(platform = process.platform, arch = process.arch) {
|
|
26
|
+
const platformName = {
|
|
27
|
+
darwin: "darwin",
|
|
28
|
+
linux: "linux",
|
|
29
|
+
win32: "windows"
|
|
30
|
+
}[platform];
|
|
31
|
+
const archName = {
|
|
32
|
+
arm64: "arm64",
|
|
33
|
+
x64: "x64"
|
|
34
|
+
}[arch];
|
|
35
|
+
|
|
36
|
+
if (!platformName || !archName) {
|
|
37
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
38
|
+
}
|
|
39
|
+
return `${platformName}-${archName}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function packagedSkillRoot(packageRoot, options = {}) {
|
|
43
|
+
if (options.skillRoot) return options.skillRoot;
|
|
44
|
+
const skillName = options.expectedSkillName || SKILL_NAME;
|
|
45
|
+
const packaged = path.join(packageRoot, "skill");
|
|
46
|
+
if (fs.existsSync(path.join(packaged, "SKILL.md"))) return packaged;
|
|
47
|
+
return path.join(packaged, skillName === "fnos-cli" ? "fnos" : skillName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJson(filePath, label) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Cannot read ${label} at ${filePath}: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePackagedBinary(packageRoot, platform, arch) {
|
|
59
|
+
const key = platformKey(platform, arch);
|
|
60
|
+
const binaryName = PLATFORM_BINARIES[key];
|
|
61
|
+
const binaryPath = path.join(packagedSkillRoot(packageRoot), "bin", binaryName);
|
|
62
|
+
if (!fs.existsSync(binaryPath)) {
|
|
63
|
+
throw new Error(`Package is missing the ${key} binary: ${binaryPath}`);
|
|
64
|
+
}
|
|
65
|
+
return binaryPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validatePackagedSkill(packageRoot, options = {}) {
|
|
69
|
+
const packageJson = readJson(path.join(packageRoot, "package.json"), "package.json");
|
|
70
|
+
const expectedPackageName = options.expectedPackageName || PACKAGE_NAME;
|
|
71
|
+
const expectedSkillName = options.expectedSkillName || SKILL_NAME;
|
|
72
|
+
if (packageJson.name !== expectedPackageName) {
|
|
73
|
+
throw new Error(`Expected package name ${expectedPackageName}, got ${packageJson.name || "<missing>"}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const skillRoot = packagedSkillRoot(packageRoot, options);
|
|
77
|
+
const skillEntry = path.join(skillRoot, "SKILL.md");
|
|
78
|
+
if (!fs.existsSync(skillEntry)) {
|
|
79
|
+
throw new Error(`Package is missing the Skill entry: ${skillEntry}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const manifest = readJson(path.join(skillRoot, "manifest.json"), "Skill manifest");
|
|
83
|
+
if (manifest.name !== expectedSkillName) {
|
|
84
|
+
throw new Error(`Expected Skill name ${expectedSkillName}, got ${manifest.name || "<missing>"}`);
|
|
85
|
+
}
|
|
86
|
+
if (manifest.version !== packageJson.version) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Package version ${packageJson.version} does not match Skill version ${manifest.version || "<missing>"}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const requiredKeys = options.requireAllPlatforms
|
|
93
|
+
? Object.keys(PLATFORM_BINARIES)
|
|
94
|
+
: [platformKey(options.platform, options.arch)];
|
|
95
|
+
for (const key of requiredKeys) {
|
|
96
|
+
const expectedRelativePath = `bin/${PLATFORM_BINARIES[key]}`;
|
|
97
|
+
if (!manifest.bin || manifest.bin[key] !== expectedRelativePath) {
|
|
98
|
+
throw new Error(`Skill manifest does not map ${key} to ${expectedRelativePath}`);
|
|
99
|
+
}
|
|
100
|
+
const binaryPath = path.join(skillRoot, expectedRelativePath);
|
|
101
|
+
if (!fs.existsSync(binaryPath)) {
|
|
102
|
+
throw new Error(`Skill is missing the ${key} binary: ${binaryPath}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { packageJson, manifest, skillRoot };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveSkillDestination({ project = false, cwd = process.cwd(), homeDir = os.homedir() } = {}) {
|
|
110
|
+
const base = project ? cwd : homeDir;
|
|
111
|
+
return path.join(base, ".agents", "skills", SKILL_NAME);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The full tree is recorded, including empty directories. A changed file,
|
|
115
|
+
// additional user file or symlink makes the installation ineligible for removal.
|
|
116
|
+
function skillDigests(root) {
|
|
117
|
+
const entries = Object.create(null);
|
|
118
|
+
if (!fs.lstatSync(root).isDirectory()) throw new Error(`Skill is not a directory: ${root}`);
|
|
119
|
+
function visit(directory, prefix = "") {
|
|
120
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
121
|
+
if (!prefix && name === OWNERSHIP_FILE) continue;
|
|
122
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
123
|
+
const absolute = path.join(directory, name);
|
|
124
|
+
const stat = fs.lstatSync(absolute);
|
|
125
|
+
if (stat.isDirectory()) {
|
|
126
|
+
entries[relative] = null;
|
|
127
|
+
visit(absolute, relative);
|
|
128
|
+
} else if (stat.isFile()) {
|
|
129
|
+
entries[relative] = createHash("sha256").update(fs.readFileSync(absolute)).digest("hex");
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error(`Skill contains a symlink or unsupported file: ${absolute}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
visit(root);
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function verifiedOwnership(root) {
|
|
140
|
+
try {
|
|
141
|
+
const markerPath = path.join(root, OWNERSHIP_FILE);
|
|
142
|
+
if (!fs.lstatSync(markerPath).isFile()) return false;
|
|
143
|
+
const marker = readJson(markerPath, "Skill ownership marker");
|
|
144
|
+
const knownPackage = marker.package === PACKAGE_NAME
|
|
145
|
+
|| (SKILL_NAME === "fnos-cli" && marker.package === "@trimjs/trim-cli");
|
|
146
|
+
const knownSkill = marker.skill === SKILL_NAME
|
|
147
|
+
|| (SKILL_NAME === "fnos-cli" && ["fnos", "trim-cli"].includes(marker.skill));
|
|
148
|
+
if (marker.schema !== 1 || !knownPackage || !knownSkill
|
|
149
|
+
|| typeof marker.version !== "string" || !marker.version
|
|
150
|
+
|| !marker.entries || typeof marker.entries !== "object" || Array.isArray(marker.entries)) return false;
|
|
151
|
+
const actual = skillDigests(root);
|
|
152
|
+
const names = Object.keys(actual);
|
|
153
|
+
return names.length > 0 && names.length === Object.keys(marker.entries).length
|
|
154
|
+
&& names.every((name) => Object.hasOwn(marker.entries, name) && marker.entries[name] === actual[name]);
|
|
155
|
+
} catch (_) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeOwnership(root) {
|
|
161
|
+
if (fs.existsSync(path.join(root, OWNERSHIP_FILE))) {
|
|
162
|
+
throw new Error("Packaged Skill unexpectedly contains an installation ownership marker");
|
|
163
|
+
}
|
|
164
|
+
const marker = {
|
|
165
|
+
schema: 1, package: PACKAGE_NAME, skill: SKILL_NAME,
|
|
166
|
+
version: runtimePackageJson.version, entries: skillDigests(root)
|
|
167
|
+
};
|
|
168
|
+
fs.writeFileSync(path.join(root, OWNERSHIP_FILE), `${JSON.stringify(marker, null, 2)}\n`, { flag: "wx" });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function pathExists(file) {
|
|
172
|
+
try { fs.lstatSync(file); return true; }
|
|
173
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function copySkillAtomically(source, destination) {
|
|
177
|
+
const parent = path.dirname(destination);
|
|
178
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
179
|
+
|
|
180
|
+
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
181
|
+
const lockPath = path.join(parent, `.${SKILL_NAME}.install.lock`);
|
|
182
|
+
const stagingPath = path.join(parent, `.${SKILL_NAME}.install-${nonce}`);
|
|
183
|
+
let lockHandle;
|
|
184
|
+
const moved = [];
|
|
185
|
+
const warnings = [];
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
try {
|
|
189
|
+
lockHandle = fs.openSync(lockPath, "wx", 0o600);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.code === "EEXIST") {
|
|
192
|
+
throw new Error(`Another ${SKILL_NAME} installation is already running: ${lockPath}`);
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.cpSync(source, stagingPath, { recursive: true, errorOnExist: true });
|
|
198
|
+
if (!fs.existsSync(path.join(stagingPath, "SKILL.md"))) {
|
|
199
|
+
throw new Error(`Staged Skill is missing SKILL.md: ${stagingPath}`);
|
|
200
|
+
}
|
|
201
|
+
writeOwnership(stagingPath);
|
|
202
|
+
|
|
203
|
+
if (pathExists(destination)) {
|
|
204
|
+
if (!verifiedOwnership(destination)) {
|
|
205
|
+
throw new Error(`Cannot verify ownership of existing Skill; preserved without changes: ${destination}. Move it aside after reviewing your files, then retry.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// An unmarked historical installation cannot be distinguished from a user's
|
|
210
|
+
// own Skill. Never infer ownership from a familiar name or SKILL.md alone.
|
|
211
|
+
const legacy = SKILL_NAME === "fnos-cli" ? ["fnos", "trim-cli"] : [];
|
|
212
|
+
const candidates = [];
|
|
213
|
+
for (const name of legacy) {
|
|
214
|
+
const old = path.join(parent, name);
|
|
215
|
+
if (!pathExists(old)) continue;
|
|
216
|
+
if (verifiedOwnership(old)) candidates.push(old);
|
|
217
|
+
else warnings.push(`Cannot verify ownership; preserved legacy Skill: ${old}. Review it manually before removing it.`);
|
|
218
|
+
}
|
|
219
|
+
if (pathExists(destination)) candidates.push(destination);
|
|
220
|
+
for (const original of candidates) {
|
|
221
|
+
// Recheck after staging so edits made during a slow copy stay protected.
|
|
222
|
+
if (!verifiedOwnership(original)) throw new Error(`Skill ownership changed during installation; preserved: ${original}`);
|
|
223
|
+
const backup = path.join(parent, `.${SKILL_NAME}.backup-${nonce}-${path.basename(original)}`);
|
|
224
|
+
fs.renameSync(original, backup);
|
|
225
|
+
moved.push({ original, backup });
|
|
226
|
+
}
|
|
227
|
+
fs.renameSync(stagingPath, destination);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
try { fs.rmSync(stagingPath, { recursive: true, force: true }); }
|
|
230
|
+
catch (cleanupError) { error.message += `; staging cleanup requires review at ${stagingPath}: ${cleanupError.message}`; }
|
|
231
|
+
for (const { original, backup } of moved.reverse()) {
|
|
232
|
+
try {
|
|
233
|
+
if (!pathExists(original) && pathExists(backup)) fs.renameSync(backup, original);
|
|
234
|
+
else error.message += `; restore requires review: ${backup} -> ${original}`;
|
|
235
|
+
} catch (restoreError) {
|
|
236
|
+
error.message += `; original data retained at ${backup}: ${restoreError.message}`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
} finally {
|
|
241
|
+
if (lockHandle !== undefined) {
|
|
242
|
+
fs.closeSync(lockHandle);
|
|
243
|
+
fs.rmSync(lockPath, { force: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Publication succeeded. Cleanup failures must not pretend the old tree can
|
|
248
|
+
// still be rolled back after some owned backups have already been removed.
|
|
249
|
+
for (const { backup } of moved) {
|
|
250
|
+
try {
|
|
251
|
+
if (!verifiedOwnership(backup)) {
|
|
252
|
+
warnings.push(`Backup ownership changed; preserved for review: ${backup}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
catch (error) { warnings.push(`Installed Skill, but could not remove owned backup ${backup}: ${error.message}`); }
|
|
258
|
+
}
|
|
259
|
+
return warnings;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
PACKAGE_NAME,
|
|
264
|
+
PRIMARY_COMMAND,
|
|
265
|
+
PLATFORM_BINARIES,
|
|
266
|
+
SKILL_NAME,
|
|
267
|
+
copySkillAtomically,
|
|
268
|
+
packagedSkillRoot,
|
|
269
|
+
platformKey,
|
|
270
|
+
readJson,
|
|
271
|
+
resolvePackagedBinary,
|
|
272
|
+
resolveSkillDestination,
|
|
273
|
+
validatePackagedSkill
|
|
274
|
+
};
|
package/npm/run.cjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { main: install } = require("./install.cjs");
|
|
6
|
+
const { PRIMARY_COMMAND, resolvePackagedBinary } = require("./package-lib.cjs");
|
|
7
|
+
const packageJson = require("../package.json");
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (args.length === 1 && (args[0] === "--version" || args[0] === "-V")) {
|
|
11
|
+
console.log(`${PRIMARY_COMMAND} ${packageJson.version}`);
|
|
12
|
+
} else if (args[0] === "install") {
|
|
13
|
+
try {
|
|
14
|
+
install(args.slice(1));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(`${PRIMARY_COMMAND} install failed: ${error.message}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
try {
|
|
21
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
22
|
+
const binary = resolvePackagedBinary(packageRoot);
|
|
23
|
+
const child = spawn(binary, args, { stdio: "inherit", windowsHide: true });
|
|
24
|
+
child.once("error", (error) => {
|
|
25
|
+
console.error(`Cannot start ${PRIMARY_COMMAND}: ${error.message}`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
});
|
|
28
|
+
child.once("exit", (code, signal) => {
|
|
29
|
+
if (signal && process.platform !== "win32") {
|
|
30
|
+
process.kill(process.pid, signal);
|
|
31
|
+
} else {
|
|
32
|
+
process.exitCode = code === null ? 1 : code;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Cannot start ${PRIMARY_COMMAND}: ${error.message}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fnnas-labs/fnos-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "fnOS CLI and Agent Skill",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/chengpeiquan/fnos-cli.git"
|
|
8
|
+
},
|
|
9
|
+
"trimCliSkillName": "fnos-cli",
|
|
10
|
+
"cliPrimaryCommand": "fnos-cli",
|
|
11
|
+
"main": "npm/dsh.mjs",
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"bin": {
|
|
19
|
+
"fnos-cli": "npm/run.cjs",
|
|
20
|
+
"trim-cli": "npm/run.cjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"npm/*.cjs",
|
|
24
|
+
"skill/**",
|
|
25
|
+
"npm/dsh.mjs",
|
|
26
|
+
"npm/dsh-integration.mjs",
|
|
27
|
+
"npm/dsh-test-support.mjs",
|
|
28
|
+
"npm/acceptance-report.cjs",
|
|
29
|
+
"npm/README.dsh.md",
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"THIRD_PARTY_NOTICES.txt"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"os": [
|
|
37
|
+
"darwin",
|
|
38
|
+
"linux",
|
|
39
|
+
"win32"
|
|
40
|
+
],
|
|
41
|
+
"cpu": [
|
|
42
|
+
"x64",
|
|
43
|
+
"arm64"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/skill/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guangzhou TR Intelligent Manufacturing Technology Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fnos-cli
|
|
3
|
+
description: Use fnos-cli to sign in to and manage fnOS devices, including files, photos, applications, Docker, downloads, logs, monitoring, networking, storage, users, and Baidu Netdisk. Use when the user mentions fnOS, Feiniu OS, or a mainland China device. Excludes Media APIs and international-edition devices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# fnOS
|
|
7
|
+
|
|
8
|
+
The command-line client for fnOS. It uses the HTTP API proxy for
|
|
9
|
+
system services and native applications: authentication, files, photos, apps,
|
|
10
|
+
Docker, downloads, logs, monitoring, networking, power, storage, and users.
|
|
11
|
+
|
|
12
|
+
Examples assume `fnos-cli` is available on PATH. An integration may provide
|
|
13
|
+
an explicit package-local launcher instead; follow that integration's instructions.
|
|
14
|
+
|
|
15
|
+
## Standalone bundle launcher
|
|
16
|
+
|
|
17
|
+
For a standalone Skill bundle containing `scripts/`, use the absolute path to
|
|
18
|
+
`scripts/fnos-cli` (macOS/Linux) or `scripts/fnos-cli.cmd` (Windows)
|
|
19
|
+
inside this Skill's directory. Resolve it from the loaded Skill location, not the
|
|
20
|
+
working directory. Check `--version` before device operations.
|
|
21
|
+
|
|
22
|
+
## Task routing
|
|
23
|
+
|
|
24
|
+
Choose the most specific entry, then follow its reference or workflow. For a
|
|
25
|
+
product-specific task not listed below, inspect the descriptions in `entries/`.
|
|
26
|
+
|
|
27
|
+
| Task | Entry |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| Sign in, sessions, connection failures, profiles | `entries/trim-shared.md`, `reference/_index.md` |
|
|
30
|
+
| Files, finder, shares, ACLs, uploads and downloads | `entries/trim-file.md`, `reference/workflows/file-routing.md` |
|
|
31
|
+
| Photo search, AI search, details and previews | `entries/trim-photos.md`, `reference/workflows/photos-routing.md` |
|
|
32
|
+
| Apps, Docker, downloads, logs and monitoring | Matching file in `entries/`, `reference/_index.md` |
|
|
33
|
+
| Storage writes | `entries/trim-storage.md`, `reference/workflows/storage-dangerous-ops.md` |
|
|
34
|
+
| Users, groups and permissions | `entries/trim-user.md`, `reference/user.md` |
|
|
35
|
+
| Device validation | `reference/workflows/device-validation.md` |
|
|
36
|
+
|
|
37
|
+
## Safety rules
|
|
38
|
+
|
|
39
|
+
- Media APIs are outside this Skill's scope; use the documented capabilities only.
|
|
40
|
+
- Authentication uses OAuth 2.0 Authorization Code + PKCE. The user handles NAS
|
|
41
|
+
credentials and 2FA in their browser; never collect passwords or old tokens.
|
|
42
|
+
- OAuth uses `client_id=YJNMPJUGA9` and the CLI's required `trim.*` scopes. See
|
|
43
|
+
`reference/oauth.md` for the full list. Authorization URLs omit `redirect_uri`.
|
|
44
|
+
- Production login is interactive: the CLI displays a link and waits while the
|
|
45
|
+
user signs in, approves access, copies the one-time code, and pastes it into the
|
|
46
|
+
CLI's `Authorization code` prompt. The Agent does not fill credentials, approve
|
|
47
|
+
access, read browser codes, or pass codes in command arguments.
|
|
48
|
+
- Run login in the actual execution environment. DSH requires the same Host,
|
|
49
|
+
system user, configuration environment and profile as the user's login terminal.
|
|
50
|
+
Remote browsers, SSH shells and containers do not automatically share sessions.
|
|
51
|
+
- Sessions hold access/refresh tokens, expiry and OAuth device information. Store
|
|
52
|
+
secrets only in platform secure storage or an explicitly selected file backend;
|
|
53
|
+
keep them out of ordinary output, logs and third-party messages.
|
|
54
|
+
- Tokens refresh near expiry. HTTP 401 from `/ogh/ac/w` or `/ogh/ac/h/*` triggers
|
|
55
|
+
one refresh and one retry. Repeated 401 or refresh failure requires a new OAuth
|
|
56
|
+
login, not a retry loop or a legacy authentication fallback.
|
|
57
|
+
- Refresh first checks that the profile's session store is writable. Concurrent
|
|
58
|
+
refreshes reuse an already-written newer token.
|
|
59
|
+
- Missing module scopes, including legacy `file photo` grants, require login
|
|
60
|
+
again; refresh cannot add scopes.
|
|
61
|
+
- `logout` removes the profile's session and OAuth pending file. It does not
|
|
62
|
+
claim to revoke server-side tokens.
|
|
63
|
+
- Remote plain HTTP requires `--allow-insecure-http`; self-signed HTTPS requires
|
|
64
|
+
`--tls-insecure`.
|
|
65
|
+
- Agent and noninteractive writes explicitly pass `--yes`. High-risk storage
|
|
66
|
+
writes also require the relevant password precheck.
|
|
67
|
+
|
|
68
|
+
## Connection options
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
--host <host> NAS host; defaults to localhost
|
|
72
|
+
--port <port> Explicit port takes precedence
|
|
73
|
+
--profile <name> Independent session profile
|
|
74
|
+
--scheme auto|http|https Transport; defaults to auto
|
|
75
|
+
--allow-insecure-http Allow remote plain HTTP
|
|
76
|
+
--tls-insecure Allow self-signed HTTPS
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Without explicit connection options, the CLI reuses the profile's saved host,
|
|
80
|
+
port, scheme and TLS settings. Defaults are `http://localhost:5666` for loopback,
|
|
81
|
+
HTTPS 5667 for remote IPs, and HTTPS 443 for domain names.
|
|
82
|
+
|
|
83
|
+
## Authentication
|
|
84
|
+
|
|
85
|
+
The user runs these commands in their own interactive terminal:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
fnos-cli --profile home --host <host> --port <port> --scheme https login
|
|
89
|
+
fnos-cli --profile home login --refresh
|
|
90
|
+
fnos-cli --profile home logout
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`login` displays the authorization link, attempts to open a browser, and waits
|
|
94
|
+
for the user to paste a one-time code. `login --no-open` keeps the same flow
|
|
95
|
+
without opening the browser automatically. For a remote plain HTTP device:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
fnos-cli --profile home --host <host> --port 5666 --scheme http --allow-insecure-http login --no-open
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Common read-only commands
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
fnos-cli --profile home system info
|
|
105
|
+
fnos-cli --profile home file ls /vol1
|
|
106
|
+
fnos-cli --profile home file ls-dir /vol1
|
|
107
|
+
fnos-cli --profile home file usage 1
|
|
108
|
+
fnos-cli --profile home photos folders
|
|
109
|
+
fnos-cli --profile home photos search <keyword> --limit 20
|
|
110
|
+
fnos-cli --profile home app list
|
|
111
|
+
fnos-cli --profile home docker image ls
|
|
112
|
+
fnos-cli --profile home download ls
|
|
113
|
+
fnos-cli --profile home monitor cpu
|
|
114
|
+
fnos-cli --profile home monitor beep-supported
|
|
115
|
+
fnos-cli --profile home storage pools
|
|
116
|
+
fnos-cli --profile home user info
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Raw requests
|
|
120
|
+
|
|
121
|
+
Use `raw` for system services without a named command, and `photos request` for
|
|
122
|
+
the native Photos HTTP API. Only audited read-only endpoints built into the CLI
|
|
123
|
+
may omit `--yes`; every other endpoint, including unknown endpoints, requires
|
|
124
|
+
confirmation and explicit `--yes`. Paths and request bodies are still validated.
|
|
125
|
+
Check both HTTP status and business response code/final state, not just exit code.
|
|
126
|
+
|
|
127
|
+
## Output
|
|
128
|
+
|
|
129
|
+
Named commands output JSON plans or business data. Photos search `total` is the
|
|
130
|
+
match count. AI search may report an exact total only when the response provides
|
|
131
|
+
a stable total; otherwise report the returned count. Preview URLs usually still
|
|
132
|
+
require a signed-in browser cookie.
|
|
133
|
+
|
|
134
|
+
`file ls-dir` collects directory chunks into a JSON array. `file usage` collects
|
|
135
|
+
capacity categories into a JSON array; repeated categories keep their last
|
|
136
|
+
result instead of being added together.
|
|
137
|
+
|
|
138
|
+
## References
|
|
139
|
+
|
|
140
|
+
- Index: `reference/_index.md`
|
|
141
|
+
- OAuth contract: `reference/oauth.md`
|
|
142
|
+
- Photos API: `reference/photos.md`
|
|
143
|
+
- Connections and validation: `entries/trim-shared.md`, `reference/workflows/device-validation.md`
|