{
  "name": "Initial",
  "script": "// Variables used by Scriptable.\n// These must be at the very top of the file. Do not edit.\n// icon-color: blue; icon-glyph: download;\n// AeroK Initial bootstrap — managed.\n// 이 파일 하나를 공유하세요. 개인 키와 일정 데이터는 포함하지 않습니다.\n\nconst AEROK_DOWNLOAD = 'https://cdn.trivial.kr/scriptable';\n\nfunction sha256(text) {\n  const bytes = Array.from(unescape(encodeURIComponent(text)), c => c.charCodeAt(0));\n  const bits = bytes.length * 8;\n  bytes.push(128);\n  while (bytes.length % 64 !== 56) bytes.push(0);\n  for (let i = 7; i >= 0; i--) bytes.push(Math.floor(bits / 2 ** (i * 8)) & 255);\n  const constants = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,\n    0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,\n    0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,\n    0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,\n    0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,\n    0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,\n    0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,\n    0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];\n  const state = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];\n  const rotate = (n, b) => n >>> b | n << (32 - b);\n  for (let offset = 0; offset < bytes.length; offset += 64) {\n    const words = [];\n    for (let i = 0; i < 16; i++) words[i] = bytes.slice(offset + i * 4, offset + i * 4 + 4).reduce((n, b) => n << 8 | b, 0);\n    for (let i = 16; i < 64; i++) {\n      const a = words[i - 15], b = words[i - 2];\n      words[i] = (words[i - 16] + (rotate(a,7) ^ rotate(a,18) ^ a >>> 3) + words[i - 7] + (rotate(b,17) ^ rotate(b,19) ^ b >>> 10)) | 0;\n    }\n    let [a,b,c,d,e,f,g,h] = state;\n    for (let i = 0; i < 64; i++) {\n      const t = (h + (rotate(e,6) ^ rotate(e,11) ^ rotate(e,25)) + (e & f ^ ~e & g) + constants[i] + words[i]) | 0;\n      const u = ((rotate(a,2) ^ rotate(a,13) ^ rotate(a,22)) + (a & b ^ a & c ^ b & c)) | 0;\n      [a,b,c,d,e,f,g,h] = [(t + u) | 0,a,b,c,(d + t) | 0,e,f,g];\n    }\n    [a,b,c,d,e,f,g,h].forEach((value, i) => { state[i] = (state[i] + value) | 0; });\n  }\n  return state.map(n => (n >>> 0).toString(16).padStart(8,'0')).join('');\n}\nfunction validateManifest(value) {\n  if (!value || value.schema !== 1 || !Number.isSafeInteger(value.version) || value.version < 1 ||\n      !Array.isArray(value.files) || value.files.length < 1 || value.files.length > 32) throw new Error('잘못된 AeroK 배포 목록입니다.');\n  const names = new Set();\n  for (const file of value.files) {\n    if (!file || typeof file.name !== 'string' || file.name.length > 80 || file.name.includes('..') ||\n        !/^(Initial|Config|AeroK[ A-Za-z0-9_-]*)\\.js$/.test(file.name) || names.has(file.name) ||\n        typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256) || typeof file.entry !== 'boolean' ||\n        file.entry && !file.name.startsWith('AeroK')) throw new Error('허용되지 않은 AeroK 배포 파일입니다.');\n    names.add(file.name);\n  }\n  if (!names.has('Initial.js')) throw new Error('Initial.js가 배포 목록에 없습니다.');\n  return value;\n}\nfunction wrapperSource(name) {\n  return `// Variables used by Scriptable.\n// These must be at the very top of the file. Do not edit.\n// icon-color: blue; icon-glyph: plane;\n// AeroK managed entry v1. 이 파일은 Initial.js가 관리합니다.\nconst __aerokEntry = ${JSON.stringify(name)};\nconst __aerokLocal = FileManager.local();\nconst __aerokFiles = __aerokLocal.isFileStoredIniCloud(module.filename) ? FileManager.iCloud() : __aerokLocal;\nconst __aerokPath = module.filename.slice(0, module.filename.lastIndexOf('/')) + '/Initial.js';\nlet __aerokSource;\ntry {\n  await __aerokFiles.downloadFileFromiCloud(__aerokPath);\n  __aerokSource = __aerokFiles.readString(__aerokPath);\n} catch (_) {\n  __aerokSource = __aerokLocal.readString(__aerokLocal.joinPath(__aerokLocal.libraryDirectory(), 'aerok-initial-cache.js'));\n}\nawait eval('(async()=>{\\\\n' + __aerokSource + '\\\\n})()');\n`;\n}\nfunction storage() {\n  const local = FileManager.local();\n  const root = module.filename.slice(0, module.filename.lastIndexOf('/'));\n  const files = local.isFileStoredIniCloud(module.filename) ? FileManager.iCloud() : local;\n  const home = local.joinPath(local.libraryDirectory(), 'aerok-managed-' + sha256(root).slice(0, 12));\n  local.createDirectory(home, true);\n  return {local, files, root, home, statePath: local.joinPath(home, 'state.json')};\n}\nfunction readState(store) {\n  try {\n    const state = JSON.parse(store.local.readString(store.statePath));\n    validateManifest(state);\n    if (!/^release-[a-zA-Z0-9-]+$/.test(state.directory) || !state.files.every(file =>\n      store.local.fileExists(store.local.joinPath(store.home, state.directory + '/' + file.name)))) return null;\n    return state;\n  } catch (_) { return null; }\n}\nasync function download(url) {\n  const request = new Request(url);\n  request.timeoutInterval = 8;\n  const body = await request.loadString();\n  if (request.response.statusCode !== 200 || !body || body.length > 1000000) throw new Error('AeroK 파일을 다운로드하지 못했습니다.');\n  return body;\n}\nasync function update() {\n  const store = storage();\n  let previous = readState(store);\n  let directory;\n  try {\n    const manifest = validateManifest(JSON.parse(await download(AEROK_DOWNLOAD + '/manifest.json')));\n    if (previous && manifest.version < previous.version) throw new Error('이전 버전의 배포 목록입니다.');\n    if (previous && manifest.version === previous.version) {\n      if (JSON.stringify(manifest.files) !== JSON.stringify(previous.files)) throw new Error('같은 버전의 배포 내용이 변경되었습니다.');\n      return {state: previous, updated: false};\n    }\n    directory = 'release-' + UUID.string();\n    const releasePath = store.local.joinPath(store.home, directory);\n    store.local.createDirectory(releasePath, true);\n    for (const file of manifest.files) {\n      const body = await download(AEROK_DOWNLOAD + '/releases/' + manifest.version + '/' + encodeURIComponent(file.name));\n      if (sha256(body) !== file.sha256) throw new Error('AeroK 파일 검증에 실패했습니다: ' + file.name);\n      new Function('return (async()=>{\\n' + body + '\\n})()');\n      store.local.writeString(store.local.joinPath(releasePath, file.name), body);\n    }\n    const entries = manifest.files.filter(file => file.entry).map(file => file.name);\n    const oldEntries = previous ? previous.files.filter(file => file.entry).map(file => file.name) : [];\n    const names = [...new Set(['Initial.js', ...entries, ...oldEntries])];\n    const originals = new Map();\n    for (const name of names) {\n      const path = store.files.joinPath(store.root, name);\n      if (store.files.fileExists(path)) {\n        await store.files.downloadFileFromiCloud(path);\n        originals.set(name, store.files.readString(path));\n      } else originals.set(name, null);\n      if (entries.includes(name) && originals.get(name) !== null && originals.get(name) !== wrapperSource(name) &&\n          (!previous || !previous.wrappers || originals.get(name) !== previous.wrappers[name])) {\n        throw new Error('같은 이름의 개인 스크립트가 있습니다. 이름을 바꾼 뒤 다시 설치하세요: ' + name);\n      }\n    }\n    // 다른 실행이 먼저 새 버전을 설치했다면 그 설치를 사용합니다.\n    const current = readState(store);\n    if (current && (!previous || current.version > previous.version)) {\n      store.local.remove(releasePath);\n      return {state: current, updated: false};\n    }\n    const state = {schema: 1, version: manifest.version, files: manifest.files, directory,\n      wrappers: Object.fromEntries(entries.map(name => [name, wrapperSource(name)]))};\n    const pending = store.local.joinPath(store.home, 'state.pending-' + directory + '.json');\n    const touched = [];\n    try {\n      store.local.writeString(pending, JSON.stringify(state));\n      for (const name of names) {\n        const path = store.files.joinPath(store.root, name);\n        if (name === 'Initial.js') {\n          const body = store.local.readString(store.local.joinPath(releasePath, name));\n          if (originals.get(name) !== body) { touched.push(name); store.files.writeString(path, body); }\n        } else if (entries.includes(name)) {\n          if (originals.get(name) !== wrapperSource(name)) { touched.push(name); store.files.writeString(path, wrapperSource(name)); }\n        } else if (originals.get(name) === wrapperSource(name) ||\n            previous && previous.wrappers && originals.get(name) === previous.wrappers[name]) {\n          touched.push(name); store.files.remove(path);\n        }\n      }\n      store.local.move(pending, store.statePath);\n    } catch (error) {\n      for (const name of touched.reverse()) {\n        const path = store.files.joinPath(store.root, name);\n        if (originals.get(name) === null) { if (store.files.fileExists(path)) store.files.remove(path); }\n        else store.files.writeString(path, originals.get(name));\n      }\n      throw error;\n    }\n    return {state, updated: true};\n  } catch (error) {\n    if (directory) {\n      const path = store.local.joinPath(store.home, directory);\n      if (store.local.fileExists(path)) store.local.remove(path);\n    }\n    previous = readState(store) || previous;\n    if (previous) return {state: previous, updated: false, warning: error.message};\n    throw error;\n  }\n}\nasync function setupKey(store) {\n  const path = store.files.joinPath(store.root, 'Secrets.js');\n  const exists = store.files.fileExists(path);\n  if (exists) {\n    await store.files.downloadFileFromiCloud(path);\n    const existing = importModule(path);\n    if (!existing || typeof existing !== 'object') throw new Error('Secrets.js는 객체를 내보내야 합니다.');\n    if (typeof existing.WIDGET_API_TOKEN === 'string' && existing.WIDGET_API_TOKEN.trim() && !/\\s/.test(existing.WIDGET_API_TOKEN)) return;\n  }\n  const key = new Alert();\n  key.title = 'AeroK 일정 연결';\n  key.message = '본인에게 발급된 위젯 키를 입력하세요. 키는 본인의 Scriptable 폴더에 저장하며 배포하지 않습니다.';\n  key.addSecureTextField('위젯 키', '');\n  key.addAction('저장'); key.addCancelAction('나중에');\n  if (await key.presentAlert() === -1) return;\n  const token = key.textFieldValue(0).trim();\n  if (!token || /\\s/.test(token)) throw new Error('키가 비어 있거나 공백을 포함합니다. Initial을 다시 실행해 등록하세요.');\n  const original = exists ? store.files.readString(path) + '\\n' : 'module.exports = {};\\n';\n  store.files.writeString(path, original + 'module.exports.WIDGET_API_TOKEN = ' + JSON.stringify(token) + ';\\n');\n}\nasync function main() {\n  const entry = typeof __aerokEntry === 'string' ? __aerokEntry : null;\n  try {\n    const result = await update();\n    const store = storage();\n    try {\n      store.local.writeString(store.local.joinPath(store.local.libraryDirectory(), 'aerok-initial-cache.js'),\n        store.local.readString(store.local.joinPath(store.home, result.state.directory + '/Initial.js')));\n    } catch (_) { /* 로컬 부트스트랩 사본 저장 실패가 일정 표시를 막지 않도록 합니다. */ }\n    if (entry) {\n      if (!result.state.files.some(file => file.name === entry && file.entry)) {\n        const widget = new ListWidget();\n        widget.addText('이 AeroK 위젯은 배포가 종료되었습니다.');\n        Script.setWidget(widget); Script.complete(); return;\n      }\n      const loaded = importModule(store.local.joinPath(store.home, result.state.directory + '/' + entry));\n      if (!loaded || typeof loaded.run !== 'function') throw new Error('AeroK 실행 모듈을 확인하세요.');\n      await loaded.run();\n      return;\n    }\n    if (config.runsInApp) {\n      await setupKey(store);\n      const notice = new Alert();\n      notice.title = result.warning ? '기존 버전 사용' : 'AeroK 설치 완료';\n      notice.message = result.warning || '홈 화면에 Scriptable 위젯을 추가하세요. 월간은 큰 위젯, 오늘·내일은 작은 위젯입니다. 이후 실행마다 업데이트를 확인합니다.';\n      notice.addAction('확인'); await notice.presentAlert();\n    }\n    Script.complete();\n  } catch (error) {\n    if (config.runsInApp) {\n      const notice = new Alert(); notice.title = 'AeroK 설치 확인'; notice.message = error.message;\n      notice.addAction('확인'); await notice.presentAlert(); Script.complete();\n    } else throw error;\n  }\n}\n\nawait main();\n",
  "icon": {
    "color": "blue",
    "glyph": "download"
  },
  "always_run_in_app": true,
  "share_sheet_inputs": []
}
