@usestatemachines/cli 0.0.0-stage → 0.1.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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -2
  3. package/dist/agents/bundled.d.ts +5 -0
  4. package/dist/agents/bundled.js +6 -0
  5. package/dist/agents/destinations.d.ts +13 -0
  6. package/dist/agents/destinations.js +16 -0
  7. package/dist/api-internal.d.ts +62 -0
  8. package/dist/api-internal.js +348 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +4 -0
  11. package/dist/command.d.ts +119 -0
  12. package/dist/command.js +195 -0
  13. package/dist/commands/api/command.d.ts +15 -0
  14. package/dist/commands/api/command.js +38 -0
  15. package/dist/commands/api/run.d.ts +4 -0
  16. package/dist/commands/api/run.js +24 -0
  17. package/dist/commands/apps/command.d.ts +19 -0
  18. package/dist/commands/apps/command.js +29 -0
  19. package/dist/commands/apps/list.d.ts +8 -0
  20. package/dist/commands/apps/list.js +10 -0
  21. package/dist/commands/connect/command.d.ts +34 -0
  22. package/dist/commands/connect/command.js +70 -0
  23. package/dist/commands/connect/run.d.ts +10 -0
  24. package/dist/commands/connect/run.js +71 -0
  25. package/dist/commands/doctor/command.d.ts +29 -0
  26. package/dist/commands/doctor/command.js +36 -0
  27. package/dist/commands/doctor/run.d.ts +13 -0
  28. package/dist/commands/doctor/run.js +12 -0
  29. package/dist/commands/env/command.d.ts +5 -0
  30. package/dist/commands/env/command.js +19 -0
  31. package/dist/commands/env/pull.d.ts +6 -0
  32. package/dist/commands/env/pull.js +6 -0
  33. package/dist/commands/environments/command.d.ts +121 -0
  34. package/dist/commands/environments/command.js +170 -0
  35. package/dist/commands/environments/create.d.ts +9 -0
  36. package/dist/commands/environments/create.js +42 -0
  37. package/dist/commands/environments/delete.d.ts +7 -0
  38. package/dist/commands/environments/delete.js +48 -0
  39. package/dist/commands/environments/get.d.ts +39 -0
  40. package/dist/commands/environments/get.js +5 -0
  41. package/dist/commands/environments/list.d.ts +41 -0
  42. package/dist/commands/environments/list.js +18 -0
  43. package/dist/commands/environments/wait.d.ts +39 -0
  44. package/dist/commands/environments/wait.js +21 -0
  45. package/dist/commands/init/command.d.ts +68 -0
  46. package/dist/commands/init/command.js +90 -0
  47. package/dist/commands/init/run.d.ts +8 -0
  48. package/dist/commands/init/run.js +422 -0
  49. package/dist/commands/link/command.d.ts +13 -0
  50. package/dist/commands/link/command.js +32 -0
  51. package/dist/commands/link/run.d.ts +12 -0
  52. package/dist/commands/link/run.js +7 -0
  53. package/dist/commands/login/command.d.ts +1 -0
  54. package/dist/commands/login/command.js +43 -0
  55. package/dist/commands/login/run.d.ts +30 -0
  56. package/dist/commands/login/run.js +43 -0
  57. package/dist/commands/login/schema.d.ts +33 -0
  58. package/dist/commands/login/schema.js +33 -0
  59. package/dist/commands/logout/command.d.ts +1 -0
  60. package/dist/commands/logout/command.js +24 -0
  61. package/dist/commands/logout/run.d.ts +10 -0
  62. package/dist/commands/logout/run.js +19 -0
  63. package/dist/commands/logout/schema.d.ts +10 -0
  64. package/dist/commands/logout/schema.js +10 -0
  65. package/dist/commands/profiles/command.d.ts +2 -0
  66. package/dist/commands/profiles/command.js +29 -0
  67. package/dist/commands/profiles/list.d.ts +20 -0
  68. package/dist/commands/profiles/list.js +10 -0
  69. package/dist/commands/profiles/schema.d.ts +24 -0
  70. package/dist/commands/profiles/schema.js +19 -0
  71. package/dist/commands/profiles/use.d.ts +7 -0
  72. package/dist/commands/profiles/use.js +7 -0
  73. package/dist/commands/requests/command.d.ts +39 -0
  74. package/dist/commands/requests/command.js +65 -0
  75. package/dist/commands/requests/get.d.ts +32 -0
  76. package/dist/commands/requests/get.js +15 -0
  77. package/dist/commands/requests/list.d.ts +27 -0
  78. package/dist/commands/requests/list.js +25 -0
  79. package/dist/commands/snapshots/command.d.ts +48 -0
  80. package/dist/commands/snapshots/command.js +80 -0
  81. package/dist/commands/snapshots/create.d.ts +7 -0
  82. package/dist/commands/snapshots/create.js +26 -0
  83. package/dist/commands/snapshots/delete.d.ts +34 -0
  84. package/dist/commands/snapshots/delete.js +8 -0
  85. package/dist/commands/snapshots/get.d.ts +35 -0
  86. package/dist/commands/snapshots/get.js +5 -0
  87. package/dist/commands/snapshots/list.d.ts +37 -0
  88. package/dist/commands/snapshots/list.js +10 -0
  89. package/dist/commands/unlink/command.d.ts +1 -0
  90. package/dist/commands/unlink/command.js +15 -0
  91. package/dist/commands/unlink/run.d.ts +7 -0
  92. package/dist/commands/unlink/run.js +8 -0
  93. package/dist/commands/whoami/command.d.ts +28 -0
  94. package/dist/commands/whoami/command.js +44 -0
  95. package/dist/commands/whoami/run.d.ts +5 -0
  96. package/dist/commands/whoami/run.js +39 -0
  97. package/dist/commands/workspaces/command.d.ts +33 -0
  98. package/dist/commands/workspaces/command.js +73 -0
  99. package/dist/commands/workspaces/list.d.ts +5 -0
  100. package/dist/commands/workspaces/list.js +42 -0
  101. package/dist/commands/workspaces/use.d.ts +5 -0
  102. package/dist/commands/workspaces/use.js +18 -0
  103. package/dist/config/project.d.ts +21 -0
  104. package/dist/config/project.js +105 -0
  105. package/dist/config/schema.d.ts +616 -0
  106. package/dist/config/schema.js +282 -0
  107. package/dist/context-types.d.ts +89 -0
  108. package/dist/context-types.js +56 -0
  109. package/dist/context.d.ts +13 -0
  110. package/dist/context.js +290 -0
  111. package/dist/credentials/sealing.d.ts +10 -0
  112. package/dist/credentials/sealing.js +72 -0
  113. package/dist/credentials/store.d.ts +17 -0
  114. package/dist/credentials/store.js +156 -0
  115. package/dist/doctor.d.ts +4 -0
  116. package/dist/doctor.js +239 -0
  117. package/dist/env-file.d.ts +10 -0
  118. package/dist/env-file.js +254 -0
  119. package/dist/errors.d.ts +784 -0
  120. package/dist/errors.js +459 -0
  121. package/dist/flags.d.ts +22 -0
  122. package/dist/flags.js +56 -0
  123. package/dist/generated/internal.d.ts +138 -0
  124. package/dist/generated/internal.js +2627 -0
  125. package/dist/generated/retry.d.ts +1 -0
  126. package/dist/generated/retry.js +11 -0
  127. package/dist/help.d.ts +408 -0
  128. package/dist/help.js +220 -0
  129. package/dist/io-types.d.ts +127 -0
  130. package/dist/io-types.js +1 -0
  131. package/dist/io.d.ts +13 -0
  132. package/dist/io.js +1017 -0
  133. package/dist/keys.d.ts +50 -0
  134. package/dist/keys.js +691 -0
  135. package/dist/link.d.ts +6 -0
  136. package/dist/link.js +58 -0
  137. package/dist/login.d.ts +92 -0
  138. package/dist/login.js +965 -0
  139. package/dist/main.d.ts +2 -0
  140. package/dist/main.js +126 -0
  141. package/dist/native-helper.d.ts +1 -0
  142. package/dist/native-helper.js +2 -0
  143. package/dist/output.d.ts +8 -0
  144. package/dist/output.js +135 -0
  145. package/dist/parser.d.ts +28 -0
  146. package/dist/parser.js +250 -0
  147. package/dist/program-types.d.ts +67 -0
  148. package/dist/program-types.js +14 -0
  149. package/dist/programs.d.ts +2 -0
  150. package/dist/programs.js +44 -0
  151. package/dist/project-roots.d.ts +3 -0
  152. package/dist/project-roots.js +26 -0
  153. package/dist/prompt.d.ts +3 -0
  154. package/dist/prompt.js +61 -0
  155. package/dist/registry.d.ts +2 -0
  156. package/dist/registry.js +44 -0
  157. package/dist/resources/create.d.ts +9 -0
  158. package/dist/resources/create.js +55 -0
  159. package/dist/resources/read.d.ts +15 -0
  160. package/dist/resources/read.js +69 -0
  161. package/dist/resources/schema.d.ts +177 -0
  162. package/dist/resources/schema.js +158 -0
  163. package/dist/text.d.ts +3 -0
  164. package/dist/text.js +19 -0
  165. package/dist/update.d.ts +22 -0
  166. package/dist/update.js +126 -0
  167. package/dist/version.d.ts +2 -0
  168. package/dist/version.js +2 -0
  169. package/package.json +52 -3
package/dist/doctor.js ADDED
@@ -0,0 +1,239 @@
1
+ import { join } from 'node:path';
2
+ import { skill } from './agents/bundled.js';
3
+ import { agentDestinations } from './agents/destinations.js';
4
+ import { withinDeadline } from './api-internal.js';
5
+ import { apiOriginFromEnvironment, productionOrigin, profileNameFromEnvironment, } from './context-types.js';
6
+ import { CliError, unconfirmedRevocations } from './errors.js';
7
+ import { commandError } from './output.js';
8
+ import { findProjectRoot } from './project-roots.js';
9
+ import { checkLatest } from './update.js';
10
+ const ok = (message) => ({ status: 'ok', message, fix: null });
11
+ const skip = (message) => ({
12
+ status: 'skipped',
13
+ message,
14
+ fix: null,
15
+ });
16
+ const warning = (message, fix) => ({
17
+ status: 'warning',
18
+ message,
19
+ fix,
20
+ });
21
+ export async function runDoctor(context, input) {
22
+ const { io } = context;
23
+ const checks = [];
24
+ async function check(name, action) {
25
+ const options = { signal: io.signal, deadline: io.now() + 5000 };
26
+ try {
27
+ const result = await withinDeadline(io, options, (signal) => action({ ...options, signal }));
28
+ checks.push({ name, ...result });
29
+ }
30
+ catch (error) {
31
+ if (io.signal.aborted) {
32
+ throw new CliError('cli.cancelled');
33
+ }
34
+ const failure = await commandError(error, io.signal);
35
+ checks.push({
36
+ name,
37
+ status: 'failed',
38
+ message: failure.message,
39
+ fix: failure.document.error.next[0]?.command ??
40
+ 'Run statemachines doctor again.',
41
+ });
42
+ }
43
+ }
44
+ const environment = io.host.environment;
45
+ async function selected(options) {
46
+ const state = await context.accounts.inspect(options);
47
+ const name = context.globals.profile ??
48
+ profileNameFromEnvironment(environment.STATEMACHINES_PROFILE) ??
49
+ state.defaultProfile;
50
+ return state.profiles.find((profile) => profile.name === name) ?? null;
51
+ }
52
+ async function origin(options) {
53
+ const configured = apiOriginFromEnvironment(environment.STATEMACHINES_BASE_URL);
54
+ if (configured !== null) {
55
+ return configured;
56
+ }
57
+ return environment.STATEMACHINES_API_KEY === undefined
58
+ ? ((await selected(options))?.origin ?? productionOrigin)
59
+ : productionOrigin;
60
+ }
61
+ await check('Node', async () => {
62
+ const [major = 0, minor = 0] = io.host.nodeVersion
63
+ .replace(/^v/, '')
64
+ .split('.')
65
+ .map(Number);
66
+ return major > 20 || (major === 20 && minor >= 20)
67
+ ? ok(`Node ${io.host.nodeVersion}`)
68
+ : {
69
+ status: 'failed',
70
+ message: `Node ${io.host.nodeVersion} is unsupported.`,
71
+ fix: 'Install Node 20.20 or later.',
72
+ };
73
+ });
74
+ await check('Credential', async (options) => {
75
+ if (environment.STATEMACHINES_API_KEY !== undefined) {
76
+ return ok(`Workspace key from STATEMACHINES_API_KEY at ${await origin(options)}.`);
77
+ }
78
+ const profile = await selected(options);
79
+ if (profile === null) {
80
+ throw new CliError('cli.no_credential');
81
+ }
82
+ const expected = await origin(options);
83
+ if (expected !== profile.origin) {
84
+ throw new CliError('cli.origin_mismatch');
85
+ }
86
+ const expires = Date.parse(profile.expiresAt);
87
+ if (expires <= io.now()) {
88
+ throw new CliError('cli.credential_expired');
89
+ }
90
+ if (expires - io.now() <= 14 * 24 * 60 * 60 * 1000) {
91
+ return warning(`Profile ${profile.name} expires within 14 days.`, 'Run statemachines login.');
92
+ }
93
+ return ok(`Profile ${profile.name} at ${profile.origin}; expires ${new Date(expires).toISOString()}.`);
94
+ });
95
+ await check('API credential', async (options) => {
96
+ if (input.offline) {
97
+ return skip('Offline mode.');
98
+ }
99
+ const account = await context.resolveAccount(options);
100
+ const me = await account.sdk.me();
101
+ const absoluteExpiry = 'expiresAt' in me.credential && me.credential.expiresAt instanceof Date
102
+ ? me.credential.expiresAt.getTime()
103
+ : null;
104
+ const idleExpiry = me.credential.kind === 'cli_token' &&
105
+ 'idleExpiresAt' in me.credential &&
106
+ me.credential.idleExpiresAt instanceof Date
107
+ ? me.credential.idleExpiresAt.getTime()
108
+ : null;
109
+ const expires = absoluteExpiry === null
110
+ ? idleExpiry
111
+ : idleExpiry === null
112
+ ? absoluteExpiry
113
+ : Math.min(absoluteExpiry, idleExpiry);
114
+ return expires !== null && expires - io.now() <= 14 * 24 * 60 * 60 * 1000
115
+ ? warning('The credential works and expires within 14 days.', 'Replace the credential before it expires.')
116
+ : ok('The credential works.');
117
+ });
118
+ await check('Workspace', async (options) => {
119
+ if (input.offline) {
120
+ return skip('Offline mode. Workspace access was not checked.');
121
+ }
122
+ const account = await context.resolveAccount(options);
123
+ const workspace = await account.resolveWorkspace(options);
124
+ return workspace === null
125
+ ? warning('No workspace selected.', 'Run statemachines link --workspace <id>.')
126
+ : ok(`${workspace.name} from ${workspace.source}.`);
127
+ });
128
+ await check('Credential store', async (options) => {
129
+ if (environment.STATEMACHINES_API_KEY !== undefined) {
130
+ return ok('The credential comes from the environment.');
131
+ }
132
+ const profile = await selected(options);
133
+ if (profile === null) {
134
+ throw new CliError('cli.no_credential');
135
+ }
136
+ if (profile.store === 'file') {
137
+ for (const name of ['config.json', 'credentials.json']) {
138
+ const inspection = await io.files.inspect(join(io.host.configHome, name), options);
139
+ if (inspection.kind !== 'file') {
140
+ throw new CliError('cli.credentials_invalid');
141
+ }
142
+ if (io.host.platform !== 'win32' && (inspection.mode & 0o077) !== 0) {
143
+ return warning(`${name} is readable by other users.`, 'Restrict the file permissions to your user.');
144
+ }
145
+ }
146
+ return ok(io.host.platform === 'win32'
147
+ ? 'Protected file in the user application data directory.'
148
+ : 'Protected files are accessible only to your user.');
149
+ }
150
+ const result = await io.nativeKeychain.executeInChild({ kind: 'probe' }, options);
151
+ if (result.kind === 'timed-out') {
152
+ throw new CliError('cli.keychain_timeout');
153
+ }
154
+ if (result.kind === 'module-unavailable') {
155
+ throw new CliError('cli.keychain_unavailable');
156
+ }
157
+ if (result.kind !== 'available') {
158
+ throw new CliError('cli.keychain_denied');
159
+ }
160
+ return ok('The keychain is available.');
161
+ });
162
+ await check('Pending account changes', async (options) => {
163
+ if (input.offline) {
164
+ const state = await context.accounts.inspect(options);
165
+ return state.pendingAcknowledgements +
166
+ state.pendingRevocations +
167
+ state.pendingCleanup ===
168
+ 0
169
+ ? ok('No pending account changes.')
170
+ : warning(`Pending account changes: ${state.pendingAcknowledgements + state.pendingRevocations + state.pendingCleanup}. Credential cleanup: ${state.pendingCleanup}.`, state.pendingCleanup > 0
171
+ ? 'Check credential-store access and run statemachines doctor again.'
172
+ : state.pendingRevocations > 0
173
+ ? 'Restore credential access and rerun doctor, or revoke the sessions in dashboard settings.'
174
+ : 'Run statemachines doctor with a network connection.');
175
+ }
176
+ const report = await context.accounts.reconcile('doctor', options);
177
+ const state = await context.accounts.inspect(options);
178
+ const unconfirmed = report.unconfirmed.length;
179
+ if (report.pending.length + state.pendingCleanup === 0) {
180
+ if (unconfirmed === 0) {
181
+ return ok('No pending account changes.');
182
+ }
183
+ const { problem, remedy } = unconfirmedRevocations(unconfirmed);
184
+ return warning(`No pending account changes. ${problem}`, remedy);
185
+ }
186
+ return warning(`Pending account changes: ${report.pending.length + state.pendingCleanup}. Credential cleanup: ${state.pendingCleanup}.`, state.pendingCleanup > 0
187
+ ? 'Check credential-store access and run statemachines doctor again.'
188
+ : state.pendingRevocations > 0
189
+ ? 'Restore credential access and rerun doctor, or revoke the sessions in dashboard settings.'
190
+ : 'Run statemachines doctor again with a network connection.');
191
+ });
192
+ await check('API readiness', async (options) => {
193
+ if (input.offline) {
194
+ return skip('Offline mode.');
195
+ }
196
+ const ready = await context
197
+ .anonymousApi(await origin(options))
198
+ .readiness(options);
199
+ return ready.ready
200
+ ? ok('The API is ready.')
201
+ : {
202
+ status: 'failed',
203
+ message: 'The API is reachable but not ready.',
204
+ fix: 'Try again later.',
205
+ };
206
+ });
207
+ await check('Repository skills', async (options) => {
208
+ const root = await findProjectRoot(io, options);
209
+ if (root === null) {
210
+ return skip('This directory is not in a repository.');
211
+ }
212
+ let found = false;
213
+ for (const agent of agentDestinations) {
214
+ const content = await io.files.read(join(root, agent.path), options);
215
+ if (content === null) {
216
+ continue;
217
+ }
218
+ found = true;
219
+ if (content.trim() !== skill.trim()) {
220
+ return warning('The installed State Machines skill differs from the bundled version.', `Run statemachines init --force --agent ${agent.id}.`);
221
+ }
222
+ }
223
+ return found
224
+ ? ok('Installed State Machines skills match the bundled version.')
225
+ : skip('No State Machines skill is installed in this repository.');
226
+ });
227
+ await check('CLI version', async (options) => {
228
+ if (input.offline) {
229
+ return skip('Offline mode.');
230
+ }
231
+ const latest = await checkLatest({ io, version: context.version, options });
232
+ return latest.latest === null
233
+ ? skip('The latest-version check is cached without a result.')
234
+ : latest.newer
235
+ ? warning(`Version ${latest.latest} is available.`, latest.command)
236
+ : ok(`Version ${context.version} is current.`);
237
+ });
238
+ return { checks };
239
+ }
@@ -0,0 +1,10 @@
1
+ import { type ApiOrigin } from './context-types.js';
2
+ import type { Io } from './io-types.js';
3
+ import type { EnvironmentFiles } from './program-types.js';
4
+ type Credential = {
5
+ readonly key: string;
6
+ readonly origin: ApiOrigin;
7
+ };
8
+ export declare function mergeEnvironment(content: string, credential: Credential): string;
9
+ export declare function createEnvironmentFiles(io: Io): EnvironmentFiles;
10
+ export {};
@@ -0,0 +1,254 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { productionOrigin } from './context-types.js';
4
+ import { CliError } from './errors.js';
5
+ const managedNames = new Set([
6
+ 'STATEMACHINES_API_KEY',
7
+ 'STATEMACHINES_BASE_URL',
8
+ ]);
9
+ function lineEnd(content, from) {
10
+ const newline = content.indexOf('\n', from);
11
+ return newline === -1 ? content.length : newline + 1;
12
+ }
13
+ function skip(content, from, characters) {
14
+ let index = from;
15
+ while (index < content.length && characters.includes(content[index] ?? '')) {
16
+ index += 1;
17
+ }
18
+ return index;
19
+ }
20
+ function span(content, offset) {
21
+ let start = skip(content, offset, ' \t\r');
22
+ for (;;) {
23
+ const line = lineEnd(content, start);
24
+ const equals = content.indexOf('=', start) - start;
25
+ if (content[start] === '#' || equals < 0 || start + equals >= line) {
26
+ return { start, end: line, name: null };
27
+ }
28
+ const name = content
29
+ .slice(start, start + equals)
30
+ .trim()
31
+ .replace(/^export /, '')
32
+ .trim();
33
+ if (name === '') {
34
+ start += equals + 1;
35
+ continue;
36
+ }
37
+ let value = skip(content, start + equals + 1, '\r');
38
+ if (content[value] === '\n') {
39
+ return { start, end: value + 1, name };
40
+ }
41
+ value = skip(content, value, ' \t\r\n');
42
+ const quote = content[value];
43
+ const closing = quote === '"' || quote === "'" || quote === '`'
44
+ ? content.indexOf(quote, value + 1)
45
+ : -1;
46
+ return {
47
+ start,
48
+ end: lineEnd(content, closing === -1 ? value : closing + 1),
49
+ name,
50
+ };
51
+ }
52
+ }
53
+ function managedValue(raw) {
54
+ const match = /^[^=]*=[ \t]*(?:'([^']*)'|"([^"]*)"|`([^`]*)`|([^\s'"`#][^#\r\n]*?|))[ \t]*(?:#[^\r\n]*)?\r?\n?$/.exec(raw);
55
+ const unquoted = match?.[4];
56
+ if (match === null || unquoted?.endsWith('\\')) {
57
+ throw new CliError('cli.env_invalid');
58
+ }
59
+ return (match[2]?.replace(/\\n/g, '\n').replace(/\\r/g, '\r') ??
60
+ match[1] ??
61
+ match[3] ??
62
+ unquoted ??
63
+ '');
64
+ }
65
+ function assignments(content) {
66
+ if (/\r(?!\n)[ \t]*(?:export[ \t]+)?STATEMACHINES_(?:API_KEY|BASE_URL)[ \t]*=/.test(content)) {
67
+ throw new CliError('cli.env_invalid');
68
+ }
69
+ const result = [];
70
+ let offset = 0;
71
+ while (offset < content.length) {
72
+ const { start, end, name } = span(content, offset);
73
+ result.push({
74
+ raw: content.slice(offset, end),
75
+ name,
76
+ value: name !== null && managedNames.has(name)
77
+ ? managedValue(content.slice(start, end))
78
+ : null,
79
+ });
80
+ offset = end;
81
+ }
82
+ return result;
83
+ }
84
+ function managed(parts) {
85
+ return parts.filter((part) => part.name !== null && managedNames.has(part.name));
86
+ }
87
+ function digest(parts) {
88
+ return createHash('sha256')
89
+ .update(JSON.stringify(managed(parts).map((part) => part.raw)))
90
+ .digest('hex');
91
+ }
92
+ function values(credential) {
93
+ const entries = new Map([['STATEMACHINES_API_KEY', credential.key]]);
94
+ if (credential.origin !== productionOrigin) {
95
+ entries.set('STATEMACHINES_BASE_URL', credential.origin);
96
+ }
97
+ for (const value of entries.values()) {
98
+ if (value.length === 0 || /[\p{Cc}\s#'"`\\]/u.test(value)) {
99
+ throw new CliError('cli.env_invalid');
100
+ }
101
+ }
102
+ return entries;
103
+ }
104
+ function matches(parts, credential) {
105
+ const expected = values(credential);
106
+ const actual = managed(parts);
107
+ return (actual.length === expected.size &&
108
+ actual.every((part) => {
109
+ if (part.name === null ||
110
+ !expected.has(part.name) ||
111
+ expected.get(part.name) !== part.value) {
112
+ return false;
113
+ }
114
+ expected.delete(part.name);
115
+ return true;
116
+ }));
117
+ }
118
+ export function mergeEnvironment(content, credential) {
119
+ const parts = assignments(content);
120
+ const entries = values(credential);
121
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
122
+ const kept = parts
123
+ .filter((part) => part.name === null || !managedNames.has(part.name))
124
+ .map((part) => part.raw)
125
+ .join('');
126
+ const bom = kept.startsWith('\ufeff') ? '\ufeff' : '';
127
+ return (bom +
128
+ [...entries].map(([name, value]) => `${name}=${value}${newline}`).join('') +
129
+ kept.slice(bom.length));
130
+ }
131
+ export function createEnvironmentFiles(io) {
132
+ const paths = io.host.platform === 'win32' ? path.win32 : path.posix;
133
+ const relative = (root, destination) => paths.relative(root, destination).split(paths.sep).join('/');
134
+ const status = (root, destination, options) => io.git.fileStatus({ repositoryRoot: root, path: relative(root, destination) }, options);
135
+ const readText = async (file, options) => {
136
+ try {
137
+ return (await io.files.read(file, options)) ?? '';
138
+ }
139
+ catch (error) {
140
+ if (error instanceof CliError && error.code === 'cli.file_not_utf8') {
141
+ throw new CliError('cli.env_invalid', { message: error.message });
142
+ }
143
+ throw error;
144
+ }
145
+ };
146
+ const validate = async (root, destination, options) => {
147
+ const within = paths.relative(root, destination);
148
+ const top = within.split(paths.sep)[0]?.toLowerCase();
149
+ if (!paths.isAbsolute(root) ||
150
+ !paths.isAbsolute(destination) ||
151
+ /[\r\n\0]/.test(destination) ||
152
+ within === '' ||
153
+ top === '.git' ||
154
+ top === '.statemachines' ||
155
+ within === '..' ||
156
+ within.startsWith(`..${paths.sep}`) ||
157
+ paths.isAbsolute(within)) {
158
+ throw new CliError('cli.env_path_refused');
159
+ }
160
+ let current = destination;
161
+ let leaf = true;
162
+ for (;;) {
163
+ const entry = await io.files.inspect(current, options);
164
+ if (entry.kind !== 'missing' &&
165
+ entry.kind !== (leaf ? 'file' : 'directory')) {
166
+ throw new CliError('cli.env_path_refused');
167
+ }
168
+ const parent = paths.dirname(current);
169
+ if (parent === current) {
170
+ break;
171
+ }
172
+ current = parent;
173
+ leaf = false;
174
+ }
175
+ if ((await status(root, destination, options)).tracked) {
176
+ throw new CliError('cli.env_path_refused');
177
+ }
178
+ };
179
+ const ignore = async (root, destination, options) => {
180
+ if ((await status(root, destination, options)).ignored) {
181
+ return;
182
+ }
183
+ for (const directory of new Set([root, paths.dirname(destination)])) {
184
+ const ignoreFile = paths.join(directory, '.gitignore');
185
+ const current = await readText(ignoreFile, options);
186
+ const newline = current.includes('\r\n') ? '\r\n' : '\n';
187
+ const rule = `/${relative(directory, destination).replace(/[\\*?[\]#! ]/g, '\\$&')}`;
188
+ const prefix = current.length > 0 && !current.endsWith('\n')
189
+ ? `${current}${newline}`
190
+ : current;
191
+ await io.files.atomicWrite(ignoreFile, `${prefix}${rule}${newline}`, 'public', options);
192
+ if ((await status(root, destination, options)).ignored) {
193
+ return;
194
+ }
195
+ }
196
+ throw new CliError('cli.git');
197
+ };
198
+ const locked = (root, options, action) => io.files.withLock(paths.join(root, '.statemachines', '.lock'), options, action);
199
+ const read = async (target, options) => {
200
+ await validate(target.repositoryRoot, target.path, options);
201
+ if (!(await status(target.repositoryRoot, target.path, options)).ignored) {
202
+ throw new CliError('cli.env_path_refused');
203
+ }
204
+ return readText(target.path, options);
205
+ };
206
+ return {
207
+ async prepare(file, options) {
208
+ if (/[\r\n\0]/.test(file)) {
209
+ throw new CliError('cli.env_path_refused');
210
+ }
211
+ const repositoryRoot = await io.files.findRepositoryRoot(io.host.cwd, options);
212
+ if (repositoryRoot === null) {
213
+ throw new CliError('cli.env_path_refused');
214
+ }
215
+ const destination = paths.resolve(io.host.cwd, file);
216
+ await validate(repositoryRoot, destination, options);
217
+ return locked(repositoryRoot, options, async () => {
218
+ await validate(repositoryRoot, destination, options);
219
+ const content = await readText(destination, options);
220
+ const expectedManagedDigest = digest(assignments(content));
221
+ await ignore(repositoryRoot, paths.join(repositoryRoot, '.statemachines'), options);
222
+ await ignore(repositoryRoot, destination, options);
223
+ return { repositoryRoot, path: destination, expectedManagedDigest };
224
+ });
225
+ },
226
+ async write(target, credential, options, commit) {
227
+ values(credential);
228
+ await validate(target.repositoryRoot, target.path, options);
229
+ await locked(target.repositoryRoot, options, async () => {
230
+ const content = await read(target, options);
231
+ const parts = assignments(content);
232
+ if (matches(parts, credential)) {
233
+ await commit();
234
+ return;
235
+ }
236
+ if (digest(parts) !== target.expectedManagedDigest) {
237
+ throw new CliError('cli.env_changed');
238
+ }
239
+ await io.files.atomicWrite(target.path, mergeEnvironment(content, credential), 'private-file', options);
240
+ await commit();
241
+ });
242
+ },
243
+ async commitIfMatches(target, credential, options, commit) {
244
+ await validate(target.repositoryRoot, target.path, options);
245
+ return locked(target.repositoryRoot, options, async () => {
246
+ const matched = matches(assignments(await read(target, options)), credential);
247
+ if (matched) {
248
+ await commit();
249
+ }
250
+ return matched;
251
+ });
252
+ },
253
+ };
254
+ }