@sequenceholdings/studio-cli 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,14 +150,19 @@ without it.
150
150
 
151
151
  | Command | What it does |
152
152
  |---------|--------------|
153
- | `seq-studio init <dir> --with <kinds>` | Scaffold an app monorepo + `sequence.app.yml` + selected primitive subfolders |
153
+ | `seq-studio init <dir> --with <kinds>` | Scaffold an app monorepo + `sequence.app.yml` + a Sequence app `README.md` + selected primitive subfolders |
154
154
  | `seq-studio add function&#124;artifact <name>` | Add a function or Artifact Studio UI to the current app (updates the manifest) |
155
155
  | `seq-studio deploy -e <env> [--yes] [--only id1,id2] [--dry-run]` | Deploy every primitive in `deploy.order` (orm → functions → artifact) |
156
156
 
157
157
  `init` supports v0 kinds `orm`, `function`, and `artifact`; pass them as a
158
158
  comma-list (`--with orm,function,artifact`) or boolean flags (`--orm`). `add`
159
159
  currently supports functions and one Artifact Studio UI; ORM is created only by
160
- `init`.
160
+ `init`. Creating a platform git-service repo (`seq-studio repos create` or the
161
+ Repositories UI) writes the same class of README into the initial commit, so a
162
+ fresh clone already has the primitives / [docs.seqholdings.com](https://docs.seqholdings.com/)
163
+ guide. `seq-studio init .` is allowed in a clone whose README contains the
164
+ seq-studio seed marker; it merges Sequence ignores into an existing
165
+ `.gitignore` and will not overwrite an ordinary docs-only README.
161
166
 
162
167
  ```bash
163
168
  # Full-stack toy (ORM + function + artifact) — workspace / internal CLI only
@@ -609,7 +614,7 @@ JSON API — the same repos `--repo <ns>/<name>` sources build from.
609
614
  | `seq-studio repos list -e <env> [--namespace <slug>] [--mine]` | repos visible on the environment (permission-filtered) |
610
615
  | `seq-studio repos namespaces [create <slug>] -e <env>` | list namespaces, or create one (creator becomes owner) |
611
616
  | `seq-studio repos show <ns>/<name> -e <env>` | detail: id, branches, clone URL; artifact project id when slug matches |
612
- | `seq-studio repos create <ns>/<name> -e <env> [--default-branch <b>]` | create an empty repo (needs namespace write) |
617
+ | `seq-studio repos create <ns>/<name> -e <env> [--default-branch <b>]` | create a repo seeded with a Sequence app README (needs namespace write) |
613
618
  | `seq-studio repos clone <ns>/<name> \| --url <clone-url> \| --id <uuid> -e <env> [--ref <r>] [--out <dir>] [--force]` | smart-HTTP `git clone` when `ATLAS_GIT_PAT` is set (`--url`/`--id` need no seqapi); otherwise JSON materialize + PAT hint |
614
619
  | `seq-studio repos pull <ns>/<name> -e <env> [--ref <r>] [--out <dir>] [--force]` | always materialize via JSON API (no `.git` dir); refuses a non-empty destination unless `--force` |
615
620
  | `seq-studio repos delete <ns>/<name> -e <env> [--yes]` | delete a repo — interactive confirm unless `--yes` |
@@ -15,8 +15,11 @@ export const APP_INIT_USAGE = `usage:
15
15
  seq-studio init <dir> --with <kinds> [options]
16
16
  seq-studio init <dir> --orm --function --artifact [options]
17
17
 
18
- Scaffold an app monorepo with ${APP_MANIFEST_FILENAME} and one subfolder per
19
- selected primitive. At least one kind is required.
18
+ Scaffold an app monorepo with ${APP_MANIFEST_FILENAME}, a Sequence app
19
+ README.md (docs.seqholdings.com + primitives), and one subfolder per
20
+ selected primitive. At least one kind is required. A git-service clone
21
+ whose README.md contains the seq-studio seed marker is a valid target;
22
+ ordinary docs-only repositories are refused so we do not overwrite them.
20
23
 
21
24
  Kinds: ${PRIMITIVE_KINDS.join(', ')}
22
25
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * README seeded by `seq-studio init` (and kept equivalent to the git-service
3
+ * createRepo initial commit). Third-party developers should be able to clone
4
+ * a new app repo and start building on Artifact Studio, the ORM, and
5
+ * Managed Functions without guessing the platform model.
6
+ *
7
+ * Keep in sync with atlas/src/server/services/git-service/initial-readme.ts.
8
+ */
9
+ import type { PrimitiveKind } from './manifest.js';
10
+ export declare const SEQUENCE_DOCS_URL = "https://docs.seqholdings.com/";
11
+ /**
12
+ * Hidden marker written into the git-service createRepo README.
13
+ * `seq-studio init .` only overwrites a non-empty target when this is present,
14
+ * so an ordinary docs-only repository is not treated as a seed clone.
15
+ *
16
+ * Keep identical to atlas/src/server/services/git-service/initial-readme.ts.
17
+ */
18
+ export declare const GIT_SERVICE_SEED_MARKER = "<!-- seq-studio:git-service-seed -->";
19
+ export declare function isGitServiceSeedReadme(body: string): boolean;
20
+ export declare const README_REQUIRED_SECTIONS: readonly ["https://docs.seqholdings.com/", "Artifact Studio", "ORM", "Managed Function", "seq-studio init", "seq.functions.invoke"];
21
+ export interface AppReadmeInput {
22
+ appId: string;
23
+ title?: string;
24
+ description?: string;
25
+ primitives: readonly PrimitiveKind[];
26
+ functionName?: string;
27
+ }
28
+ export declare function renderAppReadme(input: AppReadmeInput): string;
29
+ /** README for a brand-new git-service repo that does not yet have an app scaffold. */
30
+ export declare function renderEmptyRepoReadme({ repoName, namespaceSlug, }: {
31
+ repoName: string;
32
+ namespaceSlug?: string;
33
+ }): string;
@@ -0,0 +1,294 @@
1
+ /**
2
+ * README seeded by `seq-studio init` (and kept equivalent to the git-service
3
+ * createRepo initial commit). Third-party developers should be able to clone
4
+ * a new app repo and start building on Artifact Studio, the ORM, and
5
+ * Managed Functions without guessing the platform model.
6
+ *
7
+ * Keep in sync with atlas/src/server/services/git-service/initial-readme.ts.
8
+ */
9
+ import { titleizeSlug } from './manifest.js';
10
+ export const SEQUENCE_DOCS_URL = 'https://docs.seqholdings.com/';
11
+ /**
12
+ * Hidden marker written into the git-service createRepo README.
13
+ * `seq-studio init .` only overwrites a non-empty target when this is present,
14
+ * so an ordinary docs-only repository is not treated as a seed clone.
15
+ *
16
+ * Keep identical to atlas/src/server/services/git-service/initial-readme.ts.
17
+ */
18
+ export const GIT_SERVICE_SEED_MARKER = '<!-- seq-studio:git-service-seed -->';
19
+ export function isGitServiceSeedReadme(body) {
20
+ return body.includes(GIT_SERVICE_SEED_MARKER);
21
+ }
22
+ export const README_REQUIRED_SECTIONS = [
23
+ 'https://docs.seqholdings.com/',
24
+ 'Artifact Studio',
25
+ 'ORM',
26
+ 'Managed Function',
27
+ 'seq-studio init',
28
+ 'seq.functions.invoke',
29
+ ];
30
+ export function renderAppReadme(input) {
31
+ const title = input.title?.trim() || titleizeSlug(input.appId);
32
+ const description = input.description?.trim() ||
33
+ 'A Sequence Platform app. Keep source in Git and deploy with seq-studio.';
34
+ const kinds = new Set(input.primitives);
35
+ const functionName = input.functionName ?? 'hello';
36
+ const tree = renderAppTree({
37
+ appId: input.appId,
38
+ kinds,
39
+ functionName,
40
+ });
41
+ const primitiveRows = [
42
+ kinds.has('artifact')
43
+ ? '| **Artifact Studio** | Frontend | `artifact/` — React UI + `artifact.bundle.yml` |'
44
+ : null,
45
+ kinds.has('orm')
46
+ ? `| **ORM** | Data | \`orm/${snake(input.appId)}/\` — tables, policies, named GraphQL operations |`
47
+ : null,
48
+ kinds.has('function')
49
+ ? `| **Managed Function** | Backend | \`functions/${functionName}/\` — TypeScript handler + \`managed-function.yml\` |`
50
+ : null,
51
+ ]
52
+ .filter((row) => row !== null)
53
+ .join('\n');
54
+ return `# ${title}
55
+
56
+ ${description}
57
+
58
+ This folder is a **seq-studio app**. You keep source in Git (this repository, or a platform git-service clone). You validate, plan, and deploy with \`seq-studio\`. You do not need a checkout of Sequence Platform itself.
59
+
60
+ **Platform documentation:** [${SEQUENCE_DOCS_URL}](${SEQUENCE_DOCS_URL})
61
+
62
+ You still need an environment name, an Atlas URL, and resource grants from your platform administrator before Preview or deploy. Login proves identity; it does not grant every action.
63
+
64
+ ## What this repository is
65
+
66
+ A Sequence app is a folder with a \`sequence.app.yml\` manifest plus one subdirectory per platform primitive. The CLI owns that manifest (\`seq-studio init\` / \`seq-studio add\`). Do not invent a new layout by hand.
67
+
68
+ \`\`\`
69
+ ${tree}
70
+ \`\`\`
71
+
72
+ This app currently includes:
73
+
74
+ | Primitive | Role | Where it lives |
75
+ | --- | --- | --- |
76
+ ${primitiveRows}
77
+
78
+ Add another function later with \`seq-studio add function <name>\` from this directory. \`seq-studio add\` supports \`function\` and \`artifact\` only — an ORM namespace is created at init.
79
+
80
+ ## Platform primitives
81
+
82
+ Build each app from these three primitives. Use only the ones the product needs.
83
+
84
+ | Primitive | Role | What you write | What the platform runs |
85
+ | --- | --- | --- | --- |
86
+ | **Artifact Studio** | Frontend | React UI + \`artifact.bundle.yml\` | Sandboxed browser app inside Atlas |
87
+ | **ORM** | Data | TypeScript tables, row policies, named GraphQL operations | Governed Data API in front of Postgres (you never open a connection) |
88
+ | **Managed Function** | Backend | TypeScript handler + \`managed-function.yml\` | Server-side code with declared secrets, egress, and invoke permissions |
89
+
90
+ \`\`\`
91
+ Browser user
92
+
93
+
94
+ Artifact Studio UI
95
+ │ seq.api.* / seq.functions.invoke (declared capabilities only)
96
+ ├──► ORM Data API persisted rows, policy-scoped to the caller
97
+ └──► Managed Function secrets, egress, work that must not run in the browser
98
+ \`\`\`
99
+
100
+ **Artifact Studio** is the screen. In Atlas the UI runs in a sandboxed iframe and talks to the platform through \`seq\` from \`@sequenceholdings/artifact-studio\`. It has no raw network access and must not contain secrets. Every table, operation, API path, or function the UI uses must be listed in \`artifact.bundle.yml\` first. Prefer \`@sequenceholdings/atlas-ui\`. Docs: [Artifact Studio](${SEQUENCE_DOCS_URL}artifact-studio/overview).
101
+
102
+ **ORM** is the data layer. You author tables and policies in TypeScript (\`defineNamespace\`, \`defineTable\`) and persist reads/writes as named GraphQL documents. \`seq-studio orm apply\` compiles that into a reviewed migration and publishes persisted operations. The UI calls \`{ op: <hash>, variables }\` on the Atlas Data API. Policies are the row boundary — stamp identity from \`claim('sub')\` rather than trusting the browser. There is no ad-hoc SQL; exotic writes go through a declared table action. Public docs do not yet have an ORM guide; \`seq-studio orm --help\` and a reference app (for example Pokedex: Artifact UI + trainer-scoped \`pokemon\` rows) are the working model.
103
+
104
+ **Managed Function** is the backend. Use it for an external API, a write-only secret, or any work that must not run in the browser. The handler receives JSON and returns JSON. The manifest declares secret *names* (never values) and an egress allowlist — functions do not get arbitrary outbound network. Docs: [Managed Functions](${SEQUENCE_DOCS_URL}managed-functions/overview), [Managed Secrets](${SEQUENCE_DOCS_URL}managed-secrets/overview).
105
+
106
+ How the examples split:
107
+
108
+ | Example | Primitives | Use it for |
109
+ | --- | --- | --- |
110
+ | Pokedex-style app | Artifact + ORM | UI that reads and writes governed tables |
111
+ | [Basketball player lookup](${SEQUENCE_DOCS_URL}examples/basketball-player-lookup) | Artifact + Function + Secret | UI that calls protected server-side code |
112
+ | \`seq-studio init --with orm,function,artifact\` | All three | The intended app shape, once the ORM package is available (see below) |
113
+
114
+ A function calling the ORM under a service account is **not** a supported public path yet. Keep user-scoped table access in the artifact; keep secrets and egress in the function.
115
+
116
+ The public [CLI command reference](${SEQUENCE_DOCS_URL}seq-studio/commands) covers standalone \`artifact\` / \`functions\` / \`secrets\` commands. App-monorepo verbs (\`init\`, \`add\`, \`deploy\`) and \`orm\` are on \`seq-studio --help\`. Prefer \`--help\` when the two disagree.
117
+
118
+ ## Before you write code
119
+
120
+ Ask your administrator for:
121
+
122
+ 1. **Environment name** — the exact \`-e\` / \`--env\` value. Do not invent it from this repository name.
123
+ 2. **Atlas URL** — the origin used with \`seq-studio envs add\`. It is not docs.seqholdings.com. The CLI discovers login config at \`{url}/api/auth/cli-config\`.
124
+ 3. **What you should create** — new Artifact Studio project, new function, new ORM namespace, or updates to existing ones.
125
+ 4. **Grants** — create/deploy an artifact; runtime access to open the deployed UI; create/deploy/invoke a function; create secrets if you need them; apply an ORM namespace if you will persist data; permission to **share** the artifact (scaffolds create \`visibility: private\`, and a later deploy will not change that).
126
+ 5. **\`@sequenceholdings/orm\` v2** — \`seq-studio init --with orm\` and namespace \`package.json\` files depend on \`@sequenceholdings/orm@^2.0.0\`, which is **not published**. Public npm's latest is a v1 package. Without a v2 tarball you can still ship Artifact + Function apps.
127
+
128
+ \`local\`, \`staging\`, \`production\`, and \`banksouth\` are **reserved built-in CLI names**. \`seq-studio envs add banksouth …\` fails. Use the OpCo name you were given.
129
+
130
+ See [Before you build](${SEQUENCE_DOCS_URL}getting-started/understand-your-workspace).
131
+
132
+ ## Install and sign in
133
+
134
+ | Tool | Requirement |
135
+ | --- | --- |
136
+ | Node.js | 20 or later (22 is the documented baseline) |
137
+ | pnpm | 10.28.2 |
138
+ | Git | Required for official Artifact Studio deploys |
139
+ | \`seq-studio\` | \`@sequenceholdings/studio-cli\` (match \`studio.min_cli\` in \`sequence.app.yml\`) |
140
+
141
+ \`\`\`bash
142
+ corepack enable
143
+ corepack prepare pnpm@10.28.2 --activate
144
+ pnpm setup # once; restart the terminal afterwards
145
+ pnpm add -g @sequenceholdings/studio-cli
146
+ seq-studio version
147
+
148
+ seq-studio envs add <environment-name> <atlas-url>
149
+ seq-studio login --env <environment-name>
150
+ seq-studio artifact list -e <environment-name>
151
+ seq-studio functions list -e <environment-name>
152
+ seq-studio doctor -e <environment-name>
153
+ \`\`\`
154
+
155
+ Details: [Install seq-studio](${SEQUENCE_DOCS_URL}getting-started/prerequisites), [Authenticate](${SEQUENCE_DOCS_URL}getting-started/authentication).
156
+
157
+ \`local\` (\`http://localhost:5001\`) is a real Atlas process, not Vite. After init the CLI may print \`seq-studio deploy -e local --yes\` — ignore that unless you actually run Atlas locally.
158
+
159
+ ## Implement this app
160
+
161
+ ${renderImplementSection({ kinds, functionName, appId: input.appId })}
162
+
163
+ ## Everyday loop
164
+
165
+ | You want to… | Command |
166
+ | --- | --- |
167
+ | Validate / build an artifact | \`seq-studio artifact validate artifact\` / \`artifact build artifact\` |
168
+ | Preview uncommitted UI | \`seq-studio artifact dev artifact -e <env>\` |
169
+ | Official UI deploy | \`seq-studio artifact deploy artifact -e <env>\` from clean \`main\` |
170
+ | Build / deploy a function | \`seq-studio functions build --dir functions/<name>\` then \`functions deploy --dir … -e <env>\` |
171
+ | Function logs | \`seq-studio functions logs --fn <slug> -e <env>\` |
172
+ | Apply an ORM namespace | \`seq-studio orm apply orm/<namespace> -e <env>\` |
173
+ | Deploy every primitive | \`seq-studio deploy -e <env>\` from this directory |
174
+
175
+ Official Artifact Studio deploys require a **clean \`main\`** with a committed revision. Uncommitted work is Preview (\`artifact dev\`). Open the URL the CLI prints.
176
+
177
+ ## Rules that waste a day if you skip them
178
+
179
+ - **Manifests are the authorization surface.** A new \`seq.api\` call, ORM mutation, secret, or outbound host in source is not enough. Update \`artifact.bundle.yml\` or \`managed-function.yml\` and validate again.
180
+ - **Never commit secret values.** \`.env\` is gitignored. Never put secrets in the artifact — that bundle is browser code.
181
+ - **Delete the Chainguard \`.npmrc\`** in a new function unless you have Sequence-internal registry credentials. Without them \`pnpm install\` 401s. Removing the file is the supported public path.
182
+ - **Invoke functions with \`seq.functions.invoke('<slug>', body)\`**, not a raw \`seq.api.post\` to \`/api/managed-functions/…\`. Declare the slug under \`capabilities.functions.invoke\`.
183
+ - **Atlas wraps many \`seq.api\` responses** as \`{ success, data }\`. Use the scaffold \`unwrap\` helper. \`seq.functions.invoke\` is already the payload.
184
+ - **Do not reuse another app's identities.** If you copy files, change \`app.id\`, every primitive \`id\`, the ORM \`namespace\`, the artifact \`project_id\`, capability paths, and \`targets\`.
185
+ - **Do not edit ORM \`migrations/\`.** Change \`schema/*.ts\` / \`graphql/**/*.ts\`, then generate/apply.
186
+ - **Do not hardcode user emails** in policies or UI. Use \`claim('sub')\` / \`claim('org')\` and the Permissions / sharing UI.
187
+
188
+ ## Further reading
189
+
190
+ - [Sequence Platform docs](${SEQUENCE_DOCS_URL})
191
+ - [Create your first project](${SEQUENCE_DOCS_URL}projects/create-first-project)
192
+ - [Artifact Studio](${SEQUENCE_DOCS_URL}artifact-studio/overview)
193
+ - [Managed Functions](${SEQUENCE_DOCS_URL}managed-functions/overview)
194
+ - [Managed Secrets](${SEQUENCE_DOCS_URL}managed-secrets/overview)
195
+ - [Basketball player lookup](${SEQUENCE_DOCS_URL}examples/basketball-player-lookup)
196
+ - [CLI command reference](${SEQUENCE_DOCS_URL}seq-studio/commands)
197
+ - [Troubleshooting](${SEQUENCE_DOCS_URL}troubleshooting)
198
+ `;
199
+ }
200
+ /** README for a brand-new git-service repo that does not yet have an app scaffold. */
201
+ export function renderEmptyRepoReadme({ repoName, namespaceSlug, }) {
202
+ const title = titleizeSlug(repoName);
203
+ const qualified = namespaceSlug ? `${namespaceSlug}/${repoName}` : repoName;
204
+ return `${GIT_SERVICE_SEED_MARKER}\n${renderAppReadme({
205
+ appId: repoName,
206
+ title,
207
+ description: `Sequence Platform git-service repository \`${qualified}\`. Scaffold an app in this clone with \`seq-studio init\`, then validate and deploy with the same CLI.`,
208
+ primitives: ['orm', 'function', 'artifact'],
209
+ functionName: 'hello',
210
+ }).replace('## Implement this app', `## Scaffold the app
211
+
212
+ This repository was created empty except for this README. From the clone root:
213
+
214
+ \`\`\`bash
215
+ # into this directory (keeps the git-service remote)
216
+ seq-studio init . --with function,artifact --function-name hello --description "${title}"
217
+ # or the intended three-primitive shape, once @sequenceholdings/orm v2 is available:
218
+ # seq-studio init . --with orm,function,artifact --function-name hello
219
+ \`\`\`
220
+
221
+ \`init\` into a clone that only has this seed README (and \`.git\`) is allowed — it writes \`sequence.app.yml\` and the primitive folders, and replaces this file with an app-specific copy. \`init\` will not overwrite a README that does not contain the seed marker.
222
+
223
+ ## Implement this app`)}`;
224
+ }
225
+ function snake(appId) {
226
+ return appId.replace(/-/g, '_');
227
+ }
228
+ function renderAppTree({ appId, kinds, functionName, }) {
229
+ const lines = [
230
+ `${appId}/`,
231
+ ' README.md',
232
+ ' sequence.app.yml primitives + deploy order',
233
+ ];
234
+ if (kinds.has('artifact')) {
235
+ lines.push(' artifact/ Artifact Studio UI');
236
+ }
237
+ if (kinds.has('orm')) {
238
+ lines.push(` orm/${snake(appId)}/ ORM namespace`);
239
+ }
240
+ if (kinds.has('function')) {
241
+ lines.push(` functions/${functionName}/ Managed Function`);
242
+ }
243
+ return lines.join('\n');
244
+ }
245
+ function renderImplementSection({ kinds, functionName, appId, }) {
246
+ const steps = [];
247
+ if (kinds.has('function')) {
248
+ steps.push(`1. **Function** — implement \`functions/${functionName}/index.ts\`. Declare every outbound host under \`egress\` in \`managed-function.yml\` (the scaffold ships with none). Declare secret *names* in \`secrets:\` and keep values in an ignored \`functions/${functionName}/.env\`.
249
+
250
+ **Before \`pnpm install\`, delete \`functions/${functionName}/.npmrc\`** unless you have Sequence-internal Chainguard credentials. Without them that install 401s. Removing the file is the supported public path — the deploy worker re-resolves dependencies server-side.`);
251
+ }
252
+ if (kinds.has('orm')) {
253
+ steps.push(`2. **ORM** — author tables and named operations under \`orm/${snake(appId)}/\` (\`sequence.config.ts\`, \`schema/*.ts\`, \`graphql/**/*.ts\`). Then:
254
+
255
+ \`\`\`bash
256
+ seq-studio orm generate orm/${snake(appId)}
257
+ seq-studio orm plan orm/${snake(appId)} -e <environment-name>
258
+ seq-studio orm apply orm/${snake(appId)} -e <environment-name>
259
+ \`\`\`
260
+
261
+ Grant the artifact matching \`capabilities.data\` entries (\`<namespace>/<table>\` for reads, \`<namespace>/ops/<MutationName>\` for named writes) and the Data API paths you call.`);
262
+ }
263
+ if (kinds.has('artifact')) {
264
+ const n = steps.length + 1;
265
+ steps.push(`${n}. **UI** — \`artifact/src/App.tsx\` with \`@sequenceholdings/atlas-ui\`. Delete the scaffold's \`alert()\` Atlas ping; native \`alert\` / \`confirm\` / \`prompt\` are blocked in the sandbox.
266
+
267
+ If this app has a function, declare it and invoke it:
268
+
269
+ \`\`\`yaml
270
+ capabilities:
271
+ functions:
272
+ invoke:
273
+ - ${functionName}
274
+ \`\`\`
275
+
276
+ \`\`\`ts
277
+ import { seq } from './lib/api'
278
+ const result = await seq.functions.invoke('${functionName}', { /* input */ })
279
+ \`\`\`
280
+
281
+ \`pnpm dev\` in \`artifact/\` is standalone Vite. Platform calls only work inside Preview or an official Atlas deployment.`);
282
+ }
283
+ steps.push(`${steps.length + 1}. **Targets** — put your environment in \`artifact.bundle.yml\` \`targets\` (if this app has an artifact). Scaffolds create \`visibility: private\`. Set \`shared\` before first create if other people must open the app.
284
+
285
+ ${steps.length + 2}. **Validate, then deploy** from this directory:
286
+
287
+ \`\`\`bash
288
+ seq-studio deploy -e <environment-name> --dry-run
289
+ seq-studio deploy -e <environment-name> --yes
290
+ \`\`\`
291
+
292
+ \`--dry-run\` lists which primitives would run. Until \`git status\` is clean on \`main\`, use \`seq-studio artifact dev artifact -e <environment-name>\` for the UI.`);
293
+ return steps.join('\n\n');
294
+ }
@@ -9,8 +9,48 @@ import { functionsInitCommand } from '../functions/commands.js';
9
9
  import { parseArgs } from '../process/commands.js';
10
10
  import { currentVersion } from '../update-check.js';
11
11
  import { defaultDeployOrder, extendDeployOrder, loadAppManifest, ormNamespaceFromAppId, titleizeSlug, writeAppManifest, } from './manifest.js';
12
+ import { isGitServiceSeedReadme, renderAppReadme } from './readme.js';
12
13
  const LOG = '[seq-studio]';
13
14
  const APP_MANIFEST_LABEL = 'sequence.app.yml';
15
+ /** Metadata a checkout may have without counting as a docs-only repository. */
16
+ const INIT_METADATA_ONLY = new Set(['.git', '.gitattributes', '.DS_Store']);
17
+ /**
18
+ * Extra names allowed beside a git-service seed README. Presence of any of
19
+ * these without that seed marker is treated as a non-empty user directory.
20
+ */
21
+ const INIT_ALLOWED_EXISTING = new Set([
22
+ ...INIT_METADATA_ONLY,
23
+ '.gitignore',
24
+ 'README.md',
25
+ 'LICENSE',
26
+ 'LICENSE.md',
27
+ ]);
28
+ const DEFAULT_GITIGNORE_LINES = [
29
+ 'node_modules/',
30
+ 'dist/',
31
+ '.env',
32
+ '.env.*',
33
+ '!.env.example',
34
+ '.DS_Store',
35
+ '*.log',
36
+ ];
37
+ /** Append missing Sequence ignores; keep existing lines and comments. */
38
+ function mergeGitignore(existing) {
39
+ const defaults = DEFAULT_GITIGNORE_LINES;
40
+ if (existing === null) {
41
+ return `${defaults.join('\n')}\n`;
42
+ }
43
+ const present = new Set(existing
44
+ .split(/\r?\n/)
45
+ .map((line) => line.trim())
46
+ .filter((line) => line.length > 0 && !line.startsWith('#')));
47
+ const missing = defaults.filter((line) => !present.has(line));
48
+ if (missing.length === 0) {
49
+ return existing.endsWith('\n') ? existing : `${existing}\n`;
50
+ }
51
+ const base = existing.endsWith('\n') ? existing : `${existing}\n`;
52
+ return `${base}${missing.join('\n')}\n`;
53
+ }
14
54
  export function defaultScaffoldNames(_appId) {
15
55
  return { functionName: 'hello' };
16
56
  }
@@ -130,17 +170,36 @@ export async function initAppMonorepo({ rootDir, appId, kinds, names = defaultSc
130
170
  if (existsSync(join(appRoot, 'sequence.app.yml'))) {
131
171
  throw new Error(`${join(appRoot, 'sequence.app.yml')} already exists — refusing to re-init`);
132
172
  }
133
- if (existsSync(appRoot)) {
134
- const { readdir } = await import('node:fs/promises');
135
- const entries = await readdir(appRoot).catch((error) => {
173
+ const { readdir } = await import('node:fs/promises');
174
+ const existed = existsSync(appRoot);
175
+ const entries = existed
176
+ ? await readdir(appRoot).catch((error) => {
136
177
  if (error.code === 'ENOENT')
137
178
  return [];
138
179
  throw error;
139
- });
140
- if (entries.length > 0) {
141
- throw new Error(`${appRoot} is not empty — init into a new directory, or remove existing files first`);
180
+ })
181
+ : [];
182
+ const unexpected = entries.filter((name) => !INIT_ALLOWED_EXISTING.has(name));
183
+ if (unexpected.length > 0) {
184
+ throw new Error(`${appRoot} is not empty — init into a new directory, a git-service clone whose README contains the seq-studio seed marker, or remove existing files first`);
185
+ }
186
+ const contentful = entries.filter((name) => !INIT_METADATA_ONLY.has(name));
187
+ if (contentful.length > 0) {
188
+ const readmePath = join(appRoot, 'README.md');
189
+ if (!existsSync(readmePath)) {
190
+ throw new Error(`${appRoot} is not empty — init into a git-service clone only when README.md contains the seq-studio git-service seed marker`);
191
+ }
192
+ const existingReadme = await readFile(readmePath, 'utf8');
193
+ if (!isGitServiceSeedReadme(existingReadme)) {
194
+ throw new Error(`${appRoot} already has a README.md that is not a git-service seed — refusing to overwrite it. Init into a new directory, or keep the platform-generated README from \`seq-studio repos create\`.`);
142
195
  }
143
196
  }
197
+ const previousReadme = existsSync(join(appRoot, 'README.md'))
198
+ ? await readFile(join(appRoot, 'README.md'), 'utf8')
199
+ : null;
200
+ const previousGitignore = existsSync(join(appRoot, '.gitignore'))
201
+ ? await readFile(join(appRoot, '.gitignore'), 'utf8')
202
+ : null;
144
203
  await mkdir(appRoot, { recursive: true });
145
204
  try {
146
205
  const version = currentVersion();
@@ -168,15 +227,59 @@ export async function initAppMonorepo({ rootDir, appId, kinds, names = defaultSc
168
227
  },
169
228
  };
170
229
  await writeAppManifest({ rootDir: appRoot, manifest });
171
- await writeFile(join(appRoot, '.gitignore'), ['node_modules/', 'dist/', '.env', '.env.*', '!.env.example', '.DS_Store', '*.log'].join('\n') + '\n', 'utf8');
230
+ await writeFile(join(appRoot, '.gitignore'), mergeGitignore(previousGitignore), 'utf8');
231
+ await writeFile(join(appRoot, 'README.md'), renderAppReadme({
232
+ appId,
233
+ title: manifest.app.title,
234
+ description: manifest.app.description,
235
+ primitives: kinds,
236
+ functionName: names.functionName,
237
+ }), 'utf8');
172
238
  return manifest;
173
239
  }
174
240
  catch (error) {
175
- // Directory was empty (or newly created) — remove partial scaffolds so a
176
- // retry can `init` into the same path without a manual cleanup.
177
- await rm(appRoot, { recursive: true, force: true }).catch(() => {
178
- /* best-effort */
179
- });
241
+ if (!existed) {
242
+ // Directory was newly created remove the partial scaffold.
243
+ await rm(appRoot, { recursive: true, force: true }).catch(() => {
244
+ /* best-effort */
245
+ });
246
+ }
247
+ else {
248
+ // Reused a git-service clone — only remove what this invocation added.
249
+ await rm(join(appRoot, 'sequence.app.yml'), { force: true }).catch(() => {
250
+ /* best-effort */
251
+ });
252
+ for (const kind of kinds) {
253
+ const name = kind === 'function' ? names.functionName : appId;
254
+ const relPath = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
255
+ const top = relPath.split('/')[0];
256
+ if (top) {
257
+ await rm(join(appRoot, top), { recursive: true, force: true }).catch(() => {
258
+ /* best-effort */
259
+ });
260
+ }
261
+ }
262
+ if (previousReadme !== null) {
263
+ await writeFile(join(appRoot, 'README.md'), previousReadme, 'utf8').catch(() => {
264
+ /* best-effort */
265
+ });
266
+ }
267
+ else {
268
+ await rm(join(appRoot, 'README.md'), { force: true }).catch(() => {
269
+ /* best-effort */
270
+ });
271
+ }
272
+ if (previousGitignore !== null) {
273
+ await writeFile(join(appRoot, '.gitignore'), previousGitignore, 'utf8').catch(() => {
274
+ /* best-effort */
275
+ });
276
+ }
277
+ else {
278
+ await rm(join(appRoot, '.gitignore'), { force: true }).catch(() => {
279
+ /* best-effort */
280
+ });
281
+ }
282
+ }
180
283
  throw error;
181
284
  }
182
285
  }
@@ -238,7 +341,7 @@ export function printInitNextSteps({ appRoot, manifest, }) {
238
341
  const rel = relative(process.cwd(), appRoot);
239
342
  const display = !rel || rel.startsWith('..') ? appRoot : rel;
240
343
  console.log(`${LOG} scaffolded app "${manifest.app.id}" in ${display}`);
241
- console.log(`${LOG} wrote ${APP_MANIFEST_LABEL} with ${manifest.primitives.length} primitive(s):`);
344
+ console.log(`${LOG} wrote README.md (Sequence app docs) and ${APP_MANIFEST_LABEL} with ${manifest.primitives.length} primitive(s):`);
242
345
  for (const entry of manifest.primitives) {
243
346
  console.log(`${LOG} - ${entry.kind.padEnd(9)} ${entry.id} (${entry.path})`);
244
347
  }
@@ -96,6 +96,6 @@ export declare function reposCiRequireCommand(args: ParsedArgs): Promise<number>
96
96
  export declare function reposCiImportCommand(args: ParsedArgs): Promise<number>;
97
97
  export declare function reposCiCommand(args: ParsedArgs): Promise<number>;
98
98
  export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
99
- export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks\n seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)\n seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
99
+ export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create a repo seeded with a Sequence app README\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks\n seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)\n seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
100
100
  export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
101
101
  export {};
@@ -748,7 +748,7 @@ export const REPOS_USAGE = `usage:
748
748
  seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment
749
749
  seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces
750
750
  seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)
751
- seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo
751
+ seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create a repo seeded with a Sequence app README
752
752
  seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]
753
753
  seq-studio repos clone --url <https://…/repos/<id>/git> -e <env> [--ref r] [--out dir]
754
754
  seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequenceholdings/studio-cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Unified Sequence Studio CLI — `seq-studio init` / `add` / `deploy` (app monorepos), `seq-studio agents`, `seq-studio process` (Lattice), `seq-studio artifact`, `seq-studio functions` / `secrets`, `seq-studio repos`, and `seq-studio auth pat`. Includes Auth0 browser login shared with seqapi.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "smol-toml": "^1.4.2",
41
41
  "tsx": "^4.20.3",
42
42
  "zod": "^4.1.13",
43
- "@sequenceholdings/agent-spec": "0.1.1",
43
+ "@sequenceholdings/agent-spec": "0.1.2",
44
44
  "@sequenceholdings/artifact-studio": "0.2.2",
45
45
  "@sequenceholdings/lattice": "0.1.2"
46
46
  },