primitive-admin 1.1.0-alpha.73 → 1.1.0-alpha.75

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 (150) hide show
  1. package/README.md +18 -25
  2. package/assets/skill/skills/primitive-platform/SKILL.md +30 -14
  3. package/dist/bin/primitive.js +25 -21
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +117 -2
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/auth.js +13 -107
  8. package/dist/src/commands/auth.js.map +1 -1
  9. package/dist/src/commands/collection-type-configs.js +1 -1
  10. package/dist/src/commands/collection-type-configs.js.map +1 -1
  11. package/dist/src/commands/config.js +8 -4
  12. package/dist/src/commands/config.js.map +1 -1
  13. package/dist/src/commands/database-type-configs.js +1 -1
  14. package/dist/src/commands/database-type-configs.js.map +1 -1
  15. package/dist/src/commands/databases.js +25 -31
  16. package/dist/src/commands/databases.js.map +1 -1
  17. package/dist/src/commands/documents.js +758 -53
  18. package/dist/src/commands/documents.js.map +1 -1
  19. package/dist/src/commands/env.d.ts +1 -1
  20. package/dist/src/commands/env.js +57 -16
  21. package/dist/src/commands/env.js.map +1 -1
  22. package/dist/src/commands/functions.d.ts +20 -0
  23. package/dist/src/commands/functions.js +463 -0
  24. package/dist/src/commands/functions.js.map +1 -0
  25. package/dist/src/commands/groups.js +1 -1
  26. package/dist/src/commands/groups.js.map +1 -1
  27. package/dist/src/commands/init.d.ts +12 -15
  28. package/dist/src/commands/init.js +38 -92
  29. package/dist/src/commands/init.js.map +1 -1
  30. package/dist/src/commands/integrations.js +5 -3
  31. package/dist/src/commands/integrations.js.map +1 -1
  32. package/dist/src/commands/prompts.js +5 -7
  33. package/dist/src/commands/prompts.js.map +1 -1
  34. package/dist/src/commands/rule-sets.js +1 -1
  35. package/dist/src/commands/rule-sets.js.map +1 -1
  36. package/dist/src/commands/scripts.js +23 -11
  37. package/dist/src/commands/scripts.js.map +1 -1
  38. package/dist/src/commands/sync-app-settings.js +2 -4
  39. package/dist/src/commands/sync-app-settings.js.map +1 -1
  40. package/dist/src/commands/sync.d.ts +65 -2
  41. package/dist/src/commands/sync.js +867 -21
  42. package/dist/src/commands/sync.js.map +1 -1
  43. package/dist/src/commands/workflows.js +6 -8
  44. package/dist/src/commands/workflows.js.map +1 -1
  45. package/dist/src/lib/api-client.d.ts +163 -2
  46. package/dist/src/lib/api-client.js +227 -5
  47. package/dist/src/lib/api-client.js.map +1 -1
  48. package/dist/src/lib/app-match-guard.d.ts +44 -0
  49. package/dist/src/lib/app-match-guard.js +72 -0
  50. package/dist/src/lib/app-match-guard.js.map +1 -0
  51. package/dist/src/lib/app-settings-descriptor.js +4 -0
  52. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  53. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +11 -8
  54. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +112 -60
  55. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  56. package/dist/src/lib/config-object-descriptor.js +39 -0
  57. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  58. package/dist/src/lib/config-surface.d.ts +10 -0
  59. package/dist/src/lib/config-surface.js +17 -0
  60. package/dist/src/lib/config-surface.js.map +1 -1
  61. package/dist/src/lib/config.d.ts +15 -4
  62. package/dist/src/lib/config.js +22 -39
  63. package/dist/src/lib/config.js.map +1 -1
  64. package/dist/src/lib/credentials-store.d.ts +7 -15
  65. package/dist/src/lib/credentials-store.js +52 -85
  66. package/dist/src/lib/credentials-store.js.map +1 -1
  67. package/dist/src/lib/document-export-permissions.d.ts +30 -0
  68. package/dist/src/lib/document-export-permissions.js +54 -0
  69. package/dist/src/lib/document-export-permissions.js.map +1 -0
  70. package/dist/src/lib/env-resolver-core.d.ts +64 -16
  71. package/dist/src/lib/env-resolver-core.js +128 -17
  72. package/dist/src/lib/env-resolver-core.js.map +1 -1
  73. package/dist/src/lib/env-resolver.d.ts +3 -3
  74. package/dist/src/lib/env-resolver.js +2 -2
  75. package/dist/src/lib/function-bundle.d.ts +141 -0
  76. package/dist/src/lib/function-bundle.js +333 -0
  77. package/dist/src/lib/function-bundle.js.map +1 -0
  78. package/dist/src/lib/function-collect.d.ts +101 -0
  79. package/dist/src/lib/function-collect.js +339 -0
  80. package/dist/src/lib/function-collect.js.map +1 -0
  81. package/dist/src/lib/function-db-types.d.ts +149 -0
  82. package/dist/src/lib/function-db-types.js +587 -0
  83. package/dist/src/lib/function-db-types.js.map +1 -0
  84. package/dist/src/lib/function-grants-preflight.d.ts +118 -0
  85. package/dist/src/lib/function-grants-preflight.js +213 -0
  86. package/dist/src/lib/function-grants-preflight.js.map +1 -0
  87. package/dist/src/lib/function-sync.d.ts +191 -0
  88. package/dist/src/lib/function-sync.js +474 -0
  89. package/dist/src/lib/function-sync.js.map +1 -0
  90. package/dist/src/lib/function-triggers.d.ts +54 -0
  91. package/dist/src/lib/function-triggers.js +223 -0
  92. package/dist/src/lib/function-triggers.js.map +1 -0
  93. package/dist/src/lib/generated-config-surfaces.d.ts +389 -0
  94. package/dist/src/lib/generated-config-surfaces.js +1030 -2
  95. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  96. package/dist/src/lib/generated-sdk-types.d.ts +12 -0
  97. package/dist/src/lib/generated-sdk-types.js +13 -0
  98. package/dist/src/lib/generated-sdk-types.js.map +1 -0
  99. package/dist/src/lib/init-adopt.js +2 -2
  100. package/dist/src/lib/init-adopt.js.map +1 -1
  101. package/dist/src/lib/init-assets.js +3 -3
  102. package/dist/src/lib/init-config.d.ts +1 -1
  103. package/dist/src/lib/init-ios-links.d.ts +6 -5
  104. package/dist/src/lib/init-ios-links.js +9 -8
  105. package/dist/src/lib/init-ios-links.js.map +1 -1
  106. package/dist/src/lib/init-plan.d.ts +1 -1
  107. package/dist/src/lib/init-plan.js +1 -1
  108. package/dist/src/lib/init-xcode.d.ts +6 -5
  109. package/dist/src/lib/init-xcode.js +34 -10
  110. package/dist/src/lib/init-xcode.js.map +1 -1
  111. package/dist/src/lib/ios-app-id.d.ts +34 -0
  112. package/dist/src/lib/ios-app-id.js +69 -0
  113. package/dist/src/lib/ios-app-id.js.map +1 -0
  114. package/dist/src/lib/local-state.d.ts +1 -1
  115. package/dist/src/lib/local-state.js +1 -1
  116. package/dist/src/lib/local-test-cases.d.ts +1 -1
  117. package/dist/src/lib/local-test-cases.js +2 -1
  118. package/dist/src/lib/local-test-cases.js.map +1 -1
  119. package/dist/src/lib/migration-nag.d.ts +2 -2
  120. package/dist/src/lib/migration-nag.js +4 -3
  121. package/dist/src/lib/migration-nag.js.map +1 -1
  122. package/dist/src/lib/project-config.d.ts +38 -21
  123. package/dist/src/lib/project-config.js +57 -47
  124. package/dist/src/lib/project-config.js.map +1 -1
  125. package/dist/src/lib/snapshots.d.ts +2 -2
  126. package/dist/src/lib/snapshots.js +2 -2
  127. package/dist/src/lib/swift-codegen/generator.d.ts +1 -1
  128. package/dist/src/lib/swift-codegen/generator.js +1 -1
  129. package/dist/src/lib/sync-dir-selector.d.ts +1 -1
  130. package/dist/src/lib/sync-dir-selector.js +1 -1
  131. package/dist/src/lib/sync-paths.d.ts +52 -16
  132. package/dist/src/lib/sync-paths.js +68 -22
  133. package/dist/src/lib/sync-paths.js.map +1 -1
  134. package/dist/src/lib/sync-resource-types.d.ts +23 -4
  135. package/dist/src/lib/sync-resource-types.js +106 -8
  136. package/dist/src/lib/sync-resource-types.js.map +1 -1
  137. package/dist/src/lib/template.d.ts +2 -2
  138. package/dist/src/lib/template.js +2 -2
  139. package/dist/src/lib/test-case-keys.d.ts +1 -1
  140. package/dist/src/lib/test-case-keys.js +1 -1
  141. package/dist/src/lib/test-case-variables.d.ts +14 -0
  142. package/dist/src/lib/test-case-variables.js +42 -0
  143. package/dist/src/lib/test-case-variables.js.map +1 -1
  144. package/dist/src/lib/workflow-codegen/generator.d.ts +1 -1
  145. package/dist/src/lib/workflow-codegen/generator.js +1 -1
  146. package/dist/src/lib/workflow-usage.d.ts +198 -0
  147. package/dist/src/lib/workflow-usage.js +312 -0
  148. package/dist/src/lib/workflow-usage.js.map +1 -0
  149. package/dist/src/types/index.d.ts +12 -2
  150. package/package.json +4 -3
@@ -1,39 +1,71 @@
1
1
  /**
2
2
  * Sync data path resolution.
3
3
  *
4
- * Per issue #373, sync state is scoped by environment AND by app:
4
+ * The COMMITTED tree lives at
5
5
  *
6
- * <projectRoot>/.primitive/sync/<env>/<appId>/
6
+ * <projectRoot>/primitive/<env>/
7
7
  *
8
- * This ensures that switching environments (dev -> prod) or apps never
9
- * clobbers sync state from another slot. Push to dev and then to prod
10
- * see independent "last synced" pointers, so prod won't falsely believe
11
- * dev's changes are already in sync.
8
+ * — one directory per environment, and nothing else in the path. There is no
9
+ * app segment (#3153): since #3152 an environment names exactly one app, so
10
+ * the segment only duplicated what the environment already said, and it made
11
+ * the path unreadable. Per-environment partitioning is what issue #373 was
12
+ * actually about, and it is unchanged: push to dev and then to prod still see
13
+ * independent "last synced" pointers.
14
+ *
15
+ * MACHINE-LOCAL state stays hidden, under `<projectRoot>/.primitive/`. Snapshot
16
+ * backups keep BOTH partitions there — `<env>/<appId>/` — because an
17
+ * environment can be rebound to a different app, and a snapshot taken against
18
+ * the former one must not be restorable through the new one (#578, #373).
12
19
  *
13
20
  * When the user still passes `--dir <path>` (the legacy flag) we honor
14
21
  * it and store sync state inside that directory, same as before. This
15
22
  * keeps pre-project-config workflows working.
16
23
  */
24
+ /** The sync-state baseline, committed beside the environment's TOML (#3153). */
25
+ export declare const SYNC_STATE_FILENAME = ".sync-state.json";
26
+ /** Its pre-#3153 name. Detected so a missed rename is loud, never read. */
27
+ export declare const LEGACY_SYNC_STATE_FILENAME = ".primitive-sync.json";
17
28
  export interface ResolveSyncDirOptions {
18
- /** The resolved app ID for this sync operation. */
19
- appId: string;
20
29
  /**
21
30
  * If the user passed `--dir <path>` on the command line, forward it
22
31
  * here. Explicit user input always wins.
23
32
  */
24
33
  userDir?: string;
25
34
  }
35
+ export interface ResolveSnapshotsRootOptions extends ResolveSyncDirOptions {
36
+ /** The resolved app ID this snapshot slot belongs to. */
37
+ appId: string;
38
+ }
39
+ /**
40
+ * The sync-state baseline for a config directory.
41
+ *
42
+ * Refuses — with the same migration message every other reader gives — a
43
+ * directory that still holds the pre-#3153 filename and not the new one. A
44
+ * silently un-renamed baseline would read as "nothing has ever been synced",
45
+ * which turns the first `config diff` after a migration into a full-tree diff
46
+ * and the first push into a re-create of everything.
47
+ */
48
+ export declare function syncStatePath(configDir: string): string;
49
+ /**
50
+ * The same refusal as a preflight: asked at the TOP of a tree command, before
51
+ * a request is sent, a snapshot is copied, or a directory is created.
52
+ *
53
+ * `syncStatePath` alone refuses too late for that — a pull reaches it only
54
+ * after fetching the whole server export, backing the tree up (legacy baseline
55
+ * and all) and scaffolding the directories it is about to write. The refusal
56
+ * has to cost nothing in either tree, so it runs before any of that.
57
+ */
58
+ export declare function assertSyncStateLayout(configDir: string): void;
26
59
  /**
27
60
  * Returns the directory that should hold TOML files + sync state for the
28
61
  * current sync operation.
29
62
  *
30
63
  * Resolution order:
31
64
  * 1. If userDir is set, return it as-is (legacy behavior).
32
- * 2. If we're in project mode, return
33
- * <projectRoot>/.primitive/sync/<env>/<appId>
65
+ * 2. If we're in project mode, return <projectRoot>/primitive/<env>
34
66
  * 3. Otherwise fall back to "./config" (the old default).
35
67
  */
36
- export declare function resolveSyncDir(options: ResolveSyncDirOptions): string;
68
+ export declare function resolveSyncDir(options?: ResolveSyncDirOptions): string;
37
69
  /**
38
70
  * Returns the snapshot-backups root for the slot a given sync operation
39
71
  * belongs to (issue #578, Phase 1). Snapshots reuse the SAME resolution chain
@@ -45,11 +77,15 @@ export declare function resolveSyncDir(options: ResolveSyncDirOptions): string;
45
77
  * 1. If userDir (legacy `--dir`) is set, snapshots live in a sibling
46
78
  * `<userDir>/.snapshots/` so revert works without a project root
47
79
  * (Fork 4 — designed explicitly).
48
- * 2. In project mode, the project sibling
49
- * `<projectRoot>/.primitive/sync-backups/<env>/<appId>/`.
80
+ * 2. In project mode, the machine-local
81
+ * `<projectRoot>/.primitive/backups/<env>/<appId>/`. The app partition
82
+ * survives the tree's loss of it (#3153): backups are not committed, so
83
+ * the no-app-ID rule does not govern them, and the partition is what
84
+ * stops `config revert` restoring a former app's snapshot after an
85
+ * environment is rebound.
50
86
  * 3. Otherwise (bare `./config` legacy default), `./config-snapshots`.
51
87
  */
52
- export declare function resolveSnapshotsRoot(options: ResolveSyncDirOptions): string;
88
+ export declare function resolveSnapshotsRoot(options: ResolveSnapshotsRootOptions): string;
53
89
  export declare function assertSafePathComponent(value: string, label: string): void;
54
90
  /**
55
91
  * Returns true if the path we'd use for sync data is the auto-resolved
@@ -60,7 +96,7 @@ export declare function isAutoResolvedSyncDir(userDir: string | undefined): bool
60
96
  * Ensures the sync directory exists, creating it (and parents) if
61
97
  * necessary. Returns the path.
62
98
  */
63
- export declare function ensureSyncDir(options: ResolveSyncDirOptions): string;
99
+ export declare function ensureSyncDir(options?: ResolveSyncDirOptions): string;
64
100
  /**
65
101
  * Checks whether an old-style sync state file exists at `./config/.primitive-sync.json`
66
102
  * (the pre-project-config default path). Returns a warning message if found and
@@ -96,7 +132,7 @@ export interface LegacyDatabaseTypesDirNotice {
96
132
  * managed type's file would read as deleted.
97
133
  *
98
134
  * Both messages render RESOLVED paths. The config directory defaults to
99
- * `<projectRoot>/.primitive/sync/<env>/<appId>/` and the CLI is invoked from
135
+ * `<projectRoot>/primitive/<env>/` and the CLI is invoked from
100
136
  * the project root, so a bare `git mv database-types database-type-configs`
101
137
  * would fail there — and fail differently again under `--dir`.
102
138
  *
@@ -1,14 +1,21 @@
1
1
  /**
2
2
  * Sync data path resolution.
3
3
  *
4
- * Per issue #373, sync state is scoped by environment AND by app:
4
+ * The COMMITTED tree lives at
5
5
  *
6
- * <projectRoot>/.primitive/sync/<env>/<appId>/
6
+ * <projectRoot>/primitive/<env>/
7
7
  *
8
- * This ensures that switching environments (dev -> prod) or apps never
9
- * clobbers sync state from another slot. Push to dev and then to prod
10
- * see independent "last synced" pointers, so prod won't falsely believe
11
- * dev's changes are already in sync.
8
+ * — one directory per environment, and nothing else in the path. There is no
9
+ * app segment (#3153): since #3152 an environment names exactly one app, so
10
+ * the segment only duplicated what the environment already said, and it made
11
+ * the path unreadable. Per-environment partitioning is what issue #373 was
12
+ * actually about, and it is unchanged: push to dev and then to prod still see
13
+ * independent "last synced" pointers.
14
+ *
15
+ * MACHINE-LOCAL state stays hidden, under `<projectRoot>/.primitive/`. Snapshot
16
+ * backups keep BOTH partitions there — `<env>/<appId>/` — because an
17
+ * environment can be rebound to a different app, and a snapshot taken against
18
+ * the former one must not be restorable through the new one (#578, #373).
12
19
  *
13
20
  * When the user still passes `--dir <path>` (the legacy flag) we honor
14
21
  * it and store sync state inside that directory, same as before. This
@@ -17,27 +24,61 @@
17
24
  import { existsSync, mkdirSync } from "fs";
18
25
  import { join } from "path";
19
26
  import { getCurrentEnvironment } from "./env-resolver.js";
27
+ import { PrimitiveEnvError, PROJECT_LOCAL_STATE_DIR, PROJECT_TREE_DIR, staleLayoutMessage, } from "./env-resolver-core.js";
28
+ /** The sync-state baseline, committed beside the environment's TOML (#3153). */
29
+ export const SYNC_STATE_FILENAME = ".sync-state.json";
30
+ /** Its pre-#3153 name. Detected so a missed rename is loud, never read. */
31
+ export const LEGACY_SYNC_STATE_FILENAME = ".primitive-sync.json";
32
+ /**
33
+ * The sync-state baseline for a config directory.
34
+ *
35
+ * Refuses — with the same migration message every other reader gives — a
36
+ * directory that still holds the pre-#3153 filename and not the new one. A
37
+ * silently un-renamed baseline would read as "nothing has ever been synced",
38
+ * which turns the first `config diff` after a migration into a full-tree diff
39
+ * and the first push into a re-create of everything.
40
+ */
41
+ export function syncStatePath(configDir) {
42
+ const current = join(configDir, SYNC_STATE_FILENAME);
43
+ if (!existsSync(current)) {
44
+ const legacy = join(configDir, LEGACY_SYNC_STATE_FILENAME);
45
+ if (existsSync(legacy)) {
46
+ throw new PrimitiveEnvError("stale-layout", staleLayoutMessage(`${legacy} has not been renamed to ${SYNC_STATE_FILENAME}.`), legacy);
47
+ }
48
+ }
49
+ return current;
50
+ }
51
+ /**
52
+ * The same refusal as a preflight: asked at the TOP of a tree command, before
53
+ * a request is sent, a snapshot is copied, or a directory is created.
54
+ *
55
+ * `syncStatePath` alone refuses too late for that — a pull reaches it only
56
+ * after fetching the whole server export, backing the tree up (legacy baseline
57
+ * and all) and scaffolding the directories it is about to write. The refusal
58
+ * has to cost nothing in either tree, so it runs before any of that.
59
+ */
60
+ export function assertSyncStateLayout(configDir) {
61
+ syncStatePath(configDir);
62
+ }
20
63
  /**
21
64
  * Returns the directory that should hold TOML files + sync state for the
22
65
  * current sync operation.
23
66
  *
24
67
  * Resolution order:
25
68
  * 1. If userDir is set, return it as-is (legacy behavior).
26
- * 2. If we're in project mode, return
27
- * <projectRoot>/.primitive/sync/<env>/<appId>
69
+ * 2. If we're in project mode, return <projectRoot>/primitive/<env>
28
70
  * 3. Otherwise fall back to "./config" (the old default).
29
71
  */
30
- export function resolveSyncDir(options) {
72
+ export function resolveSyncDir(options = {}) {
31
73
  if (options.userDir)
32
74
  return options.userDir;
33
75
  const env = safeEnv();
34
76
  if (env) {
35
- // Defence-in-depth: appId is expected to be a ULID from the server, but
36
- // we must never trust it blindly when building file-system paths. Reject
37
- // anything that contains path separators or `..` sequences before
38
- // joining it into <projectRoot>/.primitive/sync/...
39
- assertSafePathComponent(options.appId, "appId");
40
- return join(env.projectRoot, ".primitive", "sync", env.name, options.appId);
77
+ // Defence-in-depth: the environment name comes from a hand-edited config
78
+ // file and is the only variable segment left in the tree path, so reject
79
+ // separators and `..` before joining it into <projectRoot>/primitive/...
80
+ assertSafePathComponent(env.name, "environment name");
81
+ return join(env.projectRoot, PROJECT_TREE_DIR, env.name);
41
82
  }
42
83
  return "./config";
43
84
  }
@@ -52,8 +93,12 @@ export function resolveSyncDir(options) {
52
93
  * 1. If userDir (legacy `--dir`) is set, snapshots live in a sibling
53
94
  * `<userDir>/.snapshots/` so revert works without a project root
54
95
  * (Fork 4 — designed explicitly).
55
- * 2. In project mode, the project sibling
56
- * `<projectRoot>/.primitive/sync-backups/<env>/<appId>/`.
96
+ * 2. In project mode, the machine-local
97
+ * `<projectRoot>/.primitive/backups/<env>/<appId>/`. The app partition
98
+ * survives the tree's loss of it (#3153): backups are not committed, so
99
+ * the no-app-ID rule does not govern them, and the partition is what
100
+ * stops `config revert` restoring a former app's snapshot after an
101
+ * environment is rebound.
57
102
  * 3. Otherwise (bare `./config` legacy default), `./config-snapshots`.
58
103
  */
59
104
  export function resolveSnapshotsRoot(options) {
@@ -64,8 +109,9 @@ export function resolveSnapshotsRoot(options) {
64
109
  if (env) {
65
110
  // Same defence-in-depth as resolveSyncDir: never trust appId blindly when
66
111
  // building a file-system path.
112
+ assertSafePathComponent(env.name, "environment name");
67
113
  assertSafePathComponent(options.appId, "appId");
68
- return join(env.projectRoot, ".primitive", "sync-backups", env.name, options.appId);
114
+ return join(env.projectRoot, PROJECT_LOCAL_STATE_DIR, "backups", env.name, options.appId);
69
115
  }
70
116
  return "./config-snapshots";
71
117
  }
@@ -92,7 +138,7 @@ export function isAutoResolvedSyncDir(userDir) {
92
138
  * Ensures the sync directory exists, creating it (and parents) if
93
139
  * necessary. Returns the path.
94
140
  */
95
- export function ensureSyncDir(options) {
141
+ export function ensureSyncDir(options = {}) {
96
142
  const dir = resolveSyncDir(options);
97
143
  if (!existsSync(dir)) {
98
144
  mkdirSync(dir, { recursive: true });
@@ -116,14 +162,14 @@ function safeEnv() {
116
162
  * their old sync data won't be picked up automatically.
117
163
  */
118
164
  export function checkLegacySyncMigration(currentSyncDir) {
119
- const legacySync = join(".", "config", ".primitive-sync.json");
165
+ const legacySync = join(".", "config", LEGACY_SYNC_STATE_FILENAME);
120
166
  if (!existsSync(legacySync))
121
167
  return null;
122
168
  // Only warn if the resolved dir is different from the legacy default.
123
169
  const legacyDir = join(".", "config");
124
170
  if (currentSyncDir === legacyDir)
125
171
  return null;
126
- return (`Detected old sync state at ./config/.primitive-sync.json. ` +
172
+ return (`Detected old sync state at ./config/${LEGACY_SYNC_STATE_FILENAME}. ` +
127
173
  `The config directory now lives at ${currentSyncDir}. ` +
128
174
  `Run 'primitive config pull' to re-sync into the new location.`);
129
175
  }
@@ -154,7 +200,7 @@ function shellQuote(path) {
154
200
  * managed type's file would read as deleted.
155
201
  *
156
202
  * Both messages render RESOLVED paths. The config directory defaults to
157
- * `<projectRoot>/.primitive/sync/<env>/<appId>/` and the CLI is invoked from
203
+ * `<projectRoot>/primitive/<env>/` and the CLI is invoked from
158
204
  * the project root, so a bare `git mv database-types database-type-configs`
159
205
  * would fail there — and fail differently again under `--dir`.
160
206
  *
@@ -1 +1 @@
1
- {"version":3,"file":"sync-paths.js","sourceRoot":"","sources":["../../../src/lib/sync-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAY1D;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC;IAE5C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,GAAG,EAAE,CAAC;QACR,wEAAwE;QACxE,yEAAyE;QACzE,kEAAkE;QAClE,oDAAoD;QACpD,uBAAuB,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CACT,GAAG,CAAC,WAAW,EACf,YAAY,EACZ,MAAM,EACN,GAAG,CAAC,IAAI,EACR,OAAO,CAAC,KAAK,CACd,CAAC;IACJ,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,GAAG,EAAE,CAAC;QACR,0EAA0E;QAC1E,+BAA+B;QAC/B,uBAAuB,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CACT,GAAG,CAAC,WAAW,EACf,YAAY,EACZ,cAAc,EACd,GAAG,CAAC,IAAI,EACR,OAAO,CAAC,KAAK,CACd,CAAC;IACJ,CAAC;IAED,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa,EAAE,KAAa;IAClE,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,KAAK,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QACnB,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACpB,KAAK,KAAK,GAAG;QACb,KAAK,KAAK,EAAE,EACZ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,WAAW,KAAK,mBAAmB,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAA2B;IAC/D,IAAI,OAAO;QAAE,OAAO,KAAK,CAAC;IAC1B,OAAO,OAAO,EAAE,KAAK,IAAI,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAA8B;IAC1D,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,OAAO;IACd,IAAI,CAAC;QACH,OAAO,qBAAqB,EAAE,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,cAAsB;IAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IAC/D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,sEAAsE;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACtC,IAAI,cAAc,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,CACL,4DAA4D;QAC5D,qCAAqC,cAAc,IAAI;QACvD,+DAA+D,CAChE,CAAC;AACJ,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAC;AAE1D,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB,CAAC;AAejE;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,2BAA2B,CACzC,SAAiB,EACjB,UAA8C,EAAE;IAEhD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;IAC9D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,oBAAoB,KAAK,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,WAAW,GAAG,QAAQ;QAC1B,CAAC,CAAC,eAAe,UAAU,iBAAiB,SAAS,UAAU;YAC7D,GAAG,UAAU,iBAAiB,SAAS,EAAE;QAC3C,CAAC,CAAC,aAAa,SAAS,IAAI,UAAU,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,QAAQ;QACpB,CAAC,CAAC,GAAG,WAAW,qDAAqD;YACnE,sEAAsE;YACtE,mEAAmE;YACnE,iCAAiC;QACnC,CAAC,CAAC,sEAAsE;YACtE,yEAAyE,CAAC;IAE9E,MAAM,OAAO,GACX,GAAG,UAAU,qDAAqD;QAClE,GAAG,WAAW,mBAAmB,KAAK,OAAO,WAAW,EAAE,CAAC;IAC7D,MAAM,UAAU,GACd,2BAA2B,UAAU,+BAA+B;QACpE,uCAAuC,WAAW,mBAAmB;QACrE,uEAAuE;QACvE,GAAG,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,sBAAsB,SAAS;QAChE,OAAO,WAAW,EAAE,CAAC;IAEvB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"sync-paths.js","sourceRoot":"","sources":["../../../src/lib/sync-paths.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * into a sync slot (issue #1659).
4
4
  *
5
5
  * `config pull` writes one file per server entity and records what it wrote in
6
- * `.primitive-sync.json` (`SyncState.entities[<slot>]`). Until #1659 it never
6
+ * `.sync-state.json` (`SyncState.entities[<slot>]`). Until #1659 it never
7
7
  * removed a local file whose entity had disappeared server-side, so the next
8
8
  * `config push` — which treats local TOML as the source of truth — silently
9
9
  * re-created the just-deleted entity.
@@ -122,7 +122,7 @@ export declare function attemptFetch<T>(fn: () => Promise<T>): Promise<{
122
122
  }>;
123
123
  /**
124
124
  * A filename must stay inside its type's directory. State is read from a file
125
- * on disk, so a corrupt or hand-edited `.primitive-sync.json` must never be
125
+ * on disk, so a corrupt or hand-edited `.sync-state.json` must never be
126
126
  * able to steer an `unlinkSync` outside the sync slot.
127
127
  */
128
128
  export declare function isSafeSyncFilename(filename: string): boolean;
@@ -264,8 +264,10 @@ export interface PruneResult {
264
264
  * sidecar `<key>.tests/` directory.
265
265
  *
266
266
  * A file with uncommitted git edits is kept and reported instead: the operator
267
- * may be mid-way through authoring a change they meant to push. Everything
268
- * removed here is recoverable from the pre-pull snapshot via `config revert`.
267
+ * may be mid-way through authoring a change they meant to push — and for an
268
+ * entity that owns source files beyond its TOML (#3179), an edit to any of them
269
+ * keeps the whole entity. Everything removed here is recoverable from the
270
+ * pre-pull snapshot via `config revert`.
269
271
  */
270
272
  export declare function applyPrune(options: {
271
273
  configDir: string;
@@ -276,6 +278,23 @@ export declare function applyPrune(options: {
276
278
  blockType?: string;
277
279
  blockKey?: string;
278
280
  }>;
281
+ /**
282
+ * Further tree-relative files this entity owns, from the prior sync state
283
+ * (#3179). A server function is its `functions/<key>.toml` PLUS the
284
+ * TypeScript its `entry` reaches, which lives at paths only the state knows —
285
+ * so removing the file alone would leave orphaned code that the next push
286
+ * would try to capture again. Returned paths are confined to the config tree;
287
+ * anything else is ignored rather than followed.
288
+ */
289
+ extraFilesForKey?: (key: string) => string[];
290
+ /**
291
+ * Tree-relative paths that SURVIVE this prune because another entity still
292
+ * owns them (#3179). Two functions may import the same module, and the
293
+ * pulled sources of both list it; deleting it with one would leave the other
294
+ * unbuildable while its sync state still called it current. A shared path is
295
+ * left alone and reported.
296
+ */
297
+ retainedFiles?: Set<string>;
279
298
  logger?: (msg: string) => void;
280
299
  }): Promise<PruneResult>;
281
300
  /** Address a prior-state entry for detail-fetch and delete. */
@@ -3,7 +3,7 @@
3
3
  * into a sync slot (issue #1659).
4
4
  *
5
5
  * `config pull` writes one file per server entity and records what it wrote in
6
- * `.primitive-sync.json` (`SyncState.entities[<slot>]`). Until #1659 it never
6
+ * `.sync-state.json` (`SyncState.entities[<slot>]`). Until #1659 it never
7
7
  * removed a local file whose entity had disappeared server-side, so the next
8
8
  * `config push` — which treats local TOML as the source of truth — silently
9
9
  * re-created the just-deleted entity.
@@ -13,7 +13,7 @@
13
13
  * test-directory block type. The pull write loops deliberately stay as they are
14
14
  * — this table is layout metadata, not a write framework.
15
15
  */
16
- import { existsSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from "fs";
16
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, } from "fs";
17
17
  import { basename, dirname, join, resolve, sep } from "path";
18
18
  import { parseConfigToml } from "./config-toml.js";
19
19
  const tomlFile = (key) => `${key}.toml`;
@@ -104,6 +104,21 @@ export const SYNC_RESOURCE_TYPES = [
104
104
  // Server drains `scriptsByApp` across all pages (#1668).
105
105
  listingComplete: true,
106
106
  },
107
+ {
108
+ // #3179 — server functions. `functions/<key>.toml` states the function and
109
+ // names its `entry`; the TypeScript the entry reaches lives beside it in
110
+ // the same tree and is captured into the version envelope on push, so the
111
+ // entity spans more than its one file. Prune removes the TOML; the sources
112
+ // it pulled are tracked in the state slot and removed with it.
113
+ label: "function",
114
+ dir: "functions",
115
+ ext: ".toml",
116
+ stateSlot: "functions",
117
+ fileFromKey: tomlFile,
118
+ // Server drains `functionsByApp` across all pages (#1668's rule, applied at
119
+ // birth in `server-functions-controller.ts`).
120
+ listingComplete: true,
121
+ },
107
122
  {
108
123
  label: "database-type-config",
109
124
  dir: "database-type-configs",
@@ -180,7 +195,7 @@ export async function attemptFetch(fn) {
180
195
  }
181
196
  /**
182
197
  * A filename must stay inside its type's directory. State is read from a file
183
- * on disk, so a corrupt or hand-edited `.primitive-sync.json` must never be
198
+ * on disk, so a corrupt or hand-edited `.sync-state.json` must never be
184
199
  * able to steer an `unlinkSync` outside the sync slot.
185
200
  */
186
201
  export function isSafeSyncFilename(filename) {
@@ -386,16 +401,37 @@ export function classifyLocalOnly(priorEntities, slot, key) {
386
401
  const managed = (priorEntities?.[slot] ?? {});
387
402
  return key in managed ? LOCAL_ONLY_ABSENT : LOCAL_ONLY_NEW;
388
403
  }
404
+ /**
405
+ * Whether `target` is still inside `root` once symlinks are resolved.
406
+ *
407
+ * A lexical prefix check answers a question about strings; this one answers the
408
+ * question the `unlinkSync` actually asks. `functions/shared` pointing at an
409
+ * external directory passes the string test and deletes someone else's file.
410
+ */
411
+ function realPathStaysInside(root, target) {
412
+ try {
413
+ if (lstatSync(target).isSymbolicLink())
414
+ return false;
415
+ const realRoot = realpathSync(root);
416
+ const real = realpathSync(target);
417
+ return real.startsWith(realRoot + sep);
418
+ }
419
+ catch {
420
+ return false;
421
+ }
422
+ }
389
423
  /**
390
424
  * Remove the local files for `keys` (as decided by `planPrune`), along with any
391
425
  * sidecar `<key>.tests/` directory.
392
426
  *
393
427
  * A file with uncommitted git edits is kept and reported instead: the operator
394
- * may be mid-way through authoring a change they meant to push. Everything
395
- * removed here is recoverable from the pre-pull snapshot via `config revert`.
428
+ * may be mid-way through authoring a change they meant to push — and for an
429
+ * entity that owns source files beyond its TOML (#3179), an edit to any of them
430
+ * keeps the whole entity. Everything removed here is recoverable from the
431
+ * pre-pull snapshot via `config revert`.
396
432
  */
397
433
  export async function applyPrune(options) {
398
- const { configDir, resourceType, keys, testCaseEntities, logger } = options;
434
+ const { configDir, resourceType, keys, testCaseEntities, extraFilesForKey, retainedFiles, logger, } = options;
399
435
  const log = logger ?? (() => { });
400
436
  const result = {
401
437
  prunedKeys: [],
@@ -409,19 +445,65 @@ export async function applyPrune(options) {
409
445
  const filePath = join(configDir, resourceType.dir, filename);
410
446
  if (!existsSync(filePath))
411
447
  continue;
448
+ // #3179 — the entity's own source files, recorded by the pull that wrote
449
+ // them. Each path is re-confined here: it came out of a JSON file on disk,
450
+ // so a corrupt or hand-edited sync state must never steer an `unlinkSync`
451
+ // out of the tree — lexically first, then against the real path, because a
452
+ // symlinked directory inside the tree resolves outside it.
453
+ const extras = [];
454
+ for (const extra of extraFilesForKey?.(key) ?? []) {
455
+ if (retainedFiles?.has(extra)) {
456
+ log(` Kept ${extra} — another entity still uses it`);
457
+ continue;
458
+ }
459
+ const target = resolve(configDir, extra);
460
+ const root = resolve(configDir);
461
+ if (target === root || !target.startsWith(root + sep)) {
462
+ log(` Skipped removing '${extra}' — it resolves outside the config tree`);
463
+ continue;
464
+ }
465
+ if (!existsSync(target))
466
+ continue;
467
+ if (!realPathStaysInside(root, target)) {
468
+ log(` Skipped removing '${extra}' — a symlink takes it outside the config tree`);
469
+ continue;
470
+ }
471
+ extras.push({ relative: extra, absolute: target });
472
+ }
412
473
  // Untracked does not mean edited: in a sync dir that was never committed
413
474
  // (or in no repository at all) every pulled file is untracked, and counting
414
475
  // that as a local edit would keep every stale file and leave #1659 unfixed.
476
+ //
477
+ // The entity's source files are asked the same question BEFORE anything is
478
+ // removed (#3179): a half-pruned function — TOML gone, edited TypeScript
479
+ // kept — is neither state the operator can act on, so an edit anywhere in
480
+ // the entity keeps all of it.
481
+ const edited = [];
415
482
  if (await hasUncommittedChanges(filePath, { includeUntracked: false })) {
483
+ edited.push(relativePath);
484
+ }
485
+ for (const extra of extras) {
486
+ if (await hasUncommittedChanges(extra.absolute, { includeUntracked: false })) {
487
+ edited.push(extra.relative);
488
+ }
489
+ }
490
+ if (edited.length > 0) {
416
491
  result.keptWithLocalEdits.push(key);
417
- log(` Kept ${relativePath} — ${ABSENT_FROM_EXPORT} but the file has ` +
418
- "uncommitted local edits; remove it yourself or re-create the entity on the server");
492
+ log(` Kept ${relativePath} — ${ABSENT_FROM_EXPORT} but ${edited.join(", ")} ` +
493
+ "has uncommitted local edits; remove it yourself or re-create the entity on the server");
419
494
  continue;
420
495
  }
421
496
  unlinkSync(filePath);
422
497
  result.prunedKeys.push(key);
423
498
  result.removed.push(relativePath);
424
499
  log(` Removed ${relativePath} — ${ABSENT_FROM_EXPORT}`);
500
+ for (const extra of extras) {
501
+ if (!existsSync(extra.absolute))
502
+ continue;
503
+ unlinkSync(extra.absolute);
504
+ result.removed.push(extra.relative);
505
+ log(` Removed ${extra.relative} — ${ABSENT_FROM_EXPORT}`);
506
+ }
425
507
  // #2645 — the entity's named config bodies go with it. They describe
426
508
  // configurations of a block the server no longer has, and leaving them
427
509
  // would have the next push re-create the block from its own leftovers.
@@ -673,6 +755,22 @@ export const PUSH_PRUNE_SPECS = [
673
755
  await c.deletePrompt(a, id, true);
674
756
  },
675
757
  },
758
+ {
759
+ // #3179 — server functions. Hard-delete rather than archive, following the
760
+ // same reasoning as every sibling: an archived function keeps holding its
761
+ // key in the app's SINGLE key namespace, which now blocks not just another
762
+ // function but a workflow, a script or a webhook wanting that key, with no
763
+ // way to reclaim it from `config`. The hard delete is also what destroys
764
+ // the versions' R2 objects, so a pruned function leaves no bundles behind.
765
+ resourceType: pruneTypeFor("function"),
766
+ keyFromFile: tomlDeclaredKeyFromFile("function", "key"),
767
+ addressOf: addressById,
768
+ fetchDetail: (c, a, id) => c.getFunction(a, id),
769
+ modifiedAtOf: pruneModifiedAt,
770
+ deleteEntity: async (c, a, id) => {
771
+ await c.deleteFunction(a, id, { hard: true });
772
+ },
773
+ },
676
774
  {
677
775
  resourceType: pruneTypeFor("workflow"),
678
776
  keyFromFile: tomlDeclaredKeyFromFile("workflow", "key"),