@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
122 lines • 6.04 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { lsModeString } from "../../../../commands/builtin/utils/formatting.js";
import { UsageError } from "../../../../commands/errors.js";
import { missingOperandError } from "../../../../commands/spec/usage.js";
import { isRefusal } from "../../../../errors/fs.js";
import { encodeText } from "../../../../shell/bytes.js";
import { FileType } from "../../../../types.js";
import { DEFAULT_DIR_MODE, DEFAULT_FILE_MODE, parseChmod } from "../../../../utils/mode.js";
import { shellQuoteAlways } from "../../../../utils/quote.js";
import { expandOperands, fail, parseLine, result } from "../shared.js";
import { beforeRecursiveChange, permissionError, resolveOperand, setattrVia, unreadError, verbosity, walkedName, walkStats, } from "./metadata.js";
// chmod's report for one file, worded as GNU chmod 9.7 prints it. Mirrors
// Python's mode_line.
export function modeLine(name, stat, mode, failed) {
const old = stat.mode ?? 0;
const shown = shellQuoteAlways(name);
const perms = lsModeString(stat.with({ mode })).slice(1);
const newOctal = mode.toString(8).padStart(4, '0');
if (!failed && old === mode)
return `mode of ${shown} retained as ${newOctal} (${perms})\n`;
const was = lsModeString(stat.with({ mode: old })).slice(1);
const oldOctal = old.toString(8).padStart(4, '0');
const lead = failed ? `failed to change mode of ${shown} from` : `mode of ${shown} changed from`;
return `${lead} ${oldOctal} (${was}) to ${newOctal} (${perms})\n`;
}
// chmod MODE FILE...: set permission bits via setattr. Follows symlinks
// (GNU chmod always dereferences). Stored, not enforced: mount mode does
// real access control. -R walks the operand's subtree and applies the mode
// to every entry, skipping symlinks the way GNU does (a traversed link
// changes neither itself nor its referent); a command-line link to a
// directory is still followed and its target walked. -v reports every
// file, -c the changed ones, and -f drops the per-file errors.
export async function handleChmod(namespace, dispatch, session, args) {
const [parsed, fl, refused] = parseLine('chmod', args, session.cwd);
if (refused !== null)
return refused;
const modeText = parsed.texts[0];
if (modeText === undefined || parsed.paths.length === 0) {
const error = missingOperandError('chmod', modeText ?? null);
return fail('chmod', `${error.message}\n`, error.exitCode);
}
if (parseChmod(modeText, 0) === null) {
return fail('chmod', `chmod: invalid mode: '${modeText}'\n`, 1);
}
const report = verbosity(fl);
const errors = [];
const out = [];
const links = namespace.sessionLinks(session);
for (const target of await expandOperands(namespace, dispatch, parsed.paths)) {
const found = await resolveOperand(links, dispatch, 'chmod', target, errors);
if (found === null) {
if (report === 'verbose')
out.push(`${shellQuoteAlways(target.rawPath)} could not be accessed\n`);
continue;
}
const [resolved, stat] = found;
if (fl.asBool('recursive')) {
try {
await beforeRecursiveChange(links.mounts(false), dispatch, resolved, 'chmod');
}
catch (err) {
if (!(err instanceof UsageError))
throw err;
errors.push(`${err.message}\n`);
continue;
}
}
const entries = fl.asBool('recursive')
? await walkStats(links, dispatch, resolved, stat)
: [[resolved, stat, null]];
for (const [path, entryStat, unread] of entries) {
// Backends without a mode default to what ls renders: 755 for
// directories, 644 for files (symbolic clauses build on this).
const pathStat = entryStat.mode !== null
? entryStat
: entryStat.with({
mode: entryStat.type === FileType.DIRECTORY ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE,
});
const current = pathStat.mode ?? 0;
const newMode = parseChmod(modeText, current);
if (newMode === null) {
return fail('chmod', `chmod: invalid mode: '${modeText}'\n`, 1);
}
const name = walkedName(target.rawPath, resolved, path);
let failed = false;
try {
await setattrVia(dispatch, path, { mode: newMode });
}
catch (err) {
if (!isRefusal(err))
throw err;
errors.push(permissionError('chmod', 'changing permissions of', name, err));
failed = true;
}
if (report === 'verbose' || (report === 'changes' && !failed && newMode !== current)) {
out.push(modeLine(name, pathStat, newMode, failed));
}
if (unread !== null)
errors.push(unreadError('chmod', name, unread));
}
}
const quiet = fl.asBool('silent') || fl.asBool('quiet');
const text = out.join('');
return result('chmod', {
out: text === '' ? null : encodeText(text),
exitCode: errors.length > 0 ? 1 : 0,
...(quiet ? {} : { stderr: errors.join('') }),
});
}
//# sourceMappingURL=chmod.js.map