UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

158 lines • 7.11 kB
// ========= 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 { IOResult } from "../../../io/types.js"; import { parseDateExpr, parsePosixTime } from "../../../utils/dates.js"; import { command } from "../../config.js"; import { specOf } from "../../spec/builtins.js"; import { gnuStrftime } from "../utils/strftime.js"; import { quoteText } from "../../quote.js"; import { extraOperandError, usageExitCode, usageHint } from "../../spec/usage.js"; import { UsageError } from "../../errors.js"; import { CommandName } from "../../spec/types.js"; import { FlagView } from "../../spec/flag_view.js"; import { LOCAL_ZONE, UTC_ZONE, zoneFromEnv } from "../../../utils/timezone.js"; import { systemDate } from "../../../utils/clock.js"; import { encodeText } from "../../../shell/bytes.js"; const ENC = new TextEncoder(); // GNU date's output formats, each chosen by one option: -I takes one // per precision, --rfc-3339 one per its narrower set, -R the RFC 5322 line, // and a line that chooses none gets the C locale's default (`%e`, so the 5th // is " 5"). All of them render through strftime, the path `+FORMAT` takes. const ISO_8601_FORMATS = { date: '%Y-%m-%d', hours: '%Y-%m-%dT%H%:z', minutes: '%Y-%m-%dT%H:%M%:z', seconds: '%Y-%m-%dT%H:%M:%S%:z', ns: '%Y-%m-%dT%H:%M:%S,%N%:z', }; const RFC_3339_FORMATS = { date: '%Y-%m-%d', seconds: '%Y-%m-%d %H:%M:%S%:z', ns: '%Y-%m-%d %H:%M:%S.%N%:z', }; const RFC_EMAIL_FORMAT = '%a, %d %b %Y %H:%M:%S %z'; const DEFAULT_FORMAT = '%a %b %e %H:%M:%S %Z %Y'; const MULTIPLE_FORMATS = 'date: multiple output formats specified\n'; // What setting the clock answers: mirage has none to set, which is what GNU // says for a user without the privilege to. const CANNOT_SET = 'date: cannot set date: Operation not permitted\n'; // The output formats the line's options choose, one per option. GNU keeps one // and refuses a second as it reads it, so any two of -I, -R and --rfc-3339 are // `multiple output formats specified`. The parser has already resolved a // precision to its whole word (`-Is` is `seconds`). One divergence: the flag // bag keeps the last of a REPEATED option, so `date -I -I` prints where GNU // refuses it. function optionFormats(fl) { const formats = []; const iso = fl.raw('iso_8601'); if (iso === true) formats.push(ISO_8601_FORMATS.date ?? ''); else if (typeof iso === 'string') formats.push(ISO_8601_FORMATS[iso] ?? ''); if (fl.asBool('rfc_email')) formats.push(RFC_EMAIL_FORMAT); const rfc3339 = fl.asStr('rfc_3339'); if (rfc3339 !== undefined) formats.push(RFC_3339_FORMATS[rfc3339] ?? ''); return formats; } function multipleFormats() { return [null, new IOResult({ exitCode: 1, stderr: ENC.encode(MULTIPLE_FORMATS) })]; } // GNU's refusal of a date it cannot read, exit 1. function invalidDate(text) { return [ null, new IOResult({ exitCode: 1, stderr: ENC.encode(`date: invalid date '${quoteText(text)}'\n`) }), ]; } // GNU's refusal of a non-`+` operand beside `-d`, a usage error. function lacksPlusError(operand) { return new UsageError(`date: the argument '${quoteText(operand)}' lacks a leading '+';\n` + 'when using an option to specify date(s), any non-option\n' + "argument must be a format string beginning with '+'\n" + usageHint(CommandName.DATE), usageExitCode(CommandName.DATE)); } // GNU `date`: the current moment, or the one `-d` names, rendered in the // zone the command runs in. The zone is `-u`'s UTC, else the TZ of the // command's own environment (`TZ=Asia/Hong_Kong date` and an exported TZ // alike, as GNU reads it), else the host's local zone. It is read from // `ctx.env`, never from process state, so concurrent workspaces cannot // move each other's clock. `%Z` is tzdata's abbreviation (`HKT`), as GNU // prints it, read from a table generated off zoneinfo since Intl has // none; the Python twin reads zoneinfo itself. An operand without `+` sets // the clock, GNU's `MMDDhhmm[[CC]YY][.ss]`: mirage has no clock to set, so it // prints the date it names and refuses the setting, as GNU does for a user // without the privilege. Beside `-d` it is a usage error. function date(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags, specOf('date')); const u = fl.asBool('utc') || fl.asBool('universal'); const d = fl.asStr('date') ?? null; const formats = optionFormats(fl); if (formats.length > 1) return multipleFormats(); if (texts.length > 1) throw extraOperandError(CommandName.DATE, texts[1] ?? ''); let setting = texts[0] ?? null; if (setting?.startsWith('+') === true) { if (formats.length > 0) return multipleFormats(); formats.push(setting.slice(1)); setting = null; } else if (setting !== null && d !== null) { throw lacksPlusError(setting); } const named = u ? UTC_ZONE : zoneFromEnv(ctx.env); const zone = named ?? LOCAL_ZONE; let dt; if (setting !== null) { const placed = parsePosixTime(setting, zone); if (placed === null) return invalidDate(setting); dt = placed; } else if (d !== null && d.trim() === '') { // GNU ACCEPTS an empty (or blank) expression, exit 0: with no item at // all it names today at midnight. Measured on coreutils 9.4: `date -d // ''` and `date -d ' '` both print today 00:00:00 in the command's // zone. const midnight = parseDateExpr(gnuStrftime(systemDate(), '%Y-%m-%d', zone), zone); // Today's own ISO date always parses; the fallback is for the type. dt = midnight ?? systemDate(); } else if (d !== null) { const parsed = parseDateExpr(d, zone); // GNU's refusal, exit 1: a NaN render with exit 0 poisons whatever // consumed it (the 0NaN-NaN-NaN corpus failure). if (parsed === null) return invalidDate(d); dt = parsed; } else { dt = systemDate(); } const fmt = formats[0] ?? DEFAULT_FORMAT; const out = encodeText(gnuStrftime(dt, fmt, zone) + '\n'); if (setting !== null) return [out, new IOResult({ exitCode: 1, stderr: ENC.encode(CANNOT_SET) })]; return [out, new IOResult()]; } export const GENERAL_DATE = command({ name: 'date', vfs: null, spec: specOf('date'), fn: date, }); //# sourceMappingURL=date.js.map