Skip to content

Commit 936ac60

Browse files
fix(cli): accept positional folder paths
1 parent d233491 commit 936ac60

12 files changed

Lines changed: 145 additions & 78 deletions

File tree

‎packages/sim-cli/README.md‎

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -113,74 +113,77 @@ also accepts its singular form: for example, `sim table list`,
113113
spellings.
114114

115115
```bash
116-
sim workflows ls [--folder </path>] [--search <text>] [--limit <n>]
117-
sim workflows list [--folder </path>] [--deployed-only] [--limit <n>]
116+
sim workflows ls [path] [--search <text>] [--limit <n>]
117+
sim workflows list [--folder <path>] [--deployed-only] [--limit <n>]
118118
sim workflows get <id>
119-
sim workflows mv <id> --folder </path>
119+
sim workflows mv <id> --folder <path>
120120
sim workflows deploy|undeploy|rollback <id>
121121

122122
sim logs list [--level error] [--workflow <id>…] [--trigger <name>…] [--start <date>]
123123
sim logs get <id>
124124
sim logs execution <executionId>
125125

126-
sim tables ls [--folder </path>] [--search <text>] [--limit <n>]
127-
sim tables list [--folder </path>]
126+
sim tables ls [path] [--search <text>] [--limit <n>]
127+
sim tables list [--folder <path>]
128128
sim tables get <tableId>
129-
sim tables mv <tableId> --folder </path>
129+
sim tables mv <tableId> --folder <path>
130130
sim tables columns <tableId>
131131
sim tables rows list <tableId> [--limit <n>]
132132
sim tables rows query <tableId> [--filter <json>] [--sort <json>] [--limit <n>]
133133
sim tables upsert <tableId> --data <json>
134134
sim tables rows batch-delete <tableId> (--row <id>… | --filter <json>) --yes
135135

136-
sim files ls [--folder </path>] [--search <text>] [--limit <n>]
137-
sim files list [--folder </path>]
136+
sim files ls [path] [--search <text>] [--limit <n>]
137+
sim files list [--folder <path>]
138138
sim files get <fileId>
139-
sim files create --name <name> [--folder </path>] [--content <value>] [--encoding utf-8|base64]
140-
sim files upload <path> [--name <name>] [--folder </path>]
139+
sim files create --name <name> [--folder <path>] [--content <value>] [--encoding utf-8|base64]
140+
sim files upload <path> [--name <name>] [--folder <path>]
141141
sim files download <fileId> [-o <path>]
142-
sim files mv --file-ids <id>… [--to </path>]
142+
sim files mv --file-ids <id>… [--to <path>]
143143
sim files batch-delete --file-ids <id>… --yes
144144
sim files delete <fileId>
145145

146-
sim knowledge ls [--folder </path>] [--search <text>] [--limit <n>]
147-
sim knowledge list [--folder </path>]
146+
sim knowledge ls [path] [--search <text>] [--limit <n>]
147+
sim knowledge list [--folder <path>]
148148
sim knowledge get <id>
149-
sim knowledge mv <id> --folder </path>
149+
sim knowledge mv <id> --folder <path>
150150
sim knowledge documents <id> [--search <text>]
151151
sim knowledge documents upload <id> <path> [--tag <value>...]
152152
sim knowledge search --query <text> --kb <id>… [--search-mode vector|hybrid]
153153
```
154154

155-
`ls` is a directory view: it combines the resources at `--folder` with that
156-
folder's direct child folders. Its `ref` column is the resource ID or canonical
157-
folder path to pass to the next command. Use `list` when you want resources only,
158-
or `folders ls` when you want folders only.
155+
`ls` is a directory view: it combines the resources at its optional path with
156+
that folder's direct child folders. It never includes deeper descendants. Its
157+
`ref` column is the resource ID or canonical folder path to pass to the next
158+
command. Use `list` when you want resources only, or `folders ls` when you want
159+
folders only.
159160

160161
Each folder-backed resource has the same path commands:
161162

162163
```bash
163-
sim tables folders ls --parent /Reports
164-
sim tables mkdir /Reports/Quarterly
165-
sim tables folders create /Reports/Quarterly
166-
sim tables folders mv /Reports/Quarterly /Archive/Quarterly
167-
sim tables folders delete /Archive/Quarterly --recursive false --yes
164+
sim tables ls Reports
165+
sim tables folders ls --parent Reports
166+
sim tables mkdir Reports/Quarterly
167+
sim tables folders create Reports/Quarterly
168+
sim tables folders mv Reports/Quarterly Archive/Quarterly
169+
sim tables folders delete Archive/Quarterly --recursive false --yes
168170
```
169171

170172
`mkdir` is the concise form of `folders create`. Replace `tables` with `files`,
171-
`workflows`, or `knowledge`. Paths are canonical, start with `/`, and use `/` for
172-
root. A slash that belongs to a folder name is percent-encoded as `%2F` rather
173-
than treated as a separator.
173+
`workflows`, or `knowledge`. The leading `/` is optional on CLI inputs; the CLI
174+
adds it before calling the API. Omit the `ls` path to list root. A slash that
175+
belongs to a folder name is percent-encoded as `%2F` rather than treated as a
176+
separator.
174177

175178
### List inputs
176179

177180
Primitive lists take space-separated values. Prefix a path with `@` to read
178181
one value per line, or use `@-` to read the list from stdin.
179182

180183
```bash
181-
sim files mv --file-ids file_1 file_2 --to /Archive
182-
sim files mv --file-ids @file-ids.txt --to /Archive
183-
printf 'file_1\nfile_2\n' | sim files mv --file-ids @- --to /Archive
184+
sim files mv --file-ids file_1 file_2 --to Archive
185+
sim files mv --file-ids @file-ids.txt --to Archive
186+
printf 'file_1\nfile_2\n' | sim files mv --file-ids @- --to Archive
184187
```
185188

186189
Arrays of objects remain JSON inputs because they cannot be represented as a

‎packages/sim-cli/src/commands/protocol/files-upload.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('files upload', () => {
9595
const logged: string[] = []
9696
vi.spyOn(console, 'log').mockImplementation((line: string) => logged.push(line))
9797

98-
await program().parseAsync(['node', 'sim', 'file', 'upload', path, '--folder', '/Reports'])
98+
await program().parseAsync(['node', 'sim', 'file', 'upload', path, '--folder', 'Reports'])
9999

100100
expect(fetchMock).toHaveBeenCalledWith(
101101
'https://storage.example/file',

‎packages/sim-cli/src/commands/protocol/files-upload.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
CompleteFileUploadResponse,
55
CreateFileUploadResponse,
66
} from '../../generated/v2-api.js'
7+
import { normalizeFolderPath } from '../../runtime/folder-path.js'
78
import { contentTypeFor, localFile } from '../../transfer/local-file.js'
89
import { finishUploadSession } from '../../transfer/upload-session.js'
910
import { printProtocolResult } from './result.js'
@@ -26,7 +27,9 @@ export function attachFileUpload(files: Command): void {
2627
name,
2728
contentType: contentTypeFor(name),
2829
size,
29-
...(options.folder ? { folderPath: options.folder } : {}),
30+
...(options.folder !== undefined
31+
? { folderPath: normalizeFolderPath(options.folder) }
32+
: {}),
3033
},
3134
})
3235
const { session, uploadToken, transfer } = created.data

‎packages/sim-cli/src/commands/protocol/resource-directory.test.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ function program(): Command {
2525
const root = new Command('sim').exitOverride()
2626
for (const group of buildGeneratedCommands()) root.addCommand(group)
2727
attachProtocolCommands(root)
28+
const override = (command: Command) => {
29+
command.exitOverride()
30+
command.commands.forEach(override)
31+
}
32+
override(root)
2833
return root
2934
}
3035

@@ -77,16 +82,7 @@ describe('resource directory', () => {
7782
const logged: string[] = []
7883
vi.spyOn(console, 'log').mockImplementation((line: string) => logged.push(line))
7984

80-
await program().parseAsync([
81-
'node',
82-
'sim',
83-
'table',
84-
'ls',
85-
'--folder',
86-
'/Reports',
87-
'--search',
88-
'r',
89-
])
85+
await program().parseAsync(['node', 'sim', 'table', 'ls', 'Reports', '--search', 'r'])
9086

9187
expect(mockRequest).toHaveBeenCalledWith('/api/v2/tables/folders', {
9288
query: {
@@ -140,11 +136,18 @@ describe('resource directory', () => {
140136
})
141137
vi.spyOn(console, 'log').mockImplementation(() => {})
142138

143-
await program().parseAsync(['node', 'sim', 'table', 'mkdir', '/Reports/Quarterly'])
139+
await program().parseAsync(['node', 'sim', 'table', 'mkdir', 'Reports/Quarterly'])
144140

145141
expect(mockRequest).toHaveBeenCalledWith('/api/v2/tables/folders', {
146142
method: 'POST',
147143
body: { workspaceId: 'ws_local', path: '/Reports/Quarterly' },
148144
})
149145
})
146+
147+
it('rejects extra directory arguments instead of silently ignoring them', async () => {
148+
await expect(
149+
program().parseAsync(['node', 'sim', 'file', 'ls', 'Reports', 'ignored'])
150+
).rejects.toThrow(/too many arguments/i)
151+
expect(mockRequest).not.toHaveBeenCalled()
152+
})
150153
})

‎packages/sim-cli/src/commands/protocol/resource-directory.ts‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../../generated/v2-api.js'
1515
import { SimApiError, type SimClient, type V2Page } from '../../http/client.js'
1616
import { type Column, printList, text, timestamp } from '../../output/render.js'
17+
import { normalizeFolderPath } from '../../runtime/folder-path.js'
1718
import { DEFAULT_LIMIT } from '../../runtime/options.js'
1819
import { renderResult } from '../../runtime/result.js'
1920

@@ -70,7 +71,6 @@ type ResourceDirectoryConfig =
7071
}
7172

7273
interface ListOptions {
73-
folder: string
7474
search?: string
7575
limit: string
7676
}
@@ -163,41 +163,43 @@ export function attachResourceDirectoryCommands(
163163
config: ResourceDirectoryConfig
164164
): void {
165165
group
166-
.command('ls')
166+
.command('ls [path]')
167+
.allowExcessArguments(false)
167168
.description(`List ${config.kind} resources and child folders together`)
168-
.option('--folder <path>', 'Canonical folder path to list', '/')
169169
.option('--search <text>', 'Filter folders and resources by name')
170170
.addOption(
171171
new Option('--limit <n>', 'Maximum combined items to return (0 for everything)').default(
172172
String(DEFAULT_LIMIT)
173173
)
174174
)
175-
.action(async (options: ListOptions, command: Command) => {
175+
.action(async (path: string | undefined, options: ListOptions, command: Command) => {
176176
const rawLimit = Number(options.limit)
177177
if (!Number.isSafeInteger(rawLimit) || rawLimit < 0) {
178178
throw new SimApiError('--limit must be a non-negative integer', 0)
179179
}
180180

181181
const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit
182+
const folderPath = normalizeFolderPath(path ?? '/')
182183
const { client, profile } = clientFrom(command)
183184
const workspaceId = client.requireWorkspace()
184185
const [folders, resources] = await Promise.all([
185-
listFolders(client, config.folders, workspaceId, options.folder, options.search),
186-
listResources(client, config, workspaceId, options.folder, options.search, limit),
186+
listFolders(client, config.folders, workspaceId, folderPath, options.search),
187+
listResources(client, config, workspaceId, folderPath, options.search, limit),
187188
])
188189
const entries = entriesFor(config, folders, resources)
189190
printList(profile.output, entries.slice(0, limit), COLUMNS)
190191
})
191192

192193
group
193194
.command('mkdir <path>')
195+
.allowExcessArguments(false)
194196
.description(`Create a ${config.kind} directory at a canonical path`)
195197
.action(async (path: string, _options: Record<string, never>, command: Command) => {
196198
const { client, profile } = clientFrom(command)
197199
const operation = V2_OPERATIONS[config.createFolder]
198200
const result = await client.request<{ data?: unknown }>(operation.path, {
199201
method: operation.method,
200-
body: { workspaceId: client.requireWorkspace(), path },
202+
body: { workspaceId: client.requireWorkspace(), path: normalizeFolderPath(path) },
201203
})
202204
renderResult(config.createFolder, profile.output, result.data ?? result, {})
203205
})

‎packages/sim-cli/src/commands/protocol/tables-import.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('tables import output', () => {
106106
'--name',
107107
'Customers',
108108
'--folder',
109-
'/Reports',
109+
'Reports',
110110
'--no-wait',
111111
])
112112

‎packages/sim-cli/src/commands/protocol/tables-import.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
GetTableImportResponse,
99
} from '../../generated/v2-api.js'
1010
import { SimApiError, type SimClient } from '../../http/client.js'
11+
import { normalizeFolderPath } from '../../runtime/folder-path.js'
1112
import { coerce, type FieldSpec } from '../../runtime/request.js'
1213
import { contentTypeFor, localFile } from '../../transfer/local-file.js'
1314
import { finishUploadSession } from '../../transfer/upload-session.js'
@@ -139,7 +140,13 @@ export function attachTableImport(tables: Command): void {
139140
if (!name) {
140141
throw new SimApiError('Pass --name <name> to say what the new table is called', 0)
141142
}
142-
target = { type: 'new', name, ...(options.folder ? { folderPath: options.folder } : {}) }
143+
target = {
144+
type: 'new',
145+
name,
146+
...(options.folder !== undefined
147+
? { folderPath: normalizeFolderPath(options.folder) }
148+
: {}),
149+
}
143150
}
144151

145152
const started = await client.request<CreateTableImportResponse>('/api/v2/tables/imports', {

0 commit comments

Comments
 (0)