create-ragen-app 0.6.1 → 0.7.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.
@@ -13,6 +13,16 @@
13
13
  * by `tests/architecture/create-ragen-app-knows-the-provider-seams.test.ts` —
14
14
  * the same arrangement `manifest.ts` already has with the `.env.example`
15
15
  * files.
16
+ *
17
+ * **Three answers, two providers.** The third answer, RustFS, is not a third
18
+ * provider: to the apps it is plain S3 on `localhost:59000`, and the seam has
19
+ * no variant for it and should not grow one. What makes it an answer of its
20
+ * own is everything *around* the app config — it is a service this install
21
+ * starts (compose's `s3` profile), and the store's keys have to be known to
22
+ * two files that are read by two different programs: Compose reads `.env` to
23
+ * give RustFS its keys, the apps read `.env.local` to present them. So a
24
+ * selection carries `composeProfiles` and `composeEnv` beside `envUpdates`,
25
+ * and `provider` stays what the config and the seam understand.
16
26
  */
17
27
  /**
18
28
  * One field of a written config, and the variable it resolves from.
@@ -28,7 +38,50 @@ export interface ConfigField {
28
38
  envVar: string;
29
39
  required: boolean;
30
40
  }
31
- export type StorageChoice = 'local' | 's3';
41
+ /** What the wizard offers. */
42
+ export type StorageChoice = 'local' | 's3' | 'rustfs';
43
+ /**
44
+ * What the app sees — `STORAGE_PROVIDER` and `ragen.config.ts`'s `provider`.
45
+ * Kept separate from `StorageChoice` so that `'rustfs'` cannot reach either:
46
+ * the seam would refuse it at boot, and the typed config would not compile.
47
+ */
48
+ export type StorageProvider = 'local' | 's3';
49
+ /** The compose profile RustFS and its two init containers sit behind. */
50
+ export declare const RUSTFS_COMPOSE_PROFILE = "s3";
51
+ /**
52
+ * Where the apps reach RustFS. The apps run on the host (compose runs only the
53
+ * backing services), so this is the published port, not `rustfs:9000` — that
54
+ * name resolves only inside the compose network. The defaults mirror
55
+ * `docker-compose.yml`, and `RUSTFS_PUBLISHED_PORTS` in `tasks.ts` is held to
56
+ * that file by `installer-ports-agree-with-compose.test.ts`.
57
+ */
58
+ export declare const RUSTFS_ENDPOINT_URL = "http://localhost:59000";
59
+ /**
60
+ * Where RustFS answers from *inside* the compose network — for the apps when
61
+ * they run as containers (`npm run ragen:up:everything`), where `localhost` is
62
+ * the container itself. `docker-compose.fullapp.yml` prefers
63
+ * `S3_CONTAINER_ENDPOINT_URL` over `S3_ENDPOINT_URL` for exactly this.
64
+ */
65
+ export declare const RUSTFS_CONTAINER_ENDPOINT_URL = "http://rustfs:9000";
66
+ /** The web console, printed at the end so someone can look inside the store. */
67
+ export declare const RUSTFS_CONSOLE_URL = "http://localhost:59001";
68
+ /** The bucket `rustfs-bucket-init` creates on every `up`. */
69
+ export declare const RUSTFS_BUCKET = "ragen";
70
+ /**
71
+ * RustFS ignores the region, but the AWS SDK refuses to sign a request without
72
+ * one and the seam requires `S3_REGION`. `us-east-1` is what every
73
+ * S3-compatible server answers to, and what the bucket init uses.
74
+ */
75
+ export declare const RUSTFS_REGION = "us-east-1";
76
+ /**
77
+ * The keys RustFS is started with. Generated per install rather than using
78
+ * compose's `ragen-local` defaults, which are printed in a public file.
79
+ */
80
+ export interface RustfsKeys {
81
+ accessKey: string;
82
+ secretKey: string;
83
+ }
84
+ export declare function generateRustfsKeys(): RustfsKeys;
32
85
  export interface S3Answers {
33
86
  bucket: string;
34
87
  region: string;
@@ -38,12 +91,44 @@ export interface S3Answers {
38
91
  endpoint: string;
39
92
  }
40
93
  export interface StorageSelection {
41
- provider: StorageChoice;
94
+ /** The answer, so later steps can tell RustFS from an S3 someone else runs. */
95
+ choice: StorageChoice;
96
+ provider: StorageProvider;
42
97
  /** Written to the root `.env.local` — credentials never go in the config. */
43
98
  envUpdates: Record<string, string>;
44
99
  /** The config fields, already named as the config names them. */
45
100
  configFields: ConfigField[];
101
+ /** Passed to `docker compose --profile …`, so choosing it starts it. */
102
+ composeProfiles: string[];
103
+ /**
104
+ * Lines for the install's `.env`, which Compose reads and the apps do not.
105
+ * Written only where the file has no value yet — see `writeComposeEnv` in
106
+ * `cli.ts` for why an existing value wins.
107
+ */
108
+ composeEnv: Record<string, string>;
46
109
  }
47
110
  export declare const STORAGE_LABELS: Record<StorageChoice, string>;
48
- export declare function resolveStorageSelection(provider: StorageChoice, answers?: S3Answers): StorageSelection;
111
+ /**
112
+ * `answers` is read for `s3`, `rustfsKeys` for `rustfs`. RustFS generates its
113
+ * keys when none are given; the wizard passes them only when the install's
114
+ * `.env` already holds keys the store was started with (see
115
+ * `withExistingRustfsKeys`).
116
+ */
117
+ export declare function resolveStorageSelection(choice: StorageChoice, answers?: S3Answers, rustfsKeys?: RustfsKeys): StorageSelection;
118
+ /**
119
+ * The same selection, with any RustFS key the install's `.env` already has
120
+ * taking the place of the generated one.
121
+ *
122
+ * `.env` wins because it is what the store was *started* with: RustFS takes
123
+ * its root keys from its environment on every boot, and rotating them in
124
+ * `.env.local` alone would leave the apps presenting keys the running store
125
+ * does not know. Rotating both would be worse on a store that already holds
126
+ * files — the new keys would work, and nothing would say the old ones were
127
+ * someone's only record. Reusing them keeps `.env` and `.env.local` agreeing,
128
+ * which is the invariant that matters: one pair of keys, written twice.
129
+ *
130
+ * Per key, not all-or-nothing, because the `.env` merge is per line too: a
131
+ * file with only one of the two keeps it and gets the other appended.
132
+ */
133
+ export declare function withExistingRustfsKeys(selection: StorageSelection, existing: Record<string, string | undefined>): StorageSelection;
49
134
  //# sourceMappingURL=storage-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-provider.d.ts","sourceRoot":"","sources":["../src/storage-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,aAAa,CAAC;IACxB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,iEAAiE;IACjE,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAGxD,CAAC;AAkBF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,SAAS,GAClB,gBAAgB,CAmDlB"}
1
+ {"version":3,"file":"storage-provider.d.ts","sourceRoot":"","sources":["../src/storage-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAID,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7C,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAElE,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAE3D,6DAA6D;AAC7D,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,UAAU,CAK/C;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,eAAe,CAAC;IAC1B,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,iEAAiE;IACjE,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,wEAAwE;IACxE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAIxD,CAAC;AAkBF;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,SAAS,EACnB,UAAU,CAAC,EAAE,UAAU,GACtB,gBAAgB,CAmBlB;AA4CD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC3C,gBAAgB,CAalB"}
@@ -14,13 +14,60 @@
14
14
  * by `tests/architecture/create-ragen-app-knows-the-provider-seams.test.ts` —
15
15
  * the same arrangement `manifest.ts` already has with the `.env.example`
16
16
  * files.
17
+ *
18
+ * **Three answers, two providers.** The third answer, RustFS, is not a third
19
+ * provider: to the apps it is plain S3 on `localhost:59000`, and the seam has
20
+ * no variant for it and should not grow one. What makes it an answer of its
21
+ * own is everything *around* the app config — it is a service this install
22
+ * starts (compose's `s3` profile), and the store's keys have to be known to
23
+ * two files that are read by two different programs: Compose reads `.env` to
24
+ * give RustFS its keys, the apps read `.env.local` to present them. So a
25
+ * selection carries `composeProfiles` and `composeEnv` beside `envUpdates`,
26
+ * and `provider` stays what the config and the seam understand.
17
27
  */
18
28
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.STORAGE_LABELS = void 0;
29
+ exports.STORAGE_LABELS = exports.RUSTFS_REGION = exports.RUSTFS_BUCKET = exports.RUSTFS_CONSOLE_URL = exports.RUSTFS_CONTAINER_ENDPOINT_URL = exports.RUSTFS_ENDPOINT_URL = exports.RUSTFS_COMPOSE_PROFILE = void 0;
30
+ exports.generateRustfsKeys = generateRustfsKeys;
20
31
  exports.resolveStorageSelection = resolveStorageSelection;
32
+ exports.withExistingRustfsKeys = withExistingRustfsKeys;
33
+ const secrets_1 = require("./secrets");
34
+ /** The compose profile RustFS and its two init containers sit behind. */
35
+ exports.RUSTFS_COMPOSE_PROFILE = 's3';
36
+ /**
37
+ * Where the apps reach RustFS. The apps run on the host (compose runs only the
38
+ * backing services), so this is the published port, not `rustfs:9000` — that
39
+ * name resolves only inside the compose network. The defaults mirror
40
+ * `docker-compose.yml`, and `RUSTFS_PUBLISHED_PORTS` in `tasks.ts` is held to
41
+ * that file by `installer-ports-agree-with-compose.test.ts`.
42
+ */
43
+ exports.RUSTFS_ENDPOINT_URL = 'http://localhost:59000';
44
+ /**
45
+ * Where RustFS answers from *inside* the compose network — for the apps when
46
+ * they run as containers (`npm run ragen:up:everything`), where `localhost` is
47
+ * the container itself. `docker-compose.fullapp.yml` prefers
48
+ * `S3_CONTAINER_ENDPOINT_URL` over `S3_ENDPOINT_URL` for exactly this.
49
+ */
50
+ exports.RUSTFS_CONTAINER_ENDPOINT_URL = 'http://rustfs:9000';
51
+ /** The web console, printed at the end so someone can look inside the store. */
52
+ exports.RUSTFS_CONSOLE_URL = 'http://localhost:59001';
53
+ /** The bucket `rustfs-bucket-init` creates on every `up`. */
54
+ exports.RUSTFS_BUCKET = 'ragen';
55
+ /**
56
+ * RustFS ignores the region, but the AWS SDK refuses to sign a request without
57
+ * one and the seam requires `S3_REGION`. `us-east-1` is what every
58
+ * S3-compatible server answers to, and what the bucket init uses.
59
+ */
60
+ exports.RUSTFS_REGION = 'us-east-1';
61
+ function generateRustfsKeys() {
62
+ return {
63
+ accessKey: (0, secrets_1.generateS3AccessKeyId)(),
64
+ secretKey: (0, secrets_1.generateS3SecretAccessKey)(),
65
+ };
66
+ }
21
67
  exports.STORAGE_LABELS = {
22
68
  local: 'Local filesystem (default — no cloud account needed)',
23
69
  s3: 'S3-compatible (AWS, Cloudflare R2, Scaleway, MinIO, Ceph)',
70
+ rustfs: 'RustFS — self-hosted object storage, started with the stack',
24
71
  };
25
72
  /**
26
73
  * A path-style endpoint is what Scaleway (dotted bucket names) and MinIO/Ceph
@@ -35,16 +82,96 @@ function needsPathStyle(endpoint) {
35
82
  return (!host.includes('amazonaws.com') &&
36
83
  !host.includes('r2.cloudflarestorage.com'));
37
84
  }
38
- function resolveStorageSelection(provider, answers) {
39
- if (provider === 'local' || !answers) {
85
+ /**
86
+ * `answers` is read for `s3`, `rustfsKeys` for `rustfs`. RustFS generates its
87
+ * keys when none are given; the wizard passes them only when the install's
88
+ * `.env` already holds keys the store was started with (see
89
+ * `withExistingRustfsKeys`).
90
+ */
91
+ function resolveStorageSelection(choice, answers, rustfsKeys) {
92
+ if (choice === 'rustfs') {
93
+ return rustfsSelection(rustfsKeys ?? generateRustfsKeys());
94
+ }
95
+ if (choice === 'local' || !answers) {
40
96
  return {
97
+ choice: 'local',
41
98
  provider: 'local',
42
99
  envUpdates: { STORAGE_PROVIDER: 'local' },
43
100
  configFields: [
44
101
  { field: 'path', envVar: 'STORAGE_LOCAL_PATH', required: false },
45
102
  ],
103
+ composeProfiles: [],
104
+ composeEnv: {},
46
105
  };
47
106
  }
107
+ return s3Selection(answers);
108
+ }
109
+ /**
110
+ * RustFS through the S3 path, not beside it: the same five variables and the
111
+ * same config fields an S3 answer pointed at `localhost:59000` would produce.
112
+ * A second hand-written list is how the two would drift, and the apps cannot
113
+ * tell them apart anyway. Path-style comes from `needsPathStyle` like it does
114
+ * for MinIO — a `localhost` host has no bucket subdomains to resolve.
115
+ */
116
+ function rustfsSelection(keys) {
117
+ const s3 = s3Selection({
118
+ bucket: exports.RUSTFS_BUCKET,
119
+ region: exports.RUSTFS_REGION,
120
+ endpoint: exports.RUSTFS_ENDPOINT_URL,
121
+ accessKeyId: keys.accessKey,
122
+ secretAccessKey: keys.secretKey,
123
+ });
124
+ return {
125
+ ...s3,
126
+ choice: 'rustfs',
127
+ composeProfiles: [exports.RUSTFS_COMPOSE_PROFILE],
128
+ // `.env` is the only file Compose reads, and it serves two readers: the
129
+ // RustFS server (its root keys) and, with the apps in containers
130
+ // (`ragen:up:everything`), the apps themselves — which otherwise never see
131
+ // `.env.local` and would quietly store to the local volume instead. So the
132
+ // S3 settings go here too, derived from the same pair of keys, with the
133
+ // container-side endpoint. The host apps are unaffected: `.env.local`
134
+ // holds the same values and wins over `.env`, and nothing on the host
135
+ // reads S3_CONTAINER_ENDPOINT_URL.
136
+ composeEnv: {
137
+ RUSTFS_ACCESS_KEY: keys.accessKey,
138
+ RUSTFS_SECRET_KEY: keys.secretKey,
139
+ STORAGE_PROVIDER: 's3',
140
+ S3_CONTAINER_ENDPOINT_URL: exports.RUSTFS_CONTAINER_ENDPOINT_URL,
141
+ S3_FORCE_PATH_STYLE: 'true',
142
+ S3_REGION: exports.RUSTFS_REGION,
143
+ S3_BUCKET_NAME: exports.RUSTFS_BUCKET,
144
+ S3_ACCESS_KEY_ID: keys.accessKey,
145
+ S3_SECRET_ACCESS_KEY: keys.secretKey,
146
+ },
147
+ };
148
+ }
149
+ /**
150
+ * The same selection, with any RustFS key the install's `.env` already has
151
+ * taking the place of the generated one.
152
+ *
153
+ * `.env` wins because it is what the store was *started* with: RustFS takes
154
+ * its root keys from its environment on every boot, and rotating them in
155
+ * `.env.local` alone would leave the apps presenting keys the running store
156
+ * does not know. Rotating both would be worse on a store that already holds
157
+ * files — the new keys would work, and nothing would say the old ones were
158
+ * someone's only record. Reusing them keeps `.env` and `.env.local` agreeing,
159
+ * which is the invariant that matters: one pair of keys, written twice.
160
+ *
161
+ * Per key, not all-or-nothing, because the `.env` merge is per line too: a
162
+ * file with only one of the two keeps it and gets the other appended.
163
+ */
164
+ function withExistingRustfsKeys(selection, existing) {
165
+ if (selection.choice !== 'rustfs') {
166
+ return selection;
167
+ }
168
+ const accessKey = existing.RUSTFS_ACCESS_KEY?.trim() ||
169
+ selection.composeEnv.RUSTFS_ACCESS_KEY;
170
+ const secretKey = existing.RUSTFS_SECRET_KEY?.trim() ||
171
+ selection.composeEnv.RUSTFS_SECRET_KEY;
172
+ return rustfsSelection({ accessKey, secretKey });
173
+ }
174
+ function s3Selection(answers) {
48
175
  const endpoint = answers.endpoint.trim();
49
176
  const envUpdates = {
50
177
  STORAGE_PROVIDER: 's3',
@@ -79,6 +206,13 @@ function resolveStorageSelection(provider, answers) {
79
206
  });
80
207
  }
81
208
  }
82
- return { provider: 's3', envUpdates, configFields };
209
+ return {
210
+ choice: 's3',
211
+ provider: 's3',
212
+ envUpdates,
213
+ configFields,
214
+ composeProfiles: [],
215
+ composeEnv: {},
216
+ };
83
217
  }
84
218
  //# sourceMappingURL=storage-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-provider.js","sourceRoot":"","sources":["../src/storage-provider.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAyDH,0DAsDC;AA3EY,QAAA,cAAc,GAAkC;IAC3D,KAAK,EAAE,sDAAsD;IAC7D,EAAE,EAAE,2DAA2D;CAChE,CAAC;AAEF;;;;GAIG;AACH,SAAS,cAAc,CAAC,QAAgB;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC;QAC/B,CAAC,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED,SAAgB,uBAAuB,CACrC,QAAuB,EACvB,OAAmB;IAEnB,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QACrC,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,UAAU,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE;YACzC,YAAY,EAAE;gBACZ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,QAAQ,EAAE,KAAK,EAAE;aACjE;SACF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEzC,MAAM,UAAU,GAA2B;QACzC,gBAAgB,EAAE,IAAI;QACtB,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE;QACrC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE;QAChC,gBAAgB,EAAE,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE;QAC5C,oBAAoB,EAAE,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE;KACrD,CAAC;IAEF,MAAM,YAAY,GAAkB;QAClC,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE;QACjE,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE;QACxD,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,IAAI,EAAE;QACpE;YACE,KAAK,EAAE,iBAAiB;YACxB,MAAM,EAAE,sBAAsB;YAC9B,QAAQ,EAAE,IAAI;SACf;KACF,CAAC;IAEF,IAAI,QAAQ,EAAE,CAAC;QACb,UAAU,CAAC,eAAe,GAAG,QAAQ,CAAC;QACtC,YAAY,CAAC,IAAI,CAAC;YAChB,KAAK,EAAE,UAAU;YACjB,MAAM,EAAE,iBAAiB;YACzB,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;QAEH,IAAI,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,UAAU,CAAC,mBAAmB,GAAG,MAAM,CAAC;YACxC,YAAY,CAAC,IAAI,CAAC;gBAChB,KAAK,EAAE,gBAAgB;gBACvB,MAAM,EAAE,qBAAqB;gBAC7B,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC;AACtD,CAAC"}
1
+ {"version":3,"file":"storage-provider.js","sourceRoot":"","sources":["../src/storage-provider.ts"],"names":[],"mappings":"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"}
package/dist/tasks.d.ts CHANGED
@@ -15,26 +15,153 @@ export interface MigrateOptions {
15
15
  }
16
16
  export declare function isDockerAvailable(): Promise<boolean>;
17
17
  /**
18
- * Whether this Docker daemon already runs a Ragen stack.
18
+ * Compose takes its project name from the **directory basename**, which is not
19
+ * the same as being unique per install.
19
20
  *
20
- * docker-compose.yml pins container, volume and network names instead of
21
- * letting Compose prefix them per project, so a second install does not just
22
- * collide on the container names — it silently attaches to the *same*
23
- * postgres and qdrant volumes as the first one. Checking the volume rather
24
- * than a container catches a stack that is merely stopped, which is the
25
- * case that looks safest and is not.
21
+ * Removing the pinned `container_name`/volume `name:` from docker-compose.yml
22
+ * scopes every resource to the project, and that is what lets two checkouts
23
+ * coexist. But `/work/a/ragen` and `/work/b/ragen` produce the same project
24
+ * name, and therefore the same containers, volumes and network — the original
25
+ * bug, narrowed to same-named directories rather than fixed. Worse than the
26
+ * port collision, because a *stopped* first stack collides silently: no port
27
+ * is held, nothing refuses, and the second install opens the first one's
28
+ * database.
26
29
  *
27
- * Reads RAGEN_STACK_NAME for the same reason compose does: someone already
28
- * running a stack under a name of their own would otherwise be told their
29
- * machine is clear, and collide with it.
30
+ * So the name is decided here, at install time, and written to the new tree's
31
+ * `.env` — which Compose reads from the project directory on every later
32
+ * `docker compose` the user runs, with no flag to remember. `.env` is the
33
+ * lowest-precedence file `scripts/load-root-env.mjs` reads and is gitignored,
34
+ * and nothing in the apps reads `COMPOSE_PROJECT_NAME`, so it cannot collide
35
+ * with the install's own configuration.
36
+ *
37
+ * The basename is kept whenever it is free, because the readability given up
38
+ * with the pinned names is worth not giving up twice. A suffix appears only
39
+ * when this daemon already runs a project of that name from somewhere else.
40
+ */
41
+ export interface ComposeProjectName {
42
+ name: string;
43
+ /** True when the plain basename was taken and a suffix was added. */
44
+ disambiguated: boolean;
45
+ /** The compose file of the project that took it, for the message. */
46
+ takenBy?: string;
47
+ /**
48
+ * True when the suffix was added because Docker could not be asked, rather
49
+ * than because a collision was seen. A different sentence to the reader:
50
+ * nothing is wrong, and the name is cautious rather than forced.
51
+ */
52
+ daemonUnreachable?: boolean;
53
+ }
54
+ /**
55
+ * Compose accepts `[a-z0-9][a-z0-9_-]*`. A directory called `My Ragen!` or
56
+ * `.ragen` is perfectly legal and would otherwise make every compose command
57
+ * fail with a validation error after the install had finished.
58
+ */
59
+ export declare function normalizeComposeProjectName(raw: string): string;
60
+ export declare function resolveComposeProjectName(targetDir: string): Promise<ComposeProjectName>;
61
+ /**
62
+ * Every host port a default `docker compose up` publishes, with the variable
63
+ * that moves it. The order is the order the warning prints them in.
64
+ *
65
+ * Defaults are duplicated from docker-compose.yml rather than parsed out of
66
+ * it, because this runs *before* the repository is cloned — there is no file
67
+ * to read yet. `ports-agree-with-compose.test.ts` holds the two in step.
68
+ */
69
+ export declare const PUBLISHED_PORTS: readonly [{
70
+ readonly service: "Postgres";
71
+ readonly variable: "POSTGRES_PORT";
72
+ readonly port: 55432;
73
+ }, {
74
+ readonly service: "Qdrant";
75
+ readonly variable: "QDRANT_PORT";
76
+ readonly port: 6333;
77
+ }, {
78
+ readonly service: "Qdrant (gRPC)";
79
+ readonly variable: "QDRANT_GRPC_PORT";
80
+ readonly port: 6334;
81
+ }, {
82
+ readonly service: "Docling";
83
+ readonly variable: "DOCLING_PORT";
84
+ readonly port: 5001;
85
+ }, {
86
+ readonly service: "Redis";
87
+ readonly variable: "REDIS_PORT";
88
+ readonly port: 56379;
89
+ }];
90
+ /** The two more, behind the `pii` profile, that only a masking install starts. */
91
+ export declare const PII_PUBLISHED_PORTS: readonly [{
92
+ readonly service: "Presidio analyzer";
93
+ readonly variable: "PRESIDIO_ANALYZER_PORT";
94
+ readonly port: 5002;
95
+ }, {
96
+ readonly service: "Presidio anonymizer";
97
+ readonly variable: "PRESIDIO_ANONYMIZER_PORT";
98
+ readonly port: 5003;
99
+ }];
100
+ /**
101
+ * The two more, behind the `s3` profile, that only a RustFS install starts:
102
+ * the S3 API the apps talk to, and the web console.
103
+ */
104
+ export declare const RUSTFS_PUBLISHED_PORTS: readonly [{
105
+ readonly service: "RustFS (S3 API)";
106
+ readonly variable: "RUSTFS_PORT";
107
+ readonly port: 59000;
108
+ }, {
109
+ readonly service: "RustFS console";
110
+ readonly variable: "RUSTFS_CONSOLE_PORT";
111
+ readonly port: 59001;
112
+ }];
113
+ export interface PublishedPort {
114
+ service: string;
115
+ variable: string;
116
+ port: number;
117
+ }
118
+ /**
119
+ * The extra ports each optional profile publishes, keyed by profile name.
120
+ *
121
+ * One table rather than a branch per profile, because the port probe, the
122
+ * recovery command and the "update .env.local" hint all have to agree on what
123
+ * a profile starts — and with two profiles the branch-per-profile version was
124
+ * already printing a command that named only one of them.
125
+ */
126
+ export declare const PROFILE_PUBLISHED_PORTS: Record<string, readonly PublishedPort[]>;
127
+ /** Every port a `docker compose up` with these profiles publishes. */
128
+ export declare function publishedPortsFor(profiles: readonly string[]): PublishedPort[];
129
+ /**
130
+ * The `docker compose up` a person should type to get the stack this install
131
+ * configured, profiles included.
132
+ *
133
+ * Built from the same list `startDockerServices` is given, so a printed
134
+ * command cannot drift from an executed one. A command that drops a profile
135
+ * starts a stack without the services whose URLs were just written to
136
+ * `.env.local` — the half-configuration the profiles exist to prevent, only
137
+ * printed instead of executed.
138
+ */
139
+ export declare function composeUpCommand(profiles?: readonly string[]): string;
140
+ /**
141
+ * Which of these host ports something is already listening on.
142
+ *
143
+ * This replaces a check for the volume `ragen-postgres-data`. That volume name
144
+ * was pinned in docker-compose.yml, which is what made a second install share
145
+ * the first one's database — and removing the pin removed the sharing, so the
146
+ * old check now answers a question nobody is asking and would answer "clear"
147
+ * for every install made after the change. A guard that quietly stops firing
148
+ * is worse than no guard.
149
+ *
150
+ * Ports are what is genuinely left: Compose prefixes names per project, and
151
+ * prefixes nothing about a published port, so two stacks still collide on
152
+ * 55432. Binding is also a better probe than asking Docker — it sees a native
153
+ * Postgres on the port as well, which is the other way this install ends up
154
+ * talking to a database it did not start.
30
155
  */
31
- export declare function ragenStackVolumeExists(): Promise<boolean>;
156
+ export declare function busyPublishedPorts(candidates?: readonly PublishedPort[]): Promise<PublishedPort[]>;
32
157
  /**
33
158
  * `profiles` names compose profiles to add to the default set.
34
159
  *
35
160
  * Without it, choosing PII masking would write the two Presidio URLs and start
36
161
  * nothing behind them: those services sit behind compose's `pii` profile, so a
37
162
  * plain `up -d` skips them and the app would point at containers nobody ran.
163
+ * RustFS is the same shape behind `s3` — the apps would be configured for an
164
+ * object store on 59000 that nothing started, and fail at the first upload.
38
165
  */
39
166
  export declare function startDockerServices({ cwd, profiles, }: RunOptions & {
40
167
  profiles?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAU1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAgB/D;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,GAAG,EACH,QAAa,GACd,EAAE,UAAU,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAMtD;AAED,wBAAsB,mBAAmB,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;AAED,wBAAsB,oBAAoB,CAAC,EACzC,GAAG,EACH,GAAG,GACJ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5B;AAED,wBAAsB,YAAY,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,UAAU,EACxB,EAAE,OAAW,EAAE,YAAmB,EAAE,GAAE,cAAmB,GACxD,OAAO,CAAC,IAAI,CAAC,CAsBf"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAU1D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,aAAa,EAAE,OAAO,CAAC;IACvB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO/D;AA4BD,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC,CA8C7B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;EAMlB,CAAC;AAEX,kFAAkF;AAClF,eAAO,MAAM,mBAAmB;;;;;;;;EAWtB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;EAOzB,CAAC;AAEX,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAI1E,CAAC;AAEJ,sEAAsE;AACtE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,aAAa,EAAE,CAKjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAMzE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,GAAE,SAAS,aAAa,EAAoB,GACrD,OAAO,CAAC,aAAa,EAAE,CAAC,CAS1B;AAyBD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,GAAG,EACH,QAAa,GACd,EAAE,UAAU,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAMtD;AAED,wBAAsB,mBAAmB,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;AAED,wBAAsB,oBAAoB,CAAC,EACzC,GAAG,EACH,GAAG,GACJ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5B;AAED,wBAAsB,YAAY,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,UAAU,EACxB,EAAE,OAAW,EAAE,YAAmB,EAAE,GAAE,cAAmB,GACxD,OAAO,CAAC,IAAI,CAAC,CAsBf"}