gitifact 0.3.2 → 0.4.0

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.
Files changed (32) hide show
  1. package/README.md +8 -1
  2. package/dist/browser/assets/Markdown-BG_9T3yA.js +68 -0
  3. package/dist/browser/assets/MetadataListItem-BJjmYUP-.js +1 -0
  4. package/dist/browser/assets/about-C5lrF_v9.js +3 -0
  5. package/dist/browser/assets/changelog-CxmMo2wk.css +1 -0
  6. package/dist/browser/assets/changelog-hNvOtw70.js +2 -0
  7. package/dist/browser/assets/{contributors._email-bq12TRAr.js → contributors._email-CwG6z4oO.js} +1 -1
  8. package/dist/browser/assets/{contributors.index-eTu4P306.js → contributors.index-B9l1YPjl.js} +1 -1
  9. package/dist/browser/assets/{features._featureId-CGRWLMyZ.js → features._featureId-uB5QcPIl.js} +1 -1
  10. package/dist/browser/assets/{features.index-_wXeEia1.js → features.index-DzLFYkRG.js} +1 -1
  11. package/dist/browser/assets/git-DJKAcbnl.js +1 -0
  12. package/dist/browser/assets/{guides._documentId-D3RCTjtd.js → guides._documentId-DHM6effA.js} +1 -1
  13. package/dist/browser/assets/{guides.index-CFMN9ggJ.js → guides.index-BfJqdqJd.js} +1 -1
  14. package/dist/browser/assets/index-jaVGAf81.js +56 -0
  15. package/dist/browser/assets/{page-header-CYf1yQV1.js → page-header-DI5eoQC1.js} +4 -4
  16. package/dist/browser/assets/product-YuwMeOC0.js +8 -0
  17. package/dist/browser/assets/{product.index-AF24IXK3.js → product.index-RgfLdFqG.js} +1 -1
  18. package/dist/browser/assets/request-state-DpUKcy6G.js +1 -0
  19. package/dist/browser/assets/{requirements-DpTMK_Uu.js → requirements-BMLc9Fpl.js} +1 -1
  20. package/dist/browser/assets/{routes-jgWFqeRv.js → routes-XUldGkLf.js} +1 -1
  21. package/dist/browser/index.html +6 -5
  22. package/dist/i18n/ko/block.md +2 -1
  23. package/dist/i18n/ko/changelog.md +12 -0
  24. package/dist/i18n/ko/docs/workflow.md +1 -1
  25. package/dist/main.js +289 -34
  26. package/package.json +1 -1
  27. package/dist/browser/assets/Markdown-C8BT0YZz.js +0 -68
  28. package/dist/browser/assets/about-DMFBT-sA.js +0 -3
  29. package/dist/browser/assets/git-CTULSVR7.js +0 -1
  30. package/dist/browser/assets/index-Cl1J8uRf.js +0 -56
  31. package/dist/browser/assets/product-BxZktNQM.js +0 -8
  32. package/dist/browser/assets/request-state-DzkYXwJR.js +0 -1
package/dist/main.js CHANGED
@@ -4030,6 +4030,70 @@ function comparePreviewBundles(before, after) {
4030
4030
  return { specChanges: specs.specChanges, changes: [...specs.changes, ...compareDocumentSets(a.documents, b.documents)] };
4031
4031
  }
4032
4032
 
4033
+ // ../../packages/core/dist/formats/changelog.js
4034
+ var changelogSections = ["added", "changed", "removed", "fixed"];
4035
+ var versionLine = /^## (\d+)\.(\d+)\.(\d+) - (\d{4})-(\d{2})-(\d{2})$/;
4036
+ var sectionLine = /^### (Added|Changed|Removed|Fixed)$/;
4037
+ var itemLine = /^- (\S.*)$/;
4038
+ var realDate = (year, month, day) => {
4039
+ const date5 = new Date(Date.UTC(year, month - 1, day));
4040
+ return date5.getUTCFullYear() === year && date5.getUTCMonth() === month - 1 && date5.getUTCDate() === day;
4041
+ };
4042
+ function parseChangelog(text2) {
4043
+ const entries = [];
4044
+ let previous;
4045
+ let entry2;
4046
+ let section;
4047
+ const fail8 = (line, key, values = {}) => new InitError("INVALID_CHANGELOG", t("changelog.atLine", { line, reason: t(key, values) }));
4048
+ const lines = text2.replace(/\r\n/g, "\n").split("\n");
4049
+ const close = (line) => {
4050
+ if (entry2 && changelogSections.every((name) => entry2[name].length === 0))
4051
+ throw fail8(line, "changelog.emptyVersion", { version: entry2.version });
4052
+ };
4053
+ lines.forEach((raw, index) => {
4054
+ const line = index + 1;
4055
+ if (raw.trim() === "")
4056
+ return;
4057
+ const version2 = versionLine.exec(raw);
4058
+ if (version2) {
4059
+ close(line);
4060
+ const core = [Number(version2[1]), Number(version2[2]), Number(version2[3])];
4061
+ if (!realDate(Number(version2[4]), Number(version2[5]), Number(version2[6])))
4062
+ throw fail8(line, "changelog.invalidDate", { text: raw });
4063
+ if (previous && !(core[0] < previous[0] || core[0] === previous[0] && (core[1] < previous[1] || core[1] === previous[1] && core[2] < previous[2]))) {
4064
+ throw fail8(line, "changelog.order", { version: core.join(".") });
4065
+ }
4066
+ previous = core;
4067
+ section = void 0;
4068
+ entry2 = { version: core.join("."), date: version2[4] + "-" + version2[5] + "-" + version2[6], added: [], changed: [], removed: [], fixed: [] };
4069
+ entries.push(entry2);
4070
+ return;
4071
+ }
4072
+ const heading = sectionLine.exec(raw);
4073
+ if (heading) {
4074
+ if (!entry2)
4075
+ throw fail8(line, "changelog.sectionBeforeVersion");
4076
+ const name = heading[1].toLowerCase();
4077
+ if (entry2[name].length > 0)
4078
+ throw fail8(line, "changelog.duplicateSection", { section: heading[1] });
4079
+ section = name;
4080
+ return;
4081
+ }
4082
+ const item = itemLine.exec(raw);
4083
+ if (item) {
4084
+ if (!entry2 || !section)
4085
+ throw fail8(line, "changelog.itemOutsideSection");
4086
+ entry2[section].push(item[1].trimEnd());
4087
+ return;
4088
+ }
4089
+ throw fail8(line, "changelog.unknownLine", { text: raw });
4090
+ });
4091
+ close(lines.length);
4092
+ if (entries.length === 0)
4093
+ throw fail8(1, "changelog.noVersions");
4094
+ return entries;
4095
+ }
4096
+
4033
4097
  // ../../packages/core/dist/use-cases/edit-spec-preview.js
4034
4098
  var fail2 = (message) => {
4035
4099
  throw new SpecPreviewError(message);
@@ -24380,16 +24444,7 @@ var repositoryStatusFailureV1 = external_exports.strictObject({
24380
24444
  });
24381
24445
  var repositoryStatusV1 = external_exports.union([repositoryStatusSuccessV1, repositoryStatusFailureV1]);
24382
24446
 
24383
- // ../../packages/contracts/dist/versions/browser-session-v1.js
24384
- var browserSessionV1 = external_exports.strictObject({
24385
- contract: external_exports.literal("browser-session"),
24386
- version: external_exports.literal(1),
24387
- sessionId: external_exports.uuid(),
24388
- repository: external_exports.strictObject({
24389
- key: external_exports.string().regex(/^repo:[0-9a-f]{64}$/),
24390
- worktreeKey: external_exports.string().regex(/^worktree:[0-9a-f]{64}$/)
24391
- })
24392
- });
24447
+ // ../../packages/contracts/dist/versions/browser-http-error-v1.js
24393
24448
  var browserHttpErrorV1 = external_exports.strictObject({
24394
24449
  contract: external_exports.literal("browser-http-error"),
24395
24450
  version: external_exports.literal(1),
@@ -24489,6 +24544,42 @@ var changelogV1 = external_exports.strictObject({
24489
24544
  }
24490
24545
  });
24491
24546
 
24547
+ // ../../packages/contracts/dist/versions/browser-session-v2.js
24548
+ var release = external_exports.string().regex(/^\d+\.\d+\.\d+$/);
24549
+ var updateStateV1 = external_exports.strictObject({
24550
+ status: external_exports.enum(["checking", "available", "up-to-date", "unavailable", "disabled"]),
24551
+ latestVersion: release.nullable()
24552
+ }).refine((value) => (value.status === "available" || value.status === "up-to-date") === (value.latestVersion !== null), "latestVersion is set exactly when the check produced a result.");
24553
+ var browserSessionV2 = external_exports.strictObject({
24554
+ contract: external_exports.literal("browser-session"),
24555
+ version: external_exports.literal(2),
24556
+ sessionId: external_exports.uuid(),
24557
+ repository: external_exports.strictObject({
24558
+ key: external_exports.string().regex(/^repo:[0-9a-f]{64}$/),
24559
+ worktreeKey: external_exports.string().regex(/^worktree:[0-9a-f]{64}$/)
24560
+ }),
24561
+ // The running CLI, the same value as `gitifact --version`. A development build may carry a pre-release suffix.
24562
+ cliVersion: external_exports.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/),
24563
+ update: updateStateV1
24564
+ });
24565
+
24566
+ // ../../packages/contracts/dist/versions/update-v1.js
24567
+ var updateV1 = external_exports.discriminatedUnion("ok", [
24568
+ external_exports.strictObject({
24569
+ contract: external_exports.literal("update"),
24570
+ version: external_exports.literal(1),
24571
+ ok: external_exports.literal(true),
24572
+ cliVersion: external_exports.string().min(1),
24573
+ update: updateStateV1,
24574
+ // Present only when a newer release exists. The command is for npm global installs; other installs differ.
24575
+ install: external_exports.strictObject({ npmGlobal: external_exports.string().min(1) }).nullable(),
24576
+ // refreshed: blocks whose version line differed were rewritten. current: every block already matched.
24577
+ // not-initialized: no .gitifact config here, so only the version check ran. no-block: no file carries a block.
24578
+ agentDocs: external_exports.strictObject({ state: external_exports.enum(["refreshed", "current", "not-initialized", "no-block"]), paths: external_exports.array(external_exports.string()) })
24579
+ }),
24580
+ external_exports.strictObject({ contract: external_exports.literal("update"), version: external_exports.literal(1), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
24581
+ ]);
24582
+
24492
24583
  // src/shared/i18n/ko/messages.json
24493
24584
  var messages_default2 = {
24494
24585
  "help.program": "\uD504\uB85C\uC81D\uD2B8\uC758 \uC694\uAD6C\uC0AC\uD56D\uACFC \uACB0\uC815 \uC774\uB825\uC744 Git\uC5D0 \uB0A8\uAE30\uB294 \uB3C4\uAD6C",
@@ -24717,7 +24808,24 @@ var messages_default2 = {
24717
24808
  "docs.summary.spec": "\uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC\xB7\uC218\uC6A9 \uC870\uAC74 \uD615\uC2DD, ID \uADDC\uCE59, spec save \uC785\uB825",
24718
24809
  "docs.summary.design": "design.md \uBAA9\uCC28, \uCC38\uC870 \uC8FC\uC11D, \uAC1C\uC815 \uBC29\uC2DD",
24719
24810
  "docs.summary.product": "PRODUCT.md\uC640 guides \uBB38\uC11C\uC758 \uC5ED\uD560\uACFC \uC800\uC7A5 \uBA85\uB839",
24720
- "docs.summary.commit": "spec commit \uC785\uB825, \uC774\uC720 \uAE30\uB85D, \uC2E4\uD328 \uD6C4 \uBCF5\uAD6C"
24811
+ "docs.summary.commit": "spec commit \uC785\uB825, \uC774\uC720 \uAE30\uB85D, \uC2E4\uD328 \uD6C4 \uBCF5\uAD6C",
24812
+ "help.browserNoUpdateCheck": "\uC0C8 \uBC84\uC804 \uD655\uC778\uC744 \uC704\uD55C npm \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C\uB97C \uB054 (GITIFACT_NO_UPDATE_CHECK\uC640 \uAC19\uC74C)",
24813
+ "help.update": "\uC0C8 \uBC84\uC804 \uD655\uC778\uACFC \uC124\uCE58 \uBC29\uBC95 \uC548\uB0B4, \uD504\uB85C\uC81D\uD2B8 \uC9C0\uCE68 \uD30C\uC77C\uC758 GITIFACT \uBE14\uB85D\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0",
24814
+ "server.badLanguage": "\uC5B8\uC5B4 \uCF54\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
24815
+ "server.changelogNotFound": "\uD328\uCE58\uB178\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
24816
+ "server.changelogUnreadable": "\uD328\uCE58\uB178\uD2B8\uB97C \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
24817
+ "update.text.current": "\uD604\uC7AC \uBC84\uC804: {version}",
24818
+ "update.text.available": "\uC0C8 \uBC84\uC804: {version}",
24819
+ "update.text.upToDate": "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.",
24820
+ "update.text.disabled": "\uC0C8 \uBC84\uC804 \uD655\uC778\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4.",
24821
+ "update.text.unavailable": "\uC0C8 \uBC84\uC804\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC138\uC694.",
24822
+ "update.text.install": "npm \uC804\uC5ED \uC124\uCE58\uC6A9: {command}",
24823
+ "update.text.afterInstall": "\uC124\uCE58\uD55C \uB4A4 gitifact update\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC9C0\uCE68 \uBE14\uB85D\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0\uB429\uB2C8\uB2E4.",
24824
+ "update.text.blockRefreshed": "\uC9C0\uCE68 \uBE14\uB85D \uAC31\uC2E0: {paths}",
24825
+ "update.text.blockCurrent": "\uC9C0\uCE68 \uBE14\uB85D\uC740 \uC774\uBBF8 \uD604\uC7AC \uBC84\uC804\uC785\uB2C8\uB2E4: {paths}",
24826
+ "update.text.noBlock": "GITIFACT \uBE14\uB85D\uC774 \uC788\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. gitifact init\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694.",
24827
+ "update.text.notInitialized": "\uCD08\uAE30\uD654\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2C8\uC5B4\uC11C \uBC84\uC804\uB9CC \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
24828
+ "update.failed": "\uC5C5\uB370\uC774\uD2B8 \uD655\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."
24721
24829
  };
24722
24830
 
24723
24831
  // src/shared/i18n/index.ts
@@ -24832,17 +24940,63 @@ function contentType(path3) {
24832
24940
 
24833
24941
  // src/server/status-session.ts
24834
24942
  import { randomUUID as randomUUID2 } from "node:crypto";
24835
- function createStatusSession(initial, read) {
24836
- const session = browserSessionV1.parse({
24837
- contract: "browser-session",
24838
- version: 1,
24839
- sessionId: randomUUID2(),
24840
- repository: { key: initial.repository.key, worktreeKey: initial.repository.worktreeKey }
24841
- });
24943
+
24944
+ // src/shared/update-check.ts
24945
+ var updateCheckTimeoutMs = 3e3;
24946
+ var release2 = /^(\d+)\.(\d+)\.(\d+)$/;
24947
+ var running = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/;
24948
+ function isNewerRelease(latest, current) {
24949
+ const next = release2.exec(latest);
24950
+ const now = running.exec(current);
24951
+ if (!next || !now) return false;
24952
+ for (let index = 1; index <= 3; index++) {
24953
+ const difference = Number(next[index]) - Number(now[index]);
24954
+ if (difference !== 0) return difference > 0;
24955
+ }
24956
+ return now[4] !== void 0;
24957
+ }
24958
+ var updateCheckDisabled = (env, flagged = false) => flagged || env.GITIFACT_NO_UPDATE_CHECK !== void 0 && env.GITIFACT_NO_UPDATE_CHECK !== "" && env.GITIFACT_NO_UPDATE_CHECK !== "0";
24959
+ var checkingUpdate = { status: "checking", latestVersion: null };
24960
+ var disabledUpdate = { status: "disabled", latestVersion: null };
24961
+ async function resolveUpdate(current, fetchLatest, signal, timeoutMs = updateCheckTimeoutMs) {
24962
+ const controller = new AbortController();
24963
+ const abort = () => controller.abort();
24964
+ const timer = setTimeout(abort, timeoutMs);
24965
+ signal?.addEventListener("abort", abort, { once: true });
24966
+ if (signal?.aborted) controller.abort();
24967
+ try {
24968
+ const latest = await Promise.race([
24969
+ fetchLatest(controller.signal),
24970
+ new Promise((_resolve, reject) => {
24971
+ if (controller.signal.aborted) reject(new Error("aborted"));
24972
+ else controller.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
24973
+ })
24974
+ ]);
24975
+ if (typeof latest !== "string" || !release2.test(latest)) return { status: "unavailable", latestVersion: null };
24976
+ return { status: isNewerRelease(latest, current) ? "available" : "up-to-date", latestVersion: latest };
24977
+ } catch {
24978
+ return { status: "unavailable", latestVersion: null };
24979
+ } finally {
24980
+ clearTimeout(timer);
24981
+ signal?.removeEventListener("abort", abort);
24982
+ }
24983
+ }
24984
+ var npmGlobalInstall = (version2) => "npm install -g gitifact@" + version2;
24985
+
24986
+ // src/server/status-session.ts
24987
+ function createStatusSession(initial, read, cliVersion = "0.0.0") {
24988
+ const identity2 = { sessionId: randomUUID2(), repository: { key: initial.repository.key, worktreeKey: initial.repository.worktreeKey } };
24989
+ let update = disabledUpdate;
24842
24990
  let latest = initial;
24843
24991
  let pending;
24844
24992
  return {
24845
- session,
24993
+ // The identity never changes; only the update state moves (checking → a final state), so the session is built per read.
24994
+ get session() {
24995
+ return browserSessionV2.parse({ contract: "browser-session", version: 2, ...identity2, cliVersion, update });
24996
+ },
24997
+ setUpdate(next) {
24998
+ update = next;
24999
+ },
24846
25000
  get latest() {
24847
25001
  return latest;
24848
25002
  },
@@ -24854,7 +25008,7 @@ function createStatusSession(initial, read) {
24854
25008
  pending = (async () => {
24855
25009
  try {
24856
25010
  const next = await read();
24857
- if (next.repository.key !== session.repository.key || next.repository.worktreeKey !== session.repository.worktreeKey) {
25011
+ if (next.repository.key !== identity2.repository.key || next.repository.worktreeKey !== identity2.repository.worktreeKey) {
24858
25012
  throw new RepositoryReadError("REPOSITORY_CHANGED");
24859
25013
  }
24860
25014
  latest = next;
@@ -25432,6 +25586,10 @@ function createSpecBrowserReader(root, sessionId, inherited = process.env) {
25432
25586
  }
25433
25587
 
25434
25588
  // src/server/browser-server.ts
25589
+ var readBundledChangelog = (language) => readFile4(new URL("./i18n/" + language + "/changelog.md", import.meta.url), "utf8").catch((error62) => {
25590
+ if (error62.code === "ENOENT") return null;
25591
+ throw error62;
25592
+ });
25435
25593
  async function startBrowserServer(options) {
25436
25594
  const controller = new AbortController();
25437
25595
  const onAbort = () => controller.abort();
@@ -25451,11 +25609,13 @@ async function startBrowserServer(options) {
25451
25609
  options.signal?.removeEventListener("abort", onAbort);
25452
25610
  throw error62;
25453
25611
  }
25454
- const store = createStatusSession(initial, () => read(controller.signal));
25455
- const { session } = store;
25612
+ const store = createStatusSession(initial, () => read(controller.signal), options.cliVersion);
25613
+ const sessionId = store.session.sessionId;
25614
+ const readChangelog = options.readChangelog ?? readBundledChangelog;
25615
+ let updatePending;
25456
25616
  let closing = false;
25457
25617
  let origin = "";
25458
- const readSpecs = createSpecBrowserReader(initial.repository.rootPath, session.sessionId, options.env);
25618
+ const readSpecs = createSpecBrowserReader(initial.repository.rootPath, sessionId, options.env);
25459
25619
  function json2(response, status, value) {
25460
25620
  response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
25461
25621
  response.end(JSON.stringify(value) + "\n");
@@ -25493,7 +25653,7 @@ async function startBrowserServer(options) {
25493
25653
  const api = path3 === "/api" || path3.startsWith("/api/");
25494
25654
  if (api) {
25495
25655
  if (request.headers["sec-fetch-site"] === "cross-site" && !allowedOrigin) return fail8(response, 403, "FORBIDDEN", t2("server.crossSite"));
25496
- if (url2.search && path3 !== "/api/v1/specs") return fail8(response, 400, "BAD_REQUEST", t2("server.noQuery"));
25656
+ if (url2.search && path3 !== "/api/v1/specs" && path3 !== "/api/v1/changelog") return fail8(response, 400, "BAD_REQUEST", t2("server.noQuery"));
25497
25657
  if (path3.startsWith("/api/v1/product/assets/")) {
25498
25658
  const name = path3.slice("/api/v1/product/assets/".length);
25499
25659
  if (request.method !== "GET") {
@@ -25510,13 +25670,13 @@ async function startBrowserServer(options) {
25510
25670
  return;
25511
25671
  }
25512
25672
  const method = path3 === "/api/v1/status/refresh" ? "POST" : "GET";
25513
- if (!["/api/v1/session", "/api/v1/status", "/api/v1/status/refresh", "/api/v1/specs"].includes(path3)) return fail8(response, 404, "NOT_FOUND", t2("server.apiNotFound"));
25673
+ if (!["/api/v1/session", "/api/v1/status", "/api/v1/status/refresh", "/api/v1/specs", "/api/v1/changelog"].includes(path3)) return fail8(response, 404, "NOT_FOUND", t2("server.apiNotFound"));
25514
25674
  if (request.method !== method) {
25515
25675
  response.setHeader("Allow", method);
25516
25676
  return fail8(response, 405, "METHOD_NOT_ALLOWED", t2("server.methodNotAllowed"));
25517
25677
  }
25518
- if (path3 === "/api/v1/session") return json2(response, 200, session);
25519
- if (request.headers["x-gitifact-session"] !== session.sessionId) return fail8(response, 409, "SESSION_CHANGED", t2("server.sessionChanged"));
25678
+ if (path3 === "/api/v1/session") return json2(response, 200, store.session);
25679
+ if (request.headers["x-gitifact-session"] !== sessionId) return fail8(response, 409, "SESSION_CHANGED", t2("server.sessionChanged"));
25520
25680
  if (path3 === "/api/v1/specs") {
25521
25681
  const cursor = url2.searchParams.get("cursor") ?? "0";
25522
25682
  const head = url2.searchParams.get("head");
@@ -25528,6 +25688,19 @@ async function startBrowserServer(options) {
25528
25688
  }
25529
25689
  return;
25530
25690
  }
25691
+ if (path3 === "/api/v1/changelog") {
25692
+ const language = url2.searchParams.get("lang") ?? defaultLanguage2;
25693
+ if ([...url2.searchParams.keys()].some((key) => key !== "lang") || url2.searchParams.getAll("lang").length > 1 || !/^[a-z]{2}(?:-[A-Z]{2})?$/.test(language)) return fail8(response, 400, "BAD_REQUEST", t2("server.badLanguage"));
25694
+ try {
25695
+ const own2 = await readChangelog(language);
25696
+ const text2 = own2 ?? (language === defaultLanguage2 ? null : await readChangelog(defaultLanguage2));
25697
+ if (text2 === null) return fail8(response, 404, "NOT_FOUND", t2("server.changelogNotFound"));
25698
+ json2(response, 200, changelogV1.parse({ contract: "changelog", version: 1, language: own2 === null ? defaultLanguage2 : language, fallback: own2 === null, entries: parseChangelog(text2) }));
25699
+ } catch (error62) {
25700
+ fail8(response, 503, "INTERNAL_ERROR", error62 instanceof Error ? error62.message : t2("server.changelogUnreadable"));
25701
+ }
25702
+ return;
25703
+ }
25531
25704
  if (method === "POST" && !allowedOrigin) return fail8(response, 403, "FORBIDDEN", t2("server.refreshOrigin"));
25532
25705
  const value = method === "POST" ? await store.refresh() : store.latest;
25533
25706
  if (!response.destroyed) json2(response, value.ok ? 200 : 503, value);
@@ -25573,6 +25746,12 @@ async function startBrowserServer(options) {
25573
25746
  const address = server.address();
25574
25747
  if (!address || typeof address === "string") throw new Error(t2("server.addressUnknown"));
25575
25748
  origin = "http://127.0.0.1:" + address.port;
25749
+ if (options.fetchLatest) {
25750
+ store.setUpdate(checkingUpdate);
25751
+ updatePending = resolveUpdate(store.session.cliVersion, options.fetchLatest, controller.signal, options.updateCheckTimeoutMs).then((state) => {
25752
+ store.setUpdate(state);
25753
+ });
25754
+ }
25576
25755
  let resolveClosed;
25577
25756
  const closed = new Promise((resolve) => {
25578
25757
  resolveClosed = resolve;
@@ -25588,6 +25767,7 @@ async function startBrowserServer(options) {
25588
25767
  server.closeAllConnections();
25589
25768
  });
25590
25769
  await store.pending;
25770
+ await updatePending;
25591
25771
  options.signal?.removeEventListener("abort", onAbort);
25592
25772
  options.signal?.removeEventListener("abort", closeOnAbort);
25593
25773
  resolveClosed();
@@ -25599,15 +25779,28 @@ async function startBrowserServer(options) {
25599
25779
  };
25600
25780
  options.signal?.addEventListener("abort", closeOnAbort, { once: true });
25601
25781
  if (options.signal?.aborted) void close();
25602
- return { url: origin, session, close, closed };
25782
+ return { url: origin, get session() {
25783
+ return store.session;
25784
+ }, close, closed };
25603
25785
  }
25604
25786
 
25787
+ // src/adapters/registry/latest-version.ts
25788
+ var manifestUrl = "https://registry.npmjs.org/gitifact";
25789
+ var fetchLatestVersion = async (signal) => {
25790
+ const response = await fetch(manifestUrl, { signal, redirect: "error", headers: { Accept: "application/vnd.npm.install-v1+json" } });
25791
+ if (!response.ok) throw new Error("registry status " + response.status);
25792
+ const manifest = await response.json();
25793
+ const latest = manifest["dist-tags"]?.latest;
25794
+ if (typeof latest !== "string") throw new Error("registry answer has no latest tag");
25795
+ return latest;
25796
+ };
25797
+
25605
25798
  // src/commands/browser.ts
25606
25799
  function parsePort(value) {
25607
25800
  if (!/^\d+$/.test(value) || Number(value) > 65535) throw new InvalidArgumentError(t2("browser.invalidPort"));
25608
25801
  return Number(value);
25609
25802
  }
25610
- async function runBrowser(options) {
25803
+ async function runBrowser(options, version2) {
25611
25804
  const controller = new AbortController();
25612
25805
  const stop = () => controller.abort();
25613
25806
  process.once("SIGINT", stop);
@@ -25617,7 +25810,10 @@ async function runBrowser(options) {
25617
25810
  cwd: process.cwd(),
25618
25811
  port: options.port,
25619
25812
  dev: options.dev ?? false,
25620
- signal: controller.signal
25813
+ signal: controller.signal,
25814
+ cliVersion: version2,
25815
+ // --no-update-check or GITIFACT_NO_UPDATE_CHECK keeps the server from contacting the npm registry.
25816
+ ...updateCheckDisabled(process.env, options.updateCheck === false) ? {} : { fetchLatest: fetchLatestVersion }
25621
25817
  });
25622
25818
  if (!controller.signal.aborted) process.stdout.write(server.url + "/\n");
25623
25819
  await server.closed;
@@ -25892,7 +26088,7 @@ async function planAgentDocs(root, options) {
25892
26088
  return { mode: "remove", paths: paths2, writes };
25893
26089
  }
25894
26090
  const block = await renderAgentBlock(options.version, options);
25895
- const { inject, create } = resolveAgentPaths(options.agent, existing);
26091
+ const { inject, create } = options.onlyExisting ? { inject: candidatePaths.filter((path3) => existing.has(path3) && findBlock(existing.get(path3)) && !isWrapperFile(existing.get(path3))), create: null } : resolveAgentPaths(options.agent, existing);
25896
26092
  for (const path3 of inject) {
25897
26093
  const previous = existing.get(path3);
25898
26094
  const next = injectBlock(previous, block, boilerplateFor(path3));
@@ -26004,6 +26200,64 @@ ${t2("init.text.agentDocs")}: ${docs}
26004
26200
  }
26005
26201
  }
26006
26202
 
26203
+ // src/commands/update.ts
26204
+ async function refreshBlocks(cwd, version2, env, controls) {
26205
+ const repo = initRepository(cwd, env);
26206
+ let first;
26207
+ let config2;
26208
+ try {
26209
+ first = await repo.inspect();
26210
+ config2 = await readConfigFile(first.state.repository.rootPath);
26211
+ } catch (error62) {
26212
+ if (error62 instanceof RepositoryReadError && error62.code === "NOT_A_REPOSITORY" || error62 instanceof InitError && error62.code === "MIGRATION_REQUIRED") return { state: "not-initialized", paths: [] };
26213
+ throw error62;
26214
+ }
26215
+ if (config2 === void 0) return { state: "not-initialized", paths: [] };
26216
+ const root = first.state.repository.rootPath;
26217
+ const plan = await planAgentDocs(root, { version: version2, onlyExisting: true, ...controls });
26218
+ if (plan.paths.length === 0) return { state: "no-block", paths: [] };
26219
+ if (plan.writes.length === 0) return { state: "current", paths: plan.paths };
26220
+ const unchanged = async () => {
26221
+ if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== config2) throw new InitError("INPUT_CHANGED", t2("init.inputChanged"));
26222
+ };
26223
+ return { state: "refreshed", paths: await applyAgentDocs(root, plan, unchanged) };
26224
+ }
26225
+ async function updateCommand(cwd, version2, env = process.env, controls = {}) {
26226
+ const [update, agentDocs] = await Promise.all([
26227
+ updateCheckDisabled(env) ? disabledUpdate : resolveUpdate(version2, controls.fetchLatest ?? fetchLatestVersion, void 0, controls.timeoutMs),
26228
+ refreshBlocks(cwd, version2, env, controls)
26229
+ ]);
26230
+ return updateV1.parse({
26231
+ contract: "update",
26232
+ version: 1,
26233
+ ok: true,
26234
+ cliVersion: version2,
26235
+ update,
26236
+ install: update.status === "available" ? { npmGlobal: npmGlobalInstall(update.latestVersion) } : null,
26237
+ agentDocs
26238
+ });
26239
+ }
26240
+ async function runUpdate(options, version2) {
26241
+ try {
26242
+ const dto = await updateCommand(process.cwd(), version2);
26243
+ if (!dto.ok) throw new Error("unreachable");
26244
+ if (options.format !== "text") {
26245
+ process.stdout.write(JSON.stringify(dto) + "\n");
26246
+ return;
26247
+ }
26248
+ const lines = [t2("update.text.current", { version: dto.cliVersion })];
26249
+ lines.push(dto.update.status === "available" ? t2("update.text.available", { version: dto.update.latestVersion }) : dto.update.status === "up-to-date" ? t2("update.text.upToDate") : dto.update.status === "disabled" ? t2("update.text.disabled") : t2("update.text.unavailable"));
26250
+ if (dto.install) lines.push(t2("update.text.install", { command: dto.install.npmGlobal }), t2("update.text.afterInstall"));
26251
+ lines.push(dto.agentDocs.state === "refreshed" ? t2("update.text.blockRefreshed", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "current" ? t2("update.text.blockCurrent", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "no-block" ? t2("update.text.noBlock") : t2("update.text.notInitialized"));
26252
+ process.stdout.write(lines.join("\n") + "\n");
26253
+ } catch (error62) {
26254
+ const known = error62 instanceof InitError || error62 instanceof RepositoryReadError;
26255
+ const failure2 = { code: known ? error62.code : "UPDATE_FAILED", message: known ? error62.message : t2("update.failed") };
26256
+ process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(updateV1.parse({ contract: "update", version: 1, ok: false, error: failure2 })) + "\n");
26257
+ process.exitCode = 1;
26258
+ }
26259
+ }
26260
+
26007
26261
  // src/commands/spec-preview.ts
26008
26262
  import { join as join12 } from "node:path";
26009
26263
  import { readFile as readFile12, stat as stat2 } from "node:fs/promises";
@@ -26682,15 +26936,16 @@ async function runMigrate(options) {
26682
26936
  }
26683
26937
 
26684
26938
  // src/main.ts
26685
- var program2 = new Command().name("gitifact").description(t2("help.program")).version("0.3.2").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
26939
+ var program2 = new Command().name("gitifact").description(t2("help.program")).version("0.4.0").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
26686
26940
  program2.command("status").description(t2("help.status")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action(async (options) => {
26687
26941
  await runStatus(options.format);
26688
26942
  });
26689
- program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).action(runBrowser);
26690
- program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.3.2"));
26943
+ program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).option("--no-update-check", t2("help.browserNoUpdateCheck")).action((options) => runBrowser(options, "0.4.0"));
26944
+ program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.4.0"));
26691
26945
  program2.command("docs").description(t2("help.docs")).argument("[topic]", t2("help.docsTopic")).allowExcessArguments(false).action(async (topic) => {
26692
26946
  await runDocs(topic);
26693
26947
  });
26948
+ program2.command("update").description(t2("help.update")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runUpdate(options, "0.4.0"));
26694
26949
  program2.command("migrate").description(t2("help.migrate")).allowExcessArguments(false).option("--dry-run", t2("help.migrateDryRun")).action(runMigrate);
26695
26950
  var spec = program2.command("spec").description(t2("help.spec"));
26696
26951
  var replaced = t2("help.deprecated");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitifact",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Git-backed requirements and change history for coding agents.",
5
5
  "license": "MIT",
6
6
  "repository": {