forgeos 0.1.0-alpha.25 → 0.1.0-alpha.26

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/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.25 input=f4bc0a51dfadbdbabe4e7083c29844716f74c2beebcb86ce30e7a70c4f9487b4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.26 input=23c00c5407aab4088b55b49905f2b195667a75b8544fa180786ac637ee67e1e5 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.26
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the field-demo loop after the Team Onboarding app exercise.
8
+
9
+ - Let `forge changed` and `forge handoff` summarize non-git workspaces with a filesystem inventory instead of reporting zero useful changes.
10
+ - Keep `forge make resource` global by default unless a tenants table exists or `--tenant-scoped` is explicit.
11
+ - Expand capability-map table detection for aliased `ctx.db` usage.
12
+ - Wait through short-lived DeltaDB writer locks before reporting `FORGE_DELTA_BUSY`.
13
+
3
14
  ## Unreleased
4
15
 
5
16
  ## 0.1.0-alpha.25
package/docs/changelog.md CHANGED
@@ -6,6 +6,15 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.26
10
+
11
+ - Hardened the field-demo loop after the Team Onboarding app exercise:
12
+ non-git workspaces now get a filesystem-backed `changed`/`handoff` summary,
13
+ `forge make resource` stays global unless tenant scope is explicit or already
14
+ modeled, capability-map extraction sees aliased `ctx.db` table usage, and
15
+ Agent Memory waits through short-lived DeltaDB writer locks before reporting
16
+ `FORGE_DELTA_BUSY`.
17
+
9
18
  ## 0.1.0-alpha.25
10
19
 
11
20
  - Hardened DeltaDB and Agent Memory for concurrent `forge dev` usage:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.25","releaseId":"forgeos@0.1.0-alpha.25+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.26","releaseId":"forgeos@0.1.0-alpha.26+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.25 input=f4bc0a51dfadbdbabe4e7083c29844716f74c2beebcb86ce30e7a70c4f9487b4 content=a7c5e9f36b973e9b5a51716753739f57a63fa77c68b0fad18f059b5407aa58fe
1
+ // @forge-generated generator=0.1.0-alpha.26 input=23c00c5407aab4088b55b49905f2b195667a75b8544fa180786ac637ee67e1e5 content=967d4e05aa540b3e3d843fa6ed4516658ab4c364c1d8c42a14f36c57b75dff71
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.25",
23
- "releaseId": "forgeos@0.1.0-alpha.25+unknown",
22
+ "packageVersion": "0.1.0-alpha.26",
23
+ "releaseId": "forgeos@0.1.0-alpha.26+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -116,7 +116,10 @@ async function openMemoryStore(
116
116
  const retryDelays = access === "write" ? [25, 75, 150] : [];
117
117
  for (let attempt = 0; ; attempt += 1) {
118
118
  try {
119
- return await DeltaStore.open(workspaceRoot, { access });
119
+ return await DeltaStore.open(workspaceRoot, {
120
+ access,
121
+ ...(access === "write" ? { waitMs: 1_500, retryDelayMs: 50 } : {}),
122
+ });
120
123
  } catch (error) {
121
124
  if (!(error instanceof DeltaStoreBusyError) || attempt >= retryDelays.length) {
122
125
  return memoryUnavailable(error, workspaceRoot);
@@ -82,8 +82,7 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
82
82
  const risks: string[] = [];
83
83
  const changed = git.changeSummary.changed;
84
84
  if (!git.available) {
85
- risks.push("git status is unavailable; changed-file analysis may be incomplete");
86
- return risks;
85
+ risks.push("git status is unavailable; using filesystem inventory as untracked-file analysis");
87
86
  }
88
87
  if (git.untracked.count > 0) {
89
88
  risks.push(`${git.untracked.count} untracked file(s) are not in git history`);
@@ -103,7 +102,7 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
103
102
 
104
103
  function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
105
104
  if (!git.available) {
106
- return ["git status --short", "forge status --json"];
105
+ return ["forge status --json", "forge handoff --json", "git init"];
107
106
  }
108
107
  if (git.changeSummary.changed.total.count === 0) {
109
108
  return ["forge status --json", "forge dev --once --json"];
@@ -202,12 +201,13 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
202
201
  const reviewFocus = buildReviewFocus(humanChanges, viewDerivedChanges);
203
202
  const generatedExplanation = buildGeneratedChangeExplanation(humanChanges, viewDerivedChanges);
204
203
  const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
204
+ const ok = git.available || git.source === "filesystem";
205
205
 
206
206
  return {
207
- ok: git.available,
207
+ ok,
208
208
  data: {
209
209
  schemaVersion: "0.1.0",
210
- ok: git.available,
210
+ ok,
211
211
  summary: {
212
212
  branch: git.branch,
213
213
  commit: git.commit,
@@ -223,6 +223,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
223
223
  },
224
224
  git: {
225
225
  available: git.available,
226
+ source: git.source,
226
227
  ...(git.error ? { error: git.error } : {}),
227
228
  branch: git.branch,
228
229
  commit: git.commit,
@@ -240,7 +241,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
240
241
  recommendedCommands,
241
242
  nextActions: recommendedCommands,
242
243
  },
243
- exitCode: git.available ? 0 : 1,
244
+ exitCode: ok ? 0 : 1,
244
245
  };
245
246
  }
246
247
 
@@ -140,6 +140,7 @@ function buildOpeningBrief(input: {
140
140
  }): string {
141
141
  const agent = input.dev.summary.agentContext;
142
142
  const changedByType = summarizeChangeTypes(input.git.changeSummary.changed);
143
+ const changedFiles = Math.max(agent.changedFiles, input.git.changed.count);
143
144
  const tests = input.recentRuns.test
144
145
  ? input.recentRuns.test.ok
145
146
  ? "last test run passed"
@@ -150,7 +151,7 @@ function buildOpeningBrief(input: {
150
151
  : "no blocking issues";
151
152
  return [
152
153
  `ForgeOS handoff: ${input.dev.ok ? "dev diagnostics are clean" : "dev diagnostics need attention"}.`,
153
- `${agent.changedFiles} changed file(s)${changedByType ? `: ${changedByType}` : ""}; ${input.git.staged.count} staged, ${input.git.untracked.count} untracked.`,
154
+ `${changedFiles} changed file(s)${changedByType ? `: ${changedByType}` : ""}; ${input.git.staged.count} staged, ${input.git.untracked.count} untracked.`,
154
155
  `${tests}; ${blockers}.`,
155
156
  `Next command: ${input.dev.summary.primaryAction?.command ?? input.dev.nextActions[0]?.command ?? "forge dev"}.`,
156
157
  ].join(" ");
@@ -168,6 +169,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
168
169
  const agent = dev.summary.agentContext;
169
170
  const risks = [
170
171
  ...agent.blockingIssues,
172
+ ...(!git.available ? ["git status is unavailable; using filesystem inventory as untracked-file analysis"] : []),
171
173
  ...(git.untracked.count > 0 ? [`${git.untracked.count} untracked file(s) are not in git history`] : []),
172
174
  ...(recentRuns.test && !recentRuns.test.ok ? ["last test run failed"] : []),
173
175
  ...(recentRuns.ui && !recentRuns.ui.ok ? ["last UI run failed"] : []),
@@ -181,6 +183,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
181
183
  agent.blockingIssues.length === 0 &&
182
184
  (!recentRuns.test || recentRuns.test.ok) &&
183
185
  (!recentRuns.ui || recentRuns.ui.ok);
186
+ const changedFiles = Math.max(agent.changedFiles, git.changed.count);
184
187
 
185
188
  return {
186
189
  schemaVersion: "0.1.0",
@@ -192,7 +195,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
192
195
  generatedChanged: agent.generatedChanged,
193
196
  generatedChangedFiles: agent.generatedChangedFiles,
194
197
  frontendReady: agent.frontendReady,
195
- changedFiles: agent.changedFiles,
198
+ changedFiles,
196
199
  stagedFiles: git.staged.count,
197
200
  unstagedFiles: git.unstaged.count,
198
201
  untrackedFiles: git.untracked.count,
@@ -186,6 +186,38 @@ function sourceText(workspaceRoot: string, file: string | undefined): string {
186
186
  return nodeFileSystem.readText(absolute) ?? "";
187
187
  }
188
188
 
189
+ function escapeRegExp(value: string): string {
190
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
191
+ }
192
+
193
+ function addDbAliasesForText(
194
+ text: string,
195
+ tableNames: Set<string>,
196
+ aliases: Map<string, string>,
197
+ ): void {
198
+ for (const match of text.matchAll(
199
+ /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*ctx\.db(?:\.([A-Za-z_$][A-Za-z0-9_$]*)|\[\s*["'`]([^"'`]+)["'`]\s*\])/g,
200
+ )) {
201
+ const alias = match[1] ?? "";
202
+ const table = match[2] ?? match[3] ?? "";
203
+ if (alias && tableNames.has(table)) {
204
+ aliases.set(alias, table);
205
+ }
206
+ }
207
+
208
+ for (const match of text.matchAll(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*ctx\.db/g)) {
209
+ const body = match[1] ?? "";
210
+ for (const part of body.split(",")) {
211
+ const [rawTable, rawAlias] = part.split(":").map((value) => value.trim());
212
+ const table = rawTable?.replace(/["'`]/g, "") ?? "";
213
+ const alias = (rawAlias ?? rawTable ?? "").replace(/\s*=.*$/, "").trim();
214
+ if (tableNames.has(table) && alias) {
215
+ aliases.set(alias, table);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
189
221
  function dbTablesForText(
190
222
  text: string,
191
223
  tableNames: Set<string>,
@@ -199,6 +231,24 @@ function dbTablesForText(
199
231
  tables.push(table);
200
232
  }
201
233
  }
234
+ for (const match of text.matchAll(/ctx\.db\s*\[\s*["'`]([^"'`]+)["'`]\s*\]\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
235
+ const table = match[1] ?? "";
236
+ const op = match[2] ?? "";
237
+ if (tableNames.has(table) && ops.has(op)) {
238
+ tables.push(table);
239
+ }
240
+ }
241
+ const aliases = new Map<string, string>();
242
+ addDbAliasesForText(text, tableNames, aliases);
243
+ for (const [alias, table] of aliases) {
244
+ const aliasPattern = new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*([A-Za-z_$][A-Za-z0-9_$]*)`, "g");
245
+ for (const match of text.matchAll(aliasPattern)) {
246
+ const op = match[1] ?? "";
247
+ if (ops.has(op)) {
248
+ tables.push(table);
249
+ }
250
+ }
251
+ }
202
252
  return uniqueSorted(tables);
203
253
  }
204
254
 
@@ -407,7 +407,7 @@ const DELTA_STORE_RETRY_DELAYS_MS = [25, 75, 150];
407
407
  async function openDeltaStoreWithRetry(workspaceRoot: string): Promise<DeltaStore> {
408
408
  for (let attempt = 0; ; attempt += 1) {
409
409
  try {
410
- return await DeltaStore.open(workspaceRoot);
410
+ return await DeltaStore.open(workspaceRoot, { waitMs: 1_500, retryDelayMs: 50 });
411
411
  } catch (error) {
412
412
  if (!(error instanceof DeltaStoreBusyError) || attempt >= DELTA_STORE_RETRY_DELAYS_MS.length) {
413
413
  throw error;
@@ -348,6 +348,12 @@ interface DeltaStoreLock {
348
348
  token: string;
349
349
  }
350
350
 
351
+ export interface DeltaStoreOpenOptions {
352
+ access?: DeltaStoreAccess;
353
+ waitMs?: number;
354
+ retryDelayMs?: number;
355
+ }
356
+
351
357
  function getDeltaLockPath(workspaceRoot: string): string {
352
358
  return join(workspaceRoot, ".forge", "delta", "delta.lock");
353
359
  }
@@ -488,6 +494,33 @@ function acquireDeltaStoreLock(workspaceRoot: string): DeltaStoreLock {
488
494
  throw new DeltaStoreBusyError(lockPath, readLockHolder(lockPath));
489
495
  }
490
496
 
497
+ function sleep(ms: number): Promise<void> {
498
+ return new Promise((resolve) => setTimeout(resolve, ms));
499
+ }
500
+
501
+ async function acquireDeltaStoreLockWithWait(
502
+ workspaceRoot: string,
503
+ options: { waitMs?: number; retryDelayMs?: number } = {},
504
+ ): Promise<DeltaStoreLock> {
505
+ const waitMs = Math.max(0, options.waitMs ?? 0);
506
+ const retryDelayMs = Math.max(10, options.retryDelayMs ?? 50);
507
+ const started = Date.now();
508
+ for (;;) {
509
+ try {
510
+ return acquireDeltaStoreLock(workspaceRoot);
511
+ } catch (error) {
512
+ if (!(error instanceof DeltaStoreBusyError)) {
513
+ throw error;
514
+ }
515
+ const elapsed = Date.now() - started;
516
+ if (elapsed >= waitMs) {
517
+ throw error;
518
+ }
519
+ await sleep(Math.min(retryDelayMs, waitMs - elapsed));
520
+ }
521
+ }
522
+ }
523
+
491
524
  export function probeDeltaStoreBusy(workspaceRoot: string): DeltaStoreBusyError | null {
492
525
  const lockPath = getDeltaLockPath(workspaceRoot);
493
526
  if (!existsSync(lockPath)) {
@@ -551,11 +584,11 @@ export class DeltaStore {
551
584
  private readonly lock: DeltaStoreLock | null,
552
585
  ) {}
553
586
 
554
- static async open(workspaceRoot: string, options: { access?: DeltaStoreAccess } = {}): Promise<DeltaStore> {
587
+ static async open(workspaceRoot: string, options: DeltaStoreOpenOptions = {}): Promise<DeltaStore> {
555
588
  const storePath = getDeltaStorePath(workspaceRoot);
556
589
  mkdirSync(dirname(storePath), { recursive: true });
557
590
  const initializedBeforeOpen = deltaStoreInitialized(storePath);
558
- const lock = options.access === "read" ? null : acquireDeltaStoreLock(workspaceRoot);
591
+ const lock = options.access === "read" ? null : await acquireDeltaStoreLockWithWait(workspaceRoot, options);
559
592
  let store: DeltaStore | null = null;
560
593
  try {
561
594
  const adapter = await createPgliteAdapter(storePath);
@@ -565,7 +598,7 @@ export class DeltaStore {
565
598
  } else if (await store.needsSchemaInit()) {
566
599
  await store.close();
567
600
  store = null;
568
- const migrateLock = acquireDeltaStoreLock(workspaceRoot);
601
+ const migrateLock = await acquireDeltaStoreLockWithWait(workspaceRoot, options);
569
602
  try {
570
603
  const migrateAdapter = await createPgliteAdapter(storePath);
571
604
  store = new DeltaStore(workspaceRoot, storePath, migrateAdapter, migrateLock);
@@ -200,6 +200,13 @@ function chooseSchemaFile(workspaceRoot: string): string {
200
200
  return "src/forge/schema.ts";
201
201
  }
202
202
 
203
+ function schemaHasTenantsTable(workspaceRoot: string): boolean {
204
+ const schema = readIfExists(workspaceRoot, chooseSchemaFile(workspaceRoot)) ?? "";
205
+ return schema.includes('name: "tenants"') ||
206
+ schema.includes("name: 'tenants'") ||
207
+ /\btenants\s*=\s*(?:defineTable|table)\s*\(/.test(schema);
208
+ }
209
+
203
210
  function choosePolicyFile(workspaceRoot: string): string {
204
211
  if (fileExists(workspaceRoot, "src/policies.ts")) {
205
212
  return "src/policies.ts";
@@ -551,6 +558,8 @@ function buildIntent(options: MakeCommandOptions): {
551
558
  kind === "command"
552
559
  ? actionName?.replace(/^(create|update|delete)/, "") || "create"
553
560
  : "read";
561
+ const tenantScoped =
562
+ options.tenantScoped || (kind === "resource" && schemaHasTenantsTable(options.workspaceRoot));
554
563
 
555
564
  return {
556
565
  diagnostics,
@@ -560,7 +569,7 @@ function buildIntent(options: MakeCommandOptions): {
560
569
  table,
561
570
  field: fieldOptions.field,
562
571
  fields,
563
- tenantScoped: options.tenantScoped || kind === "resource",
572
+ tenantScoped,
564
573
  crud: options.withCrud || kind === "resource",
565
574
  liveQuery: options.withLiveQuery || kind === "resource" || kind === "livequery",
566
575
  react:
@@ -898,7 +907,7 @@ function buildPlan(options: MakeCommandOptions): MakePlan {
898
907
  kind: "resource" as const,
899
908
  name: options.name ?? "resource",
900
909
  fields: [],
901
- tenantScoped: true,
910
+ tenantScoped: false,
902
911
  crud: true,
903
912
  liveQuery: true,
904
913
  react: true,
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.25";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.26";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { normalizePath } from "../compiler/primitives/paths.ts";
4
5
  import {
5
6
  categorizeFiles,
6
7
  classifyChangeType,
@@ -13,6 +14,7 @@ import {
13
14
 
14
15
  export interface WorkspaceGitSummary {
15
16
  available: boolean;
17
+ source?: "git" | "filesystem";
16
18
  branch?: string;
17
19
  commit?: string;
18
20
  changed: FileListSummary;
@@ -28,18 +30,64 @@ export interface WorkspaceGitSummary {
28
30
  error?: string;
29
31
  }
30
32
 
31
- function emptySummary(error?: string): WorkspaceGitSummary {
33
+ const FALLBACK_IGNORED_DIRS = new Set([
34
+ ".git",
35
+ "node_modules",
36
+ "dist",
37
+ "build",
38
+ "coverage",
39
+ ".next",
40
+ ".nuxt",
41
+ ".turbo",
42
+ ".cache",
43
+ ]);
44
+
45
+ function listWorkspaceFiles(root: string): string[] {
46
+ const files: string[] = [];
47
+ const visit = (dir: string): void => {
48
+ let entries: Array<{ name: string; isDirectory: () => boolean; isFile: () => boolean }> = [];
49
+ try {
50
+ entries = readdirSync(dir, { withFileTypes: true });
51
+ } catch {
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ if (entry.isDirectory() && FALLBACK_IGNORED_DIRS.has(entry.name)) {
56
+ continue;
57
+ }
58
+ const absolute = join(dir, entry.name);
59
+ const rel = normalizePath(relative(root, absolute));
60
+ if (!rel || rel === ".") {
61
+ continue;
62
+ }
63
+ if (entry.isDirectory()) {
64
+ visit(absolute);
65
+ continue;
66
+ }
67
+ if (entry.isFile()) {
68
+ files.push(rel);
69
+ }
70
+ }
71
+ };
72
+ visit(root);
73
+ return filterVolatileForgeState(files).sort();
74
+ }
75
+
76
+ function filesystemSummary(workspaceRoot: string, error?: string): WorkspaceGitSummary {
77
+ const files = listWorkspaceFiles(workspaceRoot);
78
+ const classify = workspaceChangeClassifier(workspaceRoot);
32
79
  return {
33
80
  available: false,
34
- changed: compactFiles([]),
81
+ source: "filesystem",
82
+ changed: compactFiles(files),
35
83
  staged: compactFiles([]),
36
84
  unstaged: compactFiles([]),
37
- untracked: compactFiles([]),
85
+ untracked: compactFiles(files),
38
86
  changeSummary: {
39
- changed: categorizeFiles([]),
87
+ changed: categorizeFiles(files, 8, classify),
40
88
  staged: categorizeFiles([]),
41
89
  unstaged: categorizeFiles([]),
42
- untracked: categorizeFiles([]),
90
+ untracked: categorizeFiles(files, 8, classify),
43
91
  },
44
92
  ...(error ? { error } : {}),
45
93
  };
@@ -227,7 +275,7 @@ function parseStatusPath(line: string): string {
227
275
  export function buildWorkspaceGitSummary(workspaceRoot: string): WorkspaceGitSummary {
228
276
  const root = runGit(["rev-parse", "--show-toplevel"], workspaceRoot);
229
277
  if (!root.ok) {
230
- return emptySummary(root.error);
278
+ return filesystemSummary(workspaceRoot, root.error);
231
279
  }
232
280
 
233
281
  const status = runGit(["status", "--porcelain=v1", "-uall"], workspaceRoot, { trim: false });
@@ -262,6 +310,7 @@ export function buildWorkspaceGitSummary(workspaceRoot: string): WorkspaceGitSum
262
310
 
263
311
  return {
264
312
  available: true,
313
+ source: "git",
265
314
  ...(branch.ok ? { branch: branch.stdout } : {}),
266
315
  ...(commit.ok ? { commit: commit.stdout } : {}),
267
316
  changed: compactFiles(changedFiles),